✨(project) Django boilerplate
This commit introduces a boilerplate inspired by https://github.com/numerique-gouv/impress. The code has been cleaned to remove unnecessary Impress logic and dependencies. Changes made: - Removed Minio, WebRTC, and create bucket from the stack. - Removed the Next.js frontend (it will be replaced by Vite). - Cleaned up impress-specific backend logics. The whole stack remains functional: - All tests pass. - Linter checks pass. - Agent Connexion sources are already set-up. Why clear out the code? To adhere to the KISS principle, we aim to maintain a minimalist codebase. Cloning Impress allowed us to quickly inherit its code quality tools and deployment configurations for staging, pre-production, and production environments. What’s broken? - The tsclient is not functional anymore. - Some make commands need to be fixed. - Helm sources are outdated. - Naming across the project sources are inconsistent (impress, visio, etc.) - CI is not configured properly. This list might be incomplete. Let's grind it.
This commit is contained in:
committed by
lebaudantoine
parent
2d81979b0a
commit
5b1a2b20de
0
src/backend/core/tests/__init__.py
Normal file
0
src/backend/core/tests/__init__.py
Normal file
0
src/backend/core/tests/authentication/__init__.py
Normal file
0
src/backend/core/tests/authentication/__init__.py
Normal file
101
src/backend/core/tests/authentication/test_backends.py
Normal file
101
src/backend/core/tests/authentication/test_backends.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Unit tests for the Authentication Backends."""
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
import pytest
|
||||
|
||||
from core import models
|
||||
from core.authentication.backends import OIDCAuthenticationBackend
|
||||
from core.factories import UserFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_no_email(
|
||||
django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""
|
||||
If an existing user matches the user's info sub, the user should be returned.
|
||||
"""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
db_user = UserFactory()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": db_user.sub}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert user == db_user
|
||||
|
||||
|
||||
def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||
"""
|
||||
If no user matches the user's info sub, a user should be created.
|
||||
User's info doesn't contain an email, created user's email should be empty.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123"}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert user.sub == "123"
|
||||
assert user.email is None
|
||||
assert user.password == "!"
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
"""
|
||||
If no user matches the user's info sub, a user should be created.
|
||||
User's email and name should be set on the identity.
|
||||
The "email" field on the User model should not be set as it is reserved for staff users.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
email = "impress@example.com"
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": email, "first_name": "John", "last_name": "Doe"}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert user.sub == "123"
|
||||
assert user.email == email
|
||||
assert user.password == "!"
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkeypatch):
|
||||
"""The user's info doesn't contain a sub."""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"test": "123",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with django_assert_num_queries(0), pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="User info contained no recognizable user identification",
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
assert models.User.objects.exists() is False
|
||||
10
src/backend/core/tests/authentication/test_urls.py
Normal file
10
src/backend/core/tests/authentication/test_urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Unit tests for the Authentication URLs."""
|
||||
|
||||
from core.authentication.urls import urlpatterns
|
||||
|
||||
|
||||
def test_urls_override_default_mozilla_django_oidc():
|
||||
"""Custom URL patterns should override default ones from Mozilla Django OIDC."""
|
||||
|
||||
url_names = [u.name for u in urlpatterns]
|
||||
assert url_names.index("oidc_logout_custom") < url_names.index("oidc_logout")
|
||||
231
src/backend/core/tests/authentication/test_views.py
Normal file
231
src/backend/core/tests/authentication/test_views.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""Unit tests for the Authentication Views."""
|
||||
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.test import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import crypto
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.authentication.views import OIDCLogoutCallbackView, OIDCLogoutView
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
|
||||
def test_view_logout_anonymous():
|
||||
"""Anonymous users calling the logout url,
|
||||
should be redirected to the specified LOGOUT_REDIRECT_URL."""
|
||||
|
||||
url = reverse("oidc_logout_custom")
|
||||
response = APIClient().get(url)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
OIDCLogoutView, "construct_oidc_logout_url", return_value="/example-logout"
|
||||
)
|
||||
def test_view_logout(mocked_oidc_logout_url):
|
||||
"""Authenticated users should be redirected to OIDC provider for logout."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("oidc_logout_custom")
|
||||
response = client.get(url)
|
||||
|
||||
mocked_oidc_logout_url.assert_called_once()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/default-redirect-logout")
|
||||
@mock.patch.object(
|
||||
OIDCLogoutView, "construct_oidc_logout_url", return_value="/default-redirect-logout"
|
||||
)
|
||||
def test_view_logout_no_oidc_provider(mocked_oidc_logout_url):
|
||||
"""Authenticated users should be logged out when no OIDC provider is available."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("oidc_logout_custom")
|
||||
|
||||
with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout:
|
||||
response = client.get(url)
|
||||
mocked_oidc_logout_url.assert_called_once()
|
||||
mock_logout.assert_called_once()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/default-redirect-logout"
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
|
||||
def test_view_logout_callback_anonymous():
|
||||
"""Anonymous users calling the logout callback url,
|
||||
should be redirected to the specified LOGOUT_REDIRECT_URL."""
|
||||
|
||||
url = reverse("oidc_logout_callback")
|
||||
response = APIClient().get(url)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_oidc_states",
|
||||
[{}, {"other_state": "foo"}],
|
||||
)
|
||||
def test_view_logout_persist_state(initial_oidc_states):
|
||||
"""State value should be persisted in session's data."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
if initial_oidc_states:
|
||||
request.session["oidc_states"] = initial_oidc_states
|
||||
request.session.save()
|
||||
|
||||
mocked_state = "mock_state"
|
||||
|
||||
OIDCLogoutView().persist_state(request, mocked_state)
|
||||
|
||||
assert "oidc_states" in request.session
|
||||
assert request.session["oidc_states"] == {
|
||||
"mock_state": {},
|
||||
**initial_oidc_states,
|
||||
}
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_LOGOUT_ENDPOINT="/example-logout")
|
||||
@mock.patch.object(OIDCLogoutView, "persist_state")
|
||||
@mock.patch.object(crypto, "get_random_string", return_value="mocked_state")
|
||||
def test_view_logout_construct_oidc_logout_url(
|
||||
mocked_get_random_string, mocked_persist_state
|
||||
):
|
||||
"""Should construct the logout URL to initiate the logout flow with the OIDC provider."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
request.session["oidc_id_token"] = "mocked_oidc_id_token"
|
||||
request.session.save()
|
||||
|
||||
redirect_url = OIDCLogoutView().construct_oidc_logout_url(request)
|
||||
|
||||
mocked_persist_state.assert_called_once()
|
||||
mocked_get_random_string.assert_called_once()
|
||||
|
||||
params = parse_qs(urlparse(redirect_url).query)
|
||||
|
||||
assert params["id_token_hint"][0] == "mocked_oidc_id_token"
|
||||
assert params["state"][0] == "mocked_state"
|
||||
|
||||
url = reverse("oidc_logout_callback")
|
||||
assert url in params["post_logout_redirect_uri"][0]
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/")
|
||||
def test_view_logout_construct_oidc_logout_url_none_id_token():
|
||||
"""If no ID token is available in the session,
|
||||
the user should be redirected to the final URL."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
redirect_url = OIDCLogoutView().construct_oidc_logout_url(request)
|
||||
|
||||
assert redirect_url == "/"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_state",
|
||||
[None, {"other_state": "foo"}],
|
||||
)
|
||||
def test_view_logout_callback_wrong_state(initial_state):
|
||||
"""Should raise an error if OIDC state doesn't match session data."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
if initial_state:
|
||||
request.session["oidc_states"] = initial_state
|
||||
request.session.save()
|
||||
|
||||
callback_view = OIDCLogoutCallbackView.as_view()
|
||||
|
||||
with pytest.raises(SuspiciousOperation) as excinfo:
|
||||
callback_view(request)
|
||||
|
||||
assert (
|
||||
str(excinfo.value) == "OIDC callback state not found in session `oidc_states`!"
|
||||
)
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
|
||||
def test_view_logout_callback():
|
||||
"""If state matches, callback should clear OIDC state and redirects."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().get("/logout-callback/", data={"state": "mocked_state"})
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
mocked_state = "mocked_state"
|
||||
|
||||
request.session["oidc_states"] = {mocked_state: {}}
|
||||
request.session.save()
|
||||
|
||||
callback_view = OIDCLogoutCallbackView.as_view()
|
||||
|
||||
with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout:
|
||||
|
||||
def clear_user(request):
|
||||
# Assert state is cleared prior to logout
|
||||
assert request.session["oidc_states"] == {}
|
||||
request.user = AnonymousUser()
|
||||
|
||||
mock_logout.side_effect = clear_user
|
||||
response = callback_view(request)
|
||||
mock_logout.assert_called_once()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
15
src/backend/core/tests/conftest.py
Normal file
15
src/backend/core/tests/conftest.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Fixtures for tests in the impress core application"""
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
USER = "user"
|
||||
TEAM = "team"
|
||||
VIA = [USER, TEAM]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user_get_teams():
|
||||
"""Mock for the "get_teams" method on the User model."""
|
||||
with mock.patch("core.models.User.get_teams") as mock_get_teams:
|
||||
yield mock_get_teams
|
||||
0
src/backend/core/tests/swagger/__init__.py
Normal file
0
src/backend/core/tests/swagger/__init__.py
Normal file
41
src/backend/core/tests/swagger/test_openapi_schema.py
Normal file
41
src/backend/core/tests/swagger/test_openapi_schema.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Test suite for generated openapi schema.
|
||||
"""
|
||||
import json
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import Client
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_openapi_client_schema():
|
||||
"""
|
||||
Generated and served OpenAPI client schema should be correct.
|
||||
"""
|
||||
# Start by generating the swagger.json file
|
||||
output = StringIO()
|
||||
call_command(
|
||||
"spectacular",
|
||||
"--api-version",
|
||||
"v1.0",
|
||||
"--urlconf",
|
||||
"core.urls",
|
||||
"--format",
|
||||
"openapi-json",
|
||||
"--file",
|
||||
"core/tests/swagger/swagger.json",
|
||||
stdout=output,
|
||||
)
|
||||
assert output.getvalue() == ""
|
||||
|
||||
response = Client().get("/v1.0/swagger.json")
|
||||
|
||||
assert response.status_code == 200
|
||||
with open(
|
||||
"core/tests/swagger/swagger.json", "r", encoding="utf-8"
|
||||
) as expected_schema:
|
||||
assert response.json() == json.load(expected_schema)
|
||||
417
src/backend/core/tests/test_api_users.py
Normal file
417
src/backend/core/tests/test_api_users.py
Normal file
@@ -0,0 +1,417 @@
|
||||
"""
|
||||
Test users API endpoints in the impress core app.
|
||||
"""
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core.api import serializers
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_users_list_anonymous():
|
||||
"""Anonymous users should not be allowed to list users."""
|
||||
factories.UserFactory()
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/users/")
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_list_authenticated():
|
||||
"""
|
||||
Authenticated users should be able to list users.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.UserFactory.create_batch(2)
|
||||
response = client.get(
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 3
|
||||
|
||||
|
||||
def test_api_users_list_query_email():
|
||||
"""
|
||||
Authenticated users should be able to list users
|
||||
and filter by email.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.UserFactory(email="david.bowman@work.com")
|
||||
nicole = factories.UserFactory(email="nicole_foole@work.com")
|
||||
frank = factories.UserFactory(email="frank_poole@work.com")
|
||||
factories.UserFactory(email="heywood_floyd@work.com")
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(dave.id)]
|
||||
|
||||
response = client.get("/api/v1.0/users/?q=oole")
|
||||
|
||||
assert response.status_code == 200
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(nicole.id), str(frank.id)]
|
||||
|
||||
|
||||
def test_api_users_retrieve_me_anonymous():
|
||||
"""Anonymous users should not be allowed to list users."""
|
||||
factories.UserFactory.create_batch(2)
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/users/me/")
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_retrieve_me_authenticated():
|
||||
"""Authenticated users should be able to retrieve their own user via the "/users/me" path."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.UserFactory.create_batch(2)
|
||||
response = client.get(
|
||||
"/api/v1.0/users/me/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_retrieve_anonymous():
|
||||
"""Anonymous users should not be allowed to retrieve a user."""
|
||||
client = APIClient()
|
||||
user = factories.UserFactory()
|
||||
response = client.get(f"/api/v1.0/users/{user.id!s}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_retrieve_authenticated_self():
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve their own user.
|
||||
The returned object should not contain the password.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.json() == {"detail": 'Method "GET" not allowed.'}
|
||||
|
||||
|
||||
def test_api_users_retrieve_authenticated_other():
|
||||
"""
|
||||
Authenticated users should be able to retrieve another user's detail view with
|
||||
limited information.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/users/{other_user.id!s}/",
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.json() == {"detail": 'Method "GET" not allowed.'}
|
||||
|
||||
|
||||
def test_api_users_create_anonymous():
|
||||
"""Anonymous users should not be able to create users via the API."""
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/users/",
|
||||
{
|
||||
"language": "fr-fr",
|
||||
"password": "mypassword",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
assert models.User.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_users_create_authenticated():
|
||||
"""Authenticated users should not be able to create users via the API."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/users/",
|
||||
{
|
||||
"language": "fr-fr",
|
||||
"password": "mypassword",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.json() == {"detail": 'Method "POST" not allowed.'}
|
||||
assert models.User.objects.exclude(id=user.id).exists() is False
|
||||
|
||||
|
||||
def test_api_users_update_anonymous():
|
||||
"""Anonymous users should not be able to update users via the API."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
new_user_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
assert value == old_user_values[key]
|
||||
|
||||
|
||||
def test_api_users_update_authenticated_self():
|
||||
"""
|
||||
Authenticated users should be able to update their own user but only "language"
|
||||
and "timezone" fields.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = dict(
|
||||
serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
)
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
new_user_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
if key in ["language", "timezone"]:
|
||||
assert value == new_user_values[key]
|
||||
else:
|
||||
assert value == old_user_values[key]
|
||||
|
||||
|
||||
def test_api_users_update_authenticated_other():
|
||||
"""Authenticated users should not be allowed to update other users."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
user = factories.UserFactory()
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
new_user_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
assert value == old_user_values[key]
|
||||
|
||||
|
||||
def test_api_users_patch_anonymous():
|
||||
"""Anonymous users should not be able to patch users via the API."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = dict(
|
||||
serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
)
|
||||
|
||||
for key, new_value in new_user_values.items():
|
||||
response = APIClient().patch(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
{key: new_value},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
assert value == old_user_values[key]
|
||||
|
||||
|
||||
def test_api_users_patch_authenticated_self():
|
||||
"""
|
||||
Authenticated users should be able to patch their own user but only "language"
|
||||
and "timezone" fields.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = dict(
|
||||
serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
)
|
||||
|
||||
for key, new_value in new_user_values.items():
|
||||
response = client.patch(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
{key: new_value},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
if key in ["language", "timezone"]:
|
||||
assert value == new_user_values[key]
|
||||
else:
|
||||
assert value == old_user_values[key]
|
||||
|
||||
|
||||
def test_api_users_patch_authenticated_other():
|
||||
"""Authenticated users should not be allowed to patch other users."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
user = factories.UserFactory()
|
||||
old_user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
new_user_values = dict(
|
||||
serializers.UserSerializer(instance=factories.UserFactory()).data
|
||||
)
|
||||
|
||||
for key, new_value in new_user_values.items():
|
||||
response = client.put(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
{key: new_value},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
assert value == old_user_values[key]
|
||||
|
||||
|
||||
def test_api_users_delete_list_anonymous():
|
||||
"""Anonymous users should not be allowed to delete a list of users."""
|
||||
factories.UserFactory.create_batch(2)
|
||||
|
||||
client = APIClient()
|
||||
response = client.delete("/api/v1.0/users/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert models.User.objects.count() == 2
|
||||
|
||||
|
||||
def test_api_users_delete_list_authenticated():
|
||||
"""Authenticated users should not be allowed to delete a list of users."""
|
||||
factories.UserFactory.create_batch(2)
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert models.User.objects.count() == 3
|
||||
|
||||
|
||||
def test_api_users_delete_anonymous():
|
||||
"""Anonymous users should not be allowed to delete a user."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
response = APIClient().delete(f"/api/v1.0/users/{user.id!s}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_users_delete_authenticated():
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a user other than themselves.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/users/{other_user.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert models.User.objects.count() == 2
|
||||
|
||||
|
||||
def test_api_users_delete_self():
|
||||
"""Authenticated users should not be able to delete their own user."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/users/{user.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert models.User.objects.count() == 1
|
||||
45
src/backend/core/tests/test_models_users.py
Normal file
45
src/backend/core/tests/test_models_users.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Unit tests for the User model
|
||||
"""
|
||||
from unittest import mock
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_models_users_str():
|
||||
"""The str representation should be the email."""
|
||||
user = factories.UserFactory()
|
||||
assert str(user) == user.email
|
||||
|
||||
|
||||
def test_models_users_id_unique():
|
||||
"""The "id" field should be unique."""
|
||||
user = factories.UserFactory()
|
||||
with pytest.raises(ValidationError, match="User with this Id already exists."):
|
||||
factories.UserFactory(id=user.id)
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_existing():
|
||||
"""The "email_user' method should send mail to the user's email address."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
with mock.patch("django.core.mail.send_mail") as mock_send:
|
||||
user.email_user("my subject", "my message")
|
||||
|
||||
mock_send.assert_called_once_with("my subject", "my message", None, [user.email])
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_missing():
|
||||
"""The "email_user' method should fail if the user has no email address."""
|
||||
user = factories.UserFactory(email=None)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
user.email_user("my subject", "my message")
|
||||
|
||||
assert str(excinfo.value) == "User has no email address."
|
||||
Reference in New Issue
Block a user