feat(backend): create improved tests
This commit is contained in:
432
backend/tests/repositories/test_schedule.py
Normal file
432
backend/tests/repositories/test_schedule.py
Normal file
@@ -0,0 +1,432 @@
|
||||
# tests/repositories/test_schedule.py
|
||||
import datetime as dt
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.models import Schedule as ScheduleModel, ScheduleStatus
|
||||
from backend.repositories import ScheduleRepository, ServiceRepository
|
||||
from backend.db import DatabaseConnection
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Additional fixtures for Schedule repository testing
|
||||
# ----------------------------------------------------------------------
|
||||
@pytest.fixture
|
||||
def schedule_repo(
|
||||
db_connection: DatabaseConnection,
|
||||
seed_lookup_tables,
|
||||
) -> ScheduleRepository:
|
||||
return ScheduleRepository(db_connection)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_repo(
|
||||
db_connection: DatabaseConnection,
|
||||
seed_lookup_tables,
|
||||
) -> ServiceRepository:
|
||||
return ServiceRepository(db_connection)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_service(service_repo: ServiceRepository) -> int:
|
||||
"""Create a sample service and return its ID."""
|
||||
service = service_repo.create(
|
||||
service_type_id=1, # 9AM from seeded data
|
||||
service_date=dt.date(2025, 9, 15)
|
||||
)
|
||||
return service.ServiceId
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clean_schedules(schedule_repo: ScheduleRepository):
|
||||
"""Clean the Schedules table before tests."""
|
||||
schedule_repo.db.execute("DELETE FROM Schedules")
|
||||
schedule_repo.db._conn.commit()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Basic CRUD Operations
|
||||
# ----------------------------------------------------------------------
|
||||
def test_create_and_get_by_id(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test basic schedule creation and retrieval."""
|
||||
# Create a schedule
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1, # Alice from seeded data
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Verify the created schedule
|
||||
assert isinstance(schedule.ScheduleId, int) and schedule.ScheduleId > 0
|
||||
assert schedule.ServiceId == sample_service
|
||||
assert schedule.MemberId == 1
|
||||
assert schedule.Status == ScheduleStatus.PENDING.value
|
||||
assert schedule.ScheduledAt is not None
|
||||
assert schedule.AcceptedAt is None
|
||||
assert schedule.DeclinedAt is None
|
||||
|
||||
# Retrieve the schedule
|
||||
fetched = schedule_repo.get_by_id(schedule.ScheduleId)
|
||||
assert fetched is not None
|
||||
assert fetched.ServiceId == sample_service
|
||||
assert fetched.MemberId == 1
|
||||
assert fetched.Status == ScheduleStatus.PENDING.value
|
||||
|
||||
|
||||
def test_get_by_id_returns_none_when_missing(schedule_repo: ScheduleRepository):
|
||||
"""Test that get_by_id returns None for non-existent schedules."""
|
||||
assert schedule_repo.get_by_id(9999) is None
|
||||
|
||||
|
||||
def test_create_with_decline_reason(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test creating a schedule with DECLINED status and reason."""
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.DECLINED,
|
||||
reason="Already committed elsewhere"
|
||||
)
|
||||
|
||||
assert schedule.Status == ScheduleStatus.DECLINED.value
|
||||
assert schedule.DeclineReason == "Already committed elsewhere"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# List Operations
|
||||
# ----------------------------------------------------------------------
|
||||
def test_list_all(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test listing all schedules."""
|
||||
# Create multiple schedules
|
||||
schedule1 = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
schedule2 = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=2,
|
||||
status=ScheduleStatus.ACCEPTED
|
||||
)
|
||||
|
||||
schedules = schedule_repo.list_all()
|
||||
assert len(schedules) == 2
|
||||
|
||||
schedule_ids = {s.ScheduleId for s in schedules}
|
||||
assert schedule1.ScheduleId in schedule_ids
|
||||
assert schedule2.ScheduleId in schedule_ids
|
||||
|
||||
|
||||
def test_get_pending_for_service(
|
||||
schedule_repo: ScheduleRepository,
|
||||
service_repo: ServiceRepository,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test getting pending schedules for a specific service."""
|
||||
# Create two services
|
||||
service1 = service_repo.create(service_type_id=1, service_date=dt.date(2025, 9, 15))
|
||||
service2 = service_repo.create(service_type_id=2, service_date=dt.date(2025, 9, 15))
|
||||
|
||||
# Create schedules with different statuses
|
||||
pending1 = schedule_repo.create(
|
||||
service_id=service1.ServiceId,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
accepted1 = schedule_repo.create(
|
||||
service_id=service1.ServiceId,
|
||||
member_id=2,
|
||||
status=ScheduleStatus.ACCEPTED
|
||||
)
|
||||
pending2 = schedule_repo.create(
|
||||
service_id=service2.ServiceId,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Get pending schedules for service1
|
||||
pending_schedules = schedule_repo.get_pending_for_service(service1.ServiceId)
|
||||
assert len(pending_schedules) == 1
|
||||
assert pending_schedules[0].ScheduleId == pending1.ScheduleId
|
||||
assert pending_schedules[0].Status == ScheduleStatus.PENDING.value
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Query Operations
|
||||
# ----------------------------------------------------------------------
|
||||
def test_get_one(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test getting one schedule by member and service ID."""
|
||||
# Create a schedule
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Find it
|
||||
found = schedule_repo.get_one(member_id=1, service_id=sample_service)
|
||||
assert found is not None
|
||||
assert found.ScheduleId == schedule.ScheduleId
|
||||
|
||||
# Try to find non-existent
|
||||
not_found = schedule_repo.get_one(member_id=999, service_id=sample_service)
|
||||
assert not_found is None
|
||||
|
||||
|
||||
def test_has_any(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test checking if member has schedules with specific statuses."""
|
||||
# Create schedules with different statuses
|
||||
schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=2,
|
||||
status=ScheduleStatus.ACCEPTED
|
||||
)
|
||||
|
||||
# Test has_any
|
||||
assert schedule_repo.has_any(1, sample_service, [ScheduleStatus.PENDING])
|
||||
assert schedule_repo.has_any(2, sample_service, [ScheduleStatus.ACCEPTED])
|
||||
assert schedule_repo.has_any(1, sample_service, [ScheduleStatus.PENDING, ScheduleStatus.ACCEPTED])
|
||||
assert not schedule_repo.has_any(1, sample_service, [ScheduleStatus.DECLINED])
|
||||
assert not schedule_repo.has_any(999, sample_service, [ScheduleStatus.PENDING])
|
||||
|
||||
# Test empty statuses list
|
||||
assert not schedule_repo.has_any(1, sample_service, [])
|
||||
|
||||
|
||||
def test_has_schedule_on_date(
|
||||
schedule_repo: ScheduleRepository,
|
||||
service_repo: ServiceRepository,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test checking if member has any schedule on a specific date."""
|
||||
# Create services on different dates
|
||||
service_today = service_repo.create(
|
||||
service_type_id=1,
|
||||
service_date=dt.date(2025, 9, 15)
|
||||
)
|
||||
service_tomorrow = service_repo.create(
|
||||
service_type_id=2,
|
||||
service_date=dt.date(2025, 9, 16)
|
||||
)
|
||||
|
||||
# Create schedule for today
|
||||
schedule_repo.create(
|
||||
service_id=service_today.ServiceId,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Test has_schedule_on_date
|
||||
assert schedule_repo.has_schedule_on_date(1, "2025-09-15")
|
||||
assert not schedule_repo.has_schedule_on_date(1, "2025-09-16")
|
||||
assert not schedule_repo.has_schedule_on_date(2, "2025-09-15")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Status Update Operations
|
||||
# ----------------------------------------------------------------------
|
||||
def test_update_status_to_accepted(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test updating schedule status to accepted."""
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Update to accepted
|
||||
schedule_repo.update_status(
|
||||
schedule_id=schedule.ScheduleId,
|
||||
new_status=ScheduleStatus.ACCEPTED
|
||||
)
|
||||
|
||||
# Verify the update
|
||||
updated = schedule_repo.get_by_id(schedule.ScheduleId)
|
||||
assert updated is not None
|
||||
assert updated.Status == ScheduleStatus.ACCEPTED.value
|
||||
assert updated.DeclinedAt is None
|
||||
assert updated.DeclineReason is None
|
||||
|
||||
|
||||
def test_update_status_to_declined(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test updating schedule status to declined with reason."""
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Update to declined
|
||||
schedule_repo.update_status(
|
||||
schedule_id=schedule.ScheduleId,
|
||||
new_status=ScheduleStatus.DECLINED,
|
||||
reason="Family emergency"
|
||||
)
|
||||
|
||||
# Verify the update
|
||||
updated = schedule_repo.get_by_id(schedule.ScheduleId)
|
||||
assert updated is not None
|
||||
assert updated.Status == ScheduleStatus.DECLINED.value
|
||||
assert updated.DeclinedAt is not None
|
||||
assert updated.DeclineReason == "Family emergency"
|
||||
|
||||
|
||||
def test_mark_accepted(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test marking a schedule as accepted."""
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Mark as accepted
|
||||
schedule_repo.mark_accepted(schedule.ScheduleId)
|
||||
|
||||
# Verify
|
||||
updated = schedule_repo.get_by_id(schedule.ScheduleId)
|
||||
assert updated is not None
|
||||
assert updated.Status == ScheduleStatus.ACCEPTED.value
|
||||
assert updated.AcceptedAt is not None
|
||||
assert updated.DeclinedAt is None
|
||||
assert updated.DeclineReason is None
|
||||
|
||||
|
||||
def test_mark_declined(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test marking a schedule as declined."""
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Mark as declined
|
||||
schedule_repo.mark_declined(
|
||||
schedule.ScheduleId,
|
||||
decline_reason="Unable to attend"
|
||||
)
|
||||
|
||||
# Verify
|
||||
updated = schedule_repo.get_by_id(schedule.ScheduleId)
|
||||
assert updated is not None
|
||||
assert updated.Status == ScheduleStatus.DECLINED.value
|
||||
assert updated.DeclinedAt is not None
|
||||
assert updated.DeclineReason == "Unable to attend"
|
||||
|
||||
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Delete Operations (Added for CLI functionality)
|
||||
# ----------------------------------------------------------------------
|
||||
def test_delete_schedule(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test deleting a schedule."""
|
||||
# Create a schedule
|
||||
schedule = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
# Verify it exists
|
||||
assert schedule_repo.get_by_id(schedule.ScheduleId) is not None
|
||||
|
||||
# Delete it
|
||||
result = schedule_repo.delete_schedule(schedule.ScheduleId)
|
||||
assert result is True
|
||||
|
||||
# Verify it's gone
|
||||
assert schedule_repo.get_by_id(schedule.ScheduleId) is None
|
||||
|
||||
# Try to delete again (should return False)
|
||||
result2 = schedule_repo.delete_schedule(schedule.ScheduleId)
|
||||
assert result2 is False
|
||||
|
||||
|
||||
def test_delete_nonexistent_schedule(schedule_repo: ScheduleRepository):
|
||||
"""Test deleting a non-existent schedule returns False."""
|
||||
result = schedule_repo.delete_schedule(9999)
|
||||
assert result is False
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Edge Cases and Error Conditions
|
||||
# ----------------------------------------------------------------------
|
||||
def test_create_with_invalid_foreign_keys(
|
||||
schedule_repo: ScheduleRepository,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test that creating schedule with invalid FKs raises appropriate errors."""
|
||||
# This should fail due to FK constraint (assuming constraints are enforced)
|
||||
with pytest.raises(Exception): # SQLite foreign key constraint error
|
||||
schedule_repo.create(
|
||||
service_id=9999, # Non-existent service
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
|
||||
|
||||
def test_unique_constraint_member_service(
|
||||
schedule_repo: ScheduleRepository,
|
||||
sample_service: int,
|
||||
clean_schedules
|
||||
):
|
||||
"""Test that UNIQUE constraint prevents duplicate member/service schedules."""
|
||||
# Create first schedule
|
||||
schedule1 = schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.PENDING
|
||||
)
|
||||
assert schedule1 is not None
|
||||
|
||||
# Attempting to create second schedule for same member/service should fail
|
||||
with pytest.raises(Exception): # SQLite UNIQUE constraint error
|
||||
schedule_repo.create(
|
||||
service_id=sample_service,
|
||||
member_id=1,
|
||||
status=ScheduleStatus.DECLINED
|
||||
)
|
||||
Reference in New Issue
Block a user