Switch to faker for test data

This commit is contained in:
Keannu Christian Bernasol 2025-09-14 00:35:28 +08:00
parent 1e88f1ba53
commit 7e9501e75f
13 changed files with 190 additions and 126 deletions

View file

@ -27,6 +27,7 @@ dependencies = [
"pytest-django>=4.11.1", "pytest-django>=4.11.1",
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",
"isort>=6.0.1", "isort>=6.0.1",
"faker>=37.6.0",
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]

View file

@ -5,7 +5,6 @@ Common model schemas
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -21,7 +20,3 @@ class CustomUser(AbstractUser):
@property @property
def full_name(self): def full_name(self):
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
@property
def admin_url(self):
return reverse("admin:users_customuser_change", args=(self.pk,))

View file

@ -40,7 +40,7 @@ class CustomUserSerializer(BaseUserSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class UserRegistrationSerializer(serializers.ModelSerializer): class CustomUserRegistrationSerializer(serializers.ModelSerializer):
email = serializers.EmailField(required=True) email = serializers.EmailField(required=True)
username = serializers.CharField(required=True) username = serializers.CharField(required=True)
password = serializers.CharField( password = serializers.CharField(

View file

@ -8,7 +8,9 @@ from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache from django.core.cache import cache
from djoser.conf import settings from djoser.conf import settings
from djoser.views import UserViewSet as DjoserUserViewSet from djoser.views import UserViewSet as DjoserUserViewSet
from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.models import CustomUser from accounts.models import CustomUser
@ -23,6 +25,9 @@ class CustomUserViewSet(DjoserUserViewSet):
token_generator = default_token_generator token_generator = default_token_generator
def get_queryset(self): def get_queryset(self):
"""
Overriden class function for injection of cache invalidation
"""
user = self.request.user user = self.request.user
if user.is_superuser: if user.is_superuser:
@ -43,6 +48,9 @@ class CustomUserViewSet(DjoserUserViewSet):
return CustomUser.objects.none() return CustomUser.objects.none()
def perform_update(self, serializer, *args, **kwargs): def perform_update(self, serializer, *args, **kwargs):
"""
Overriden class function for injection of cache invalidation
"""
user = self.request.user user = self.request.user
super().perform_update(serializer, *args, **kwargs) super().perform_update(serializer, *args, **kwargs)
@ -51,6 +59,9 @@ class CustomUserViewSet(DjoserUserViewSet):
cache.delete(f"users:{user.id}") cache.delete(f"users:{user.id}")
def perform_create(self, serializer, *args, **kwargs): def perform_create(self, serializer, *args, **kwargs):
"""
Overriden class function for injection of cache invalidation
"""
user = serializer.save(*args, **kwargs) user = serializer.save(*args, **kwargs)
# Try-catch block for email sending # Try-catch block for email sending
@ -70,9 +81,14 @@ class CustomUserViewSet(DjoserUserViewSet):
methods=["post"], detail=False, url_path="activation", url_name="activation" methods=["post"], detail=False, url_path="activation", url_name="activation"
) )
def activation(self, request, *args, **kwargs): def activation(self, request, *args, **kwargs):
"""
Overriden class function for injection of cache invalidation
"""
user = self.request.user user = self.request.user
super().activation(request, *args, **kwargs) super().activation(request, *args, **kwargs)
# Clear cache # Clear cache
cache.delete("users") cache.delete("users")
cache.delete(f"users:{user.id}") cache.delete(f"users:{user.id}")
return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -1,7 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from drf_spectacular.views import (SpectacularAPIView, SpectacularRedocView, from drf_spectacular.views import (
SpectacularSwaggerView) SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
from core.settings import config from core.settings import config

View file

@ -6,8 +6,14 @@ import re
from datetime import timedelta from datetime import timedelta
from typing import Literal from typing import Literal
from pydantic import (BaseModel, EmailStr, Field, StrictStr, field_validator, from pydantic import (
model_validator) BaseModel,
EmailStr,
Field,
StrictStr,
field_validator,
model_validator,
)
from pydantic_extra_types.timezone_name import TimeZoneName from pydantic_extra_types.timezone_name import TimeZoneName
@ -62,9 +68,17 @@ class Config(BaseModel):
REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field( REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field(
default=timedelta(days=3), description="Refresh token lifetime in days" 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( DEBUG_USER_PASSWORD: StrictStr = Field(
json_schema_extra={"required": True}, json_schema_extra={"required": DEBUG},
description="Password for test users created during development", description="Password for administrator and test users created during development",
) )
CACHE_USERNAME: StrictStr = Field( CACHE_USERNAME: StrictStr = Field(
json_schema_extra={"required": True}, json_schema_extra={"required": True},

View file

@ -8,7 +8,7 @@ from core.settings import * # noqa: F403
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "test_db.sqlite3", # noqa: F405 "NAME": BASE_DIR / "db.sqlite3", # noqa: F405
} }
} }

View file

@ -2,71 +2,89 @@
Post-migrate signal handlers for creating initial data for accounts app. Post-migrate signal handlers for creating initial data for accounts app.
""" """
import json
import logging import logging
import os from faker import Faker
from accounts.models import CustomUser from accounts.models import CustomUser
from core.settings import TESTS_DIR, config from core.settings import config
logger = logging.getLogger(__name__) 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: USER = CustomUser.objects.create_user(
# Load JSON data username=fake.user_name(),
data = json.loads(f.read()) email=fake.email(),
return data 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() USER = CustomUser.objects.filter(is_superuser=True).first()
if not USER:
for user in data["users"]: CustomUser.objects.create_superuser(
# Check if user already exists username=config.DEBUG_USER_USERNAME,
USER = CustomUser.objects.filter(username=user["username"]).first() email=config.DEBUG_USER_EMAIL,
if not USER: password=config.DEBUG_USER_PASSWORD,
# Create user )
if user["is_superuser"]: return USER
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()
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: USERS = []
data = get_users_json()
for user in data["users"]: # Superuser
# Check if user already exists USERS.append(generate_superuser())
USER = CustomUser.objects.filter(username=user["username"]).first()
if USER: # Regular users
USER.delete() USERS.extend([generate_random_user(active=active) for _ in range(count)])
else:
logger.warning( return USERS
f"Skipping user deletion for {user['username']}: Does not exist"
)
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"
)

View file

@ -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

View file

@ -1,38 +1,45 @@
import pytest import pytest
import users
from accounts.models import CustomUser from accounts.models import CustomUser
from users import generate_test_users, remove_test_users
def assert_users_created(): def assert_users_exist(USERS: list[CustomUser] = []):
data = users.get_users_json() """
Asserts that each user in the provided list exists in the database.
for user in data["users"]: Args:
USER = CustomUser.objects.filter(username=user["username"]).first() USERS (list[CustomUser], optional): A list of CustomUser instances to check for existence. Defaults to an empty list.
# Assert user exists Raises:
assert USER AssertionError: If any user in the list does not exist in the database.
"""
if user["is_superuser"]: for USER in USERS:
# Assert is superuser assert CustomUser.objects.filter(username=USER.username).first()
assert USER.is_superuser
def assert_users_removed(): def assert_users_removed(USERS: list[CustomUser] = []):
data = users.get_users_json() """
for user in data["users"]: Asserts that the specified users have been removed from the database.
USER = CustomUser.objects.filter(username=user["username"]).first()
# Assert user does not exist Args:
assert not USER 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) @pytest.mark.django_db(transaction=True)
def test_user_creation_deletion(): def test_user_creation_deletion():
""" """
Test user creation and deletion Test multiple instances of user creations and deletions
""" """
users.generate_test_users() USERS = generate_test_users()
assert_users_created() assert_users_exist(USERS)
users.remove_test_users() remove_test_users(USERS)
assert_users_removed() assert_users_removed(USERS)

View file

@ -1,6 +1,6 @@
import pytest import pytest
import users
from rest_framework.test import APIClient from rest_framework.test import APIClient
from users import generate_test_users
from core.settings import config from core.settings import config
@ -10,19 +10,15 @@ client = APIClient()
@pytest.mark.django_db(transaction=True) @pytest.mark.django_db(transaction=True)
def test_user_login(): 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 for USER in USERS:
users.generate_test_users()
for user in data["users"]:
login_response = client.post( login_response = client.post(
"/api/v1/accounts/jwt/create/", "/api/v1/accounts/jwt/create/",
{"username": user["username"], {"username": USER.username, "password": config.DEBUG_USER_PASSWORD},
"password": config.DEBUG_USER_PASSWORD},
format="json", format="json",
).json() ).json()

View file

@ -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"
}
]
}

14
uv.lock generated
View file

@ -432,6 +432,7 @@ dependencies = [
{ name = "djoser" }, { name = "djoser" },
{ name = "drf-spectacular" }, { name = "drf-spectacular" },
{ name = "drf-spectacular-sidecar" }, { name = "drf-spectacular-sidecar" },
{ name = "faker" },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "isort" }, { name = "isort" },
{ name = "pydantic", extra = ["email"] }, { name = "pydantic", extra = ["email"] },
@ -458,6 +459,7 @@ requires-dist = [
{ name = "djoser", specifier = ">=2.3.3" }, { name = "djoser", specifier = ">=2.3.3" },
{ name = "drf-spectacular", specifier = ">=0.28.0" }, { name = "drf-spectacular", specifier = ">=0.28.0" },
{ name = "drf-spectacular-sidecar", specifier = ">=2025.8.1" }, { name = "drf-spectacular-sidecar", specifier = ">=2025.8.1" },
{ name = "faker", specifier = ">=37.6.0" },
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
{ name = "isort", specifier = ">=6.0.1" }, { name = "isort", specifier = ">=6.0.1" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" }, { 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" }, { 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]] [[package]]
name = "gprof2dot" name = "gprof2dot"
version = "2025.4.14" version = "2025.4.14"