diff --git a/poetry.lock b/poetry.lock index ebbbafa3..d427e3f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1450,6 +1450,16 @@ files = [ {file = "html_void_elements-0.1.0-py3-none-any.whl", hash = "sha256:784cf39db03cdeb017320d9301009f8f3480f9d7b254d0974272e80e0cb5e0d2"}, ] +[[package]] +name = "httpretty" +version = "1.1.4" +description = "HTTP client mock for Python" +optional = false +python-versions = ">=3" +files = [ + {file = "httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68"}, +] + [[package]] name = "hypothesis" version = "6.84.2" @@ -1743,6 +1753,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1844,6 +1864,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mimesis" +version = "11.1.0" +description = "Mimesis: Fake Data Generator." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "mimesis-11.1.0-py3-none-any.whl", hash = "sha256:574715564c937cd40eba23a0d184febbe04e538d5d120bfa5b951775200f3084"}, + {file = "mimesis-11.1.0.tar.gz", hash = "sha256:5f3839751190f6eef7f453dfafb8f2f38dbdcda11bb3ad742589c216c24985f1"}, +] + [[package]] name = "more-itertools" version = "10.1.0" @@ -2583,6 +2614,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2590,8 +2622,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2608,6 +2647,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2615,6 +2655,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2831,7 +2872,8 @@ files = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, @@ -3442,4 +3484,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [metadata] lock-version = "2.0" python-versions = "3.11.5" -content-hash = "b6f24d93edb443bee450e4b8200340134cc77eb2441c980e0ea264df4b23da94" +content-hash = "c697926e50349215cf187de2bd2e76a077ce76f5e852574fb764964e3af3d1c8" diff --git a/pyproject.toml b/pyproject.toml index 19bbf6e4..b8b40624 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ requests = "^2.28" attrs = "^23.1" pydantic = "^2.3" punq = "^0.6" +mimesis = "^11.1.0" +isort = "^5.12.0" +httpretty = "^1.1.4" [tool.poetry.group.dev.dependencies] django-debug-toolbar = "^4.2" @@ -93,3 +96,7 @@ format_attribute_template_tags = true [tool.nitpick] style = "https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/0.18.0/styles/nitpick-style-wemake.toml" + + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "server.settings" diff --git a/server/apps/identity/intrastructure/services/placeholder.py b/server/apps/identity/intrastructure/services/placeholder.py index 4944ef4e..bb4dfca3 100644 --- a/server/apps/identity/intrastructure/services/placeholder.py +++ b/server/apps/identity/intrastructure/services/placeholder.py @@ -33,7 +33,7 @@ def __call__( timeout=self._api_timeout, ) response.raise_for_status() - return UserResponse.parse_raw(response.text) + return UserResponse.model_validate_json(response.text) @final diff --git a/server/apps/identity/logic/usecases/user_create_new.py b/server/apps/identity/logic/usecases/user_create_new.py index 9aac50e8..fd0e94d4 100644 --- a/server/apps/identity/logic/usecases/user_create_new.py +++ b/server/apps/identity/logic/usecases/user_create_new.py @@ -30,7 +30,7 @@ def __call__(self, user: User) -> None: https://sobolevn.me/2019/02/engineering-guide-to-user-stories """ new_ids = self._create_lead(user) - return self._update_user_ids(user, new_ids) + self._update_user_ids(user, new_ids) def _create_lead(self, user: User) -> placeholder.UserResponse: return placeholder.LeadCreate( @@ -41,8 +41,8 @@ def _create_lead(self, user: User) -> placeholder.UserResponse: def _update_user_ids( self, user: User, - new_ids: placeholder.UserResponse, + response: placeholder.UserResponse, ) -> None: # This can be moved to some other place once this becomes too complex: - user.lead_id = new_ids.id + user.lead_id = response.id user.save(update_fields=['lead_id']) diff --git a/tests/plugins/identity/.gitkeep b/tests/__init__.py similarity index 100% rename from tests/plugins/identity/.gitkeep rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py index 96baa556..d9df8fd8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,8 +7,8 @@ """ pytest_plugins = [ - # Should be the first custom one: - 'plugins.django_settings', - - # TODO: add your own plugins here! + 'tests.plugins.django_settings', + 'tests.plugins.identity.user', + 'tests.plugins.pictures.pictures', + 'tests.plugins.slow', ] diff --git a/tests/plugins/identity/user.py b/tests/plugins/identity/user.py new file mode 100644 index 00000000..29a04164 --- /dev/null +++ b/tests/plugins/identity/user.py @@ -0,0 +1,157 @@ +import datetime as dt +from typing import Any, Unpack, TYPE_CHECKING +from typing import Callable, Protocol, TypeAlias, TypedDict, final + +import pytest +from mimesis import Field, Schema +from mimesis.locales import Locale + +from server.apps.identity.intrastructure.services.placeholder import UserResponse +from server.apps.identity.models import User + +if TYPE_CHECKING: + from django.test import Client + + +@final +class UserData(TypedDict, total=False): + """ + Represent the user data that is required to create a new user. + + Does not include `password`, because it's special field in Django. + """ + + email: str + first_name: str + last_name: str + date_of_birth: dt.datetime + address: str + job_title: str + phone: str + lead_id: int + is_staff: bool + is_active: bool + + +UserAssertion: TypeAlias = Callable[[str, UserData], None] +NotLeadUserAssertion: TypeAlias = Callable[[str], None] + + +@final +class RegistrationData(UserData, total=False): + """Represent the user data that is required to create a new user.""" + + password_first: str + password_second: str + + +@final +class RegistrationDataFactory(Protocol): + """User data factory protocol.""" + + def __call__(self, **fields: Unpack[RegistrationData]) -> RegistrationData: + """User data factory protocol.""" + + +@pytest.fixture() +def registration_data_factory() -> RegistrationDataFactory: + """Fxture that generate fake registration data.""" + + def factory(**fields: Unpack[RegistrationData]) -> RegistrationData: + field = Field(locale=Locale.RU, seed=fields.pop('seed')) + password = field('password') + schema = Schema(schema=lambda: { + 'email': field('person.email'), + 'first_name': field('person.first_name'), + 'last_name': field('person.last_name'), + 'date_of_birth': field('datetime.date'), + 'address': field('address.city'), + 'job_title': field('person.occupation'), + 'phone': field('person.telephone'), + }) + return { + **schema.create()[0], + **{'password_first': password, 'password_second': password}, + **fields, + } + + return factory + + +@pytest.fixture(scope='session') +def assert_correct_user() -> UserAssertion: + """Fixture that check created user attrs from database.""" + + def factory(email: str, expected: UserData) -> None: + user = User.objects.get(email=email) + assert user.id + assert user.is_active + assert not user.is_superuser + assert not user.is_staff + for field, value_name in expected.items(): + assert getattr(user, field) == value_name + + return factory + + +@pytest.fixture() +def reg_data(registration_data_factory) -> RegistrationData: + """Fixture that return user reg data.""" + return registration_data_factory(seed=1) + + +@pytest.fixture() +def expected_user_data(reg_data: RegistrationData) -> dict[str, Any]: + """Fixture that return exeected user data.""" + return { + key: value_name + for key, value_name in reg_data.items() + if not key.startswith('password') + } + + +@pytest.fixture() +def expected_serialized_user(reg_data: RegistrationData) -> dict[str, Any]: + """Serialized user's key-values that expected in test.""" + return { + 'name': reg_data['first_name'], + 'last_name': reg_data['last_name'], + 'birthday': reg_data['date_of_birth'].strftime('%d.%m.%Y'), + 'city_of_birth': reg_data['address'], + 'position': reg_data['job_title'], + 'email': reg_data['email'], + 'phone': reg_data['phone'], + } + + +@pytest.fixture() +def user( + expected_user_data: RegistrationData, +) -> 'User': + """Return created user in database.""" + return User.objects.create(**expected_user_data) + + +@pytest.fixture() +def expected_user_response( + expected_user_data: RegistrationData, +) -> 'UserResponse': + """Return user response obj.""" + return UserResponse(id=1) + + +@pytest.fixture(scope='session') +def assert_not_lead_user() -> NotLeadUserAssertion: + """Check that user is not lead.""" + def factory(email: str) -> None: + user = User.objects.get(email=email) + assert user.lead_id is None + + return factory + + +@pytest.fixture() +def authorized_client(client: 'Client', user: 'User') -> 'Client': + """Authorized User object.""" + client.force_login(user) + return client diff --git a/tests/plugins/pictures/pictures.py b/tests/plugins/pictures/pictures.py new file mode 100644 index 00000000..7c8368c7 --- /dev/null +++ b/tests/plugins/pictures/pictures.py @@ -0,0 +1,90 @@ +import json +from typing import Generator, final, Protocol, TypedDict, Any +from urllib.parse import urljoin + +import httpretty +import pytest +from mimesis import Field, Locale, Schema + +from server.apps.pictures.intrastructure.services.placeholder import PictureResponse +from server.common.django.types import Settings + + +@final +class PicturesData(TypedDict, total=False): + """Represent the pictures data response.""" + + id: int + url: str + + +@final +class PicturesDataFactory(Protocol): + """Pictures data factory protocol.""" + + def __call__(self, iterations: int) -> list[PicturesData]: + """Pictures data factory protocol.""" + + +@pytest.fixture() +def pictures_data_factory(seed: int) -> PicturesDataFactory: + def factory(iterations: int) -> list[PicturesData]: + field = Field(locale=Locale.EN, seed=seed) + schema = Schema( + schema=lambda: + { + 'id': field('numeric.increment'), + 'url': str(field('url')), + }, + iterations=iterations, + ) + return schema.create() + + return factory + + +@pytest.fixture() +def seed() -> int: + """Seed for generation random data.""" + return Field()('integer_number') + + +@pytest.fixture() +def expected_pictures_response( + pictures_data_factory: PicturesDataFactory, +) -> list[PictureResponse]: + """Create fake external pictures api response, 10 elements.""" + pictures = pictures_data_factory(iterations=10) + return [PictureResponse(**pic) for pic in pictures] + + +@pytest.fixture() +def expected_picture_response( + pictures_data_factory: PicturesDataFactory, +) -> PictureResponse: + """Create fake external pictures api response, 1 element.""" + picture = pictures_data_factory(iterations=1) + return PictureResponse(**picture[0]) + + +@pytest.fixture() +def failed_pydantic_fields( +) -> dict[str, Any]: + """Return failed fields for pydantic validations.""" + return {'TEST': 1} + + +@pytest.fixture() +def mock_pictures_api( + settings: Settings, + expected_pictures_response: list[PictureResponse], +) -> Generator: + """Mock external `PLACEHOLDER_API_URL` calls.""" + with httpretty.httprettized(): + httpretty.register_uri( + method=httpretty.GET, + body=json.dumps([picture.model_dump() for picture in expected_pictures_response]), + uri=urljoin(settings.PLACEHOLDER_API_URL, "/photos"), + ) + yield expected_pictures_response + assert httpretty.has_request() diff --git a/tests/plugins/slow.py b/tests/plugins/slow.py new file mode 100644 index 00000000..7c389cd6 --- /dev/null +++ b/tests/plugins/slow.py @@ -0,0 +1,24 @@ +import pytest + +SLOW_TIMEOUT = 1 + + +def pytest_addoption(parser): + parser.addoption( + '--with-slow', action='store_true', default=False, + help='enable slow tests (only run in CI)', + ) + + +@pytest.hookimpl(optionalhook=True) +def pytest_collection_modifyitems( + session: pytest.Session, + config: pytest.Config, + items: list[pytest.Item], +): + for item in items: + slow_marker = item.get_closest_marker(name='slow') + if slow_marker: + item.add_marker(pytest.mark.timeout(SLOW_TIMEOUT)) + if not config.getoption('with_slow'): + item.add_marker(pytest.mark.skip('slow test (add --with-slow)')) diff --git a/tests/test_apps/test_pictures/.gitkeep b/tests/test_apps/test_pictures/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_server/test_apps/test_identity/test_models.py b/tests/test_server/test_apps/test_identity/test_models.py new file mode 100644 index 00000000..303c0dab --- /dev/null +++ b/tests/test_server/test_apps/test_identity/test_models.py @@ -0,0 +1,23 @@ +from typing import Any + +import pytest + +from server.apps.identity.models import _UserManager # noqa: WPS450 + + +class TestUserManager: # noqa: WPS306 + """Test class for testing _UserManager.""" + + @pytest.mark.django_db() + def test_create_user( + self, + reg_data, + expected_user_data: dict[str, Any], + assert_correct_user, + ) -> None: + """Testing create_user method in _UserManager.""" + _UserManager().create_user( + password=reg_data['password_first'], + **expected_user_data, + ) + assert_correct_user(reg_data['email'], expected_user_data) diff --git a/tests/test_server/test_apps/test_identity/test_placeholder.py b/tests/test_server/test_apps/test_identity/test_placeholder.py new file mode 100644 index 00000000..0912a2d2 --- /dev/null +++ b/tests/test_server/test_apps/test_identity/test_placeholder.py @@ -0,0 +1,84 @@ +# isort: skip_file +# flake8: noqa +from typing import Any + +import pytest +from pydantic import ValidationError + +from server.apps.identity.intrastructure.services.placeholder import ( + LeadCreate, + LeadUpdate, + UserResponse, + _serialize_user, +) +from server.apps.identity.models import User +from server.common.django.types import Settings +from tests.plugins.identity.user import RegistrationData, UserAssertion + +pytestmark = pytest.mark.django_db + + +def test_success_lead_create( + user: User, + reg_data: RegistrationData, + expected_user_data: dict[str, Any], + assert_correct_user: UserAssertion, + settings: Settings, +) -> None: + """Testing service that create lead user.""" + assert_correct_user(reg_data['email'], expected_user_data) + actual_id = 11 + actual_response = UserResponse(id=actual_id) + expected_id = LeadCreate( + api_timeout=settings.PLACEHOLDER_API_TIMEOUT, + api_url=settings.PLACEHOLDER_API_URL, + )(user=user) + assert actual_response == expected_id + + +def test_success_url_path(settings: Settings) -> None: + """Testing method that create URL path for the request.""" + expected_url_path = 'https://jsonplaceholder.typicode.com/users' + fetcher = LeadCreate( + api_timeout=settings.PLACEHOLDER_API_TIMEOUT, + api_url=settings.PLACEHOLDER_API_URL, + ) + actual_url_path = fetcher.url_path() + assert actual_url_path == expected_url_path + + +@pytest.mark.slow() +def test_success_lead_update( + user: User, + reg_data: RegistrationData, + expected_user_data: dict[str, Any], + assert_correct_user: UserAssertion, + settings: Settings, +) -> None: + """Testing service that update lead user.""" + assert_correct_user(reg_data['email'], expected_user_data) + LeadUpdate( + api_timeout=settings.PLACEHOLDER_API_TIMEOUT, + api_url=settings.PLACEHOLDER_API_URL, + )(user=user) + + +def test_success_validate_user_response(expected_user_response: UserResponse) -> None: + """Testing UserResponse model, success case.""" + actual_user_response = UserResponse.model_validate({'id': 1}) + assert actual_user_response == expected_user_response + + +def test_failed_validate_user_response(failed_pydantic_fields: dict[str, Any]) -> None: + """Testing UserResponse model, failed case.""" + with pytest.raises(ValidationError): + UserResponse.model_validate(failed_pydantic_fields) # noqa: WPS428 + + +def test_success_serialize_user( + user: User, + expected_serialized_user: dict[str, Any], +) -> None: + """Testing _serialize_user function.""" + actial_serialization = _serialize_user(user=user) + assert actial_serialization == expected_serialized_user diff --git a/tests/test_server/test_apps/test_identity/test_user_create_new.py b/tests/test_server/test_apps/test_identity/test_user_create_new.py new file mode 100644 index 00000000..ab581a32 --- /dev/null +++ b/tests/test_server/test_apps/test_identity/test_user_create_new.py @@ -0,0 +1,52 @@ +from typing import Any + +import pytest + +from server.apps.identity.intrastructure.services.placeholder import ( + UserResponse, +) +from server.apps.identity.logic.usecases.user_create_new import UserCreateNew +from server.apps.identity.models import User +from server.common.django.types import Settings +from tests.plugins.identity.user import ( + UserAssertion, + RegistrationData, + NotLeadUserAssertion, +) + +pytestmark = pytest.mark.django_db + + +@pytest.mark.slow() +def test_success_create_new_user( + user: User, + settings: Settings, + assert_correct_user: UserAssertion, + assert_not_lead_user: NotLeadUserAssertion, + reg_data: RegistrationData, + expected_user_data: dict[str, Any], +) -> None: + """Testing usecase that create new user.""" + assert_correct_user(reg_data['email'], expected_user_data) + assert_not_lead_user(reg_data['email']) + + UserCreateNew(settings=settings)(user=user) + user.refresh_from_db() + assert user.lead_id is not None + + +def test_success_update_user_ids( + user: User, + settings: Settings, + assert_correct_user: UserAssertion, + reg_data: RegistrationData, + expected_user_data: dict[str, Any], +) -> None: + """Testing usecase that update user id.""" + assert_correct_user(reg_data['email'], expected_user_data) + response = UserResponse(id=100) + UserCreateNew(settings=settings)._update_user_ids( # noqa: WPS437 + user=user, response=response, + ) + actual_user = User.objects.get(email=reg_data['email']) + assert actual_user.lead_id == response.id diff --git a/tests/test_server/test_apps/test_identity/test_views_user.py b/tests/test_server/test_apps/test_identity/test_views_user.py new file mode 100644 index 00000000..f5852eaa --- /dev/null +++ b/tests/test_server/test_apps/test_identity/test_views_user.py @@ -0,0 +1,37 @@ +from http import HTTPStatus +from typing import TYPE_CHECKING + +import pytest +from django.urls import reverse + +if TYPE_CHECKING: + from django.test import Client + +pytestmark = pytest.mark.django_db + + +def test_user_update_view( + authorized_client: 'Client', +) -> None: + """Test update auth user view.""" + response = authorized_client.get(reverse('identity:user_update')) + assert response.status_code == HTTPStatus.OK + assert response.template_name.__contains__('identity/pages/user_update.html') + + +def test_non_auth_user_update_view(client: 'Client'): + response = client.post(reverse('identity:user_update'), data={}) + assert response.status_code == HTTPStatus.FOUND + assert response.get('Location').startswith(reverse('identity:login')) + + +def test_non_auth_user_registration_view(client: 'Client') -> None: + response = client.get(reverse('identity:login')) + assert response.status_code == HTTPStatus.OK + assert response.get('Content-Type').startswith('text/html') + + +def test_auth_user_registration_view(authorized_client: 'Client') -> None: + response = authorized_client.get(reverse('identity:login')) + assert response.status_code == HTTPStatus.FOUND + assert response.get('Location') == reverse('pictures:dashboard') diff --git a/tests/test_apps/test_identity/.gitkeep b/tests/test_server/test_apps/test_pictures/.gitkeep similarity index 100% rename from tests/test_apps/test_identity/.gitkeep rename to tests/test_server/test_apps/test_pictures/.gitkeep diff --git a/tests/test_server/test_apps/test_pictures/test_pictures_placeholder.py b/tests/test_server/test_apps/test_pictures/test_pictures_placeholder.py new file mode 100644 index 00000000..ef0b168c --- /dev/null +++ b/tests/test_server/test_apps/test_pictures/test_pictures_placeholder.py @@ -0,0 +1,41 @@ +from typing import Any + +import pytest +from pydantic import ValidationError + +from server.apps.pictures.intrastructure.services.placeholder import PicturesFetch, PictureResponse +from server.common.django.types import Settings +from tests.plugins.pictures.pictures import PicturesDataFactory + + +def test_pictures_fetch_service( + settings: Settings, + seed: int, + expected_pictures_response: list[PictureResponse], + mock_pictures_api, +) -> None: + """Test Pictures Fetch service.""" + actual_picture_response = PicturesFetch( + api_url=settings.PLACEHOLDER_API_URL, + api_timeout=settings.PLACEHOLDER_API_TIMEOUT, + )(limit=seed) + assert actual_picture_response == expected_pictures_response + + +def test_success_validate_pictures_response( + expected_picture_response: PictureResponse, + pictures_data_factory: PicturesDataFactory, +) -> None: + """Testing PictureResponse model, success case.""" + actual_picture_response = PictureResponse.model_validate( + pictures_data_factory(iterations=1)[0] + ) + assert actual_picture_response == expected_picture_response + + +def test_failed_validate_pictures_response( + failed_pydantic_fields: dict[str, Any], +) -> None: + """Testing PictureResponse model, failed case.""" + with pytest.raises(ValidationError): + PictureResponse.model_validate(failed_pydantic_fields) # noqa: WPS428