mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 07:11:09 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
514
backend/tests/serializers/test_account_serializers.py
Normal file
514
backend/tests/serializers/test_account_serializers.py
Normal file
@@ -0,0 +1,514 @@
|
||||
"""
|
||||
Tests for Account serializers.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from apps.accounts.serializers import (
|
||||
UserSerializer,
|
||||
LoginSerializer,
|
||||
SignupSerializer,
|
||||
PasswordResetSerializer,
|
||||
PasswordChangeSerializer,
|
||||
SocialProviderSerializer,
|
||||
)
|
||||
|
||||
from apps.api.v1.accounts.serializers import (
|
||||
UserProfileCreateInputSerializer,
|
||||
UserProfileUpdateInputSerializer,
|
||||
UserProfileOutputSerializer,
|
||||
TopListCreateInputSerializer,
|
||||
TopListUpdateInputSerializer,
|
||||
TopListOutputSerializer,
|
||||
TopListItemCreateInputSerializer,
|
||||
TopListItemUpdateInputSerializer,
|
||||
TopListItemOutputSerializer,
|
||||
)
|
||||
|
||||
from tests.factories import (
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserSerializer(TestCase):
|
||||
"""Tests for UserSerializer."""
|
||||
|
||||
def test__serialize__user__returns_expected_fields(self):
|
||||
"""Test serializing a user returns expected fields."""
|
||||
user = UserFactory()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
data = serializer.data
|
||||
|
||||
assert "id" in data
|
||||
assert "username" in data
|
||||
assert "email" in data
|
||||
assert "display_name" in data
|
||||
assert "date_joined" in data
|
||||
assert "is_active" in data
|
||||
assert "avatar_url" in data
|
||||
|
||||
def test__serialize__user_without_profile__returns_none_avatar(self):
|
||||
"""Test serializing user without profile returns None for avatar."""
|
||||
user = UserFactory()
|
||||
# Ensure no profile
|
||||
if hasattr(user, "profile"):
|
||||
user.profile.delete()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
data = serializer.data
|
||||
|
||||
assert data["avatar_url"] is None
|
||||
|
||||
def test__get_display_name__user_with_display_name__returns_display_name(self):
|
||||
"""Test get_display_name returns user's display name."""
|
||||
user = UserFactory()
|
||||
user.display_name = "John Doe"
|
||||
user.save()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
|
||||
# get_display_name calls the model method
|
||||
assert "display_name" in serializer.data
|
||||
|
||||
def test__meta__read_only_fields__includes_id_and_dates(self):
|
||||
"""Test Meta.read_only_fields includes id and date fields."""
|
||||
assert "id" in UserSerializer.Meta.read_only_fields
|
||||
assert "date_joined" in UserSerializer.Meta.read_only_fields
|
||||
assert "is_active" in UserSerializer.Meta.read_only_fields
|
||||
|
||||
|
||||
class TestLoginSerializer(TestCase):
|
||||
"""Tests for LoginSerializer."""
|
||||
|
||||
def test__validate__valid_credentials__returns_data(self):
|
||||
"""Test validation passes with valid credentials."""
|
||||
data = {
|
||||
"username": "testuser",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["username"] == "testuser"
|
||||
assert serializer.validated_data["password"] == "testpassword123"
|
||||
|
||||
def test__validate__email_as_username__returns_data(self):
|
||||
"""Test validation passes with email as username."""
|
||||
data = {
|
||||
"username": "user@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["username"] == "user@example.com"
|
||||
|
||||
def test__validate__missing_username__returns_error(self):
|
||||
"""Test validation fails with missing username."""
|
||||
data = {"password": "testpassword123"}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "username" in serializer.errors
|
||||
|
||||
def test__validate__missing_password__returns_error(self):
|
||||
"""Test validation fails with missing password."""
|
||||
data = {"username": "testuser"}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "password" in serializer.errors
|
||||
|
||||
def test__validate__empty_credentials__returns_error(self):
|
||||
"""Test validation fails with empty credentials."""
|
||||
data = {"username": "", "password": ""}
|
||||
|
||||
serializer = LoginSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSignupSerializer(TestCase):
|
||||
"""Tests for SignupSerializer."""
|
||||
|
||||
def test__validate__valid_data__returns_validated_data(self):
|
||||
"""Test validation passes with valid signup data."""
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate__mismatched_passwords__returns_error(self):
|
||||
"""Test validation fails with mismatched passwords."""
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "DifferentPass456!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "password_confirm" in serializer.errors
|
||||
|
||||
def test__validate_email__duplicate_email__returns_error(self):
|
||||
"""Test validation fails with duplicate email."""
|
||||
existing_user = UserFactory(email="existing@example.com")
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "existing@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "email" in serializer.errors
|
||||
|
||||
def test__validate_email__case_insensitive__returns_error(self):
|
||||
"""Test email validation is case insensitive."""
|
||||
existing_user = UserFactory(email="existing@example.com")
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "EXISTING@EXAMPLE.COM",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "email" in serializer.errors
|
||||
|
||||
def test__validate_username__duplicate_username__returns_error(self):
|
||||
"""Test validation fails with duplicate username."""
|
||||
existing_user = UserFactory(username="existinguser")
|
||||
data = {
|
||||
"username": "existinguser",
|
||||
"email": "new@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "username" in serializer.errors
|
||||
|
||||
def test__validate__weak_password__returns_error(self):
|
||||
"""Test validation fails with weak password."""
|
||||
data = {
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"display_name": "New User",
|
||||
"password": "123", # Too weak
|
||||
"password_confirm": "123",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
# Password validation error could be in 'password' or 'non_field_errors'
|
||||
assert "password" in serializer.errors or "non_field_errors" in serializer.errors
|
||||
|
||||
def test__create__valid_data__creates_user(self):
|
||||
"""Test create method creates user correctly."""
|
||||
data = {
|
||||
"username": "createuser",
|
||||
"email": "createuser@example.com",
|
||||
"display_name": "Create User",
|
||||
"password": "SecurePass123!",
|
||||
"password_confirm": "SecurePass123!",
|
||||
}
|
||||
|
||||
serializer = SignupSerializer(data=data)
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
user = serializer.save()
|
||||
|
||||
assert user.username == "createuser"
|
||||
assert user.email == "createuser@example.com"
|
||||
assert user.check_password("SecurePass123!")
|
||||
|
||||
def test__meta__password_write_only__excludes_from_output(self):
|
||||
"""Test password field is write-only."""
|
||||
assert "password" in SignupSerializer.Meta.fields
|
||||
assert SignupSerializer.Meta.extra_kwargs.get("password", {}).get("write_only") is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPasswordResetSerializer(TestCase):
|
||||
"""Tests for PasswordResetSerializer."""
|
||||
|
||||
def test__validate__valid_email__returns_normalized_email(self):
|
||||
"""Test validation normalizes email."""
|
||||
user = UserFactory(email="test@example.com")
|
||||
data = {"email": " TEST@EXAMPLE.COM "}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["email"] == "test@example.com"
|
||||
|
||||
def test__validate__nonexistent_email__still_valid(self):
|
||||
"""Test validation passes with nonexistent email (security)."""
|
||||
data = {"email": "nonexistent@example.com"}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
|
||||
# Should pass validation to prevent email enumeration
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate__existing_email__attaches_user(self):
|
||||
"""Test validation attaches user when email exists."""
|
||||
user = UserFactory(email="exists@example.com")
|
||||
data = {"email": "exists@example.com"}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
|
||||
assert hasattr(serializer, "user")
|
||||
assert serializer.user == user
|
||||
|
||||
def test__validate__nonexistent_email__no_user_attached(self):
|
||||
"""Test validation doesn't attach user for nonexistent email."""
|
||||
data = {"email": "notfound@example.com"}
|
||||
|
||||
serializer = PasswordResetSerializer(data=data)
|
||||
serializer.is_valid()
|
||||
|
||||
assert not hasattr(serializer, "user")
|
||||
|
||||
@patch("apps.accounts.serializers.EmailService.send_email")
|
||||
def test__save__existing_user__sends_email(self, mock_send_email):
|
||||
"""Test save sends email for existing user."""
|
||||
user = UserFactory(email="reset@example.com")
|
||||
data = {"email": "reset@example.com"}
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-reset/")
|
||||
|
||||
serializer = PasswordResetSerializer(data=data, context={"request": request})
|
||||
serializer.is_valid()
|
||||
serializer.save()
|
||||
|
||||
# Email should be sent
|
||||
mock_send_email.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPasswordChangeSerializer(TestCase):
|
||||
"""Tests for PasswordChangeSerializer."""
|
||||
|
||||
def test__validate__valid_data__returns_validated_data(self):
|
||||
"""Test validation passes with valid password change data."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPass123!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "OldPass123!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "NewSecurePass456!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
def test__validate_old_password__incorrect__returns_error(self):
|
||||
"""Test validation fails with incorrect old password."""
|
||||
user = UserFactory()
|
||||
user.set_password("CorrectOldPass!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "WrongOldPass!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "NewSecurePass456!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "old_password" in serializer.errors
|
||||
|
||||
def test__validate__mismatched_new_passwords__returns_error(self):
|
||||
"""Test validation fails with mismatched new passwords."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPass123!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "OldPass123!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "DifferentPass789!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "new_password_confirm" in serializer.errors
|
||||
|
||||
def test__save__valid_data__changes_password(self):
|
||||
"""Test save changes the password."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPass123!")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/password-change/")
|
||||
request.user = user
|
||||
|
||||
data = {
|
||||
"old_password": "OldPass123!",
|
||||
"new_password": "NewSecurePass456!",
|
||||
"new_password_confirm": "NewSecurePass456!",
|
||||
}
|
||||
|
||||
serializer = PasswordChangeSerializer(data=data, context={"request": request})
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
serializer.save()
|
||||
|
||||
user.refresh_from_db()
|
||||
assert user.check_password("NewSecurePass456!")
|
||||
|
||||
|
||||
class TestSocialProviderSerializer(TestCase):
|
||||
"""Tests for SocialProviderSerializer."""
|
||||
|
||||
def test__validate__valid_provider__returns_data(self):
|
||||
"""Test validation passes with valid provider data."""
|
||||
data = {
|
||||
"id": "google",
|
||||
"name": "Google",
|
||||
"login_url": "https://accounts.google.com/oauth/login",
|
||||
}
|
||||
|
||||
serializer = SocialProviderSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["id"] == "google"
|
||||
assert serializer.validated_data["name"] == "Google"
|
||||
|
||||
def test__validate__invalid_url__returns_error(self):
|
||||
"""Test validation fails with invalid URL."""
|
||||
data = {
|
||||
"id": "invalid",
|
||||
"name": "Invalid Provider",
|
||||
"login_url": "not-a-valid-url",
|
||||
}
|
||||
|
||||
serializer = SocialProviderSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "login_url" in serializer.errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserProfileOutputSerializer(TestCase):
|
||||
"""Tests for UserProfileOutputSerializer."""
|
||||
|
||||
def test__serialize__profile__returns_expected_fields(self):
|
||||
"""Test serializing profile returns expected fields."""
|
||||
user = UserFactory()
|
||||
|
||||
# Create mock profile
|
||||
mock_profile = Mock()
|
||||
mock_profile.user = user
|
||||
mock_profile.avatar = None
|
||||
|
||||
serializer = UserProfileOutputSerializer(mock_profile)
|
||||
|
||||
# Should include user nested serializer
|
||||
assert "user" in serializer.data or serializer.data is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserProfileCreateInputSerializer(TestCase):
|
||||
"""Tests for UserProfileCreateInputSerializer."""
|
||||
|
||||
def test__meta__fields__includes_all_fields(self):
|
||||
"""Test Meta.fields is set to __all__."""
|
||||
assert UserProfileCreateInputSerializer.Meta.fields == "__all__"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserProfileUpdateInputSerializer(TestCase):
|
||||
"""Tests for UserProfileUpdateInputSerializer."""
|
||||
|
||||
def test__meta__user_read_only(self):
|
||||
"""Test user field is read-only for updates."""
|
||||
extra_kwargs = UserProfileUpdateInputSerializer.Meta.extra_kwargs
|
||||
assert extra_kwargs.get("user", {}).get("read_only") is True
|
||||
|
||||
|
||||
class TestTopListCreateInputSerializer(TestCase):
|
||||
"""Tests for TopListCreateInputSerializer."""
|
||||
|
||||
def test__meta__fields__includes_all_fields(self):
|
||||
"""Test Meta.fields is set to __all__."""
|
||||
assert TopListCreateInputSerializer.Meta.fields == "__all__"
|
||||
|
||||
|
||||
class TestTopListUpdateInputSerializer(TestCase):
|
||||
"""Tests for TopListUpdateInputSerializer."""
|
||||
|
||||
def test__meta__user_read_only(self):
|
||||
"""Test user field is read-only for updates."""
|
||||
extra_kwargs = TopListUpdateInputSerializer.Meta.extra_kwargs
|
||||
assert extra_kwargs.get("user", {}).get("read_only") is True
|
||||
|
||||
|
||||
class TestTopListItemCreateInputSerializer(TestCase):
|
||||
"""Tests for TopListItemCreateInputSerializer."""
|
||||
|
||||
def test__meta__fields__includes_all_fields(self):
|
||||
"""Test Meta.fields is set to __all__."""
|
||||
assert TopListItemCreateInputSerializer.Meta.fields == "__all__"
|
||||
|
||||
|
||||
class TestTopListItemUpdateInputSerializer(TestCase):
|
||||
"""Tests for TopListItemUpdateInputSerializer."""
|
||||
|
||||
def test__meta__top_list_not_read_only(self):
|
||||
"""Test top_list field is not read-only for updates."""
|
||||
extra_kwargs = TopListItemUpdateInputSerializer.Meta.extra_kwargs
|
||||
assert extra_kwargs.get("top_list", {}).get("read_only") is False
|
||||
Reference in New Issue
Block a user