Files
thrillwiki_django_no_react/backend/tests/serializers/test_account_serializers.py

515 lines
17 KiB
Python

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