mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 09:11:09 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
6
backend/tests/services/__init__.py
Normal file
6
backend/tests/services/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Service layer tests.
|
||||
|
||||
This module contains tests for service classes that encapsulate
|
||||
business logic following Django styleguide patterns.
|
||||
"""
|
||||
290
backend/tests/services/test_park_media_service.py
Normal file
290
backend/tests/services/test_park_media_service.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
Tests for ParkMediaService.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
from apps.parks.services.media_service import ParkMediaService
|
||||
from apps.parks.models import ParkPhoto
|
||||
|
||||
from tests.factories import (
|
||||
ParkFactory,
|
||||
ParkPhotoFactory,
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
CloudflareImageFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceUploadPhoto(TestCase):
|
||||
"""Tests for ParkMediaService.upload_photo."""
|
||||
|
||||
@patch("apps.parks.services.media_service.MediaService.validate_image_file")
|
||||
@patch("apps.parks.services.media_service.MediaService.process_image")
|
||||
@patch("apps.parks.services.media_service.MediaService.generate_default_caption")
|
||||
@patch("apps.parks.services.media_service.MediaService.extract_exif_date")
|
||||
def test__upload_photo__valid_image__creates_photo(
|
||||
self,
|
||||
mock_exif,
|
||||
mock_caption,
|
||||
mock_process,
|
||||
mock_validate,
|
||||
):
|
||||
"""Test upload_photo creates photo with valid image."""
|
||||
mock_validate.return_value = (True, None)
|
||||
mock_process.return_value = Mock()
|
||||
mock_caption.return_value = "Photo by testuser"
|
||||
mock_exif.return_value = None
|
||||
|
||||
park = ParkFactory()
|
||||
user = UserFactory()
|
||||
image_file = SimpleUploadedFile(
|
||||
"test.jpg", b"fake image content", content_type="image/jpeg"
|
||||
)
|
||||
|
||||
photo = ParkMediaService.upload_photo(
|
||||
park=park,
|
||||
image_file=image_file,
|
||||
user=user,
|
||||
caption="Test caption",
|
||||
alt_text="Test alt",
|
||||
is_primary=False,
|
||||
auto_approve=True,
|
||||
)
|
||||
|
||||
assert photo.park == park
|
||||
assert photo.caption == "Test caption"
|
||||
assert photo.alt_text == "Test alt"
|
||||
assert photo.uploaded_by == user
|
||||
assert photo.is_approved is True
|
||||
|
||||
@patch("apps.parks.services.media_service.MediaService.validate_image_file")
|
||||
def test__upload_photo__invalid_image__raises_value_error(self, mock_validate):
|
||||
"""Test upload_photo raises ValueError for invalid image."""
|
||||
mock_validate.return_value = (False, "Invalid file type")
|
||||
|
||||
park = ParkFactory()
|
||||
user = UserFactory()
|
||||
image_file = SimpleUploadedFile(
|
||||
"test.txt", b"not an image", content_type="text/plain"
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
ParkMediaService.upload_photo(
|
||||
park=park,
|
||||
image_file=image_file,
|
||||
user=user,
|
||||
)
|
||||
|
||||
assert "Invalid file type" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceGetParkPhotos(TestCase):
|
||||
"""Tests for ParkMediaService.get_park_photos."""
|
||||
|
||||
def test__get_park_photos__approved_only_true__filters_approved(self):
|
||||
"""Test get_park_photos with approved_only filters unapproved photos."""
|
||||
park = ParkFactory()
|
||||
approved = ParkPhotoFactory(park=park, is_approved=True)
|
||||
unapproved = ParkPhotoFactory(park=park, is_approved=False)
|
||||
|
||||
result = ParkMediaService.get_park_photos(park, approved_only=True)
|
||||
|
||||
assert approved in result
|
||||
assert unapproved not in result
|
||||
|
||||
def test__get_park_photos__approved_only_false__returns_all(self):
|
||||
"""Test get_park_photos with approved_only=False returns all photos."""
|
||||
park = ParkFactory()
|
||||
approved = ParkPhotoFactory(park=park, is_approved=True)
|
||||
unapproved = ParkPhotoFactory(park=park, is_approved=False)
|
||||
|
||||
result = ParkMediaService.get_park_photos(park, approved_only=False)
|
||||
|
||||
assert approved in result
|
||||
assert unapproved in result
|
||||
|
||||
def test__get_park_photos__primary_first__orders_primary_first(self):
|
||||
"""Test get_park_photos with primary_first orders primary photos first."""
|
||||
park = ParkFactory()
|
||||
non_primary = ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
|
||||
primary = ParkPhotoFactory(park=park, is_primary=True, is_approved=True)
|
||||
|
||||
result = ParkMediaService.get_park_photos(park, primary_first=True)
|
||||
|
||||
# Primary should be first
|
||||
assert result[0] == primary
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceGetPrimaryPhoto(TestCase):
|
||||
"""Tests for ParkMediaService.get_primary_photo."""
|
||||
|
||||
def test__get_primary_photo__has_primary__returns_primary(self):
|
||||
"""Test get_primary_photo returns primary photo when exists."""
|
||||
park = ParkFactory()
|
||||
primary = ParkPhotoFactory(park=park, is_primary=True, is_approved=True)
|
||||
ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
|
||||
|
||||
result = ParkMediaService.get_primary_photo(park)
|
||||
|
||||
assert result == primary
|
||||
|
||||
def test__get_primary_photo__no_primary__returns_none(self):
|
||||
"""Test get_primary_photo returns None when no primary exists."""
|
||||
park = ParkFactory()
|
||||
ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
|
||||
|
||||
result = ParkMediaService.get_primary_photo(park)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test__get_primary_photo__unapproved_primary__returns_none(self):
|
||||
"""Test get_primary_photo ignores unapproved primary photos."""
|
||||
park = ParkFactory()
|
||||
ParkPhotoFactory(park=park, is_primary=True, is_approved=False)
|
||||
|
||||
result = ParkMediaService.get_primary_photo(park)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceSetPrimaryPhoto(TestCase):
|
||||
"""Tests for ParkMediaService.set_primary_photo."""
|
||||
|
||||
def test__set_primary_photo__valid_photo__sets_as_primary(self):
|
||||
"""Test set_primary_photo sets photo as primary."""
|
||||
park = ParkFactory()
|
||||
photo = ParkPhotoFactory(park=park, is_primary=False)
|
||||
|
||||
result = ParkMediaService.set_primary_photo(park, photo)
|
||||
|
||||
photo.refresh_from_db()
|
||||
assert result is True
|
||||
assert photo.is_primary is True
|
||||
|
||||
def test__set_primary_photo__unsets_existing_primary(self):
|
||||
"""Test set_primary_photo unsets existing primary photo."""
|
||||
park = ParkFactory()
|
||||
old_primary = ParkPhotoFactory(park=park, is_primary=True)
|
||||
new_primary = ParkPhotoFactory(park=park, is_primary=False)
|
||||
|
||||
ParkMediaService.set_primary_photo(park, new_primary)
|
||||
|
||||
old_primary.refresh_from_db()
|
||||
new_primary.refresh_from_db()
|
||||
|
||||
assert old_primary.is_primary is False
|
||||
assert new_primary.is_primary is True
|
||||
|
||||
def test__set_primary_photo__wrong_park__returns_false(self):
|
||||
"""Test set_primary_photo returns False for photo from different park."""
|
||||
park1 = ParkFactory()
|
||||
park2 = ParkFactory()
|
||||
photo = ParkPhotoFactory(park=park2)
|
||||
|
||||
result = ParkMediaService.set_primary_photo(park1, photo)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceApprovePhoto(TestCase):
|
||||
"""Tests for ParkMediaService.approve_photo."""
|
||||
|
||||
def test__approve_photo__unapproved_photo__approves_it(self):
|
||||
"""Test approve_photo approves an unapproved photo."""
|
||||
photo = ParkPhotoFactory(is_approved=False)
|
||||
staff_user = StaffUserFactory()
|
||||
|
||||
result = ParkMediaService.approve_photo(photo, staff_user)
|
||||
|
||||
photo.refresh_from_db()
|
||||
assert result is True
|
||||
assert photo.is_approved is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceDeletePhoto(TestCase):
|
||||
"""Tests for ParkMediaService.delete_photo."""
|
||||
|
||||
def test__delete_photo__valid_photo__deletes_it(self):
|
||||
"""Test delete_photo deletes a photo."""
|
||||
photo = ParkPhotoFactory()
|
||||
photo_id = photo.pk
|
||||
staff_user = StaffUserFactory()
|
||||
|
||||
result = ParkMediaService.delete_photo(photo, staff_user)
|
||||
|
||||
assert result is True
|
||||
assert not ParkPhoto.objects.filter(pk=photo_id).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceGetPhotoStats(TestCase):
|
||||
"""Tests for ParkMediaService.get_photo_stats."""
|
||||
|
||||
def test__get_photo_stats__returns_correct_counts(self):
|
||||
"""Test get_photo_stats returns correct statistics."""
|
||||
park = ParkFactory()
|
||||
ParkPhotoFactory(park=park, is_approved=True)
|
||||
ParkPhotoFactory(park=park, is_approved=True)
|
||||
ParkPhotoFactory(park=park, is_approved=False)
|
||||
ParkPhotoFactory(park=park, is_approved=True, is_primary=True)
|
||||
|
||||
stats = ParkMediaService.get_photo_stats(park)
|
||||
|
||||
assert stats["total_photos"] == 4
|
||||
assert stats["approved_photos"] == 3
|
||||
assert stats["pending_photos"] == 1
|
||||
assert stats["has_primary"] is True
|
||||
|
||||
def test__get_photo_stats__no_photos__returns_zeros(self):
|
||||
"""Test get_photo_stats returns zeros when no photos."""
|
||||
park = ParkFactory()
|
||||
|
||||
stats = ParkMediaService.get_photo_stats(park)
|
||||
|
||||
assert stats["total_photos"] == 0
|
||||
assert stats["approved_photos"] == 0
|
||||
assert stats["pending_photos"] == 0
|
||||
assert stats["has_primary"] is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestParkMediaServiceBulkApprovePhotos(TestCase):
|
||||
"""Tests for ParkMediaService.bulk_approve_photos."""
|
||||
|
||||
def test__bulk_approve_photos__multiple_photos__approves_all(self):
|
||||
"""Test bulk_approve_photos approves multiple photos."""
|
||||
park = ParkFactory()
|
||||
photo1 = ParkPhotoFactory(park=park, is_approved=False)
|
||||
photo2 = ParkPhotoFactory(park=park, is_approved=False)
|
||||
photo3 = ParkPhotoFactory(park=park, is_approved=False)
|
||||
staff_user = StaffUserFactory()
|
||||
|
||||
count = ParkMediaService.bulk_approve_photos([photo1, photo2, photo3], staff_user)
|
||||
|
||||
assert count == 3
|
||||
photo1.refresh_from_db()
|
||||
photo2.refresh_from_db()
|
||||
photo3.refresh_from_db()
|
||||
assert photo1.is_approved is True
|
||||
assert photo2.is_approved is True
|
||||
assert photo3.is_approved is True
|
||||
|
||||
def test__bulk_approve_photos__empty_list__returns_zero(self):
|
||||
"""Test bulk_approve_photos with empty list returns 0."""
|
||||
staff_user = StaffUserFactory()
|
||||
|
||||
count = ParkMediaService.bulk_approve_photos([], staff_user)
|
||||
|
||||
assert count == 0
|
||||
381
backend/tests/services/test_ride_service.py
Normal file
381
backend/tests/services/test_ride_service.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
Tests for RideService.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from apps.rides.services import RideService
|
||||
from apps.rides.models import Ride
|
||||
|
||||
from tests.factories import (
|
||||
ParkFactory,
|
||||
RideFactory,
|
||||
RideModelFactory,
|
||||
ParkAreaFactory,
|
||||
UserFactory,
|
||||
ManufacturerCompanyFactory,
|
||||
DesignerCompanyFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceCreateRide(TestCase):
|
||||
"""Tests for RideService.create_ride."""
|
||||
|
||||
def test__create_ride__valid_data__creates_ride(self):
|
||||
"""Test create_ride creates ride with valid data."""
|
||||
park = ParkFactory()
|
||||
user = UserFactory()
|
||||
|
||||
ride = RideService.create_ride(
|
||||
name="Test Ride",
|
||||
park_id=park.pk,
|
||||
description="A test ride",
|
||||
status="OPERATING",
|
||||
category="TR",
|
||||
created_by=user,
|
||||
)
|
||||
|
||||
assert ride.name == "Test Ride"
|
||||
assert ride.park == park
|
||||
assert ride.description == "A test ride"
|
||||
assert ride.status == "OPERATING"
|
||||
assert ride.category == "TR"
|
||||
|
||||
def test__create_ride__with_manufacturer__sets_manufacturer(self):
|
||||
"""Test create_ride sets manufacturer when provided."""
|
||||
park = ParkFactory()
|
||||
manufacturer = ManufacturerCompanyFactory()
|
||||
|
||||
ride = RideService.create_ride(
|
||||
name="Test Ride",
|
||||
park_id=park.pk,
|
||||
category="RC",
|
||||
manufacturer_id=manufacturer.pk,
|
||||
)
|
||||
|
||||
assert ride.manufacturer == manufacturer
|
||||
|
||||
def test__create_ride__with_designer__sets_designer(self):
|
||||
"""Test create_ride sets designer when provided."""
|
||||
park = ParkFactory()
|
||||
designer = DesignerCompanyFactory()
|
||||
|
||||
ride = RideService.create_ride(
|
||||
name="Test Ride",
|
||||
park_id=park.pk,
|
||||
category="RC",
|
||||
designer_id=designer.pk,
|
||||
)
|
||||
|
||||
assert ride.designer == designer
|
||||
|
||||
def test__create_ride__with_ride_model__sets_ride_model(self):
|
||||
"""Test create_ride sets ride model when provided."""
|
||||
park = ParkFactory()
|
||||
ride_model = RideModelFactory()
|
||||
|
||||
ride = RideService.create_ride(
|
||||
name="Test Ride",
|
||||
park_id=park.pk,
|
||||
category="RC",
|
||||
ride_model_id=ride_model.pk,
|
||||
)
|
||||
|
||||
assert ride.ride_model == ride_model
|
||||
|
||||
def test__create_ride__with_park_area__sets_park_area(self):
|
||||
"""Test create_ride sets park area when provided."""
|
||||
park = ParkFactory()
|
||||
area = ParkAreaFactory(park=park)
|
||||
|
||||
ride = RideService.create_ride(
|
||||
name="Test Ride",
|
||||
park_id=park.pk,
|
||||
category="TR",
|
||||
park_area_id=area.pk,
|
||||
)
|
||||
|
||||
assert ride.park_area == area
|
||||
|
||||
def test__create_ride__invalid_park__raises_exception(self):
|
||||
"""Test create_ride raises exception for invalid park."""
|
||||
with pytest.raises(Exception):
|
||||
RideService.create_ride(
|
||||
name="Test Ride",
|
||||
park_id=99999, # Non-existent
|
||||
category="TR",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceUpdateRide(TestCase):
|
||||
"""Tests for RideService.update_ride."""
|
||||
|
||||
def test__update_ride__valid_updates__updates_ride(self):
|
||||
"""Test update_ride updates ride with valid data."""
|
||||
ride = RideFactory(name="Original Name", description="Original desc")
|
||||
|
||||
updated_ride = RideService.update_ride(
|
||||
ride_id=ride.pk,
|
||||
updates={"name": "Updated Name", "description": "Updated desc"},
|
||||
)
|
||||
|
||||
assert updated_ride.name == "Updated Name"
|
||||
assert updated_ride.description == "Updated desc"
|
||||
|
||||
def test__update_ride__partial_updates__updates_only_specified_fields(self):
|
||||
"""Test update_ride only updates specified fields."""
|
||||
ride = RideFactory(name="Original", status="OPERATING")
|
||||
|
||||
updated_ride = RideService.update_ride(
|
||||
ride_id=ride.pk,
|
||||
updates={"name": "New Name"},
|
||||
)
|
||||
|
||||
assert updated_ride.name == "New Name"
|
||||
assert updated_ride.status == "OPERATING" # Unchanged
|
||||
|
||||
def test__update_ride__nonexistent_ride__raises_exception(self):
|
||||
"""Test update_ride raises exception for non-existent ride."""
|
||||
with pytest.raises(Ride.DoesNotExist):
|
||||
RideService.update_ride(
|
||||
ride_id=99999,
|
||||
updates={"name": "New Name"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceCloseRideTemporarily(TestCase):
|
||||
"""Tests for RideService.close_ride_temporarily."""
|
||||
|
||||
def test__close_ride_temporarily__operating_ride__changes_status(self):
|
||||
"""Test close_ride_temporarily changes status to CLOSED_TEMP."""
|
||||
ride = RideFactory(status="OPERATING")
|
||||
user = UserFactory()
|
||||
|
||||
result = RideService.close_ride_temporarily(ride_id=ride.pk, user=user)
|
||||
|
||||
assert result.status == "CLOSED_TEMP"
|
||||
|
||||
def test__close_ride_temporarily__nonexistent_ride__raises_exception(self):
|
||||
"""Test close_ride_temporarily raises exception for non-existent ride."""
|
||||
with pytest.raises(Ride.DoesNotExist):
|
||||
RideService.close_ride_temporarily(ride_id=99999)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceMarkRideSBNO(TestCase):
|
||||
"""Tests for RideService.mark_ride_sbno."""
|
||||
|
||||
def test__mark_ride_sbno__operating_ride__changes_status(self):
|
||||
"""Test mark_ride_sbno changes status to SBNO."""
|
||||
ride = RideFactory(status="OPERATING")
|
||||
user = UserFactory()
|
||||
|
||||
result = RideService.mark_ride_sbno(ride_id=ride.pk, user=user)
|
||||
|
||||
assert result.status == "SBNO"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceScheduleRideClosing(TestCase):
|
||||
"""Tests for RideService.schedule_ride_closing."""
|
||||
|
||||
def test__schedule_ride_closing__valid_data__schedules_closing(self):
|
||||
"""Test schedule_ride_closing schedules ride closing."""
|
||||
from datetime import date, timedelta
|
||||
|
||||
ride = RideFactory(status="OPERATING")
|
||||
user = UserFactory()
|
||||
closing_date = date.today() + timedelta(days=30)
|
||||
|
||||
result = RideService.schedule_ride_closing(
|
||||
ride_id=ride.pk,
|
||||
closing_date=closing_date,
|
||||
post_closing_status="DEMOLISHED",
|
||||
user=user,
|
||||
)
|
||||
|
||||
assert result.status == "CLOSING"
|
||||
assert result.closing_date == closing_date
|
||||
assert result.post_closing_status == "DEMOLISHED"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceCloseRidePermanently(TestCase):
|
||||
"""Tests for RideService.close_ride_permanently."""
|
||||
|
||||
def test__close_ride_permanently__operating_ride__changes_status(self):
|
||||
"""Test close_ride_permanently changes status to CLOSED_PERM."""
|
||||
ride = RideFactory(status="OPERATING")
|
||||
user = UserFactory()
|
||||
|
||||
result = RideService.close_ride_permanently(ride_id=ride.pk, user=user)
|
||||
|
||||
assert result.status == "CLOSED_PERM"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceDemolishRide(TestCase):
|
||||
"""Tests for RideService.demolish_ride."""
|
||||
|
||||
def test__demolish_ride__closed_ride__changes_status(self):
|
||||
"""Test demolish_ride changes status to DEMOLISHED."""
|
||||
ride = RideFactory(status="CLOSED_PERM")
|
||||
user = UserFactory()
|
||||
|
||||
result = RideService.demolish_ride(ride_id=ride.pk, user=user)
|
||||
|
||||
assert result.status == "DEMOLISHED"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceRelocateRide(TestCase):
|
||||
"""Tests for RideService.relocate_ride."""
|
||||
|
||||
def test__relocate_ride__valid_data__relocates_ride(self):
|
||||
"""Test relocate_ride moves ride to new park."""
|
||||
old_park = ParkFactory()
|
||||
new_park = ParkFactory()
|
||||
ride = RideFactory(park=old_park, status="OPERATING")
|
||||
user = UserFactory()
|
||||
|
||||
result = RideService.relocate_ride(
|
||||
ride_id=ride.pk,
|
||||
new_park_id=new_park.pk,
|
||||
user=user,
|
||||
)
|
||||
|
||||
assert result.park == new_park
|
||||
assert result.status == "RELOCATED"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceReopenRide(TestCase):
|
||||
"""Tests for RideService.reopen_ride."""
|
||||
|
||||
def test__reopen_ride__closed_temp_ride__changes_status(self):
|
||||
"""Test reopen_ride changes status to OPERATING."""
|
||||
ride = RideFactory(status="CLOSED_TEMP")
|
||||
user = UserFactory()
|
||||
|
||||
result = RideService.reopen_ride(ride_id=ride.pk, user=user)
|
||||
|
||||
assert result.status == "OPERATING"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRideServiceHandleNewEntitySuggestions(TestCase):
|
||||
"""Tests for RideService.handle_new_entity_suggestions."""
|
||||
|
||||
@patch("apps.rides.services.ModerationService.create_edit_submission_with_queue")
|
||||
def test__handle_new_entity_suggestions__new_manufacturer__creates_submission(
|
||||
self, mock_create_submission
|
||||
):
|
||||
"""Test handle_new_entity_suggestions creates submission for new manufacturer."""
|
||||
mock_submission = Mock()
|
||||
mock_submission.id = 1
|
||||
mock_create_submission.return_value = mock_submission
|
||||
|
||||
user = UserFactory()
|
||||
form_data = {
|
||||
"manufacturer_search": "New Manufacturer",
|
||||
"manufacturer": None,
|
||||
"designer_search": "",
|
||||
"designer": None,
|
||||
"ride_model_search": "",
|
||||
"ride_model": None,
|
||||
}
|
||||
|
||||
result = RideService.handle_new_entity_suggestions(
|
||||
form_data=form_data,
|
||||
submitter=user,
|
||||
)
|
||||
|
||||
assert result["total_submissions"] == 1
|
||||
assert 1 in result["manufacturers"]
|
||||
mock_create_submission.assert_called_once()
|
||||
|
||||
@patch("apps.rides.services.ModerationService.create_edit_submission_with_queue")
|
||||
def test__handle_new_entity_suggestions__new_designer__creates_submission(
|
||||
self, mock_create_submission
|
||||
):
|
||||
"""Test handle_new_entity_suggestions creates submission for new designer."""
|
||||
mock_submission = Mock()
|
||||
mock_submission.id = 2
|
||||
mock_create_submission.return_value = mock_submission
|
||||
|
||||
user = UserFactory()
|
||||
form_data = {
|
||||
"manufacturer_search": "",
|
||||
"manufacturer": None,
|
||||
"designer_search": "New Designer",
|
||||
"designer": None,
|
||||
"ride_model_search": "",
|
||||
"ride_model": None,
|
||||
}
|
||||
|
||||
result = RideService.handle_new_entity_suggestions(
|
||||
form_data=form_data,
|
||||
submitter=user,
|
||||
)
|
||||
|
||||
assert result["total_submissions"] == 1
|
||||
assert 2 in result["designers"]
|
||||
|
||||
@patch("apps.rides.services.ModerationService.create_edit_submission_with_queue")
|
||||
def test__handle_new_entity_suggestions__new_ride_model__creates_submission(
|
||||
self, mock_create_submission
|
||||
):
|
||||
"""Test handle_new_entity_suggestions creates submission for new ride model."""
|
||||
mock_submission = Mock()
|
||||
mock_submission.id = 3
|
||||
mock_create_submission.return_value = mock_submission
|
||||
|
||||
user = UserFactory()
|
||||
manufacturer = ManufacturerCompanyFactory()
|
||||
form_data = {
|
||||
"manufacturer_search": "",
|
||||
"manufacturer": manufacturer,
|
||||
"designer_search": "",
|
||||
"designer": None,
|
||||
"ride_model_search": "New Model",
|
||||
"ride_model": None,
|
||||
}
|
||||
|
||||
result = RideService.handle_new_entity_suggestions(
|
||||
form_data=form_data,
|
||||
submitter=user,
|
||||
)
|
||||
|
||||
assert result["total_submissions"] == 1
|
||||
assert 3 in result["ride_models"]
|
||||
|
||||
def test__handle_new_entity_suggestions__no_new_entities__returns_empty(self):
|
||||
"""Test handle_new_entity_suggestions with no new entities returns empty result."""
|
||||
user = UserFactory()
|
||||
manufacturer = ManufacturerCompanyFactory()
|
||||
form_data = {
|
||||
"manufacturer_search": "Existing Mfr",
|
||||
"manufacturer": manufacturer, # Already selected
|
||||
"designer_search": "",
|
||||
"designer": None,
|
||||
"ride_model_search": "",
|
||||
"ride_model": None,
|
||||
}
|
||||
|
||||
result = RideService.handle_new_entity_suggestions(
|
||||
form_data=form_data,
|
||||
submitter=user,
|
||||
)
|
||||
|
||||
assert result["total_submissions"] == 0
|
||||
assert len(result["manufacturers"]) == 0
|
||||
assert len(result["designers"]) == 0
|
||||
assert len(result["ride_models"]) == 0
|
||||
332
backend/tests/services/test_user_deletion_service.py
Normal file
332
backend/tests/services/test_user_deletion_service.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Tests for UserDeletionService and AccountService.
|
||||
|
||||
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 django.utils import timezone
|
||||
|
||||
from apps.accounts.services import UserDeletionService, AccountService
|
||||
from apps.accounts.models import User
|
||||
|
||||
from tests.factories import (
|
||||
UserFactory,
|
||||
StaffUserFactory,
|
||||
SuperUserFactory,
|
||||
ParkReviewFactory,
|
||||
RideReviewFactory,
|
||||
ParkFactory,
|
||||
RideFactory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserDeletionServiceGetOrCreateDeletedUser(TestCase):
|
||||
"""Tests for UserDeletionService.get_or_create_deleted_user."""
|
||||
|
||||
def test__get_or_create_deleted_user__first_call__creates_user(self):
|
||||
"""Test get_or_create_deleted_user creates deleted user placeholder."""
|
||||
deleted_user = UserDeletionService.get_or_create_deleted_user()
|
||||
|
||||
assert deleted_user.username == UserDeletionService.DELETED_USER_USERNAME
|
||||
assert deleted_user.email == UserDeletionService.DELETED_USER_EMAIL
|
||||
assert deleted_user.is_active is False
|
||||
assert deleted_user.is_banned is True
|
||||
|
||||
def test__get_or_create_deleted_user__second_call__returns_existing(self):
|
||||
"""Test get_or_create_deleted_user returns existing user on subsequent calls."""
|
||||
first_call = UserDeletionService.get_or_create_deleted_user()
|
||||
second_call = UserDeletionService.get_or_create_deleted_user()
|
||||
|
||||
assert first_call.pk == second_call.pk
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserDeletionServiceCanDeleteUser(TestCase):
|
||||
"""Tests for UserDeletionService.can_delete_user."""
|
||||
|
||||
def test__can_delete_user__regular_user__returns_true(self):
|
||||
"""Test can_delete_user returns True for regular user."""
|
||||
user = UserFactory()
|
||||
|
||||
can_delete, reason = UserDeletionService.can_delete_user(user)
|
||||
|
||||
assert can_delete is True
|
||||
assert reason is None
|
||||
|
||||
def test__can_delete_user__superuser__returns_false(self):
|
||||
"""Test can_delete_user returns False for superuser."""
|
||||
user = SuperUserFactory()
|
||||
|
||||
can_delete, reason = UserDeletionService.can_delete_user(user)
|
||||
|
||||
assert can_delete is False
|
||||
assert "superuser" in reason.lower()
|
||||
|
||||
def test__can_delete_user__deleted_user_placeholder__returns_false(self):
|
||||
"""Test can_delete_user returns False for deleted user placeholder."""
|
||||
deleted_user = UserDeletionService.get_or_create_deleted_user()
|
||||
|
||||
can_delete, reason = UserDeletionService.can_delete_user(deleted_user)
|
||||
|
||||
assert can_delete is False
|
||||
assert "placeholder" in reason.lower()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserDeletionServiceDeleteUserPreserveSubmissions(TestCase):
|
||||
"""Tests for UserDeletionService.delete_user_preserve_submissions."""
|
||||
|
||||
def test__delete_user_preserve_submissions__user_with_reviews__preserves_reviews(self):
|
||||
"""Test delete_user_preserve_submissions preserves user's reviews."""
|
||||
user = UserFactory()
|
||||
park = ParkFactory()
|
||||
ride = RideFactory()
|
||||
|
||||
# Create reviews
|
||||
park_review = ParkReviewFactory(user=user, park=park)
|
||||
ride_review = RideReviewFactory(user=user, ride=ride)
|
||||
|
||||
user_pk = user.pk
|
||||
|
||||
result = UserDeletionService.delete_user_preserve_submissions(user)
|
||||
|
||||
# User should be deleted
|
||||
assert not User.objects.filter(pk=user_pk).exists()
|
||||
|
||||
# Reviews should still exist
|
||||
park_review.refresh_from_db()
|
||||
ride_review.refresh_from_db()
|
||||
|
||||
deleted_user = UserDeletionService.get_or_create_deleted_user()
|
||||
assert park_review.user == deleted_user
|
||||
assert ride_review.user == deleted_user
|
||||
|
||||
def test__delete_user_preserve_submissions__returns_summary(self):
|
||||
"""Test delete_user_preserve_submissions returns correct summary."""
|
||||
user = UserFactory()
|
||||
park = ParkFactory()
|
||||
ParkReviewFactory(user=user, park=park)
|
||||
|
||||
result = UserDeletionService.delete_user_preserve_submissions(user)
|
||||
|
||||
assert "deleted_user" in result
|
||||
assert "preserved_submissions" in result
|
||||
assert "transferred_to" in result
|
||||
assert result["preserved_submissions"]["park_reviews"] == 1
|
||||
|
||||
def test__delete_user_preserve_submissions__deleted_user_placeholder__raises_error(self):
|
||||
"""Test delete_user_preserve_submissions raises error for placeholder."""
|
||||
deleted_user = UserDeletionService.get_or_create_deleted_user()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
UserDeletionService.delete_user_preserve_submissions(deleted_user)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAccountServiceValidatePassword(TestCase):
|
||||
"""Tests for AccountService.validate_password."""
|
||||
|
||||
def test__validate_password__valid_password__returns_true(self):
|
||||
"""Test validate_password returns True for valid password."""
|
||||
result = AccountService.validate_password("SecurePass123")
|
||||
|
||||
assert result is True
|
||||
|
||||
def test__validate_password__too_short__returns_false(self):
|
||||
"""Test validate_password returns False for short password."""
|
||||
result = AccountService.validate_password("Short1")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test__validate_password__no_uppercase__returns_false(self):
|
||||
"""Test validate_password returns False for password without uppercase."""
|
||||
result = AccountService.validate_password("lowercase123")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test__validate_password__no_lowercase__returns_false(self):
|
||||
"""Test validate_password returns False for password without lowercase."""
|
||||
result = AccountService.validate_password("UPPERCASE123")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test__validate_password__no_numbers__returns_false(self):
|
||||
"""Test validate_password returns False for password without numbers."""
|
||||
result = AccountService.validate_password("NoNumbers")
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAccountServiceChangePassword(TestCase):
|
||||
"""Tests for AccountService.change_password."""
|
||||
|
||||
def test__change_password__correct_old_password__changes_password(self):
|
||||
"""Test change_password changes password with correct old password."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPassword123")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/change-password/")
|
||||
request.user = user
|
||||
request.session = {}
|
||||
|
||||
with patch.object(AccountService, "_send_password_change_confirmation"):
|
||||
result = AccountService.change_password(
|
||||
user=user,
|
||||
old_password="OldPassword123",
|
||||
new_password="NewPassword456",
|
||||
request=request,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
user.refresh_from_db()
|
||||
assert user.check_password("NewPassword456")
|
||||
|
||||
def test__change_password__incorrect_old_password__returns_error(self):
|
||||
"""Test change_password returns error with incorrect old password."""
|
||||
user = UserFactory()
|
||||
user.set_password("CorrectPassword123")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/change-password/")
|
||||
request.user = user
|
||||
|
||||
result = AccountService.change_password(
|
||||
user=user,
|
||||
old_password="WrongPassword123",
|
||||
new_password="NewPassword456",
|
||||
request=request,
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "incorrect" in result["message"].lower()
|
||||
|
||||
def test__change_password__weak_new_password__returns_error(self):
|
||||
"""Test change_password returns error with weak new password."""
|
||||
user = UserFactory()
|
||||
user.set_password("OldPassword123")
|
||||
user.save()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/change-password/")
|
||||
request.user = user
|
||||
|
||||
result = AccountService.change_password(
|
||||
user=user,
|
||||
old_password="OldPassword123",
|
||||
new_password="weak", # Too weak
|
||||
request=request,
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "8 characters" in result["message"]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAccountServiceInitiateEmailChange(TestCase):
|
||||
"""Tests for AccountService.initiate_email_change."""
|
||||
|
||||
@patch("apps.accounts.services.AccountService._send_email_verification")
|
||||
def test__initiate_email_change__valid_email__initiates_change(self, mock_send):
|
||||
"""Test initiate_email_change initiates email change for valid email."""
|
||||
user = UserFactory()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/change-email/")
|
||||
|
||||
result = AccountService.initiate_email_change(
|
||||
user=user,
|
||||
new_email="newemail@example.com",
|
||||
request=request,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
user.refresh_from_db()
|
||||
assert user.pending_email == "newemail@example.com"
|
||||
mock_send.assert_called_once()
|
||||
|
||||
def test__initiate_email_change__empty_email__returns_error(self):
|
||||
"""Test initiate_email_change returns error for empty email."""
|
||||
user = UserFactory()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/change-email/")
|
||||
|
||||
result = AccountService.initiate_email_change(
|
||||
user=user,
|
||||
new_email="",
|
||||
request=request,
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "required" in result["message"].lower()
|
||||
|
||||
def test__initiate_email_change__duplicate_email__returns_error(self):
|
||||
"""Test initiate_email_change returns error for duplicate email."""
|
||||
existing_user = UserFactory(email="existing@example.com")
|
||||
user = UserFactory()
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.post("/change-email/")
|
||||
|
||||
result = AccountService.initiate_email_change(
|
||||
user=user,
|
||||
new_email="existing@example.com",
|
||||
request=request,
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "already in use" in result["message"].lower()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserDeletionServiceRequestUserDeletion(TestCase):
|
||||
"""Tests for UserDeletionService.request_user_deletion."""
|
||||
|
||||
@patch("apps.accounts.services.UserDeletionService.send_deletion_verification_email")
|
||||
def test__request_user_deletion__regular_user__creates_request(self, mock_send):
|
||||
"""Test request_user_deletion creates deletion request for regular user."""
|
||||
user = UserFactory()
|
||||
|
||||
deletion_request = UserDeletionService.request_user_deletion(user)
|
||||
|
||||
assert deletion_request.user == user
|
||||
assert deletion_request.verification_code is not None
|
||||
mock_send.assert_called_once()
|
||||
|
||||
def test__request_user_deletion__superuser__raises_error(self):
|
||||
"""Test request_user_deletion raises error for superuser."""
|
||||
user = SuperUserFactory()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
UserDeletionService.request_user_deletion(user)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserDeletionServiceCancelDeletionRequest(TestCase):
|
||||
"""Tests for UserDeletionService.cancel_deletion_request."""
|
||||
|
||||
@patch("apps.accounts.services.UserDeletionService.send_deletion_verification_email")
|
||||
def test__cancel_deletion_request__existing_request__cancels_it(self, mock_send):
|
||||
"""Test cancel_deletion_request cancels existing request."""
|
||||
user = UserFactory()
|
||||
UserDeletionService.request_user_deletion(user)
|
||||
|
||||
result = UserDeletionService.cancel_deletion_request(user)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test__cancel_deletion_request__no_request__returns_false(self):
|
||||
"""Test cancel_deletion_request returns False when no request exists."""
|
||||
user = UserFactory()
|
||||
|
||||
result = UserDeletionService.cancel_deletion_request(user)
|
||||
|
||||
assert result is False
|
||||
Reference in New Issue
Block a user