diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3dc0b46..b0157b5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,17 +14,26 @@ jobs: runs-on: ubuntu-latest env: - DB_USER: postgres - DB_PASSWORD: 123456 - TEST_DB_NAME: test_database + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} + TEST_AUTH_PASSWORD: ${{ secrets.TEST_AUTH_PASSWORD }} + TEST_AUTH_USER: ${{ secrets.TEST_AUTH_USER }} + TEST_AUTH_USER_SUB: ${{ secrets.TEST_AUTH_USER_SUB }} + AUTH0_AUDIENCE: ${{ vars.AUTH0_AUDIENCE }} + AUTH0_AUTHORIZE_URL: ${{ vars.AUTH0_AUTHORIZE_URL }} + AUTH0_CONNECTION: ${{ vars.AUTH0_CONNECTION }} + AUTH0_ISSUER: ${{ vars.AUTH0_ISSUER }} + AUTH0_JWKS_URI: ${{ vars.AUTH0_JWKS_URI }} + AUTH0_REGISTER_URL: ${{ vars.AUTH0_REGISTER_URL }} + TEST_DB_URL : ${{ vars.TEST_DB_URL }} services: postgres: image: postgres env: POSTGRES_USER: postgres - POSTGRES_PASSWORD: 123456 - POSTGRES_DB: test_database + POSTGRES_PASSWORD: postgres + POSTGRES_DB: costy_test ports: [ '5432:5432' ] options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 @@ -34,12 +43,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.10" - - name: Set env variables - run: | - echo "DB_USER=$DB_USER" >> $GITHUB_ENV - echo "DB_PASSWORD=$DB_PASSWORD" >> $GITHUB_ENV - echo "TEST_DB_NAME=$TEST_DB_NAME" >> $GITHUB_ENV - echo "DB_HOST=localhost:${{ job.services.postgres.ports[5432] }}" >> $GITHUB_ENV - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/src/costy/adapters/auth/auth_gateway.py b/src/costy/adapters/auth/auth_gateway.py index 9af0575..1826d90 100644 --- a/src/costy/adapters/auth/auth_gateway.py +++ b/src/costy/adapters/auth/auth_gateway.py @@ -1,4 +1,4 @@ -from aiohttp import ClientSession +from httpx import AsyncClient from sqlalchemy import Table from sqlalchemy.ext.asyncio import AsyncSession @@ -12,7 +12,7 @@ class AuthGateway(AuthLoger, AuthRegister): def __init__( self, db_session: AsyncSession, - web_session: ClientSession, + web_session: AsyncClient, table: Table, settings: AuthSettings ) -> None: @@ -31,13 +31,13 @@ async def authenticate(self, email: str, password: str) -> str: "audience": self.settings.audience, "grant_type": self.settings.grant_type } - async with self.web_session.post(url, data=data) as response: - response_data = await response.json() - if response.status == 200: - token: str | None = response_data.get("access_token") - if token: - return token - raise AuthenticationError(response_data) + response = await self.web_session.post(url, data=data) + response_data = response.json() + if response.status_code == 200: + token: str | None = response_data.get("access_token") + if token: + return token + raise AuthenticationError(response_data) async def register(self, email: str, password: str) -> str: url = self.settings.register_url @@ -48,8 +48,8 @@ async def register(self, email: str, password: str) -> str: "client_secret": self.settings.client_secret, "connection": self.settings.connection } - async with self.web_session.post(url, data=data) as response: - response_data = await response.json() - if response.status == 200: - return response_data["_id"] - raise RegisterError(response_data) + response = await self.web_session.post(url, data=data) + response_data = response.json() + if response.status_code == 200: + return response_data["_id"] + raise RegisterError(response_data) diff --git a/src/costy/adapters/auth/token.py b/src/costy/adapters/auth/token.py index cde8dd5..4ede9b0 100644 --- a/src/costy/adapters/auth/token.py +++ b/src/costy/adapters/auth/token.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from typing import Any, Literal -from aiohttp import ClientSession +from httpx import AsyncClient from jose import exceptions as jwt_exc from jose import jwt @@ -73,7 +73,7 @@ def validate_token(self, token: str, jwks: dict[Any, Any]) -> str: class KeySetProvider: - def __init__(self, uri: str, session: ClientSession, expired: timedelta): + def __init__(self, uri: str, session: AsyncClient, expired: timedelta): self.session = session self.jwks: dict[str, str] = {} self.expired = expired @@ -89,9 +89,9 @@ async def get_key_set(self) -> dict[Any, Any]: return self.jwks async def _request_new_key_set(self) -> None: - async with self.session.get(self.uri) as response: - self.jwks = await response.json() - self.last_updated = datetime.now() + response = await self.session.get(self.uri) + self.jwks = response.json() + self.last_updated = datetime.now() class TokenIdProvider(IdProvider): diff --git a/src/costy/application/operation/dto.py b/src/costy/application/operation/dto.py index e215200..f83ca9b 100644 --- a/src/costy/application/operation/dto.py +++ b/src/costy/application/operation/dto.py @@ -7,7 +7,7 @@ @dataclass(kw_only=True) class NewOperationDTO: amount: int - description: str | None + description: str | None = None time: int = int(datetime.now().timestamp()) category_id: CategoryId diff --git a/src/costy/infrastructure/auth.py b/src/costy/infrastructure/auth.py index 6017a61..738a12f 100644 --- a/src/costy/infrastructure/auth.py +++ b/src/costy/infrastructure/auth.py @@ -1,7 +1,7 @@ from datetime import timedelta from typing import Any, Callable, Coroutine -from aiohttp import ClientSession +from httpx import AsyncClient from costy.adapters.auth.token import ( Algorithm, @@ -16,7 +16,7 @@ def create_id_provider_factory( algorithm: Algorithm, issuer: str, jwsk_uri: str, - web_session: ClientSession, + web_session: AsyncClient, jwsk_expired: timedelta = timedelta(days=1) ) -> Callable[[], Coroutine[Any, Any, TokenIdProvider]]: token_processor = JwtTokenProcessor(algorithm, audience, issuer) diff --git a/src/costy/infrastructure/db/main.py b/src/costy/infrastructure/db/main.py index 89473da..af07f65 100644 --- a/src/costy/infrastructure/db/main.py +++ b/src/costy/infrastructure/db/main.py @@ -1,4 +1,3 @@ -import pytest from sqlalchemy import MetaData from sqlalchemy.ext.asyncio import ( AsyncEngine, @@ -7,14 +6,9 @@ create_async_engine, ) -from costy.infrastructure.config import SettingError - def get_engine(url: str) -> AsyncEngine: - try: - return create_async_engine(url, future=True) - except SettingError: - pytest.skip("Auth settings env var are not exists.") + return create_async_engine(url, future=True) def get_sessionmaker(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: diff --git a/src/costy/main/ioc.py b/src/costy/main/ioc.py index 660fa89..8f83499 100644 --- a/src/costy/main/ioc.py +++ b/src/costy/main/ioc.py @@ -3,7 +3,7 @@ from typing import AsyncIterator from adaptix import Retort -from aiohttp import ClientSession +from httpx import AsyncClient from sqlalchemy import Table from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker @@ -32,7 +32,7 @@ @dataclass class Depends: session: AsyncSession - web_session: ClientSession + web_session: AsyncClient uow: OrmUoW user_gateway: UserGateway @@ -41,7 +41,7 @@ class IoC(InteractorFactory): def __init__( self, session_factory: async_sessionmaker[AsyncSession], - web_session: ClientSession, + web_session: AsyncClient, tables: dict[str, Table], retort: Retort, auth_settings: AuthSettings diff --git a/src/costy/main/web.py b/src/costy/main/web.py index b128089..06aadf7 100644 --- a/src/costy/main/web.py +++ b/src/costy/main/web.py @@ -1,7 +1,7 @@ from typing import Any, Callable, Coroutine, TypeVar from adaptix import Retort -from aiohttp import ClientSession +from httpx import AsyncClient from litestar import Litestar from litestar.di import Provide @@ -33,12 +33,15 @@ async def func() -> T: return func -def init_app() -> Litestar: +def init_app(db_url: str | None = None) -> Litestar: + if not db_url: + db_url = get_db_connection_url() + base_metadata = get_metadata() tables = create_tables(base_metadata) - session_factory = get_sessionmaker(get_engine(get_db_connection_url())) - web_session = ClientSession() + session_factory = get_sessionmaker(get_engine(db_url)) + web_session = AsyncClient() auth_settings = get_auth_settings() ioc = IoC(session_factory, web_session, tables, Retort(), auth_settings) @@ -51,6 +54,9 @@ def init_app() -> Litestar: web_session ) + async def finalization(): + await web_session.aclose() + return Litestar( route_handlers=( AuthenticationController, @@ -63,6 +69,6 @@ def init_app() -> Litestar: "id_provider": Provide(get_id_provider), "id_provider_pure": Provide(id_provider_factory) }, - on_shutdown=[lambda: web_session.close()], + on_shutdown=[finalization], debug=True ) diff --git a/src/costy/presentation/api/operation.py b/src/costy/presentation/api/operation.py index 5c62b9d..ab4bd57 100644 --- a/src/costy/presentation/api/operation.py +++ b/src/costy/presentation/api/operation.py @@ -9,16 +9,6 @@ class OperationController(Controller): path = '/operations' - @get("/{operation_id:int}") - async def get_operation( - self, - ioc: InteractorFactory, - id_provider: IdProvider, - operation_id: OperationId - ) -> Operation | None: - async with ioc.read_operation(id_provider) as read_operation: - return await read_operation(operation_id) - @get() async def get_list_operations( self, diff --git a/tests/adapters/test_auth_adapter.py b/tests/adapters/test_auth_adapter.py index 65c951e..3aa01bf 100644 --- a/tests/adapters/test_auth_adapter.py +++ b/tests/adapters/test_auth_adapter.py @@ -1,43 +1,10 @@ -import os - import pytest from pytest_asyncio import fixture from sqlalchemy import Table, insert from sqlalchemy.ext.asyncio import AsyncSession -from costy.adapters.auth.auth_gateway import AuthGateway from costy.application.common.auth_gateway import AuthLoger from costy.domain.models.user import UserId -from costy.infrastructure.config import AuthSettings, get_auth_settings - - -@fixture -async def auth_sub() -> str: # type: ignore - try: - return os.environ["TEST_AUTH_USER_SUB"] - except KeyError: - pytest.skip("No test user sub environment variable.") - - -@fixture -async def auth_settings() -> AuthSettings: - return get_auth_settings() - - -@fixture -async def auth_adapter(db_session, web_session, db_tables, auth_settings: AuthSettings) -> AuthLoger: - return AuthGateway(db_session, web_session, db_tables["users"], auth_settings) - - -@fixture -async def credentials() -> dict[str, str]: # type: ignore - try: - return { - "username": os.environ["TEST_AUTH_USER"], - "password": os.environ["TEST_AUTH_PASSWORD"] - } - except KeyError: - pytest.skip("No test user credentials.") @fixture diff --git a/tests/adapters/test_category_adapter.py b/tests/adapters/test_category_adapter.py index 68f8d6b..860385a 100644 --- a/tests/adapters/test_category_adapter.py +++ b/tests/adapters/test_category_adapter.py @@ -1,21 +1,6 @@ import pytest -from pytest_asyncio import fixture -from sqlalchemy import insert -from costy.adapters.db.category_gateway import CategoryGateway from costy.domain.models.category import Category, CategoryType -from costy.domain.models.user import UserId - - -@fixture -def category_gateway(db_session, db_tables, retort) -> CategoryGateway: - return CategoryGateway(db_session, db_tables["categories"], retort) - - -@fixture() -async def db_user_id(db_session, db_tables) -> UserId: - created_user_record = await db_session.execute(insert(db_tables["users"]).values(auth_id="test")) - return UserId(created_user_record.inserted_primary_key[0]) @pytest.mark.asyncio diff --git a/tests/adapters/test_operation_adapter.py b/tests/adapters/test_operation_adapter.py index 2a13767..6886dc1 100644 --- a/tests/adapters/test_operation_adapter.py +++ b/tests/adapters/test_operation_adapter.py @@ -1,32 +1,11 @@ import pytest from pytest_asyncio import fixture -from sqlalchemy import insert -from costy.adapters.db.operation_gateway import OperationGateway -from costy.domain.models.category import CategoryId from costy.domain.models.operation import Operation -from costy.domain.models.user import UserId @fixture -def operation_gateway(db_session, db_tables, retort) -> OperationGateway: - return OperationGateway(db_session, db_tables["operations"], retort) - - -@fixture() -async def db_user_id(db_session, db_tables) -> UserId: - created_user_record = await db_session.execute(insert(db_tables["users"]).values(auth_id="test")) - return UserId(created_user_record.inserted_primary_key[0]) - - -@fixture -async def db_category_id(db_session, db_tables) -> CategoryId: - created_category_record = await db_session.execute(insert(db_tables["categories"]).values(name="test")) - return CategoryId(created_category_record.inserted_primary_key[0]) - - -@fixture -def operation_entity(db_user_id, db_category_id) -> Operation: +async def operation_entity(db_user_id, db_category_id) -> Operation: return Operation( id=None, amount=100, diff --git a/tests/adapters/test_user_adapter.py b/tests/adapters/test_user_adapter.py index 5335459..661bee4 100644 --- a/tests/adapters/test_user_adapter.py +++ b/tests/adapters/test_user_adapter.py @@ -1,33 +1,14 @@ import pytest -from pytest_asyncio import fixture - -from costy.adapters.db.user_gateway import UserGateway -from costy.domain.models.user import User - - -@fixture -def auth_id() -> str: - return "auth_id" - - -@fixture -def user_gateway(db_session, db_tables, retort) -> UserGateway: - return UserGateway(db_session, db_tables["users"], retort) - - -@fixture -def user_entity() -> User: - return User(id=None, auth_id="auth_id") @pytest.mark.asyncio -async def test_get_user_id_by_auth_id(created_user, user_gateway, auth_id: str) -> None: - assert await user_gateway.get_user_id_by_auth_id(auth_id) == created_user +async def test_get_user_id_by_auth_id(created_auth_user, user_gateway, auth_id: str) -> None: + assert await user_gateway.get_user_id_by_auth_id(auth_id) == created_auth_user @pytest.mark.asyncio -async def test_get_user_by_id(created_user, user_gateway): - assert (await user_gateway.get_user_by_id(created_user)).id == created_user +async def test_get_user_by_id(created_auth_user, user_gateway): + assert (await user_gateway.get_user_by_id(created_auth_user)).id == created_auth_user @pytest.mark.asyncio diff --git a/tests/application/category/test_create_category.py b/tests/application/category/test_create_category.py index 6bb71da..1ed239f 100644 --- a/tests/application/category/test_create_category.py +++ b/tests/application/category/test_create_category.py @@ -1,7 +1,7 @@ from unittest.mock import Mock import pytest -from pytest import fixture +from pytest_asyncio import fixture from costy.application.category.create_category import CreateCategory from costy.application.category.dto import NewCategoryDTO @@ -13,12 +13,7 @@ @fixture -def category_info() -> NewCategoryDTO: - return NewCategoryDTO("test") - - -@fixture -def interactor(id_provider: IdProvider, category_id: CategoryId, user_id: UserId, category_info: NewCategoryDTO) -> CreateCategory: +async def interactor(id_provider: IdProvider, category_id: CategoryId, user_id: UserId, category_info: NewCategoryDTO) -> CreateCategory: category_service = Mock() category_service.create.return_value = Category( id=None, diff --git a/tests/application/operation/test_create_operation.py b/tests/application/operation/test_create_operation.py index 71e06bf..d6bc46d 100644 --- a/tests/application/operation/test_create_operation.py +++ b/tests/application/operation/test_create_operation.py @@ -1,30 +1,19 @@ from unittest.mock import Mock import pytest -from pytest import fixture +from pytest_asyncio import fixture from costy.application.common.id_provider import IdProvider from costy.application.common.operation_gateway import OperationSaver from costy.application.common.uow import UoW from costy.application.operation.create_operation import CreateOperation from costy.application.operation.dto import NewOperationDTO -from costy.domain.models.category import CategoryId from costy.domain.models.operation import Operation, OperationId from costy.domain.models.user import UserId @fixture -def operation_info() -> NewOperationDTO: - return NewOperationDTO( - amount=100, - description="description", - time=10000, - category_id=CategoryId(999) - ) - - -@fixture -def interactor(id_provider: IdProvider, operation_id: OperationId, user_id: UserId, operation_info: NewOperationDTO) -> CreateOperation: +async def interactor(id_provider: IdProvider, operation_id: OperationId, user_id: UserId, operation_info: NewOperationDTO) -> CreateOperation: operation_service = Mock() operation_service.create.return_value = Operation( id=None, diff --git a/tests/application/test_authenticate.py b/tests/application/test_authenticate.py index 37b0120..9d4a5ae 100644 --- a/tests/application/test_authenticate.py +++ b/tests/application/test_authenticate.py @@ -1,7 +1,7 @@ from unittest.mock import Mock import pytest -from pytest import fixture +from pytest_asyncio import fixture from costy.application.authenticate import Authenticate, LoginInputDTO from costy.application.common.auth_gateway import AuthLoger @@ -10,19 +10,9 @@ @fixture -def login_info() -> LoginInputDTO: - return LoginInputDTO(email="test@email.com", password="password") - - -@fixture -def token() -> str: - return "token" - - -@fixture -def interactor(user_id: UserId, login_info: LoginInputDTO) -> Authenticate: +async def interactor(user_id: UserId, login_info: LoginInputDTO, token) -> Authenticate: auth_gateway = Mock(spec=AuthLoger) - auth_gateway.authenticate.return_value = "token" + auth_gateway.authenticate.return_value = token uow = Mock(spec=UoW) return Authenticate(auth_gateway, uow) diff --git a/tests/application/user/test_create_user.py b/tests/application/user/test_create_user.py index 8c7de00..7ebc591 100644 --- a/tests/application/user/test_create_user.py +++ b/tests/application/user/test_create_user.py @@ -1,7 +1,7 @@ from unittest.mock import Mock import pytest -from pytest import fixture +from pytest_asyncio import fixture from costy.application.common.auth_gateway import AuthRegister from costy.application.common.uow import UoW @@ -13,12 +13,7 @@ @fixture -def user_info() -> NewUserDTO: - return NewUserDTO(email="test@email.com", password="password") - - -@fixture -def interactor(user_id: UserId, user_info: NewUserDTO) -> CreateUser: +async def interactor(user_id: UserId, user_info: NewUserDTO) -> CreateUser: user_service = Mock(spec=UserService) user_service.create.return_value = User(id=None, auth_id="auth_id") diff --git a/tests/conftest.py b/tests/conftest.py index fb13b5e..f2d5391 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,39 +1,6 @@ -from unittest.mock import Mock - -from pytest_asyncio import fixture -from sqlalchemy import insert - -from costy.application.common.id_provider import IdProvider -from costy.domain.models.category import CategoryId -from costy.domain.models.operation import OperationId -from costy.domain.models.user import UserId - -pytest_plugins = ["tests.infrastructure"] - - -@fixture -def user_id() -> UserId: - return UserId(999) - - -@fixture -def operation_id() -> OperationId: - return OperationId(999) - - -@fixture -def category_id() -> CategoryId: - return CategoryId(999) - - -@fixture() -async def created_user(db_session, db_tables, auth_id) -> UserId: - result = await db_session.execute(insert(db_tables["users"]).values(auth_id=auth_id)) - return UserId(result.inserted_primary_key[0]) - - -@fixture -def id_provider(user_id: UserId) -> IdProvider: - provider = Mock(spec=IdProvider) - provider.get_current_user_id.return_value = user_id - return provider +pytest_plugins = [ + "tests.fixtures.adapters", + "tests.fixtures.infrastructure", + "tests.fixtures.data", + "tests.fixtures.database", +] diff --git a/tests/domain/test_create.py b/tests/domain/test_create.py index 27308d2..99e3869 100644 --- a/tests/domain/test_create.py +++ b/tests/domain/test_create.py @@ -25,5 +25,6 @@ Category(None, "test", CategoryType.GENERAL.value, UserId(9999)) ), ]) -def test_create_domain_service(domain_service, data, expected_model): # type: ignore +@pytest.mark.asyncio +async def test_create_domain_service(domain_service, data, expected_model): # type: ignore assert domain_service.create(*data) == expected_model diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/adapters.py b/tests/fixtures/adapters.py new file mode 100644 index 0000000..bafa33f --- /dev/null +++ b/tests/fixtures/adapters.py @@ -0,0 +1,44 @@ +from unittest.mock import Mock + +from pytest_asyncio import fixture + +from costy.adapters.auth.auth_gateway import AuthGateway +from costy.adapters.db.category_gateway import CategoryGateway +from costy.adapters.db.operation_gateway import OperationGateway +from costy.adapters.db.user_gateway import UserGateway +from costy.application.common.auth_gateway import AuthLoger +from costy.application.common.id_provider import IdProvider +from costy.domain.models.user import UserId +from costy.infrastructure.config import AuthSettings, get_auth_settings + + +@fixture(scope="session") +async def auth_settings() -> AuthSettings: + return get_auth_settings() + + +@fixture +async def auth_adapter(db_session, web_session, db_tables, auth_settings: AuthSettings) -> AuthLoger: + return AuthGateway(db_session, web_session, db_tables["users"], auth_settings) + + +@fixture +async def user_gateway(db_session, db_tables, retort) -> UserGateway: + return UserGateway(db_session, db_tables["users"], retort) + + +@fixture +async def category_gateway(db_session, db_tables, retort) -> CategoryGateway: + return CategoryGateway(db_session, db_tables["categories"], retort) + + +@fixture +async def operation_gateway(db_session, db_tables, retort) -> OperationGateway: + return OperationGateway(db_session, db_tables["operations"], retort) + + +@fixture +async def id_provider(user_id: UserId) -> IdProvider: + provider = Mock(spec=IdProvider) + provider.get_current_user_id.return_value = user_id + return provider diff --git a/tests/fixtures/data.py b/tests/fixtures/data.py new file mode 100644 index 0000000..102d63e --- /dev/null +++ b/tests/fixtures/data.py @@ -0,0 +1,102 @@ +import os + +import pytest +from pytest_asyncio import fixture + +from costy.application.authenticate import LoginInputDTO +from costy.application.category.dto import NewCategoryDTO +from costy.application.operation.dto import NewOperationDTO +from costy.application.user.dto import NewUserDTO +from costy.domain.models.category import CategoryId +from costy.domain.models.operation import OperationId +from costy.domain.models.user import User, UserId + + +@fixture +async def user_id() -> UserId: + return UserId(999) + + +@fixture +async def operation_id() -> OperationId: + return OperationId(999) + + +@fixture +async def category_id() -> CategoryId: + return CategoryId(999) + + +@fixture +async def user_entity() -> User: + return User(id=None, auth_id="auth_id") + + +@fixture +async def category_info() -> NewCategoryDTO: + return NewCategoryDTO("test") + + +@fixture +async def operation_info() -> NewOperationDTO: + return NewOperationDTO( + amount=100, + description="description", + time=10000, + category_id=CategoryId(999) + ) + + +@fixture +async def user_info() -> NewUserDTO: + return NewUserDTO(email="test@email.com", password="password") + + +@fixture +async def login_info() -> LoginInputDTO: + return LoginInputDTO(email="test@email.com", password="password") + + +@fixture +async def token() -> str: + return "token" + + +@fixture +async def auth_id() -> str: + return "auth_id" + + +# global is used because tests cannot use a "session" fixed fixture in this case +user_token_state = None + + +@fixture +async def user_token(auth_adapter, credentials): # type: ignore + global user_token_state + if not user_token_state: + response = await auth_adapter.authenticate(credentials["username"], credentials["password"]) + if response: + return response + pytest.fail("Failed to test user authenticate.") + else: + return user_token_state + + +@fixture +async def auth_sub() -> str: # type: ignore + try: + return os.environ["TEST_AUTH_USER_SUB"].replace("auth|0", "") + except KeyError: + pytest.fail("No test user sub environment variable.") + + +@fixture +async def credentials() -> dict[str, str]: # type: ignore + try: + return { + "username": os.environ["TEST_AUTH_USER"], + "password": os.environ["TEST_AUTH_PASSWORD"] + } + except KeyError: + pytest.fail("No test user credentials.") diff --git a/tests/fixtures/database.py b/tests/fixtures/database.py new file mode 100644 index 0000000..4e13077 --- /dev/null +++ b/tests/fixtures/database.py @@ -0,0 +1,44 @@ +from pytest_asyncio import fixture +from sqlalchemy import delete, insert, select + +from costy.domain.models.category import CategoryId +from costy.domain.models.user import UserId + + +@fixture +async def db_category_id(db_session, db_tables) -> CategoryId: + created_category_record = await db_session.execute(insert(db_tables["categories"]).values(name="test")) + await db_session.commit() + return CategoryId(created_category_record.inserted_primary_key[0]) + + +@fixture +async def db_user_id(db_session, db_tables) -> UserId: + stmt = select(db_tables["users"]).where(db_tables["users"].c.auth_id == "test") + result = next((await db_session.execute(stmt)).mappings(), None) + if result: + return result["id"] + created_user_record = await db_session.execute(insert(db_tables["users"]).values(auth_id="test")) + return UserId(created_user_record.inserted_primary_key[0]) + + +@fixture() +async def created_auth_user(db_session, db_tables, auth_id) -> UserId: + result = await db_session.execute(insert(db_tables["users"]).values(auth_id=auth_id)) + return UserId(result.inserted_primary_key[0]) + + +@fixture +async def create_sub_user(db_session, db_tables, auth_sub) -> UserId: + result = await db_session.execute(insert(db_tables["users"]).values(auth_id=auth_sub.replace("auth0|", ""))) + await db_session.commit() + return UserId(result.inserted_primary_key[0]) + + +@fixture +async def clean_up_db(db_session, db_tables): + tables_order = ["operations", "categories", "users"] + yield + for table in tables_order: + await db_session.execute(delete(db_tables[table])) + await db_session.commit() diff --git a/tests/infrastructure.py b/tests/fixtures/infrastructure.py similarity index 82% rename from tests/infrastructure.py rename to tests/fixtures/infrastructure.py index 7326246..2358a5a 100644 --- a/tests/infrastructure.py +++ b/tests/fixtures/infrastructure.py @@ -4,6 +4,8 @@ import pytest from adaptix import Retort from aiohttp import ClientSession +from httpx import AsyncClient +from litestar import Litestar from pytest_asyncio import fixture from sqlalchemy import Table from sqlalchemy.exc import OperationalError @@ -16,6 +18,7 @@ from costy.infrastructure.db.main import get_metadata from costy.infrastructure.db.orm import create_tables +from costy.main.web import init_app @fixture(scope='session') @@ -23,7 +26,7 @@ async def db_url() -> str: # type: ignore try: return os.environ['TEST_DB_URL'] except KeyError: - pytest.skip("TEST_DB_URL env variable not set") + pytest.fail("TEST_DB_URL env variable not set") @fixture(scope='session') @@ -54,7 +57,7 @@ async def db_tables(db_engine: AsyncEngine) -> AsyncGenerator[None, dict[str, Ta await conn.run_sync(metadata.drop_all) await conn.run_sync(metadata.create_all) except OperationalError: - pytest.skip("Connection to database is faield.") + pytest.fail("Connection to database is faield.") yield tables @@ -64,10 +67,15 @@ async def db_tables(db_engine: AsyncEngine) -> AsyncGenerator[None, dict[str, Ta @fixture async def web_session() -> AsyncIterator[ClientSession]: - async with ClientSession() as session: - yield session + async with AsyncClient() as client: + yield client @fixture -def retort() -> Retort: +async def app(db_url) -> Litestar: + return init_app(db_url) + + +@fixture +async def retort() -> Retort: return Retort() diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_authenticate.py b/tests/integration/test_authenticate.py new file mode 100644 index 0000000..ef39199 --- /dev/null +++ b/tests/integration/test_authenticate.py @@ -0,0 +1,25 @@ +import os + +import pytest +from litestar.testing import AsyncTestClient +from pytest_asyncio import fixture + + +@fixture +async def credentials() -> dict[str, str]: # type: ignore + try: + return { + "email": os.environ["TEST_AUTH_USER"], + "password": os.environ["TEST_AUTH_PASSWORD"] + } + except KeyError: + pytest.fail("No test user credentials.") + + +@pytest.mark.asyncio +async def test_authenticate(app, credentials): + async with AsyncTestClient(app=app) as client: + response = await client.post("/auth", json=credentials) + + assert response.status_code == 200 + assert response.json().get("token") diff --git a/tests/integration/test_category.py b/tests/integration/test_category.py new file mode 100644 index 0000000..9db0d0a --- /dev/null +++ b/tests/integration/test_category.py @@ -0,0 +1,68 @@ +import pytest +from adaptix import P, loader, name_mapping +from litestar.testing import AsyncTestClient +from sqlalchemy import insert + +from costy.domain.models.category import Category, CategoryType + + +@pytest.mark.asyncio +async def test_create_category(app, user_token, create_sub_user, clean_up_db): + async with AsyncTestClient(app) as client: + headers = {"Authorization": f"Bearer {user_token}"} + data = { + "name": "test" + } + result = await client.post("/categories", json=data, headers=headers) + + assert result.status_code == 201 + assert isinstance(result.json(), int) + + +@pytest.mark.asyncio +async def test_get_list_categoris( + app, + user_token, + create_sub_user, + db_session, + db_tables, + retort, + clean_up_db +): + loader_retort = retort.extend( + recipe=[ + loader(P[Category].id, lambda _: None) + ] + ) + retort = retort.extend( + recipe=[ + name_mapping( + Category, + skip=['id'], + ), + ] + ) + categories = [ + Category( + id=None, + name=f"test_category {i}", + kind=CategoryType.PERSONAL.value, + user_id=create_sub_user + ) for i in range(5) + ] + [ + Category( + id=None, + name=f"test_category {i}", + kind=CategoryType.GENERAL.value + ) for i in range(5) + ] + stmt = insert(db_tables["categories"]).values(retort.dump(categories, list[Category])) + await db_session.execute(stmt) + await db_session.commit() + + async with AsyncTestClient(app) as client: + headers = {"Authorization": f"Bearer {user_token}"} + + result = await client.get("/categories", headers=headers) + + assert loader_retort.load(result.json(), list[Category]) == categories diff --git a/tests/integration/test_operation.py b/tests/integration/test_operation.py new file mode 100644 index 0000000..b76dcec --- /dev/null +++ b/tests/integration/test_operation.py @@ -0,0 +1,67 @@ +import pytest +from adaptix import P, loader, name_mapping +from litestar.testing import AsyncTestClient +from sqlalchemy import insert + +from costy.domain.models.operation import Operation + + +@pytest.mark.asyncio +async def test_create_operation(app, user_token, create_sub_user, db_category_id, clean_up_db): + async with AsyncTestClient(app) as client: + headers = {"Authorization": f"Bearer {user_token}"} + data = { + "amount": 100, + "category_id": db_category_id + } + result = await client.post("/operations", json=data, headers=headers) + + assert result.status_code == 201 + assert isinstance(result.json(), int) + + +@pytest.mark.asyncio +async def test_get_list_operations( + app, + user_token, + create_sub_user, + db_session, + db_tables, + db_category_id, + retort, + clean_up_db +): + loader_retort = retort.extend( + recipe=[ + loader(P[Operation].id, lambda _: None) + ] + ) + retort = retort.extend( + recipe=[ + name_mapping( + Operation, + skip=['id'], + ), + ] + ) + operations = [ + Operation( + id=None, + amount=100, + description="test", + category_id=db_category_id, + time=1111, + user_id=create_sub_user + ) + for _ in range(10) + ] + stmt = insert(db_tables["operations"]).values(retort.dump(operations, list[Operation])) + await db_session.execute(stmt) + await db_session.commit() + + async with AsyncTestClient(app) as client: + headers = {"Authorization": f"Bearer {user_token}"} + + result = await client.get("/operations", headers=headers) + + assert loader_retort.load(result.json(), list[Operation]) == operations