Skip to content

Commit 0d37c0c

Browse files
committed
working tests
Signed-off-by: rafsaf <rafal.safin12@gmail.com>
1 parent 49c296d commit 0d37c0c

11 files changed

Lines changed: 66 additions & 63 deletions

File tree

alembic.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ truncate_slug_length = 40
4848
# version_path_separator = ;
4949
# version_path_separator = space
5050
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
51-
51+
path_separator=os
5252
# set to 'true' to search source files recursively
5353
# in each "version_locations" directory
5454
# new in Alembic version 1.10

app/auth/tests/test_access_token.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ async def test_login_access_token_has_response_status_code(
2828
},
2929
headers={"Content-Type": "application/x-www-form-urlencoded"},
3030
)
31-
32-
assert response.status_code == status.HTTP_200_OK
31+
assert response.status_code == status.HTTP_200_OK, response.text
3332

3433

3534
@pytest.mark.asyncio(loop_scope="session")
@@ -45,7 +44,7 @@ async def test_login_access_token_jwt_has_valid_token_type(
4544
},
4645
headers={"Content-Type": "application/x-www-form-urlencoded"},
4746
)
48-
47+
assert response.status_code == status.HTTP_200_OK, response.text
4948
token = response.json()
5049
assert token["token_type"] == "Bearer"
5150

@@ -64,7 +63,7 @@ async def test_login_access_token_jwt_has_valid_expire_time(
6463
},
6564
headers={"Content-Type": "application/x-www-form-urlencoded"},
6665
)
67-
66+
assert response.status_code == status.HTTP_200_OK, response.text
6867
token = response.json()
6968
current_timestamp = int(time.time())
7069
assert (
@@ -87,6 +86,7 @@ async def test_login_access_token_returns_valid_jwt_access_token(
8786
},
8887
headers={"Content-Type": "application/x-www-form-urlencoded"},
8988
)
89+
assert response.status_code == status.HTTP_200_OK, response.text
9090

9191
now = int(time.time())
9292
token = response.json()
@@ -110,6 +110,7 @@ async def test_login_access_token_refresh_token_has_valid_expire_time(
110110
},
111111
headers={"Content-Type": "application/x-www-form-urlencoded"},
112112
)
113+
assert response.status_code == status.HTTP_200_OK, response.text
113114

114115
token = response.json()
115116
current_time = int(time.time())
@@ -133,6 +134,7 @@ async def test_login_access_token_refresh_token_exists_in_db(
133134
},
134135
headers={"Content-Type": "application/x-www-form-urlencoded"},
135136
)
137+
assert response.status_code == status.HTTP_200_OK, response.text
136138

137139
token = response.json()
138140

@@ -156,6 +158,7 @@ async def test_login_access_token_refresh_token_in_db_has_valid_fields(
156158
},
157159
headers={"Content-Type": "application/x-www-form-urlencoded"},
158160
)
161+
assert response.status_code == status.HTTP_200_OK, response.text
159162

160163
token = response.json()
161164
result = await session.scalars(
@@ -181,7 +184,7 @@ async def test_auth_access_token_fail_for_not_existing_user_with_message(
181184
headers={"Content-Type": "application/x-www-form-urlencoded"},
182185
)
183186

184-
assert response.status_code == status.HTTP_400_BAD_REQUEST
187+
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text
185188
assert response.json() == {"detail": api_messages.PASSWORD_INVALID}
186189

187190

@@ -199,5 +202,5 @@ async def test_auth_access_token_fail_for_invalid_password_with_message(
199202
headers={"Content-Type": "application/x-www-form-urlencoded"},
200203
)
201204

202-
assert response.status_code == status.HTTP_400_BAD_REQUEST
205+
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text
203206
assert response.json() == {"detail": api_messages.PASSWORD_INVALID}
Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from fastapi import routing, status
2+
from fastapi import status
33
from freezegun import freeze_time
44
from httpx import AsyncClient
55
from sqlalchemy import delete
@@ -8,62 +8,52 @@
88
from app.auth import api_messages
99
from app.auth.jwt import create_jwt_token
1010
from app.auth.models import User
11-
from app.auth.views import router
11+
from app.main import app
1212

1313

1414
@pytest.mark.asyncio(loop_scope="session")
15-
@pytest.mark.parametrize("api_route", router.routes)
16-
async def test_api_routes_raise_401_on_jwt_decode_errors(
15+
async def test_api_raise_401_on_jwt_decode_errors(
1716
client: AsyncClient,
18-
api_route: routing.APIRoute,
1917
) -> None:
20-
for method in api_route.methods:
21-
response = await client.request(
22-
method=method,
23-
url=api_route.path,
24-
headers={"Authorization": "Bearer garbage-invalid-jwt"},
25-
)
26-
assert response.status_code == status.HTTP_401_UNAUTHORIZED
27-
assert response.json() == {"detail": "Token invalid: Not enough segments"}
18+
response = await client.get(
19+
app.url_path_for("read_current_user"),
20+
headers={"Authorization": "Bearer garbage-invalid-jwt"},
21+
)
22+
23+
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.text
24+
assert response.json() == {"detail": "Token invalid: Not enough segments"}
2825

2926

3027
@pytest.mark.asyncio(loop_scope="session")
31-
@pytest.mark.parametrize("api_route", router.routes)
32-
async def test_api_routes_raise_401_on_jwt_expired_token(
28+
async def test_api_raise_401_on_jwt_expired_token(
3329
client: AsyncClient,
3430
default_user: User,
35-
api_route: routing.APIRoute,
3631
) -> None:
3732
with freeze_time("2023-01-01"):
3833
jwt = create_jwt_token(default_user.user_id)
3934
with freeze_time("2023-02-01"):
40-
for method in api_route.methods:
41-
response = await client.request(
42-
method=method,
43-
url=api_route.path,
44-
headers={"Authorization": f"Bearer {jwt.access_token}"},
45-
)
46-
assert response.status_code == status.HTTP_401_UNAUTHORIZED
47-
assert response.json() == {"detail": "Token invalid: Signature has expired"}
35+
response = await client.get(
36+
app.url_path_for("read_current_user"),
37+
headers={"Authorization": f"Bearer {jwt.access_token}"},
38+
)
39+
40+
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.text
41+
assert response.json() == {"detail": "Token invalid: Signature has expired"}
4842

4943

5044
@pytest.mark.asyncio(loop_scope="session")
51-
@pytest.mark.parametrize("api_route", router.routes)
52-
async def test_api_routes_raise_401_on_jwt_user_deleted(
45+
async def test_api_raise_401_on_jwt_user_deleted(
5346
client: AsyncClient,
5447
default_user_headers: dict[str, str],
5548
default_user: User,
56-
api_route: routing.APIRoute,
5749
session: AsyncSession,
5850
) -> None:
5951
await session.execute(delete(User).where(User.user_id == default_user.user_id))
6052
await session.commit()
6153

62-
for method in api_route.methods:
63-
response = await client.request(
64-
method=method,
65-
url=api_route.path,
66-
headers=default_user_headers,
67-
)
68-
assert response.status_code == status.HTTP_401_UNAUTHORIZED
69-
assert response.json() == {"detail": api_messages.JWT_ERROR_USER_REMOVED}
54+
response = await client.get(
55+
app.url_path_for("read_current_user"),
56+
headers=default_user_headers,
57+
)
58+
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.text
59+
assert response.json() == {"detail": api_messages.JWT_ERROR_USER_REMOVED}

app/auth/tests/test_jwt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_jwt_error_with_invalid_secret_key() -> None:
7676
user_id = "test_user_id"
7777
token = jwt.create_jwt_token(user_id)
7878

79-
get_settings().security.jwt_secret_key = SecretStr("the secret has changed now!")
79+
get_settings().security.jwt_secret_key = SecretStr("x" * 32)
8080

8181
with pytest.raises(HTTPException) as e:
8282
jwt.verify_jwt_token(token=token.access_token)

app/auth/views.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
UserResponse,
2323
UserUpdatePasswordRequest,
2424
)
25-
from app.core import database_session
2625
from app.core.config import get_settings
26+
from app.core.database_session import new_async_session
2727

2828
router = APIRouter(responses=api_messages.UNAUTHORIZED_RESPONSES)
2929

@@ -42,7 +42,7 @@ async def read_current_user(
4242
)
4343
async def delete_current_user(
4444
current_user: User = Depends(dependencies.get_current_user),
45-
session: AsyncSession = Depends(database_session.new_async_session),
45+
session: AsyncSession = Depends(new_async_session),
4646
) -> None:
4747
await session.execute(delete(User).where(User.user_id == current_user.user_id))
4848
await session.commit()
@@ -55,7 +55,7 @@ async def delete_current_user(
5555
)
5656
async def reset_current_user_password(
5757
user_update_password: UserUpdatePasswordRequest,
58-
session: AsyncSession = Depends(database_session.new_async_session),
58+
session: AsyncSession = Depends(new_async_session),
5959
current_user: User = Depends(dependencies.get_current_user),
6060
) -> None:
6161
current_user.hashed_password = get_password_hash(user_update_password.password)
@@ -70,7 +70,7 @@ async def reset_current_user_password(
7070
description="OAuth2 compatible token, get an access token for future requests using username and password",
7171
)
7272
async def login_access_token(
73-
session: AsyncSession = Depends(database_session.new_async_session),
73+
session: AsyncSession = Depends(new_async_session),
7474
form_data: OAuth2PasswordRequestForm = Depends(),
7575
) -> AccessTokenResponse:
7676
user = await session.scalar(select(User).where(User.email == form_data.username))
@@ -116,7 +116,7 @@ async def login_access_token(
116116
)
117117
async def refresh_token(
118118
data: RefreshTokenRequest,
119-
session: AsyncSession = Depends(database_session.new_async_session),
119+
session: AsyncSession = Depends(new_async_session),
120120
) -> AccessTokenResponse:
121121
token = await session.scalar(
122122
select(RefreshToken)
@@ -169,7 +169,7 @@ async def refresh_token(
169169
)
170170
async def register_new_user(
171171
new_user: UserCreateRequest,
172-
session: AsyncSession = Depends(database_session.new_async_session),
172+
session: AsyncSession = Depends(new_async_session),
173173
) -> User:
174174
user = await session.scalar(select(User).where(User.email == new_user.email))
175175
if user is not None:

app/conftest.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from app.auth.models import User
1818
from app.core import database_session
1919
from app.core.config import PROJECT_DIR, get_settings
20+
from app.core.database_session import new_async_session
2021
from app.main import app as fastapi_app
2122
from app.tests.factories import (
2223
SQLAASyncPersistence,
@@ -26,7 +27,7 @@
2627

2728

2829
@pytest_asyncio.fixture(scope="session", autouse=True)
29-
async def fixture_setup_new_test_database() -> None:
30+
async def fixture_setup_new_test_database() -> AsyncGenerator[None]:
3031
worker_name = os.getenv("PYTEST_XDIST_WORKER", "gw0")
3132
test_db_name = f"test_db_{worker_name}"
3233

@@ -37,6 +38,9 @@ async def fixture_setup_new_test_database() -> None:
3738
await conn.execute(sqlalchemy.text(f"CREATE DATABASE {test_db_name}"))
3839
await conn.close()
3940

41+
# dispose the original engine before switching to test database
42+
await database_session._ASYNC_ENGINE.dispose()
43+
4044
session_mpatch = pytest.MonkeyPatch()
4145
session_mpatch.setenv("DATABASE__DB", test_db_name)
4246
session_mpatch.setenv("SECURITY__PASSWORD_BCRYPT_ROUNDS", "4")
@@ -66,6 +70,11 @@ def alembic_upgrade() -> None:
6670
loop = asyncio.get_running_loop()
6771
await loop.run_in_executor(None, alembic_upgrade)
6872

73+
yield
74+
75+
# cleanup: dispose the test engine
76+
await engine.dispose()
77+
6978

7079
@pytest_asyncio.fixture(scope="function", autouse=True)
7180
async def fixture_clean_get_settings_between_tests() -> AsyncGenerator[None]:
@@ -98,9 +107,7 @@ async def fixture_session_with_rollback(
98107
lambda: session,
99108
)
100109

101-
fastapi_app.dependency_overrides[database_session.new_async_session] = (
102-
lambda: session
103-
)
110+
fastapi_app.dependency_overrides[new_async_session] = lambda: session
104111

105112
persistence_handler = SQLAASyncPersistence(session=session)
106113
setattr(SQLAlchemySessionMixin, "__async_persistence__", persistence_handler)
@@ -109,7 +116,7 @@ async def fixture_session_with_rollback(
109116

110117
setattr(SQLAlchemySessionMixin, "__async_persistence__", None)
111118

112-
fastapi_app.dependency_overrides.pop(database_session.new_async_session, None)
119+
fastapi_app.dependency_overrides.pop(new_async_session, None)
113120

114121
await session.close()
115122
await transaction.rollback()
@@ -131,5 +138,5 @@ async def fixture_default_user(session: AsyncSession) -> AsyncGenerator[User]:
131138

132139
@pytest_asyncio.fixture(name="default_user_headers", scope="function")
133140
async def fixture_default_user_headers(default_user: User) -> dict[str, str]:
134-
access_token = create_jwt_token(user_id=default_user.user_id)
141+
access_token = create_jwt_token(user_id=default_user.user_id).access_token
135142
return {"Authorization": f"Bearer {access_token}"}

app/core/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727

2828
class Security(BaseModel):
2929
jwt_issuer: str = "my-app"
30-
jwt_secret_key: SecretStr = SecretStr("sk-change-me")
30+
jwt_secret_key: SecretStr = SecretStr(
31+
"change-me-to-a-strong-secret-key-at-least-32-chars-long"
32+
)
3133
jwt_access_token_expire_secs: int = Field(default=15 * 60, gt=10) # 15min
3234
jwt_refresh_token_expire_secs: int = Field(default=28 * 24 * 3600, gt=60) # 28d
3335
jwt_algorithm: str = "HS256"

app/core/database_session.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def new_async_engine(uri: URL) -> AsyncEngine:
3535
_ASYNC_SESSIONMAKER = async_sessionmaker(_ASYNC_ENGINE, expire_on_commit=False)
3636

3737

38-
async def new_async_session() -> AsyncGenerator[AsyncSession]:
38+
async def new_async_session() -> AsyncGenerator[AsyncSession]: # pragma: no cover
3939
session = _ASYNC_SESSIONMAKER()
4040
try:
4141
yield session
@@ -44,7 +44,9 @@ async def new_async_session() -> AsyncGenerator[AsyncSession]:
4444

4545

4646
@asynccontextmanager
47-
async def new_script_async_session() -> AsyncGenerator[AsyncSession]:
47+
async def new_script_async_session() -> AsyncGenerator[
48+
AsyncSession
49+
]: # pragma: no cover
4850
_engine = create_async_engine(
4951
get_settings().sqlalchemy_database_uri, pool_pre_ping=True
5052
)

app/tests/auth.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
from app.auth.password import get_password_hash
2-
31
TESTS_USER_PASSWORD = "geralt"
4-
TESTS_USER_PASSWORD_HASH = get_password_hash(TESTS_USER_PASSWORD)

app/tests/factories.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from polyfactory.fields import Use
1010

1111
from app.auth.models import User
12-
from app.tests.auth import TESTS_USER_PASSWORD_HASH
12+
from app.auth.password import get_password_hash
13+
from app.tests.auth import TESTS_USER_PASSWORD
1314

1415
logging.getLogger("factory").setLevel(logging.ERROR)
1516

@@ -26,4 +27,4 @@ class SQLAlchemySessionMixin[T]:
2627

2728
class UserFactory(SQLAlchemySessionMixin[User], SQLAlchemyFactory[User]):
2829
email = Use(Faker().email)
29-
hashed_password = TESTS_USER_PASSWORD_HASH
30+
hashed_password = Use(lambda: get_password_hash(TESTS_USER_PASSWORD))

0 commit comments

Comments
 (0)