Skip to content
This repository was archived by the owner on Mar 19, 2024. It is now read-only.
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
4 changes: 4 additions & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
version: "3.8"
services:

db:
ports:
- "5432:5432"

web:
ports:
# We only bind ports directly in development:
Expand Down
266 changes: 178 additions & 88 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ pytest-randomly = "^3.15"
pytest-timeout = "^2.1"
hypothesis = "^6.84"
django-test-migrations = "^1.3"
httpretty = "^1.1.4"
mimesis = "^11.1"
factory-boy = "^3.3"

django-stubs = { version = "^4.2", extras = ["compatible-mypy"] }
types-requests = "^2.31"
Expand Down
2 changes: 1 addition & 1 deletion server/apps/pictures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ class FavouritePicture(TimedMixin, models.Model):
url = models.URLField()

def __str__(self) -> str:
"""Beatuful representation."""
"""Beautiful representation."""

Choose a reason for hiding this comment

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

note: If you're using VSCode, there's a plugin called 'Code Spell Checker'.

It's useful for catching typos, though it can produce false errors with variable names.

return '<Picture {0} by {1}>'.format(self.foreign_id, self.user_id)
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
'plugins.django_settings',

# TODO: add your own plugins here!
'plugins.identity.user',
]
Empty file removed tests/plugins/identity/.gitkeep
Empty file.
71 changes: 71 additions & 0 deletions tests/plugins/identity/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from datetime import datetime
from typing import Callable, Protocol, TypeAlias, TypedDict, Unpack, final

import pytest
from mimesis.locales import Locale
from mimesis.schema import Field, Schema

from server.apps.identity.models import User


class UserData(TypedDict, total=False):
email: str
first_name: str
last_name: str
date_of_birth: datetime
address: str
job_title: str
phone: str
phone_type: int


UserAssertion: TypeAlias = Callable[[str, UserData], None]


@final
class RegistrationData(UserData, total=False):

Choose a reason for hiding this comment

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

suggestion: Remove total=false

By default, all attributes are required. Isn't this the case with password1 and password2?

Suggested change
class RegistrationData(UserData, total=False):
class RegistrationData(UserData):

password1: str
password2: str


@final
class RegistrationDataFactory(Protocol): # type: ignore[misc]
def __call__(self, **fields: Unpack[RegistrationData]) -> RegistrationData:
"""User registration data factory."""


@pytest.fixture(scope="session")
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.

polish: Please use either single or double quotes consistently.

Linters typically flag such inconsistencies. I noticed you used single quotes below. Plugins can assist with this, and you can even set up auto-formatting on save to automate this.

There are many minor improvements in the code quality that can be made automatically using linters at no extra cost.

Suggested change
@pytest.fixture(scope="session")
@pytest.fixture(scope='session')

def assert_correct_user() -> UserAssertion:
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_name, data_value in expected.items():
assert getattr(user, field_name) == data_value

return factory


@pytest.fixture(scope='function')
def registration_data_factory(faker_seed: int) -> RegistrationDataFactory:
def factory(**fields: Unpack[RegistrationData]) -> RegistrationData:
mf = Field(locale=Locale.RU, seed=faker_seed)
password = mf('password')
schema = Schema(schema=lambda: {
'email': mf('person.email'),
'first_name': mf('person.first_name'),
'last_name': mf('person.last_name'),
'date_of_birth': mf('datetime.date'),
'job_title': mf('person.occupation'),
'phone': mf('person.telephone'),
'phone_type': mf('choice', items=[1, 2, 3]),

Choose a reason for hiding this comment

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

А это поле было в лекции, но в проекте такого поля у User'а нет. Вроде в assert_correct_user должны споткнуться об это, но если тесты проходят - стоит обратить внимание на это

}, iterations=1)
return {
**next(schema), # type: ignore[typeddict-item]
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.

praise: Using next(schema) is much cleaner than mine schema.create()[0]

Does it work fine? =)

**{'password1': password, 'password2': password},
**fields
}

return factory
Empty file.
26 changes: 26 additions & 0 deletions tests/test_apps/test_identity/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
from tests.plugins.identity.user import (
RegistrationData,
RegistrationDataFactory,
UserData,
)


@pytest.fixture
def valid_registration_data(
registration_data_factory: 'RegistrationDataFactory'
) -> 'RegistrationData':
return registration_data_factory()


@pytest.fixture()
def valid_user_data(valid_registration_data: 'RegistrationData') -> 'UserData':
return { # type: ignore[return-value]
key_name: value_part
for key_name, value_part in valid_registration_data.items()
if not key_name.startswith('password')
}
57 changes: 57 additions & 0 deletions tests/test_apps/test_identity/test_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from http import HTTPStatus
from typing import TYPE_CHECKING

import pytest
from django.test import Client
from django.urls import reverse

from server.apps.identity.models import User

if TYPE_CHECKING:
from tests.plugins.identity.user import (
RegistrationData,
RegistrationDataFactory,
UserAssertion,
UserData,
)


def test_registration_page_exists(client: Client) -> None:
response = client.get(reverse('identity:registration'))
assert response.status_code == HTTPStatus.OK


@pytest.mark.django_db()
def test_registration_success(
client: Client,
valid_registration_data: 'RegistrationData',
valid_user_data: 'UserData',
assert_correct_user: 'UserAssertion',
) -> None:
"""Test that registration is successfully with valid registration data."""
response = client.post(
reverse('identity:registration'),
data=valid_registration_data
)

assert response.status_code == HTTPStatus.OK
assert_correct_user(valid_registration_data['email'], valid_user_data)


@pytest.mark.django_db()
@pytest.mark.parametrize('field', User.REQUIRED_FIELDS + [User.USERNAME_FIELD])
def test_registration_missing_required_field(
client: Client,
registration_data_factory: 'RegistrationDataFactory',
field: str,
) -> None:
post_data = registration_data_factory(
**{field: ''}, # type: ignore[arg-type]
)

response = client.post(
reverse('identity:registration'),
data=post_data,
)
assert response.status_code == HTTPStatus.OK
assert not User.objects.filter(email=post_data['email'])
2 changes: 1 addition & 1 deletion tests/test_server/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.test import Client


@pytest.mark.django_db()()
@pytest.mark.django_db()
def test_health_check(client: Client) -> None:
"""This test ensures that health check is accessible."""
response = client.get('/health/')
Expand Down