diff --git a/poetry.lock b/poetry.lock
index ebbbafa3..a0f96f56 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "alabaster"
@@ -839,7 +839,7 @@ files = [
django = "*"
django-stubs-ext = ">=4.2.2"
mypy = [
- {version = ">=1.0.0", optional = true, markers = "extra != \"compatible-mypy\""},
+ {version = ">=1.0.0"},
{version = "==1.5.*", optional = true, markers = "extra == \"compatible-mypy\""},
]
types-pytz = "*"
@@ -1034,6 +1034,20 @@ files = [
[package.extras]
tests = ["asttokens", "littleutils", "pytest", "rich"]
+[[package]]
+name = "faker"
+version = "12.0.1"
+description = "Faker is a Python package that generates fake data for you."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "Faker-12.0.1-py3-none-any.whl", hash = "sha256:1dc2811f20e163892fefe7006f2ce00778f8099a40aee265bfa60a13400de63d"},
+ {file = "Faker-12.0.1.tar.gz", hash = "sha256:aa7103805ae793277abbb85da9f6f05e76a1a295a9384a8e17c2fba2b3a690cb"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.4"
+
[[package]]
name = "flake8"
version = "6.1.0"
@@ -1450,6 +1464,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"
@@ -1844,6 +1868,23 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
+[[package]]
+name = "mixer"
+version = "7.2.2"
+description = "Mixer -- Is a fixtures replacement. Supported Django ORM, SqlAlchemy ORM, Mongoengine ODM and custom python objects."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mixer-7.2.2-py3-none-any.whl", hash = "sha256:8089b8e2d00288c77e622936198f5dd03c8ac1603a1530a4f870dc213363b2ae"},
+ {file = "mixer-7.2.2.tar.gz", hash = "sha256:9b3f1a261b56d8f2394f39955f83adbc7ff3ab4bb1065ebfec19a10d3e8501e0"},
+]
+
+[package.dependencies]
+Faker = ">=5.4.0,<12.1"
+
+[package.extras]
+tests = ["Django (>=3.0)", "Flask (>=1.0)", "Marshmallow (>=3.9)", "SQLAlchemy (>=1.1.4)", "flask-sqlalchemy (>=2.1)", "mongoengine (>=0.10.1)", "peewee (>=3.7.0)", "pony (>=0.7)", "psycopg2-binary (>=2.8.4)", "pytest"]
+
[[package]]
name = "more-itertools"
version = "10.1.0"
@@ -2543,6 +2584,20 @@ files = [
[package.dependencies]
pytest = ">=5.0.0"
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
[[package]]
name = "python-decouple"
version = "3.8"
@@ -2583,6 +2638,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 +2646,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 +2671,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 +2679,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 +2896,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 +3508,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 = "bf5cfa374e3a32eef8c395bf8cf25f3cf13a853cf57f31692627c27a6a380f24"
diff --git a/pyproject.toml b/pyproject.toml
index 19bbf6e4..0ac3d41c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -66,6 +66,8 @@ dennis = "^1.1"
dump-env = "^1.3"
ipython = "^8.15"
import-linter = "^1.11"
+mixer = "^7.2.2"
+httpretty = "^1.1.4"
[tool.poetry.group.docs]
optional = true
diff --git a/server/apps/pictures/templates/pictures/pages/dashboard.html b/server/apps/pictures/templates/pictures/pages/dashboard.html
index efdd78de..b5e0cf52 100644
--- a/server/apps/pictures/templates/pictures/pages/dashboard.html
+++ b/server/apps/pictures/templates/pictures/pages/dashboard.html
@@ -51,6 +51,10 @@
Профиль
+
{% endfor %}
diff --git a/server/apps/pictures/urls.py b/server/apps/pictures/urls.py
index e750c1b3..b4357c06 100644
--- a/server/apps/pictures/urls.py
+++ b/server/apps/pictures/urls.py
@@ -1,10 +1,11 @@
from django.urls import path
-from server.apps.pictures.views import DashboardView, FavouritePicturesView
+from server.apps.pictures.views import DashboardView, FavouritePicturesView, FavouriteDeleteView
app_name = 'pictures'
urlpatterns = [
path('dashboard', DashboardView.as_view(), name='dashboard'),
path('favourites', FavouritePicturesView.as_view(), name='favourites'),
+ path('favourites/', FavouriteDeleteView.as_view(), name='remove_favourite'),
]
diff --git a/server/apps/pictures/views.py b/server/apps/pictures/views.py
index a52a3b9f..b47bab84 100644
--- a/server/apps/pictures/views.py
+++ b/server/apps/pictures/views.py
@@ -3,8 +3,9 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import QuerySet
-from django.http import HttpResponse
-from django.urls import reverse_lazy
+from django.http import HttpResponse, HttpResponseRedirect
+from django.urls import reverse_lazy, reverse
+from django.views import View
from django.views.generic import CreateView, ListView, TemplateView
from server.apps.pictures.container import container
@@ -70,3 +71,12 @@ def get_queryset(self) -> QuerySet[FavouritePicture]:
"""Return matching pictures."""
list_favourites = container.instantiate(favourites_list.FavouritesList)
return list_favourites(self.request.user.id)
+
+
+@final
+@dispatch_decorator(login_required)
+class FavouriteDeleteView(View):
+
+ def post(self, request, picture_id):
+ FavouritePicture.objects.filter(foreign_id=picture_id, user=request.user).delete()
+ return HttpResponseRedirect(reverse('pictures:dashboard'))
diff --git a/setup.cfg b/setup.cfg
index 34c14369..11be7f65 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -61,7 +61,7 @@ DJANGO_SETTINGS_MODULE = server.settings
# You should adjust this value to be as low as possible.
# Configuration:
# https://pypi.org/project/pytest-timeout/
-timeout = 5
+timeout = 3
# Strict `@xfail` by default:
xfail_strict = true
diff --git a/tests/conftest.py b/tests/conftest.py
index 96baa556..f9555dd9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,7 @@
1. https://docs.python.org/3/library/doctest.html
2. https://docs.pytest.org/en/latest/doctest.html
"""
+import pytest
pytest_plugins = [
# Should be the first custom one:
@@ -12,3 +13,23 @@
# TODO: add your own plugins here!
]
+
+
+@pytest.fixture()
+def mixer():
+ """Util for create models."""
+ from mixer.backend.django import mixer # noqa: WPS433 names conflict
+ return mixer
+
+
+@pytest.fixture()
+def exists_user(mixer):
+ """User registered in system."""
+ return mixer.blend('identity.User')
+
+
+@pytest.fixture()
+def user_client(exists_user, client):
+ """Registered user HTTP client."""
+ client.force_login(exists_user)
+ return client
diff --git a/tests/test_apps/test_identity/test_create_user.py b/tests/test_apps/test_identity/test_create_user.py
new file mode 100644
index 00000000..165f8595
--- /dev/null
+++ b/tests/test_apps/test_identity/test_create_user.py
@@ -0,0 +1,16 @@
+import pytest
+
+from server.apps.identity.models import User
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+def test_without_email():
+ """Test create user via manager with incorrect email."""
+ with pytest.raises(ValueError, match='Users must have an email address'):
+ User.objects.create_user( # noqa: S106 not secure issue
+ email='',
+ password='SeCr3t',
+ )
diff --git a/tests/test_apps/test_identity/test_login.py b/tests/test_apps/test_identity/test_login.py
new file mode 100644
index 00000000..6583b97a
--- /dev/null
+++ b/tests/test_apps/test_identity/test_login.py
@@ -0,0 +1,35 @@
+from http import HTTPStatus
+
+import pytest
+
+from server.apps.identity.models import User
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+def test(client):
+ """Test get login page."""
+ got = client.get('/identity/login')
+
+ assert got.status_code == HTTPStatus.OK
+
+
+def test_registration(client):
+ """Test registration."""
+ got = client.post('/identity/registration', data={
+ 'email': 'my@email.com',
+ 'first_name': 'My name',
+ 'last_name': 'My name',
+ 'date_of_birth': '1970-09-18',
+ 'address': 'My name',
+ 'job_title': 'My name',
+ 'phone': 'My name',
+ 'password1': 'My name',
+ 'password2': 'My name',
+ })
+
+ assert got.status_code == HTTPStatus.FOUND
+ assert got.headers['location'] == '/identity/login'
+ assert User.objects.count() == 1
diff --git a/tests/test_apps/test_identity/test_placeholder.py b/tests/test_apps/test_identity/test_placeholder.py
new file mode 100644
index 00000000..1aec0c45
--- /dev/null
+++ b/tests/test_apps/test_identity/test_placeholder.py
@@ -0,0 +1,52 @@
+import datetime
+
+import httpretty
+import pytest
+
+from server.apps.identity.intrastructure.services.placeholder import (
+ LeadCreate,
+ LeadUpdate,
+ UserResponse,
+)
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+@httpretty.activate(allow_net_connect=False, verbose=True)
+def test_create(mixer):
+ """Test LeadCreate object."""
+ httpretty.register_uri(
+ httpretty.POST,
+ 'https://domain.com/users',
+ body='{"id": 1}',
+ )
+ got = LeadCreate('https://domain.com', 5)(user=mixer.blend('identity.User'))
+
+ assert got == UserResponse(id=1)
+
+
+@httpretty.activate(allow_net_connect=False, verbose=True)
+def test_create_with_birthday(mixer, faker):
+ """Test LeadCreate object with birthday."""
+ httpretty.register_uri(
+ httpretty.POST, 'https://domain.com/users', body='{"id": 1}',
+ )
+ got = LeadCreate('https://domain.com', 5)(user=mixer.blend(
+ 'identity.User',
+ date_of_birth=datetime.datetime(1965, 6, 14), # noqa: WPS432
+ ))
+
+ assert got == UserResponse(id=1)
+
+
+@httpretty.activate(allow_net_connect=False, verbose=True)
+def test_update(mixer):
+ """Test LeadUpdate object."""
+ httpretty.register_uri(
+ httpretty.PATCH, 'https://domain.com/users/1', body='{"id": 1}',
+ )
+ LeadUpdate('https://domain.com', 5)(
+ user=mixer.blend('identity.User', lead_id=1),
+ )
diff --git a/tests/test_apps/test_identity/test_templates.py b/tests/test_apps/test_identity/test_templates.py
new file mode 100644
index 00000000..c8a1fcce
--- /dev/null
+++ b/tests/test_apps/test_identity/test_templates.py
@@ -0,0 +1,18 @@
+from django.http import HttpResponse
+from django.shortcuts import render
+from django.test import RequestFactory
+
+from server.apps.identity.intrastructure.django.forms import RegistrationForm
+
+
+def test_registration():
+ """Test registration template."""
+ page = render(
+ RequestFactory().get('/identity/registration'),
+ 'identity/pages/registration.html',
+ {
+ 'form': RegistrationForm(),
+ },
+ )
+
+ assert isinstance(page, HttpResponse)
diff --git a/tests/test_apps/test_identity/test_update_user.py b/tests/test_apps/test_identity/test_update_user.py
new file mode 100644
index 00000000..9674397d
--- /dev/null
+++ b/tests/test_apps/test_identity/test_update_user.py
@@ -0,0 +1,30 @@
+from http import HTTPStatus
+
+import pytest
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+def test_get(user_client):
+ """Test get form for update user."""
+ got = user_client.get('/identity/update')
+
+ assert got.status_code == HTTPStatus.OK
+
+
+def test_update(user_client):
+ """Test update exists user."""
+ got = user_client.post('/identity/update', data={
+ 'email': 'my@email.com',
+ 'first_name': 'My name',
+ 'last_name': 'My name',
+ 'date_of_birth': '1970-09-18',
+ 'address': 'My name',
+ 'job_title': 'My name',
+ 'phone': 'My name',
+ })
+
+ assert got.status_code == HTTPStatus.FOUND
+ assert got.headers['location'] == '/identity/update'
diff --git a/tests/test_apps/test_pictures/test_dashboard.py b/tests/test_apps/test_pictures/test_dashboard.py
new file mode 100644
index 00000000..1c4c3720
--- /dev/null
+++ b/tests/test_apps/test_pictures/test_dashboard.py
@@ -0,0 +1,54 @@
+from http import HTTPStatus
+
+import pytest
+
+from server.apps.pictures.models import FavouritePicture
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+@pytest.fixture()
+def existed_picture(exists_user, mixer):
+ return mixer.blend('pictures.FavouritePicture', user=exists_user)
+
+
+def test_get(user_client):
+ """Test get pictures dashboard."""
+ got = user_client.get('/pictures/dashboard')
+
+ assert got.status_code == HTTPStatus.OK
+
+
+def test_create(user_client, exists_user):
+ """Test add picture to favourite."""
+ got = user_client.post('/pictures/dashboard', data={
+ 'foreign_id': 1,
+ 'url': 'https://via.placeholder.com/600/92c952',
+ })
+
+ assert got.status_code == HTTPStatus.FOUND
+ assert got.headers['location'] == '/pictures/dashboard'
+ assert FavouritePicture.objects.filter(
+ user_id=exists_user.id,
+ foreign_id=1,
+ url='https://via.placeholder.com/600/92c952',
+ ).count() == 1
+
+
+@pytest.mark.usefixtures()
+def test_delete(user_client, exists_user, existed_picture):
+ """Test delete picture from favourite."""
+ got = user_client.delete('/pictures/favourites/{0}'.format(existed_picture.foreign_id))
+
+ assert got.status_code == HTTPStatus.FOUND
+ assert got.headers['location'] == '/pictures/dashboard'
+ assert FavouritePicture.objects.count() == 0
+
+
+def test_get_favourites(user_client):
+ """Test get favourite pictures."""
+ got = user_client.get('/pictures/favourites')
+
+ assert got.status_code == HTTPStatus.OK
diff --git a/tests/test_apps/test_pictures/test_forms.py b/tests/test_apps/test_pictures/test_forms.py
new file mode 100644
index 00000000..ac724a1e
--- /dev/null
+++ b/tests/test_apps/test_pictures/test_forms.py
@@ -0,0 +1,12 @@
+import pytest
+
+from server.apps.pictures.intrastructure.django.forms import FavouritesForm
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+def test_favourites_form(exists_user):
+ """Test favourites form."""
+ FavouritesForm(user=exists_user).save(False)
diff --git a/tests/test_apps/test_pictures/test_models.py b/tests/test_apps/test_pictures/test_models.py
new file mode 100644
index 00000000..bff10e9c
--- /dev/null
+++ b/tests/test_apps/test_pictures/test_models.py
@@ -0,0 +1,20 @@
+import pytest
+
+pytestmark = [
+ pytest.mark.django_db,
+]
+
+
+@pytest.fixture()
+def favour_picture(mixer, exists_user):
+ """Favourite picture."""
+ return mixer.blend(
+ 'pictures.FavouritePicture',
+ user_id=exists_user.id,
+ foreign_id=10,
+ )
+
+
+def test_str(favour_picture, exists_user):
+ """Test FavouritePicture string representation."""
+ assert str(favour_picture) == '