feat(backend): create improved tests

This commit is contained in:
2025-08-29 23:50:44 -04:00
parent 1dbfbb9ce6
commit b25191d99a
9 changed files with 3031 additions and 177 deletions

View File

@@ -0,0 +1,566 @@
# backend/tests/repositories/test_service.py
# ------------------------------------------------------------
# Comprehensive pytest suite for the ServiceRepository.
# ------------------------------------------------------------
import pytest
from datetime import date, datetime, timedelta
from typing import List
from backend.models import Service as ServiceModel
from backend.repositories import ServiceRepository
# ----------------------------------------------------------------------
# Helper fixtures for test data
# ----------------------------------------------------------------------
@pytest.fixture
def sample_dates():
"""Return a set of dates for testing."""
today = date.today()
return {
'past': today - timedelta(days=30),
'yesterday': today - timedelta(days=1),
'today': today,
'tomorrow': today + timedelta(days=1),
'next_week': today + timedelta(days=7),
'future': today + timedelta(days=30),
}
@pytest.fixture
def clean_services(service_repo: ServiceRepository):
"""Clean the Services table for tests that need isolation."""
# Clear any existing services to start fresh
service_repo.db.execute(f"DELETE FROM {service_repo._TABLE}")
service_repo.db._conn.commit()
# ----------------------------------------------------------------------
# 1<0F> Basic CRUD  create & get_by_id
# ----------------------------------------------------------------------
def test_create_and_get_by_id(service_repo: ServiceRepository, sample_dates):
"""Test basic service creation and retrieval by ID."""
service = service_repo.create(
service_type_id=1,
service_date=sample_dates['tomorrow']
)
# Verify creation
assert isinstance(service.ServiceId, int) and service.ServiceId > 0
assert service.ServiceTypeId == 1
assert service.ServiceDate == sample_dates['tomorrow']
# Retrieve the same service
fetched = service_repo.get_by_id(service.ServiceId)
assert fetched is not None
assert fetched.ServiceId == service.ServiceId
assert fetched.ServiceTypeId == 1
assert fetched.ServiceDate == sample_dates['tomorrow']
def test_get_by_id_returns_none_when_missing(service_repo: ServiceRepository):
"""Test that get_by_id returns None for nonexistent IDs."""
result = service_repo.get_by_id(99999)
assert result is None
def test_create_with_date_object(service_repo: ServiceRepository):
"""Test creating service with a date object."""
test_date = date(2025, 12, 25)
service = service_repo.create(service_type_id=2, service_date=test_date)
assert service.ServiceDate == test_date
assert service.ServiceTypeId == 2
# ----------------------------------------------------------------------
# 2<0F> list_all  bulk operations
# ----------------------------------------------------------------------
def test_list_all(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test listing all services."""
# Create multiple services
services_data = [
(1, sample_dates['today']),
(2, sample_dates['tomorrow']),
(1, sample_dates['next_week']),
]
created_services = []
for service_type_id, service_date in services_data:
service = service_repo.create(service_type_id, service_date)
created_services.append(service)
all_services = service_repo.list_all()
assert len(all_services) == 3
# Verify all created services are in the list
service_ids = {s.ServiceId for s in all_services}
expected_ids = {s.ServiceId for s in created_services}
assert service_ids == expected_ids
def test_list_all_empty_table(service_repo: ServiceRepository, clean_services):
"""Test list_all when table is empty."""
all_services = service_repo.list_all()
assert all_services == []
# ----------------------------------------------------------------------
# 3<0F> upcoming  date-based filtering
# ----------------------------------------------------------------------
def test_upcoming_default_behavior(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test upcoming() with default parameters (from today, limit 100)."""
# Create services on various dates
past_service = service_repo.create(1, sample_dates['past'])
today_service = service_repo.create(1, sample_dates['today'])
future_service = service_repo.create(1, sample_dates['future'])
upcoming = service_repo.upcoming()
# Should include today and future, but not past
upcoming_ids = {s.ServiceId for s in upcoming}
assert today_service.ServiceId in upcoming_ids
assert future_service.ServiceId in upcoming_ids
assert past_service.ServiceId not in upcoming_ids
def test_upcoming_with_specific_after_date(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test upcoming() with a specific after date."""
# Create services
service_repo.create(1, sample_dates['yesterday'])
tomorrow_service = service_repo.create(1, sample_dates['tomorrow'])
future_service = service_repo.create(1, sample_dates['future'])
# Get services from tomorrow onwards
upcoming = service_repo.upcoming(after=sample_dates['tomorrow'])
upcoming_ids = {s.ServiceId for s in upcoming}
assert tomorrow_service.ServiceId in upcoming_ids
assert future_service.ServiceId in upcoming_ids
assert len(upcoming) == 2
def test_upcoming_with_limit(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test upcoming() with a limit parameter."""
# Create multiple future services
for i in range(5):
service_repo.create(1, sample_dates['today'] + timedelta(days=i))
upcoming = service_repo.upcoming(limit=3)
assert len(upcoming) == 3
def test_upcoming_chronological_order(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test that upcoming() returns services in chronological order."""
# Create services in non-chronological order
service_dates = [
sample_dates['future'],
sample_dates['tomorrow'],
sample_dates['next_week'],
sample_dates['today'],
]
for service_date in service_dates:
service_repo.create(1, service_date)
upcoming = service_repo.upcoming()
# Verify chronological order
for i in range(len(upcoming) - 1):
assert upcoming[i].ServiceDate <= upcoming[i + 1].ServiceDate
def test_upcoming_no_results(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test upcoming() when no future services exist."""
# Create only past services
service_repo.create(1, sample_dates['past'])
service_repo.create(1, sample_dates['yesterday'])
upcoming = service_repo.upcoming()
assert upcoming == []
# ----------------------------------------------------------------------
# 4<0F> by_type  service type filtering
# ----------------------------------------------------------------------
def test_by_type_single_type(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test filtering by a single service type."""
# Create services of different types
type1_service = service_repo.create(1, sample_dates['today'])
type2_service = service_repo.create(2, sample_dates['tomorrow'])
type1_service2 = service_repo.create(1, sample_dates['future'])
type1_services = service_repo.by_type([1])
# Should only include type 1 services
type1_ids = {s.ServiceId for s in type1_services}
assert type1_service.ServiceId in type1_ids
assert type1_service2.ServiceId in type1_ids
assert type2_service.ServiceId not in type1_ids
assert len(type1_services) == 2
def test_by_type_multiple_types(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test filtering by multiple service types."""
# Create services of different types
type1_service = service_repo.create(1, sample_dates['today'])
type2_service = service_repo.create(2, sample_dates['tomorrow'])
type3_service = service_repo.create(3, sample_dates['future'])
multi_type_services = service_repo.by_type([1, 3])
# Should include type 1 and 3, but not type 2
multi_ids = {s.ServiceId for s in multi_type_services}
assert type1_service.ServiceId in multi_ids
assert type3_service.ServiceId in multi_ids
assert type2_service.ServiceId not in multi_ids
assert len(multi_type_services) == 2
def test_by_type_empty_list(service_repo: ServiceRepository):
"""Test by_type() with empty type list returns empty result without DB query."""
result = service_repo.by_type([])
assert result == []
def test_by_type_nonexistent_types(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test by_type() with nonexistent service types."""
service_repo.create(1, sample_dates['today'])
result = service_repo.by_type([999, 1000])
assert result == []
def test_by_type_chronological_order(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test that by_type() returns services in chronological order."""
# Create services of same type in non-chronological order
service_dates = [
sample_dates['future'],
sample_dates['today'],
sample_dates['tomorrow'],
]
for service_date in service_dates:
service_repo.create(1, service_date)
services = service_repo.by_type([1])
# Verify chronological order
for i in range(len(services) - 1):
assert services[i].ServiceDate <= services[i + 1].ServiceDate
# ----------------------------------------------------------------------
# 5<0F> reschedule  update operations
# ----------------------------------------------------------------------
def test_reschedule_service(service_repo: ServiceRepository, sample_dates):
"""Test rescheduling a service to a new date."""
original_date = sample_dates['today']
new_date = sample_dates['future']
service = service_repo.create(1, original_date)
assert service.ServiceDate == original_date
# Reschedule the service
service_repo.reschedule(service.ServiceId, new_date)
# Verify the change
updated_service = service_repo.get_by_id(service.ServiceId)
assert updated_service is not None
assert updated_service.ServiceDate == new_date
assert updated_service.ServiceTypeId == 1 # Should remain unchanged
def test_reschedule_nonexistent_service(service_repo: ServiceRepository, sample_dates):
"""Test rescheduling a nonexistent service (should not raise error)."""
# This should not raise an exception
service_repo.reschedule(99999, sample_dates['tomorrow'])
# ----------------------------------------------------------------------
# 6<0F> Edge cases and error conditions
# ----------------------------------------------------------------------
def test_get_by_id_with_negative_id(service_repo: ServiceRepository):
"""Test get_by_id with negative ID (should return None)."""
result = service_repo.get_by_id(-1)
assert result is None
def test_get_by_id_with_zero_id(service_repo: ServiceRepository):
"""Test get_by_id with zero ID (should return None)."""
result = service_repo.get_by_id(0)
assert result is None
def test_create_with_invalid_service_type_id_raises_error(service_repo: ServiceRepository, sample_dates):
"""Test creating service with invalid service type ID raises foreign key error."""
with pytest.raises(Exception): # SQLite IntegrityError for FK constraint
service_repo.create(999, sample_dates['today'])
def test_create_with_very_old_date(service_repo: ServiceRepository):
"""Test creating service with a very old date."""
very_old_date = date(1900, 1, 1)
service = service_repo.create(1, very_old_date)
assert service.ServiceDate == very_old_date
def test_create_with_far_future_date(service_repo: ServiceRepository):
"""Test creating service with a far future date."""
far_future_date = date(2100, 12, 31)
service = service_repo.create(1, far_future_date)
assert service.ServiceDate == far_future_date
def test_upcoming_with_zero_limit(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test upcoming() with zero limit."""
service_repo.create(1, sample_dates['tomorrow'])
upcoming = service_repo.upcoming(limit=0)
assert upcoming == []
def test_upcoming_with_negative_limit(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test upcoming() with negative limit (SQLite behavior)."""
service_repo.create(1, sample_dates['tomorrow'])
# SQLite treats negative LIMIT as unlimited
upcoming = service_repo.upcoming(limit=-1)
assert len(upcoming) >= 0 # Should not crash
def test_by_type_with_duplicate_type_ids(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test by_type() with duplicate type IDs in the list."""
service1 = service_repo.create(1, sample_dates['today'])
service2 = service_repo.create(2, sample_dates['tomorrow'])
# Pass duplicate type IDs
services = service_repo.by_type([1, 1, 2, 1])
# Should return services of both types (no duplicates in result)
service_ids = {s.ServiceId for s in services}
assert service1.ServiceId in service_ids
assert service2.ServiceId in service_ids
assert len(services) == 2
# ----------------------------------------------------------------------
# 7<0F> Data integrity and consistency tests
# ----------------------------------------------------------------------
def test_service_model_data_integrity(service_repo: ServiceRepository, sample_dates):
"""Test that Service model preserves data integrity."""
original_type_id = 2
original_date = sample_dates['tomorrow']
service = service_repo.create(original_type_id, original_date)
original_id = service.ServiceId
# Retrieve and verify data is preserved
retrieved = service_repo.get_by_id(original_id)
assert retrieved is not None
assert retrieved.ServiceId == original_id
assert retrieved.ServiceTypeId == original_type_id
assert retrieved.ServiceDate == original_date
# Verify through list_all as well
all_services = service_repo.list_all()
matching_services = [s for s in all_services if s.ServiceId == original_id]
assert len(matching_services) == 1
assert matching_services[0].ServiceTypeId == original_type_id
assert matching_services[0].ServiceDate == original_date
def test_multiple_services_same_date_and_type(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test creating multiple services on same date and type."""
# Should be allowed - multiple services can exist for same date/type
service1 = service_repo.create(1, sample_dates['today'])
service2 = service_repo.create(1, sample_dates['today'])
assert service1.ServiceId != service2.ServiceId
assert service1.ServiceTypeId == service2.ServiceTypeId
assert service1.ServiceDate == service2.ServiceDate
# Both should be findable
assert service_repo.get_by_id(service1.ServiceId) is not None
assert service_repo.get_by_id(service2.ServiceId) is not None
# ----------------------------------------------------------------------
# 8<0F> Parameterized tests for comprehensive coverage
# ----------------------------------------------------------------------
@pytest.mark.parametrize("service_type_id", [1, 2, 3])
def test_create_with_valid_service_type_ids(service_repo: ServiceRepository, service_type_id):
"""Test creating services with valid service type IDs."""
test_date = date.today()
service = service_repo.create(service_type_id, test_date)
assert service.ServiceTypeId == service_type_id
assert service.ServiceDate == test_date
assert isinstance(service.ServiceId, int)
assert service.ServiceId > 0
@pytest.mark.parametrize("invalid_service_type_id", [999, -1, 0])
def test_create_with_invalid_service_type_ids_raises_error(service_repo: ServiceRepository, invalid_service_type_id):
"""Test creating services with invalid service type IDs raises foreign key errors."""
test_date = date.today()
with pytest.raises(Exception): # SQLite IntegrityError for FK constraint
service_repo.create(invalid_service_type_id, test_date)
@pytest.mark.parametrize(
"days_offset,should_be_included",
[
(-30, False), # Past
(-1, False), # Yesterday
(0, True), # Today
(1, True), # Tomorrow
(7, True), # Next week
(30, True), # Future
]
)
def test_upcoming_date_filtering(
service_repo: ServiceRepository,
clean_services,
days_offset: int,
should_be_included: bool
):
"""Test upcoming() date filtering logic."""
test_date = date.today() + timedelta(days=days_offset)
service = service_repo.create(1, test_date)
upcoming = service_repo.upcoming()
upcoming_ids = {s.ServiceId for s in upcoming}
if should_be_included:
assert service.ServiceId in upcoming_ids
else:
assert service.ServiceId not in upcoming_ids
@pytest.mark.parametrize("limit", [1, 5, 10, 50, 100])
def test_upcoming_limit_parameter(service_repo: ServiceRepository, clean_services, limit: int):
"""Test upcoming() with various limit values."""
# Create more services than the limit
for i in range(limit + 5):
service_repo.create(1, date.today() + timedelta(days=i))
upcoming = service_repo.upcoming(limit=limit)
assert len(upcoming) == limit
# ----------------------------------------------------------------------
# 9<0F> Integration and workflow tests
# ----------------------------------------------------------------------
def test_complete_service_workflow(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test a complete workflow with multiple operations."""
initial_count = len(service_repo.list_all())
# Step 1: Create a new service
original_date = sample_dates['tomorrow']
service = service_repo.create(2, original_date)
assert service.ServiceTypeId == 2
assert service.ServiceDate == original_date
# Step 2: Verify it exists in list_all
all_services = service_repo.list_all()
assert len(all_services) == initial_count + 1
service_ids = {s.ServiceId for s in all_services}
assert service.ServiceId in service_ids
# Step 3: Find it in upcoming services
upcoming = service_repo.upcoming()
upcoming_ids = {s.ServiceId for s in upcoming}
assert service.ServiceId in upcoming_ids
# Step 4: Find it by type
by_type = service_repo.by_type([2])
by_type_ids = {s.ServiceId for s in by_type}
assert service.ServiceId in by_type_ids
# Step 5: Reschedule it
new_date = sample_dates['future']
service_repo.reschedule(service.ServiceId, new_date)
# Step 6: Verify the reschedule
updated_service = service_repo.get_by_id(service.ServiceId)
assert updated_service is not None
assert updated_service.ServiceDate == new_date
assert updated_service.ServiceTypeId == 2
def test_complex_date_filtering_scenario(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test complex scenarios with multiple date filters."""
# Create services across different dates and types
services_data = [
(1, sample_dates['past']), # Should not appear in upcoming
(1, sample_dates['today']), # Should appear in upcoming
(2, sample_dates['tomorrow']), # Should appear in upcoming
(1, sample_dates['future']), # Should appear in upcoming
(3, sample_dates['next_week']), # Should appear in upcoming
]
created_services = {}
for service_type_id, service_date in services_data:
service = service_repo.create(service_type_id, service_date)
created_services[(service_type_id, service_date)] = service
# Test upcoming from tomorrow
upcoming_from_tomorrow = service_repo.upcoming(after=sample_dates['tomorrow'])
upcoming_dates = {s.ServiceDate for s in upcoming_from_tomorrow}
assert sample_dates['tomorrow'] in upcoming_dates
assert sample_dates['future'] in upcoming_dates
assert sample_dates['next_week'] in upcoming_dates
assert sample_dates['past'] not in upcoming_dates
assert sample_dates['today'] not in upcoming_dates
# Test by specific types
type1_services = service_repo.by_type([1])
type1_dates = {s.ServiceDate for s in type1_services}
assert sample_dates['past'] in type1_dates
assert sample_dates['today'] in type1_dates
assert sample_dates['future'] in type1_dates
# Test combination: upcoming type 1 services (filter by date intersection)
upcoming_all = service_repo.upcoming()
upcoming_type1 = [s for s in upcoming_all if s.ServiceTypeId == 1]
upcoming_type1_dates = {s.ServiceDate for s in upcoming_type1}
assert sample_dates['today'] in upcoming_type1_dates
assert sample_dates['future'] in upcoming_type1_dates
assert sample_dates['past'] not in upcoming_type1_dates
def test_service_repository_consistency(service_repo: ServiceRepository, sample_dates, clean_services):
"""Test repository consistency across different query methods."""
# Create a mix of services
test_services = []
for i in range(5):
service = service_repo.create(
service_type_id=(i % 3) + 1,
service_date=sample_dates['today'] + timedelta(days=i)
)
test_services.append(service)
# All services should be found through list_all
all_services = service_repo.list_all()
all_ids = {s.ServiceId for s in all_services}
test_ids = {s.ServiceId for s in test_services}
assert test_ids.issubset(all_ids)
# Each service should be retrievable individually
for service in test_services:
retrieved = service_repo.get_by_id(service.ServiceId)
assert retrieved is not None
assert retrieved.ServiceId == service.ServiceId
assert retrieved.ServiceTypeId == service.ServiceTypeId
assert retrieved.ServiceDate == service.ServiceDate
# Services should appear in appropriate filtered queries
future_services = service_repo.upcoming(after=sample_dates['tomorrow'])
future_ids = {s.ServiceId for s in future_services}
for service in test_services:
if service.ServiceDate >= sample_dates['tomorrow']:
assert service.ServiceId in future_ids
else:
assert service.ServiceId not in future_ids