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

Home work-1 a_petrushkin #65

Closed
wants to merge 20 commits into from
Closed
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
220 changes: 132 additions & 88 deletions poetry.lock

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ doc8 = "^1.0"

pytest = "^7.4"
pytest-django = "^4.5"
pytest-cov = "^4.0"
pytest-cov = "^4.1.0"
django-coverage-plugin = "^3.1"
covdefaults = "^2.3"
pytest-randomly = "^3.15"
Expand All @@ -66,6 +66,8 @@ dennis = "^1.1"
dump-env = "^1.3"
ipython = "^8.15"
import-linter = "^1.11"
httpretty = "^1.1.4"
mimesis = "^11.1.0"

[tool.poetry.group.docs]
optional = true
Expand Down Expand Up @@ -93,3 +95,14 @@ 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.ruff]
select = ["E", "F", "Q"]
line-length = 80
show-fixes = true


[tool.ruff.flake8-quotes]
inline-quotes = "single"
docstring-quotes = "double"
6 changes: 5 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ max-imports = 14
exclude = .git,__pycache__,.venv,.eggs,*.egg,frontend,landing

# Disable some pydocstyle checks:
ignore = D100, D104, D106, D401, X100, W504, RST303, RST304, DAR103, DAR203
ignore = D100, D104, D106, D401, X100, W504, RST303, RST304, DAR103, DAR203, DJ10, DJ11, DJ08, WPS305

# Docs: https://github.com/snoack/flake8-per-file-ignores
# You can completely or partially disable our custom checks,
Expand Down Expand Up @@ -99,6 +99,10 @@ filterwarnings =
# but, we want to list them here:
ignore::DeprecationWarning:password_reset.*:
ignore::DeprecationWarning:pytest_freezegun:
ignore::DeprecationWarning:pkg_resources*:
ignore:.*pkg_resources is deprecated as an API*:DeprecationWarning
# TODO disable until version 3 of pydantic is released.
ignore::DeprecationWarning:pydantic.*:


[coverage:run]
Expand Down
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',
]
161 changes: 161 additions & 0 deletions tests/plugins/identity/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import datetime as dt
from collections.abc import Callable
from random import SystemRandom
from typing import Protocol, TypeAlias, TypedDict, Unpack, cast, final

import pytest
from django.utils.crypto import RANDOM_STRING_CHARS, get_random_string
from mimesis import BaseProvider, Field, Locale
from mimesis.schema import Schema

from server.apps.identity.models import User

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

min_len, max_len = 10, 20
Copy link

Choose a reason for hiding this comment

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

Константы лучше в верхнем регистре сделать, и подобрать имя более конкретное, типа MIN_STRING_LENGTH



class FakeProvider(BaseProvider):
"""Represents a fake test data provider."""

class Meta: # noqa: WPS306
name = 'FakeProvider'

def random_seed(self) -> int:
"""Generate random seed."""
return self.random.randint(0, 100)

def user_factory(
self,
**fields: Unpack['RegistrationData'],
) -> 'RegistrationData':
"""Factory for generating user data used during registration."""
mf = Field(locale=Locale.RU, seed=self.random_seed())
password = mf('password') # by default passwords are equal
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'),
'address': mf('address.city'),
'job_title': mf('person.occupation'),
'phone': mf('person.telephone'),
},
)
return {
**schema.create()[0], # type: ignore[typeddict-item]
**{'password1': password, 'password2': password},
**fields,
}

def random_string(
self,
min_default_len: int = min_len,
max_default_len: int = max_len,
) -> str:
"""Create a random string."""
return get_random_string(
length=SystemRandom().randrange(
start=min_default_len,
stop=max_default_len,
),
allowed_chars=RANDOM_STRING_CHARS,
)


class UserData(TypedDict, total=False):
"""
Represent the simplified user data that is required to create a new user.

It does not include ``password``, because it is very special in django.
Importing this type is only allowed under ``if TYPE_CHECKING`` in tests.
"""

email: str
first_name: str
last_name: str
date_of_birth: dt.datetime
address: str
job_title: str
phone: str


@final
class RegistrationData(UserData, total=False):
"""
Represent the registration data that is required to create a new user.

Importing this type is only allowed under ``if TYPE_CHECKING`` in tests.
"""

password1: str
password2: str


class RegistrationDataFactory(Protocol):
"""User data factory protocol."""

def __call__(self, **fields: Unpack[RegistrationData]) -> RegistrationData:
"""Must implement in logic."""


@pytest.fixture()
def expected_user_data(registration_data: 'RegistrationData') -> 'UserData':
"""
We need to simplify registration data to drop passwords.

Basically, it is the same as ``registration_data``, but without passwords.
"""
return cast(
UserData,
{
key_name: value_part
for key_name, value_part in registration_data.items()
if not key_name.startswith('password')
},
)


@pytest.fixture()
def registration_data(
registration_data_factory: RegistrationDataFactory,
) -> RegistrationData:
"""Returns fake random data for registration."""
return registration_data_factory()


@pytest.fixture()
def registration_data_factory() -> RegistrationDataFactory:
"""Returns factory for fake random data for registration."""
return FakeProvider().user_factory


@pytest.fixture()
def create_user(expected_user_data: UserData) -> User:
"""Create a user."""
return User.objects.create(**expected_user_data)


@pytest.fixture(scope='session')
def assert_correct_user() -> UserAssertion:
"""Asserts that user with the given email exists and has expected data."""

def factory(email: str, expected: UserData) -> None:
user = User.objects.get(email=email)
# Special fields:
assert user.id
assert user.is_active
assert not user.is_superuser
assert not user.is_staff
# All other fields:
for field_name, data_value in expected.items():
assert getattr(user, field_name) == data_value

return factory


@pytest.fixture()
def random_string() -> Callable[..., str]:
"""Give a fixture that can generate a string of a given length."""
return FakeProvider().random_string
21 changes: 21 additions & 0 deletions tests/test_apps/test_identity/test_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from http import HTTPStatus

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

from server.apps.identity.models import User


@pytest.mark.django_db()
def test_valid_login(
client: Client,
create_user: User,
) -> None:
"""A valid user login must redirect to the home page."""
client.force_login(create_user)

response = client.get(reverse('index'))

assert response.status_code == HTTPStatus.OK
assert 'Личный кабинет'.encode('utf-8') in response.content
95 changes: 95 additions & 0 deletions tests/test_apps/test_identity/test_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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,
)

invalid_emails = [
'plain_address', # Простой адрес без @
'@missing_username.com', # Отсутствует имя пользователя перед @
'[email protected]', # Отсутствует доменное имя после @
'[email protected]', # Два точечных символа в домене
'[email protected]', # Домен заканчивается недопустимым символом
'[email protected]', # Домен начинается с точечного символа
'[email protected]', # Имя пользователя начинается с точечного символа
'username@domain,com', # Использует запятую вместо точки
'username@', # Отсутствует доменное имя
'username@[email protected]', # Содержит два символа @
'[email protected]', # Содержит два точечных символа
# FIXME: domain.-com - дает ложно отрицательный результат в тесте
# '[email protected]', # Домен начинается с недопустимого символа
]


@pytest.mark.django_db()
def test_valid_registration(
client: Client,
registration_data: 'RegistrationData',
expected_user_data: 'UserData',
assert_correct_user: 'UserAssertion',
) -> None:
"""Test that registration works with correct user data."""
response = client.post(
reverse('identity:registration'),
data=registration_data,
)
assert response.status_code == HTTPStatus.FOUND
assert response.get('Location') == reverse('identity:login')
assert_correct_user(registration_data['email'], expected_user_data)


@pytest.mark.django_db()
def test_registration_missing_required_field(
client: Client,
registration_data_factory: 'RegistrationDataFactory',
) -> None:
"""Test that missing required will fail the registration."""
post_data = registration_data_factory(email='')
response = client.post(
reverse('identity:registration'),
data=post_data,
)
assert response.status_code == HTTPStatus.OK
assert not User.objects.filter(email=post_data['email'])


@pytest.mark.parametrize('bad_email', invalid_emails)
@pytest.mark.django_db()
def test_bad_format_email_required_field(
bad_email: str,
client: Client,
registration_data_factory: 'RegistrationDataFactory',
) -> None:
"""Test to check with different non-valid email formats."""
user_with_invalid_email = registration_data_factory(email=bad_email)
response = client.post(
reverse('identity:registration'),
data=user_with_invalid_email,
)
assert response.status_code == HTTPStatus.OK
assert not User.objects.filter(email=user_with_invalid_email['email'])


@pytest.mark.django_db()
def test_user_manager_create_error(random_string):
"""If email is missing, error is called."""
random_user = {
'email': None,
'password': random_string(),
'first_name': random_string(),
'last_name': random_string(),
'phone': random_string(),
}
with pytest.raises(ValueError, match='Users must have an email address'):
User.objects.create_user(**random_user)
23 changes: 23 additions & 0 deletions tests/test_apps/test_identity/test_user_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from http import HTTPStatus
from typing import TYPE_CHECKING

import pytest
from django.urls import reverse

if TYPE_CHECKING:
from django.test import Client

from server.apps.identity.models import User


@pytest.mark.django_db()
def test_user_update_template(
client: 'Client',
create_user: 'User',
) -> None:
"""Test get template for update user."""
client.force_login(create_user)
response = client.get(reverse('identity:user_update'))

assert response.status_code == HTTPStatus.OK
assert 'Редактировать профиль'.encode('utf-8') in response.content
19 changes: 19 additions & 0 deletions tests/test_apps/test_pictures/test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

from server.apps.pictures.models import FavouritePicture

favourite_picture_params = [
(1234567890, 'some_test_foreign_id'),
(1234567890, None),
(None, 'some_test_foreign_id'),
(None, None),
]


@pytest.mark.parametrize(('foreign_id', 'user_id'), favourite_picture_params)
@pytest.mark.django_db()
def test_output_correct_str(foreign_id, user_id):
"""Test that string representation is correct."""
string_format = FavouritePicture(foreign_id=foreign_id, user_id=user_id)

assert str(string_format) == f'<Picture {foreign_id} by {user_id}>'