Skip to content
This repository has been archived by the owner on Mar 19, 2024. It is now read-only.

Lesson1 updates #68

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ services:
image: "postgres:15-alpine"
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- webnet
- postgresnet
Expand Down
198 changes: 106 additions & 92 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dennis = "^1.1"
dump-env = "^1.3"
ipython = "^8.15"
import-linter = "^1.11"
mimesis = "^11.1.0"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Минорный момент: лучше не продуктовые зависимости добавлять в dev группу.
Например, это можно сделать командой poetry add --group dev xxx или вручную переставить в файле.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

оно вроде и есть в dev-зависимостях?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Точно, проглядел.
My bad :(


[tool.poetry.group.docs]
optional = true
Expand Down
4 changes: 1 addition & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
"""

pytest_plugins = [
# Should be the first custom one:
'plugins.django_settings',

# TODO: add your own plugins here!
'plugins.identity.user',
]
10 changes: 10 additions & 0 deletions tests/plugins/django_settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from random import SystemRandom

import pytest
from django.conf import LazySettings
from django.core.cache import BaseCache, caches

MAX_RANDOM_INT = 999999


@pytest.fixture(autouse=True)
def _media_root(
Expand Down Expand Up @@ -36,6 +40,12 @@ def _debug(settings: LazySettings) -> None:
template['OPTIONS']['debug'] = True


@pytest.fixture(scope='session', autouse=True)
def faker_seed() -> int:
"""Create a random seed for the Faker library."""
return SystemRandom().randint(0, MAX_RANDOM_INT)


@pytest.fixture(autouse=True)
def cache(settings: LazySettings) -> BaseCache:
"""Modifies how cache is used in Django tests."""
Expand Down
Empty file.
101 changes: 101 additions & 0 deletions tests/plugins/identity/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Identity data related plugins."""
from datetime import datetime
from typing import Callable, Protocol, TypeAlias, TypedDict, Unpack, final

import pytest
from django.contrib.auth import get_user_model
from django.test import Client
from django.contrib.auth.models import User
from mimesis.locales import Locale
from mimesis.schema import Field, Schema


@final
class ProfileData(TypedDict, total=False):
"""Represent the simplified profile data."""

first_name: str
last_name: str
date_of_birth: datetime
address: str
job_title: str
phone: str


ProfileAssertion: TypeAlias = Callable[[str, ProfileData], None]


class ProfileDataFactory(Protocol):
"""Factory for representation of the simplified profile data."""

def __call__(self, **fields: Unpack[ProfileData]) -> ProfileData:
"""User data factory protocol."""


@pytest.fixture()
def profile_data_factory(
faker_seed: int,
) -> ProfileDataFactory:
"""Returns factory for fake random profile data."""

def factory(**fields: Unpack[ProfileData]) -> ProfileData:
mf = Field(locale=Locale.EN, seed=faker_seed)
schema = Schema(
schema=lambda: {
'first_name': mf('person.first_name'),
'last_name': mf('person.last_name'),
'date_of_birth': mf('datetime.date'),
'address': mf('address.city'),
'job_title': mf('person.occupation'),
'phone': mf('person.telephone'),
},
iterations=1,
)
return {
**schema.create()[0], # type: ignore[typeddict-item]
**fields,
}

return factory


@pytest.fixture(scope='session')
def assert_correct_profile() -> ProfileAssertion:
"""All profile fields are equal to reference."""

def factory(email: str, expected: ProfileData) -> None:
user = get_user_model().objects.get(email=email)
assert user.id
assert user.is_active
for field_name, data_value in expected.items():
assert getattr(user, field_name) == data_value
return factory


@pytest.fixture(scope='session')
def assert_incorrect_profile() -> ProfileAssertion:
"""At least one field does not match."""

def factory(email: str, expected: ProfileData) -> None:
user = get_user_model().objects.get(email=email)
assert user.id
assert user.is_active
matches = []
for field_name, data_value in expected.items():
matches.append(getattr(user, field_name) == data_value)

assert not all(matches)

return factory


@pytest.fixture(scope='function')
def logged_user_client(client: Client, django_user_model: User):
"""Client for a logged in user."""
password, email = 'password', '[email protected]'
user = django_user_model.objects.create_user(
email,
password,
)
client.force_login(user)
return client
56 changes: 56 additions & 0 deletions tests/test_server/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import pytest
from django.test import Client
from plugins.identity.user import (
ProfileAssertion,
ProfileDataFactory,
Copy link

@alexbarev alexbarev Sep 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: If we treat plugins as standalone modules, will this affect test or CI duration?

As @sobolevn suggested, we could remove __init__.py from the plugin subdirectories. We should then import classes only for type checking under the TYPE_CHECKING condition.

If plugins become significantly large, will this change influence the startup time or CI duration?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, TYPE_CHECKING was used in the presented code examples for the case of mypy type check - if mypy is not used, switching them off may speed up a bit.

I'm not really sure about __init__.py and auto-importing, but afaik they are imported only once per execution, not sure that might save a lot of time.

logged_user_client
)


@pytest.mark.django_db()()
Expand Down Expand Up @@ -43,6 +48,57 @@ def test_admin_docs_authorized(admin_client: Client) -> None:
assert b'docutils' not in response.content


@pytest.mark.parametrize("url_found", [
'/pictures/dashboard',
'/pictures/favourites'
])
def test_picture_pages_unauthorized(client: Client, url_found: str) -> None:
"""This test ensures that picture management pages require auth."""
response = client.get(url_found)
assert response.status_code == HTTPStatus.FOUND


@pytest.mark.django_db()
@pytest.mark.parametrize("url_accessible", [
'/pictures/dashboard',
'/pictures/favourites'
])
def test_picture_pages_authorized(
logged_user_client: Client,
url_accessible: str
) -> None:
"""Ensures picture management pages are accessible for authorized user."""

response = logged_user_client.get(url_accessible)
assert response.status_code == HTTPStatus.OK


@pytest.mark.django_db()
def test_profile_update_authorized(
logged_user_client: Client,
profile_data_factory: 'ProfileDataFactory',
assert_correct_profile: 'ProfileAssertion',
assert_incorrect_profile: 'ProfileAssertion',
) -> None:
"""This test ensures profile updating for an authorized user."""
user_data = profile_data_factory()

# that is an email for `logged_user_client` fixture
# maybe add indirect parametrization?
email = '[email protected]'

# there might be a probability of accidental match, but disregard it for now
assert_incorrect_profile(email, user_data)

response = logged_user_client.post(
'/identity/update',
data=user_data,
)
assert response.status_code == HTTPStatus.FOUND
assert response.get('Location') == '/identity/update'
assert_correct_profile(email, user_data)


@pytest.mark.parametrize('page', [
'/robots.txt',
'/humans.txt',
Expand Down
Loading