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

Homework Week 1 #70

Open
wants to merge 26 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 @@ -17,6 +17,8 @@ services:
- webnet
- postgresnet
env_file: ./config/.env
ports:
- "5432:5432"

web:
<<: &web
Expand Down
266 changes: 150 additions & 116 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ requests = "^2.28"
attrs = "^23.1"
pydantic = "^2.3"
punq = "^0.6"
pytest = "^7.4.2"
mimesis = "^11.1.0"

[tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^4.2"
Expand Down
55 changes: 0 additions & 55 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -115,61 +115,6 @@ omit =
server/settings/components/logging.py


[mypy]
# Mypy configuration:
# https://mypy.readthedocs.io/en/latest/config_file.html
enable_error_code =
truthy-bool,
truthy-iterable,
redundant-expr,
unused-awaitable,
ignore-without-code,
possibly-undefined,
redundant-self,

extra_checks = true

disable_error_code =
literal-required,

enable_incomplete_feature =
Unpack,

allow_redefinition = false
check_untyped_defs = true
disallow_untyped_decorators = true
disallow_any_explicit = false
disallow_any_generics = true
disallow_untyped_calls = true
disallow_incomplete_defs = true
explicit_package_bases = true
ignore_errors = false
ignore_missing_imports = true
implicit_reexport = false
local_partial_types = true
strict_optional = true
strict_equality = true
no_implicit_optional = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unreachable = true
warn_no_return = true

plugins =
mypy_django_plugin.main,
pydantic.mypy

[mypy-server.apps.*.migrations.*]
# Django migrations should not produce any errors (they are tested anyway):
ignore_errors = true

[mypy.plugins.django-stubs]
# Docs: https://github.com/typeddjango/django-stubs
django_settings_module = server.settings
strict_settings = false


[doc8]
# doc8 configuration:
# https://github.com/pycqa/doc8
Expand Down
Empty file added tests/plugins/__init__.py
Empty file.
Empty file.
138 changes: 138 additions & 0 deletions tests/plugins/identity/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import datetime as dt
from typing import Callable, Protocol, TypeAlias, TypedDict, final

import pytest
from mimesis import Field, Schema
from typing_extensions import Unpack

from server.apps.identity.models import User


# User Data
class UserData(TypedDict, total=False):
"""Represent the user data."""

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


@final
class UserDataFactory(Protocol): # type: ignore[misc]
"""User data factory protocol."""

def __call__(self, **fields: Unpack[UserData]) -> UserData:
"""Create instance of ``UserData`` with overwritten fields."""


@pytest.fixture()
def user_data_factory(field: Field) -> UserDataFactory:
"""Generate random user data.

:param field: mimesis field instance
"""

def factory(**fields) -> UserData:
"""Factory.

:param fields: fields to overwrite
"""
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'),
},
iterations=1,
)
return {
**schema.create()[0], # ignore this line
**fields,
}

return factory


# Registration Data
@final
class RegistrationData(UserData, total=False):
"""User data and passwords for registration."""

password1: str
password2: str


@final
class RegistrationDataFactory(Protocol): # type: ignore[misc]
"""User data factory protocol."""

def __call__(self, **fields: Unpack[RegistrationData]) -> RegistrationData:
"""Create instance of ``RegistrationData`` with overwritten fields."""


@pytest.fixture(scope='session')
def registration_data_factory(
field: Field,
user_data_factory: UserDataFactory,
) -> RegistrationDataFactory:
"""Returns factory for fake random data for registration."""

def factory(**fields: Unpack[RegistrationData]) -> RegistrationData:
password = field('password') # by default passwords are equal
user_data = user_data_factory()

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

return factory


@pytest.fixture()
def user_registration_data(
registration_data_factory: RegistrationDataFactory,
) -> RegistrationData:
"""Create instance of ordinary user (not staff or admin)."""
return registration_data_factory()


@final
class LoginData(TypedDict, total=False):
"""Represent the login data that is required to authenticate a user."""

username: str
password: str


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


@pytest.fixture(scope='session')
def assert_correct_user() -> UserAssertion:
"""Check that user created correctly."""

def factory(expected: RegistrationData) -> None:
user = User.objects.get(email=expected['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():
if not field_name.startswith('password'):
assert getattr(user, field_name) == data_value

return factory
53 changes: 53 additions & 0 deletions tests/test_apps/test_identity/test_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from http import HTTPStatus

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

from tests.plugins.identity.user import (
RegistrationData,
UserAssertion,
UserDataExtractor,
)


@pytest.mark.parametrize('page', [
'/identity/login',
'/identity/registration',
])
def test_identity_pages_unauthenticated(client: Client, page: str) -> None:
"""Test accessibility of identity pages for unauthenticated users."""
response = client.get(page)

assert response.status_code == HTTPStatus.OK


@pytest.mark.parametrize('page', [
'/pictures/dashboard', '/pictures/favourites',
])
def test_pictures_pages_unauthenticated(client: Client, page: str) -> None:
"""Test ensures that unauthenticated users are redirected to login page."""
response = client.get(page)

assert response.status_code == HTTPStatus.FOUND
assert response.url == '/identity/login?next={0}'.format(page)


def test_valid_registration(
client: Client,
registration_data: RegistrationData,
user_data: UserDataExtractor,
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'],
user_data(registration_data),
)
3 changes: 2 additions & 1 deletion tests/test_server/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import pytest
from django.test import Client

pytestmark = 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
Loading