Skip to content

Commit ccb47b0

Browse files
authored
Merge a74da26 into ccf51e7
2 parents ccf51e7 + a74da26 commit ccb47b0

File tree

7 files changed

+131
-33
lines changed

7 files changed

+131
-33
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# code-jam-management
22
Management microservice for Python Discord Code Jams
33

4-
## Running tests
5-
6-
To run tests, export `DATABASE_URL` to point to a empty PostgreSQL database.
7-
Afterwards, you can simply run `pytest` to run tests in the repository.
4+
## Running tests:
5+
- Create an `.env` file with:
6+
```DATABASE_URL=postgresql+asyncpg://codejam_management:codejam_management@localhost:7777```
7+
- Then, run `docker-compose up`
8+
- And finally, you can run `pytest` locally.

tests/conftest.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
from sqlalchemy import text
1111
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
1212

13+
from api.constants import DATABASE_URL
1314
from api.database import Base
1415
from api.dependencies import get_db_session
15-
from api.constants import DATABASE_URL
1616
from api.main import app as main_app
1717

1818
test_engine = create_async_engine(DATABASE_URL, future=True, isolation_level="AUTOCOMMIT")
1919

2020

2121
@pytest.fixture(scope="session")
22-
def event_loop(request) -> Generator:
22+
def event_loop() -> Generator:
2323
"""Yields back an asyncio event loop, then closes it."""
2424
loop = asyncio.get_event_loop()
2525
yield loop
@@ -28,6 +28,7 @@ def event_loop(request) -> Generator:
2828

2929
@pytest.fixture(scope="session")
3030
async def create_test_database_engine() -> Generator:
31+
"""Yield back a Database engine object."""
3132
async with test_engine.begin() as conn:
3233
await conn.execute(text("CREATE DATABASE test;"))
3334
test_db_url = urlsplit(DATABASE_URL)._replace(path="/test")
@@ -39,11 +40,7 @@ async def create_test_database_engine() -> Generator:
3940

4041
@pytest.fixture()
4142
async def session(create_test_database_engine: AsyncEngine) -> AsyncSession:
42-
"""
43-
Yields back an Asynchronous database session
44-
This fixture requests `create_test_database_engine` fixture, as its dependency, this way the session
45-
will point to the database created using the current worker's id.
46-
"""
43+
"""Yields back an Asynchronous database session."""
4744
async with create_test_database_engine.begin() as conn:
4845
await conn.run_sync(Base.metadata.drop_all)
4946
await conn.run_sync(Base.metadata.create_all)
@@ -53,17 +50,18 @@ async def session(create_test_database_engine: AsyncEngine) -> AsyncSession:
5350

5451

5552
@pytest.fixture()
56-
def override_db_session(session: AsyncSession):
57-
"""Yields back the modified Database session that uses the correspondent Database"""
53+
def override_db_session(session: AsyncSession) -> AsyncSession:
54+
"""Yields back the modified Database session that uses the correspondent Database."""
5855

59-
async def _override_db_session():
56+
async def _override_db_session() -> AsyncSession:
6057
yield session
6158

6259
yield _override_db_session
6360

6461

6562
@pytest.fixture()
66-
def app(override_db_session: Callable):
63+
def app(override_db_session: Callable) -> FastAPI:
64+
"""Overrides the default FastAPI app to use the overridden DB session."""
6765
main_app.dependency_overrides[get_db_session] = override_db_session
6866
yield main_app
6967

tests/routers/conftest.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Fixtures for tests of the `routers` package."""
22
import pytest
3+
from fastapi import FastAPI
34
from httpx import AsyncClient
45
from sqlalchemy import delete
56
from sqlalchemy.ext.asyncio import AsyncSession
67
from sqlalchemy.future import select
78

89
from api import models
9-
from api.database import Jam, Team, TeamUser, User
10+
from api.database import Infraction, Jam, Team, TeamUser, User
1011

1112

1213
async def delete_jam(jam: models.CodeJamResponse, session: AsyncSession) -> None:
@@ -58,4 +59,27 @@ async def created_codejam(
5859
created_jam = response.json()
5960
parsed = models.CodeJamResponse(**created_jam)
6061
yield parsed
61-
await delete_jam(parsed, session)
62+
63+
64+
@pytest.fixture
65+
async def created_infraction(
66+
client: AsyncClient,
67+
app: FastAPI,
68+
session: AsyncSession,
69+
created_codejam: models.CodeJamResponse
70+
) -> models.InfractionResponse:
71+
"""Create a test Infraction via the API and yield it."""
72+
# Select one of the test users, so that we can issue an infraction to that user
73+
user_id = created_codejam.teams[0].users[0].user_id
74+
jam_id = created_codejam.id
75+
response = await client.post(
76+
app.url_path_for("create_infraction"),
77+
json={"user_id": user_id, "jam_id": jam_id, "reason": "Too good to be true", "infraction_type": "warning"}
78+
)
79+
parsed_infraction = models.InfractionResponse(**response.json())
80+
assert response.status_code == 200
81+
# Check whether the infraction was actually created, and insterted into the db
82+
assert (
83+
await (session.execute(select(Infraction).where(Infraction.id == parsed_infraction.id)))
84+
).unique().scalars().one_or_none(), "Failed to create Infraction"
85+
yield parsed_infraction

tests/routers/test_codejams.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import pytest
33
from fastapi import FastAPI
44
from httpx import AsyncClient
5+
from sqlalchemy.ext.asyncio import AsyncSession
6+
from sqlalchemy.future import select
57

68
from api import models
9+
from api.database import User
710

811
pytestmark = pytest.mark.asyncio
912

@@ -12,31 +15,39 @@
1215
# If the database has entries in it, cleanup was not performed properly!
1316
async def test_list_codejams_without_db_entries(client: AsyncClient, app: FastAPI) -> None:
1417
"""No codejams should be returned when the database is empty."""
15-
response = await client.get('/codejams')
18+
response = await client.get(app.url_path_for("get_codejams"))
1619
jams = response.json()
1720

1821
assert response.status_code == 200
1922
assert not jams
2023

2124

22-
async def test_get_nonexistent_code_jam(client: AsyncClient) -> None:
25+
async def test_get_nonexistent_code_jam(client: AsyncClient, app: FastAPI) -> None:
2326
"""Getting a nonexistent code jam should return a 404."""
24-
response = await client.get('/codejams/41902')
27+
response = await client.get(app.url_path_for("get_codejam", codejam_id=41902))
2528
assert response.status_code == 404
2629

2730

28-
async def test_get_existing_code_jam(client: AsyncClient, created_codejam: models.CodeJamResponse) -> None:
31+
async def test_get_existing_code_jam(
32+
client: AsyncClient,
33+
created_codejam: models.CodeJamResponse,
34+
app: FastAPI
35+
) -> None:
2936
"""Getting an existing code jam should return 200."""
30-
response = await client.get(f'/codejams/{created_codejam.id}')
37+
response = await client.get(app.url_path_for("get_codejam", codejam_id=created_codejam.id))
3138
assert response.status_code == 200
3239
raw = response.json()
3340
jam = models.CodeJamResponse(**raw)
3441
assert jam == created_codejam
3542

3643

37-
async def test_list_codejams_with_existing_jam(client: AsyncClient, created_codejam: models.CodeJamResponse) -> None:
44+
async def test_list_codejams_with_existing_jam(
45+
client: AsyncClient,
46+
created_codejam: models.CodeJamResponse,
47+
app: FastAPI
48+
) -> None:
3849
"""Listing all code jams should return the created jam."""
39-
response = await client.get('/codejams')
50+
response = await client.get(app.url_path_for("get_codejams"))
4051
assert response.status_code == 200
4152
raw = response.json()
4253

@@ -47,3 +58,24 @@ async def test_list_codejams_with_existing_jam(client: AsyncClient, created_code
4758
# Ensure the code jam in the "single jam" endpoint matches the
4859
# code jam we get returned from the API here.
4960
assert jam == created_codejam
61+
62+
63+
async def test_create_codejams_rejects_invalid_data(client: AsyncClient, app: FastAPI) -> None:
64+
"""Posting invalid JSON data should return 422."""
65+
response = await client.post(app.url_path_for("create_codejam"), json={"name": "test"})
66+
assert response.status_code == 422
67+
68+
69+
async def test_create_codejams_accepts_valid_data_and_creates_user(
70+
client: AsyncClient,
71+
app: FastAPI, session: AsyncSession
72+
) -> None:
73+
"""Posting a valid JSON data should return 200 and the record should exitst in the DB."""
74+
response = await client.post(app.url_path_for("create_codejam"), json={
75+
"name": "CodeJam Test",
76+
"teams": [{"name": "Dramatic Dragonflies", "users": [{"user_id": 1, "is_leader": True}]}]
77+
})
78+
assert response.status_code == 200
79+
80+
# Checks whether the previously added user was actually inserted into the database.
81+
assert (await session.execute(select(User).where(User.id == 1))).scalars().unique().one_or_none()

tests/routers/test_infractions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Tests for the infractions router."""
2+
import pytest
3+
from fastapi import FastAPI
4+
from httpx import AsyncClient
5+
6+
from api import models
7+
8+
pytestmark = pytest.mark.asyncio
9+
10+
11+
async def test_list_infractions_without_db_entries(client: AsyncClient, app: FastAPI) -> None:
12+
"""No infractions should be returned when the database is empty."""
13+
response = await client.get(app.url_path_for("get_infractions"))
14+
infractions = response.json()
15+
assert response.status_code == 200
16+
assert not infractions
17+
18+
19+
async def test_get_nonexsistent_infraction(client: AsyncClient, app: FastAPI) -> None:
20+
"""Getting a nonexistent infraction should return a 404."""
21+
response = await client.get(app.url_path_for("get_infraction", infraction_id=41902))
22+
assert response.status_code == 404
23+
24+
25+
async def test_get_existent_infraction(
26+
client: AsyncClient,
27+
app: FastAPI,
28+
created_infraction: models.InfractionResponse
29+
) -> None:
30+
"""Getting an existing infraction should return 200."""
31+
response = await client.get(app.url_path_for("get_infraction", infraction_id=created_infraction.id))
32+
assert response.status_code == 200
33+
34+
35+
async def test_create_infractions_rejects_invalid_data(client: AsyncClient, app: FastAPI) -> None:
36+
"""Posting invalid JSON data should return 422."""
37+
response = await client.post(app.url_path_for("create_infraction"), json={"reason": "Yes"})
38+
assert response.status_code == 422

tests/routers/test_users.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,48 @@
11
"""Tests for the users router."""
22
import pytest
3+
from fastapi import FastAPI
34
from httpx import AsyncClient
45

56
from api import models
67

78
pytestmark = pytest.mark.asyncio
89

910

10-
async def test_list_users_without_db_entries(client: AsyncClient) -> None:
11+
async def test_list_users_without_db_entries(client: AsyncClient, app: FastAPI) -> None:
1112
"""No users should be returned when the database is empty."""
12-
response = await client.get('/users')
13+
response = await client.get(app.url_path_for("get_users"))
1314

1415
assert response.status_code == 200
1516
users = response.json()
1617
assert not users
1718

1819

19-
async def test_get_nonexistent_user(client: AsyncClient) -> None:
20+
async def test_get_nonexistent_user(client: AsyncClient, app: FastAPI) -> None:
2021
"""Getting a nonexistent user should return a 404."""
21-
response = await client.get('/users/129841')
22+
response = await client.get(app.url_path_for("get_user", user_id=129841))
2223
assert response.status_code == 404
2324

2425

25-
async def test_list_users_with_existing_jam(client: AsyncClient, created_codejam: models.CodeJamResponse) -> None:
26+
async def test_list_users_with_existing_jam(
27+
client: AsyncClient,
28+
created_codejam: models.CodeJamResponse,
29+
app: FastAPI
30+
) -> None:
2631
"""Listing users with an existing jam should display the users in the jam."""
27-
response = await client.get('/users')
32+
response = await client.get(app.url_path_for("get_users"))
2833
assert response.status_code == 200
2934
raw = response.json()
3035
users = [models.UserResponse(**user) for user in raw]
3136
assert users
3237

3338

3439
async def test_get_users_from_existing_jam(
35-
client: AsyncClient, codejam: models.CodeJam, created_codejam: models.CodeJamResponse
40+
client: AsyncClient, codejam: models.CodeJam, created_codejam: models.CodeJamResponse, app: FastAPI
3641
) -> None:
3742
"""Getting users from an existing jam should return the users properly."""
3843
for team in codejam.teams:
3944
for user in team.users:
40-
response = await client.get(f'/users/{user.user_id}')
45+
response = await client.get(app.url_path_for("get_user", user_id=user.user_id))
4146
assert response.status_code == 200
4247
raw = response.json()
4348
parsed = models.UserResponse(**raw)

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ max-line-length=120
33
docstring-convention=all
44
import-order-style=pycharm
55
application_import_names=api
6-
exclude=.cache,.venv,.git,alembic/*,tests/*
6+
exclude=.cache,.venv,.git,alembic/*
77
ignore=
88
B311,W503,E226,S311,T000,B008
99
# Missing Docstrings

0 commit comments

Comments
 (0)