From 7e9501e75f6f3129dc6da8c463cd0b6447276993 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sun, 14 Sep 2025 00:35:28 +0800 Subject: [PATCH] Switch to faker for test data --- pyproject.toml | 1 + src/accounts/models.py | 5 - src/accounts/serializers.py | 2 +- src/accounts/views.py | 16 +++ src/api/urls.py | 7 +- src/core/config/models.py | 22 +++- src/tests/settings.py | 2 +- src/tests/users/__init__.py | 120 ++++++++++-------- src/tests/users/test_user_activation.py | 32 +++++ .../users/test_user_creation_deletion.py | 49 ++++--- src/tests/users/test_user_login_query_self.py | 14 +- src/tests/users/users.json | 32 ----- uv.lock | 14 ++ 13 files changed, 190 insertions(+), 126 deletions(-) create mode 100644 src/tests/users/test_user_activation.py delete mode 100644 src/tests/users/users.json diff --git a/pyproject.toml b/pyproject.toml index 88fe582..14dfc1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "pytest-django>=4.11.1", "pytest-cov>=7.0.0", "isort>=6.0.1", + "faker>=37.6.0", ] [tool.pytest.ini_options] diff --git a/src/accounts/models.py b/src/accounts/models.py index 9f1375c..5fbd747 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -5,7 +5,6 @@ Common model schemas from datetime import timedelta from django.contrib.auth.models import AbstractUser -from django.urls import reverse from django.utils import timezone @@ -21,7 +20,3 @@ class CustomUser(AbstractUser): @property def full_name(self): return f"{self.first_name} {self.last_name}" - - @property - def admin_url(self): - return reverse("admin:users_customuser_change", args=(self.pk,)) diff --git a/src/accounts/serializers.py b/src/accounts/serializers.py index b5a1cc9..7d3f56a 100644 --- a/src/accounts/serializers.py +++ b/src/accounts/serializers.py @@ -40,7 +40,7 @@ class CustomUserSerializer(BaseUserSerializer): return super().update(instance, validated_data) -class UserRegistrationSerializer(serializers.ModelSerializer): +class CustomUserRegistrationSerializer(serializers.ModelSerializer): email = serializers.EmailField(required=True) username = serializers.CharField(required=True) password = serializers.CharField( diff --git a/src/accounts/views.py b/src/accounts/views.py index 67ac3ec..1fbe907 100644 --- a/src/accounts/views.py +++ b/src/accounts/views.py @@ -8,7 +8,9 @@ from django.contrib.auth.tokens import default_token_generator from django.core.cache import cache from djoser.conf import settings from djoser.views import UserViewSet as DjoserUserViewSet +from rest_framework import status from rest_framework.decorators import action +from rest_framework.response import Response from accounts import serializers from accounts.models import CustomUser @@ -23,6 +25,9 @@ class CustomUserViewSet(DjoserUserViewSet): token_generator = default_token_generator def get_queryset(self): + """ + Overriden class function for injection of cache invalidation + """ user = self.request.user if user.is_superuser: @@ -43,6 +48,9 @@ class CustomUserViewSet(DjoserUserViewSet): return CustomUser.objects.none() def perform_update(self, serializer, *args, **kwargs): + """ + Overriden class function for injection of cache invalidation + """ user = self.request.user super().perform_update(serializer, *args, **kwargs) @@ -51,6 +59,9 @@ class CustomUserViewSet(DjoserUserViewSet): cache.delete(f"users:{user.id}") def perform_create(self, serializer, *args, **kwargs): + """ + Overriden class function for injection of cache invalidation + """ user = serializer.save(*args, **kwargs) # Try-catch block for email sending @@ -70,9 +81,14 @@ class CustomUserViewSet(DjoserUserViewSet): methods=["post"], detail=False, url_path="activation", url_name="activation" ) def activation(self, request, *args, **kwargs): + """ + Overriden class function for injection of cache invalidation + """ user = self.request.user super().activation(request, *args, **kwargs) # Clear cache cache.delete("users") cache.delete(f"users:{user.id}") + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/api/urls.py b/src/api/urls.py index 30c9900..6b6040a 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -1,7 +1,10 @@ from django.contrib import admin from django.urls import include, path -from drf_spectacular.views import (SpectacularAPIView, SpectacularRedocView, - SpectacularSwaggerView) +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) from core.settings import config diff --git a/src/core/config/models.py b/src/core/config/models.py index 21810a4..3993dd5 100644 --- a/src/core/config/models.py +++ b/src/core/config/models.py @@ -6,8 +6,14 @@ import re from datetime import timedelta from typing import Literal -from pydantic import (BaseModel, EmailStr, Field, StrictStr, field_validator, - model_validator) +from pydantic import ( + BaseModel, + EmailStr, + Field, + StrictStr, + field_validator, + model_validator, +) from pydantic_extra_types.timezone_name import TimeZoneName @@ -62,9 +68,17 @@ class Config(BaseModel): REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field( default=timedelta(days=3), description="Refresh token lifetime in days" ) + DEBUG_USER_EMAIL: StrictStr = Field( + json_schema_extra={"required": DEBUG}, + description="Administrator user email used for development", + ) + DEBUG_USER_USERNAME: StrictStr = Field( + json_schema_extra={"required": DEBUG}, + description="Administrator username used for development", + ) DEBUG_USER_PASSWORD: StrictStr = Field( - json_schema_extra={"required": True}, - description="Password for test users created during development", + json_schema_extra={"required": DEBUG}, + description="Password for administrator and test users created during development", ) CACHE_USERNAME: StrictStr = Field( json_schema_extra={"required": True}, diff --git a/src/tests/settings.py b/src/tests/settings.py index 17a38b4..5bb6637 100644 --- a/src/tests/settings.py +++ b/src/tests/settings.py @@ -8,7 +8,7 @@ from core.settings import * # noqa: F403 DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "test_db.sqlite3", # noqa: F405 + "NAME": BASE_DIR / "db.sqlite3", # noqa: F405 } } diff --git a/src/tests/users/__init__.py b/src/tests/users/__init__.py index 6204a44..dcd3809 100644 --- a/src/tests/users/__init__.py +++ b/src/tests/users/__init__.py @@ -2,71 +2,89 @@ Post-migrate signal handlers for creating initial data for accounts app. """ -import json import logging -import os +from faker import Faker from accounts.models import CustomUser -from core.settings import TESTS_DIR, config +from core.settings import config logger = logging.getLogger(__name__) +fake = Faker() -def get_users_json(): +def generate_random_user(active: bool = False): """ - Function to read test user data from JSON file + Function to generate a single random user + + Args: + active (bool, optional): User active status. Defaults to False. + + Returns: + CustomUser: Newly created USER instance """ - with open(os.path.join(TESTS_DIR, "users", "users.json"), "r") as f: - # Load JSON data - data = json.loads(f.read()) - return data + USER = CustomUser.objects.create_user( + username=fake.user_name(), + email=fake.email(), + password=config.DEBUG_USER_PASSWORD, + ) + + # Additional user fields not covered by create() method + USER.first_name = fake.first_name() + USER.last_name = fake.last_name() + USER.is_active = active + USER.save() + + return USER -def generate_test_users(): +def generate_superuser(): """ - Function to create test users. + Function to generate a test superuser account """ - data = get_users_json() - - for user in data["users"]: - # Check if user already exists - USER = CustomUser.objects.filter(username=user["username"]).first() - if not USER: - # Create user - if user["is_superuser"]: - USER = CustomUser.objects.create_superuser( - username=user["username"], - email=user["email"], - password=config.DEBUG_USER_PASSWORD, - ) - logger.info("Created Superuser:", user["username"]) - else: - USER = CustomUser.objects.create_user( - username=user["username"], - email=user["email"], - password=config.DEBUG_USER_PASSWORD, - ) - logger.info("Created User:", user["username"]) - - # Additional user fields not covered by create() methods - USER.first_name = user["first_name"] - USER.last_name = user["last_name"] - USER.is_active = True - USER.save() + USER = CustomUser.objects.filter(is_superuser=True).first() + if not USER: + CustomUser.objects.create_superuser( + username=config.DEBUG_USER_USERNAME, + email=config.DEBUG_USER_EMAIL, + password=config.DEBUG_USER_PASSWORD, + ) + return USER -def remove_test_users(): +def generate_test_users(count=3, active: bool = False) -> list[CustomUser]: """ - Function to remove test users in DEBUG mode. + Function to generate a list of test users. + + Args: + count (int, optional): Number of regular test users to generate. Defaults to 10. + + Returns: + list[CustomUser]: List containing the test superuser and generated regular users. """ - if config.DEBUG: - data = get_users_json() - for user in data["users"]: - # Check if user already exists - USER = CustomUser.objects.filter(username=user["username"]).first() - if USER: - USER.delete() - else: - logger.warning( - f"Skipping user deletion for {user['username']}: Does not exist" - ) + USERS = [] + + # Superuser + USERS.append(generate_superuser()) + + # Regular users + USERS.extend([generate_random_user(active=active) for _ in range(count)]) + + return USERS + + +def remove_test_users( + USERS: list[CustomUser] = CustomUser.objects.filter( + is_superuser=False), +): + """ + Function to remove test users. + """ + + for USER in USERS: + USER = CustomUser.objects.filter(username=USER.username).first() + if USER: + USER.delete() + else: + logger.warning( + f"Skipping user deletion for {USER.username}: Does not exist" + ) diff --git a/src/tests/users/test_user_activation.py b/src/tests/users/test_user_activation.py new file mode 100644 index 0000000..dc3900b --- /dev/null +++ b/src/tests/users/test_user_activation.py @@ -0,0 +1,32 @@ +import pytest +import users +from django.contrib.auth.tokens import default_token_generator +from django.test.client import RequestFactory +from djoser.utils import encode_uid + +from accounts.views import CustomUserViewSet + +factory = RequestFactory() + + +@pytest.mark.django_db(transaction=True) +def test_user_activation(): + """ + Test activation + """ + # Generate test user + USER = users.generate_random_user(active=False) + + uid = encode_uid(USER.pk) + token = default_token_generator.make_token(USER) + + view = CustomUserViewSet.as_view(actions={"post": "activation"}) + + request = factory.post( + "/accounts/users/activation/", data={"uid": uid, "token": token} + ) + + response = view(request) + response.render() + + assert response.status_code == 204 diff --git a/src/tests/users/test_user_creation_deletion.py b/src/tests/users/test_user_creation_deletion.py index d318c80..dfd4d25 100644 --- a/src/tests/users/test_user_creation_deletion.py +++ b/src/tests/users/test_user_creation_deletion.py @@ -1,38 +1,45 @@ import pytest -import users from accounts.models import CustomUser +from users import generate_test_users, remove_test_users -def assert_users_created(): - data = users.get_users_json() +def assert_users_exist(USERS: list[CustomUser] = []): + """ + Asserts that each user in the provided list exists in the database. - for user in data["users"]: - USER = CustomUser.objects.filter(username=user["username"]).first() + Args: + USERS (list[CustomUser], optional): A list of CustomUser instances to check for existence. Defaults to an empty list. - # Assert user exists - assert USER + Raises: + AssertionError: If any user in the list does not exist in the database. + """ - if user["is_superuser"]: - # Assert is superuser - assert USER.is_superuser + for USER in USERS: + assert CustomUser.objects.filter(username=USER.username).first() -def assert_users_removed(): - data = users.get_users_json() - for user in data["users"]: - USER = CustomUser.objects.filter(username=user["username"]).first() +def assert_users_removed(USERS: list[CustomUser] = []): + """ + Asserts that the specified users have been removed from the database. - # Assert user does not exist - assert not USER + Args: + USERS (list[CustomUser], optional): A list of user objects (dictionaries) containing at least the 'username' key. + Defaults to an empty list. + + Raises: + AssertionError: If any user in the USERS list still exists in the database. + """ + for USER in USERS: + assert not CustomUser.objects.filter(username=USER.username).first() @pytest.mark.django_db(transaction=True) def test_user_creation_deletion(): """ - Test user creation and deletion + Test multiple instances of user creations and deletions """ - users.generate_test_users() - assert_users_created() - users.remove_test_users() - assert_users_removed() + USERS = generate_test_users() + assert_users_exist(USERS) + remove_test_users(USERS) + assert_users_removed(USERS) diff --git a/src/tests/users/test_user_login_query_self.py b/src/tests/users/test_user_login_query_self.py index 45a977b..b3b0841 100644 --- a/src/tests/users/test_user_login_query_self.py +++ b/src/tests/users/test_user_login_query_self.py @@ -1,6 +1,6 @@ import pytest -import users from rest_framework.test import APIClient +from users import generate_test_users from core.settings import config @@ -10,19 +10,15 @@ client = APIClient() @pytest.mark.django_db(transaction=True) def test_user_login(): """ - Test login + Test login with multiple user accounts """ - data = users.get_users_json() + USERS = generate_test_users(active=True) - # Generate test users - users.generate_test_users() - - for user in data["users"]: + for USER in USERS: login_response = client.post( "/api/v1/accounts/jwt/create/", - {"username": user["username"], - "password": config.DEBUG_USER_PASSWORD}, + {"username": USER.username, "password": config.DEBUG_USER_PASSWORD}, format="json", ).json() diff --git a/src/tests/users/users.json b/src/tests/users/users.json deleted file mode 100644 index 3b46440..0000000 --- a/src/tests/users/users.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "users": [ - { - "username": "admin", - "email": "admin@test.com", - "is_superuser": true, - "first_name": "Test", - "last_name": "Admin" - }, - { - "username": "testuser1", - "email": "user1@test.com", - "is_superuser": false, - "first_name": "Test", - "last_name": "User 1" - }, - { - "username": "testuser2", - "email": "user2@test.com", - "is_superuser": false, - "first_name": "Test", - "last_name": "User 2" - }, - { - "username": "testuser3", - "email": "user3@test.com", - "is_superuser": false, - "first_name": "Test", - "last_name": "User 3" - } - ] -} \ No newline at end of file diff --git a/uv.lock b/uv.lock index c22eab7..34c47f3 100644 --- a/uv.lock +++ b/uv.lock @@ -432,6 +432,7 @@ dependencies = [ { name = "djoser" }, { name = "drf-spectacular" }, { name = "drf-spectacular-sidecar" }, + { name = "faker" }, { name = "gunicorn" }, { name = "isort" }, { name = "pydantic", extra = ["email"] }, @@ -458,6 +459,7 @@ requires-dist = [ { name = "djoser", specifier = ">=2.3.3" }, { name = "drf-spectacular", specifier = ">=0.28.0" }, { name = "drf-spectacular-sidecar", specifier = ">=2025.8.1" }, + { name = "faker", specifier = ">=37.6.0" }, { name = "gunicorn", specifier = ">=23.0.0" }, { name = "isort", specifier = ">=6.0.1" }, { name = "pydantic", extras = ["email"], specifier = ">=2.11.7" }, @@ -483,6 +485,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "faker" +version = "37.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/cd/f7679c20f07d9e2013123b7f7e13809a3450a18d938d58e86081a486ea15/faker-37.6.0.tar.gz", hash = "sha256:0f8cc34f30095184adf87c3c24c45b38b33ad81c35ef6eb0a3118f301143012c", size = 1907960, upload-time = "2025-08-26T15:56:27.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7d/8b50e4ac772719777be33661f4bde320793400a706f5eb214e4de46f093c/faker-37.6.0-py3-none-any.whl", hash = "sha256:3c5209b23d7049d596a51db5d76403a0ccfea6fc294ffa2ecfef6a8843b1e6a7", size = 1949837, upload-time = "2025-08-26T15:56:25.33Z" }, +] + [[package]] name = "gprof2dot" version = "2025.4.14"