From 0a8cdd1134b6a910006e456e01fd1f120fc91a91 Mon Sep 17 00:00:00 2001 From: ibogretsov Date: Tue, 23 Jan 2024 14:09:53 +0300 Subject: [PATCH] M2-4180: Add tests --- conftest.py | 19 +++-- src/apps/authentication/api/auth.py | 2 +- src/apps/authentication/tests/test_auth.py | 78 ++++++++++++++----- src/apps/logs/crud/user_activity_log.py | 1 - src/apps/logs/db/schemas.py | 2 +- .../user_activity_log.py => services.py} | 1 + src/apps/logs/tests/conftest.py | 17 ++++ src/apps/logs/tests/test_services.py | 25 ++++++ .../logs/{tests.py => tests/tests_api.py} | 0 src/apps/shared/test/base.py | 5 +- src/apps/users/api/users.py | 1 + src/apps/users/tests/fixtures/__init__.py | 0 src/apps/users/tests/fixtures/users.py | 42 ++++++++++ src/infrastructure/database/base.py | 1 - src/infrastructure/http/deps.py | 5 +- 15 files changed, 165 insertions(+), 34 deletions(-) rename src/apps/logs/{services/user_activity_log.py => services.py} (94%) create mode 100644 src/apps/logs/tests/conftest.py create mode 100644 src/apps/logs/tests/test_services.py rename src/apps/logs/{tests.py => tests/tests_api.py} (100%) create mode 100644 src/apps/users/tests/fixtures/__init__.py create mode 100644 src/apps/users/tests/fixtures/users.py diff --git a/conftest.py b/conftest.py index 70645d5fc7d..c1892198d08 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,5 @@ import os -from typing import Any, AsyncGenerator, cast +from typing import Any, AsyncGenerator, Generator, cast import pytest import taskiq_fastapi @@ -8,6 +8,7 @@ from fastapi import FastAPI from pytest import Parser from pytest_asyncio import is_async_test +from pytest_mock import MockerFixture from sqlalchemy import event from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, AsyncSession from sqlalchemy.orm import Session, SessionTransaction @@ -26,6 +27,7 @@ "apps.activities.tests.fixtures.items", "apps.activities.tests.fixtures.conditional_logic", "apps.activities.tests.fixtures.scores_reports", + "apps.users.tests.fixtures.users", ] @@ -169,14 +171,13 @@ def remote_image() -> str: @pytest.fixture -async def mock_kiq_report(mocker): +def mock_kiq_report(mocker: MockerFixture): mock = mocker.patch("apps.answers.service.create_report.kiq") yield mock - mock.stop_all() @pytest.fixture -async def mock_report_server_response(mocker): +def mock_report_server_response(mocker: MockerFixture) -> Generator: def json_(): return dict( pdf="cGRmIGJvZHk=", @@ -192,11 +193,15 @@ def json_(): mock.return_value.__aenter__.return_value.status = 200 mock.return_value.__aenter__.return_value.json.side_effect = json_ yield mock - mock.stop_all() @pytest.fixture -def mock_reencrypt_kiq(mocker): +def mock_reencrypt_kiq(mocker: MockerFixture) -> Generator: mock = mocker.patch("apps.users.api.password.reencrypt_answers.kiq") yield mock - mock.stop_all() + + +@pytest.fixture +def mock_activity_log(mocker: MockerFixture) -> Generator: + mock = mocker.patch("apps.logs.services.UserActivityLogService.create_log") + yield mock diff --git a/src/apps/authentication/api/auth.py b/src/apps/authentication/api/auth.py index b74a6ea6bec..1293b7fdadc 100644 --- a/src/apps/authentication/api/auth.py +++ b/src/apps/authentication/api/auth.py @@ -23,7 +23,7 @@ ) from apps.authentication.services.security import AuthenticationService from apps.logs.domain.constants import UserActivityEvent, UserActivityEventType -from apps.logs.services.user_activity_log import UserActivityLogService +from apps.logs.services import UserActivityLogService from apps.shared.domain.response import Response from apps.shared.response import EmptyResponse from apps.users import UsersCRUD diff --git a/src/apps/authentication/tests/test_auth.py b/src/apps/authentication/tests/test_auth.py index 0e1c2ad5623..e68aa7e9bbb 100644 --- a/src/apps/authentication/tests/test_auth.py +++ b/src/apps/authentication/tests/test_auth.py @@ -1,4 +1,8 @@ +import http import uuid +from unittest.mock import ANY, AsyncMock + +import pytest from apps.authentication.domain.login import UserLoginRequest from apps.authentication.domain.token import RefreshAccessTokenRequest @@ -9,6 +13,12 @@ from apps.users import UsersCRUD from apps.users.domain import UserCreateRequest from apps.users.router import router as user_router +from infrastructure.http.domain import MindloggerContentSource + + +@pytest.fixture +def device_id() -> uuid.UUID: + return uuid.uuid4() class TestAuthentication(BaseTest): @@ -44,7 +54,7 @@ async def test_get_token(self, session, client): email=self.create_request_user.dict()["email"] ) - assert response.status_code == 200 + assert response.status_code == http.HTTPStatus.OK data = response.json()["result"] assert set(data.keys()) == {"user", "token"} assert data["user"]["id"] == str(user.id) @@ -68,7 +78,7 @@ async def test_delete_access_token(self, client): url=self.delete_token_url, ) - assert response.status_code == 200 + assert response.status_code == http.HTTPStatus.OK async def test_refresh_access_token(self, client): # Creating new user @@ -91,24 +101,24 @@ async def test_refresh_access_token(self, client): data=refresh_access_token_request.dict(), ) - assert response.status_code == 200 + assert response.status_code == http.HTTPStatus.OK - async def test_login_and_logout_device(self, client): + async def test_login_and_logout_device(self, client, device_id): await client.post( self.user_create_url, data=self.create_request_user.dict() ) - device_id = str(uuid.uuid4()) login_request_user: UserLoginRequest = UserLoginRequest( - **self.create_request_user.dict(), device_id=device_id + **self.create_request_user.dict(), device_id=str(device_id) ) response = await client.post( url=self.login_url, data=login_request_user.dict(), ) - assert response.status_code == 200 + assert response.status_code == http.HTTPStatus.OK await client.login( + self.login_url, self.create_request_user.email, self.create_request_user.password, ) @@ -118,25 +128,57 @@ async def test_login_and_logout_device(self, client): data=dict(device_id=device_id), ) - assert response.status_code == 200 + assert response.status_code == http.HTTPStatus.OK - async def test_login_event_log_is_crated_after_login(self, client): + async def test_login_event_log_is_created_after_login( + self, mock_activity_log: AsyncMock, client + ): await client.post( self.user_create_url, data=self.create_request_user.dict() ) - device_id = str(uuid.uuid4()) - login_request_user: UserLoginRequest = UserLoginRequest( - **self.create_request_user.dict(), device_id=device_id + **self.create_request_user.dict() ) response = await client.post( url=self.login_url, data=login_request_user.dict(), ) - assert response.status_code == 200 - - await client.login( - self.login_url, - self.create_request_user.email, - self.create_request_user.password, + assert response.status_code == http.HTTPStatus.OK + mock_activity_log.assert_awaited_once() + + @pytest.mark.parametrize( + "header_value,dest_value", + ( + (MindloggerContentSource.admin, MindloggerContentSource.admin), + (MindloggerContentSource.mobile, MindloggerContentSource.mobile), + (MindloggerContentSource.web, MindloggerContentSource.web), + ( + MindloggerContentSource.undefined, + MindloggerContentSource.undefined, + ), + ("test", MindloggerContentSource.undefined), + ), + ) + async def test_login_if_default_mindollger_content_source_header_is_undefined( # noqa: E501 + self, + mock_activity_log: AsyncMock, + client, + header_value: str, + dest_value: str, + ): + await client.post( + self.user_create_url, data=self.create_request_user.dict() + ) + login_request_user: UserLoginRequest = UserLoginRequest( + **self.create_request_user.dict() + ) + response = await client.post( + url=self.login_url, + data=login_request_user.dict(), + headers={"Mindlogger-Content-Source": header_value}, + ) + assert response.status_code == http.HTTPStatus.OK + user_id = uuid.UUID(response.json()["result"]["user"]["id"]) + mock_activity_log.assert_awaited_once_with( + user_id, None, ANY, ANY, ANY, dest_value ) diff --git a/src/apps/logs/crud/user_activity_log.py b/src/apps/logs/crud/user_activity_log.py index fd68b97cd66..270fc93d820 100644 --- a/src/apps/logs/crud/user_activity_log.py +++ b/src/apps/logs/crud/user_activity_log.py @@ -8,5 +8,4 @@ class UserActivityLogCRUD(BaseCRUD[UserActivityLogSchema]): async def save( self, schema: UserActivityLogSchema ) -> UserActivityLogSchema: - """Return UserActivityLogSchema instance.""" return await self._create(schema) diff --git a/src/apps/logs/db/schemas.py b/src/apps/logs/db/schemas.py index ebdbb601794..8f89327f0f9 100644 --- a/src/apps/logs/db/schemas.py +++ b/src/apps/logs/db/schemas.py @@ -32,7 +32,7 @@ class UserActivityLogSchema(Base): user_agent = Column(String(), nullable=True) mindlogger_content = Column(String(), nullable=False) - def __repr__(self): + def __repr__(self) -> str: return ( f"UserActivityLogSchema(id='{self.id}', user_id='{self.user_id}'," f" event_type='{self.event_type}', event='{self.event}')" diff --git a/src/apps/logs/services/user_activity_log.py b/src/apps/logs/services.py similarity index 94% rename from src/apps/logs/services/user_activity_log.py rename to src/apps/logs/services.py index 3e8b794fa1b..7a76608bf72 100644 --- a/src/apps/logs/services/user_activity_log.py +++ b/src/apps/logs/services.py @@ -18,6 +18,7 @@ async def create_log( user_agent: str | None, mindlogger_content: str, ) -> UserActivityLogSchema: + # TODO: remove this remporary solution when mobile is ready if ( mindlogger_content == MindloggerContentSource.undefined.name and firebase_token_id diff --git a/src/apps/logs/tests/conftest.py b/src/apps/logs/tests/conftest.py new file mode 100644 index 00000000000..b65e9c9685c --- /dev/null +++ b/src/apps/logs/tests/conftest.py @@ -0,0 +1,17 @@ +from typing import Any + +import pytest + +from apps.logs.domain.constants import UserActivityEvent, UserActivityEventType +from infrastructure.http.domain import MindloggerContentSource + + +@pytest.fixture +def base_log_data() -> dict[str, Any]: + return { + "firebase_token_id": None, + "event": UserActivityEvent.LOGIN, + "event_type": UserActivityEventType.LOGIN, + "user_agent": "test", + "mindlogger_content": MindloggerContentSource.undefined, + } diff --git a/src/apps/logs/tests/test_services.py b/src/apps/logs/tests/test_services.py new file mode 100644 index 00000000000..c82dd79c767 --- /dev/null +++ b/src/apps/logs/tests/test_services.py @@ -0,0 +1,25 @@ +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.logs.services import UserActivityLogService +from apps.users.db.schemas import UserSchema +from infrastructure.http.domain import MindloggerContentSource + + +async def test_create_log_success( + session: AsyncSession, user_tom: UserSchema, base_log_data: dict[str, Any] +) -> None: + base_log_data["user_id"] = user_tom.id + log = await UserActivityLogService(session).create_log(**base_log_data) + assert log.user_id == user_tom.id + + +async def test_create_log_undefined_with_device_is_mobile_content( + session: AsyncSession, user_tom: UserSchema, base_log_data: dict[str, Any] +) -> None: + base_log_data["user_id"] = user_tom.id + base_log_data["firebase_token_id"] = "token" + base_log_data["mindlogger_content"] = MindloggerContentSource.undefined + log = await UserActivityLogService(session).create_log(**base_log_data) + assert log.mindlogger_content == MindloggerContentSource.mobile diff --git a/src/apps/logs/tests.py b/src/apps/logs/tests/tests_api.py similarity index 100% rename from src/apps/logs/tests.py rename to src/apps/logs/tests/tests_api.py diff --git a/src/apps/shared/test/base.py b/src/apps/shared/test/base.py index c7611b16bca..3c357646e68 100644 --- a/src/apps/shared/test/base.py +++ b/src/apps/shared/test/base.py @@ -14,9 +14,10 @@ class BaseTest: fixtures: list[str] = [] @pytest.fixture(scope="class", autouse=True) - async def initialize(self): - await truncate_tables() + async def initialize(self, request): await self.populate_db() + yield + await truncate_tables() @pytest.fixture(autouse=True) async def clear_mails(self): diff --git a/src/apps/users/api/users.py b/src/apps/users/api/users.py index 9c4cfb848b9..ee76e061aa7 100644 --- a/src/apps/users/api/users.py +++ b/src/apps/users/api/users.py @@ -28,6 +28,7 @@ async def user_create( raise PasswordHasSpacesError() async with atomic(session): email_hash = hash_sha224(user_create_schema.email) + # TODO: Move Logic to the service user_schema = await UsersCRUD(session).save( UserSchema( email=email_hash, diff --git a/src/apps/users/tests/fixtures/__init__.py b/src/apps/users/tests/fixtures/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/apps/users/tests/fixtures/users.py b/src/apps/users/tests/fixtures/users.py new file mode 100644 index 00000000000..8a2bf1787a6 --- /dev/null +++ b/src/apps/users/tests/fixtures/users.py @@ -0,0 +1,42 @@ +from typing import AsyncGenerator + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.authentication.services import AuthenticationService +from apps.shared.hashing import hash_sha224 +from apps.users.cruds.user import UsersCRUD +from apps.users.db.schemas import UserSchema +from apps.users.domain import UserCreateRequest + + +@pytest.fixture +def user_tom_create() -> UserCreateRequest: + # Use tom data for replacing json fixtures with pytest fixtures + # without failing tests + return UserCreateRequest( + email="tom@mindlogger.com", + password="Test1234!", + first_name="Tom", + last_name="Isaak", + ) + + +@pytest.fixture +async def user_tom( + user_tom_create: UserCreateRequest, session: AsyncSession +) -> AsyncGenerator: + email_hash = hash_sha224(user_tom_create.email) + hashed_password = AuthenticationService.get_password_hash( + user_tom_create.password + ) + user = await UsersCRUD(session).save( + UserSchema( + email=email_hash, + email_encrypted=user_tom_create.email, + first_name=user_tom_create.first_name, + last_name=user_tom_create.last_name, + hashed_password=hashed_password, + ) + ) + yield user diff --git a/src/infrastructure/database/base.py b/src/infrastructure/database/base.py index 72a12ab97f2..ffd677b8267 100644 --- a/src/infrastructure/database/base.py +++ b/src/infrastructure/database/base.py @@ -6,7 +6,6 @@ from sqlalchemy.ext.hybrid import hybrid_method from sqlalchemy.orm import declarative_base, declarative_mixin - __all__ = ["Base", "MigratedMixin"] meta = MetaData( diff --git a/src/infrastructure/http/deps.py b/src/infrastructure/http/deps.py index 47b589163bb..265e3739b9b 100644 --- a/src/infrastructure/http/deps.py +++ b/src/infrastructure/http/deps.py @@ -10,9 +10,8 @@ async def get_mindlogger_content_source( return getattr( MindloggerContentSource, - request.headers.get( - "mindlogger-content-source", MindloggerContentSource.undefined.name - ), + request.headers.get("mindlogger-content-source", ""), + MindloggerContentSource.undefined, )