Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ case "$1" in
alembic upgrade head
exec uvicorn app.main.run:make_app --factory --host 0.0.0.0 --port "$PORT" --reload
;;
pytest)
alembic upgrade head
shift
exec pytest "$@"
;;
*)
exec "$@"
;;
Expand Down
1 change: 0 additions & 1 deletion src/app/core/commands/set_user_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ async def execute(self, request: SetUserPasswordRequest) -> None:
target=user,
),
)

await self._user_service.change_password(
user,
password,
Expand Down
6 changes: 2 additions & 4 deletions src/app/infrastructure/auth_ctx/handlers/log_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from dataclasses import dataclass
from typing import Final

from app.core.commands.exceptions import UserNotFoundError
from app.core.common.authorization.current_user_service import CurrentUserService
from app.core.common.services.user import UserService
from app.core.common.value_objects.raw_password import RawPassword
Expand All @@ -15,7 +14,6 @@
from app.infrastructure.auth_ctx.sqla_user_tx_storage import AuthSqlaUserTxStorage

AUTH_ACCOUNT_INACTIVE: Final[str] = "Your account is inactive. Please contact support."
AUTH_PASSWORD_INVALID: Final[str] = "Invalid password." # noqa: S105

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -60,10 +58,10 @@ async def execute(self, request: LogInRequest) -> None:
password = RawPassword(request.password)
user = await self._user_tx_storage.get_by_username(username)
if user is None:
raise UserNotFoundError
raise AuthenticationError

if not await self._user_service.is_password_valid(user, password):
raise AuthenticationError(AUTH_PASSWORD_INVALID)
raise AuthenticationError

if not user.is_active:
raise AuthenticationError(AUTH_ACCOUNT_INACTIVE)
Expand Down
2 changes: 0 additions & 2 deletions src/app/presentation/http/account/log_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from fastapi import APIRouter, status
from fastapi_error_map import ErrorAwareRouter

from app.core.commands.exceptions import UserNotFoundError
from app.core.common.authorization.exceptions import AuthorizationError
from app.core.common.exceptions import BusinessTypeError
from app.infrastructure.adapters.exceptions import PasswordHasherBusyError
Expand All @@ -26,7 +25,6 @@ def make_log_in_router() -> APIRouter:
AuthorizationError: status.HTTP_403_FORBIDDEN,
AlreadyAuthenticatedError: status.HTTP_403_FORBIDDEN,
BusinessTypeError: status.HTTP_400_BAD_REQUEST,
UserNotFoundError: status.HTTP_404_NOT_FOUND,
AuthenticationError: status.HTTP_401_UNAUTHORIZED,
PasswordHasherBusyError: HTTP_503_SERVICE_UNAVAILABLE_RULE,
},
Expand Down
Empty file.
7 changes: 7 additions & 0 deletions tests/integration/with_infra/account/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from typing import Final

AUTH_COOKIE_NAME: Final[str] = "auth_token"
SIGN_UP_ENDPOINT: Final[str] = "/api/v1/account/signup/"
LOG_IN_ENDPOINT: Final[str] = "/api/v1/account/login/"
LOG_OUT_ENDPOINT: Final[str] = "/api/v1/account/logout/"
CHANGE_PASSWORD_ENDPOINT: Final[str] = "/api/v1/account/password/"
89 changes: 89 additions & 0 deletions tests/integration/with_infra/account/test_change_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import httpx
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.common.services.user import UserService
from app.core.common.value_objects.raw_password import RawPassword
from tests.integration.with_infra.account.constants import CHANGE_PASSWORD_ENDPOINT
from tests.integration.with_infra.authentication import authenticate
from tests.integration.with_infra.factories import create_raw_password, create_user_with_password


async def test_returns_204_and_changes_password(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)
old_password_hash = user.password_hash
payload = {"current_password": password, "new_password": create_raw_password()}

r = await it_client.put(CHANGE_PASSWORD_ENDPOINT, json=payload)

assert r.status_code == 204
await it_session.refresh(user)
assert user.password_hash != old_password_hash


async def test_returns_400_when_new_password_is_too_short(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)
payload = {"current_password": password, "new_password": "x" * (RawPassword.MIN_LEN - 1)}

r = await it_client.put(CHANGE_PASSWORD_ENDPOINT, json=payload)

assert r.status_code == 400


async def test_returns_400_when_new_password_equals_current(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)
payload = {"current_password": password, "new_password": password}

r = await it_client.put(CHANGE_PASSWORD_ENDPOINT, json=payload)

assert r.status_code == 400


async def test_returns_401_when_not_authenticated(
it_client: httpx.AsyncClient,
) -> None:
payload = {"current_password": create_raw_password(), "new_password": create_raw_password()}

r = await it_client.put(CHANGE_PASSWORD_ENDPOINT, json=payload)

assert r.status_code == 401


async def test_returns_403_when_current_password_is_wrong(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)
payload = {"current_password": create_raw_password(), "new_password": create_raw_password()}

r = await it_client.put(CHANGE_PASSWORD_ENDPOINT, json=payload)

assert r.status_code == 403
108 changes: 108 additions & 0 deletions tests/integration/with_infra/account/test_log_in.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import httpx
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.common.services.user import UserService
from app.core.common.value_objects.raw_password import RawPassword
from app.core.common.value_objects.username import Username
from tests.integration.with_infra.account.constants import AUTH_COOKIE_NAME, LOG_IN_ENDPOINT
from tests.integration.with_infra.authentication import authenticate
from tests.integration.with_infra.factories import (
create_raw_password,
create_raw_username,
create_user_with_password,
)


async def test_returns_204_and_sets_cookie(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
payload = {"username": user.username.value, "password": password}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 204
assert AUTH_COOKIE_NAME in r.cookies


async def test_returns_400_when_username_is_too_short(
it_client: httpx.AsyncClient,
) -> None:
payload = {"username": "x" * (Username.MIN_LEN - 1), "password": create_raw_password()}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 400


async def test_returns_400_when_password_is_too_short(
it_client: httpx.AsyncClient,
) -> None:
payload = {"username": create_raw_username(), "password": "x" * (RawPassword.MIN_LEN - 1)}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 400


async def test_returns_401_when_user_does_not_exist(
it_client: httpx.AsyncClient,
) -> None:
payload = {"username": create_raw_username(), "password": create_raw_password()}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 401


async def test_returns_401_when_password_is_wrong(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
user = await create_user_with_password(it_user_service)
it_session.add(user)
await it_session.commit()
payload = {"username": user.username.value, "password": create_raw_password()}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 401


async def test_returns_401_when_user_is_inactive(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password, is_active=False)
it_session.add(user)
await it_session.commit()
payload = {"username": user.username.value, "password": password}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 401


async def test_returns_403_when_already_authenticated(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)
payload = {"username": user.username.value, "password": password}

r = await it_client.post(LOG_IN_ENDPOINT, json=payload)

assert r.status_code == 403
32 changes: 32 additions & 0 deletions tests/integration/with_infra/account/test_log_out.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import httpx
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.common.services.user import UserService
from tests.integration.with_infra.account.constants import AUTH_COOKIE_NAME, LOG_OUT_ENDPOINT
from tests.integration.with_infra.authentication import authenticate
from tests.integration.with_infra.factories import create_raw_password, create_user_with_password


async def test_returns_204_and_clears_cookie(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)

r = await it_client.delete(LOG_OUT_ENDPOINT)

assert r.status_code == 204
assert AUTH_COOKIE_NAME not in it_client.cookies


async def test_returns_401_when_not_authenticated(
it_client: httpx.AsyncClient,
) -> None:
r = await it_client.delete(LOG_OUT_ENDPOINT)

assert r.status_code == 401
92 changes: 92 additions & 0 deletions tests/integration/with_infra/account/test_sign_up.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import httpx
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.common.entities.types_ import UserRole
from app.core.common.entities.user import User
from app.core.common.services.user import UserService
from app.core.common.value_objects.raw_password import RawPassword
from app.core.common.value_objects.username import Username
from app.infrastructure.persistence_sqla.mappings.user import users_table
from tests.integration.with_infra.account.constants import SIGN_UP_ENDPOINT
from tests.integration.with_infra.authentication import authenticate
from tests.integration.with_infra.factories import (
create_raw_password,
create_raw_username,
create_user,
create_user_with_password,
)


async def test_returns_204_and_creates_user(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
) -> None:
username = create_raw_username()
password = create_raw_password()
payload = {"username": username, "password": password}

r = await it_client.post(SIGN_UP_ENDPOINT, json=payload)

assert r.status_code == 204
stmt = select(User).where(users_table.c.username == username)
user = await it_session.scalar(stmt)
assert isinstance(user, User)
assert user.role == UserRole.USER
assert user.is_active is True


async def test_returns_400_when_username_is_too_short(
it_client: httpx.AsyncClient,
) -> None:
payload = {"username": "x" * (Username.MIN_LEN - 1), "password": create_raw_password()}

r = await it_client.post(SIGN_UP_ENDPOINT, json=payload)

assert r.status_code == 400


async def test_returns_400_when_password_is_too_short(
it_client: httpx.AsyncClient,
) -> None:
payload = {"username": create_raw_username(), "password": "x" * (RawPassword.MIN_LEN - 1)}

r = await it_client.post(SIGN_UP_ENDPOINT, json=payload)

assert r.status_code == 400


async def test_returns_409_when_username_already_exists(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
username = create_raw_username()
user = create_user(it_user_service, raw_username=username)
it_session.add(user)
await it_session.commit()
payload = {"username": username, "password": create_raw_password()}

r = await it_client.post(SIGN_UP_ENDPOINT, json=payload)

assert r.status_code == 409
stmt = select(func.count()).select_from(User)
count = await it_session.scalar(stmt)
assert count == 1


async def test_returns_403_when_already_authenticated(
it_client: httpx.AsyncClient,
it_session: AsyncSession,
it_user_service: UserService,
) -> None:
password = create_raw_password()
user = await create_user_with_password(it_user_service, raw_password=password)
it_session.add(user)
await it_session.commit()
await authenticate(it_client, user.username.value, password)
payload = {"username": create_raw_username(), "password": create_raw_password()}

r = await it_client.post(SIGN_UP_ENDPOINT, json=payload)

assert r.status_code == 403
Loading