Files
nimbusflow/backend/tests/repositories/test_service_availability.py

652 lines
28 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# backend/tests/repositories/test_service_availability.py
# ------------------------------------------------------------
# Comprehensive pytest suite for the ServiceAvailabilityRepository.
# ------------------------------------------------------------
import pytest
from typing import List
from backend.models import ServiceAvailability as ServiceAvailabilityModel
from backend.repositories import ServiceAvailabilityRepository
# ----------------------------------------------------------------------
# Helper fixtures for test data
# ----------------------------------------------------------------------
@pytest.fixture
def clean_service_availability(service_availability_repo: ServiceAvailabilityRepository):
"""Clean the ServiceAvailability table for tests that need isolation."""
# Clear any existing service availability records to start fresh
service_availability_repo.db.execute(f"DELETE FROM {service_availability_repo._TABLE}")
service_availability_repo.db._conn.commit()
# ----------------------------------------------------------------------
# 1⃣ Basic CRUD create, get, delete
# ----------------------------------------------------------------------
def test_create_and_get(service_availability_repo: ServiceAvailabilityRepository):
"""Test basic service availability creation and retrieval."""
# Create a new availability record
availability = service_availability_repo.create(member_id=1, service_type_id=1)
# Verify creation
assert isinstance(availability.ServiceAvailabilityId, int)
assert availability.ServiceAvailabilityId > 0
assert availability.MemberId == 1
assert availability.ServiceTypeId == 1
# Retrieve the same record
fetched = service_availability_repo.get(member_id=1, service_type_id=1)
assert fetched is not None
assert fetched.ServiceAvailabilityId == availability.ServiceAvailabilityId
assert fetched.MemberId == 1
assert fetched.ServiceTypeId == 1
def test_get_returns_none_when_missing(service_availability_repo: ServiceAvailabilityRepository):
"""Test that get returns None for nonexistent member/service type pairs."""
result = service_availability_repo.get(member_id=999, service_type_id=999)
assert result is None
def test_create_is_idempotent(service_availability_repo: ServiceAvailabilityRepository):
"""Test that create returns existing record if pair already exists."""
# Create first record
first = service_availability_repo.create(member_id=1, service_type_id=1)
# Create again with same parameters - should return existing record
second = service_availability_repo.create(member_id=1, service_type_id=1)
# Should be the same record
assert first.ServiceAvailabilityId == second.ServiceAvailabilityId
assert first.MemberId == second.MemberId
assert first.ServiceTypeId == second.ServiceTypeId
def test_delete_by_id(service_availability_repo: ServiceAvailabilityRepository):
"""Test deleting availability record by primary key."""
# Create a record
availability = service_availability_repo.create(member_id=1, service_type_id=1)
original_id = availability.ServiceAvailabilityId
# Verify it exists
assert service_availability_repo.get(member_id=1, service_type_id=1) is not None
# Delete it
service_availability_repo.delete(original_id)
# Verify it's gone
assert service_availability_repo.get(member_id=1, service_type_id=1) is None
def test_delete_nonexistent_record(service_availability_repo: ServiceAvailabilityRepository):
"""Test deleting a nonexistent record (should not raise error)."""
# This should not raise an exception
service_availability_repo.delete(99999)
# ----------------------------------------------------------------------
# 2⃣ Grant and revoke operations
# ----------------------------------------------------------------------
def test_grant_and_revoke(service_availability_repo: ServiceAvailabilityRepository):
"""Test the grant and revoke convenience methods."""
# Grant access
granted = service_availability_repo.grant(member_id=1, service_type_id=1)
assert granted.MemberId == 1
assert granted.ServiceTypeId == 1
# Verify it was granted
fetched = service_availability_repo.get(member_id=1, service_type_id=1)
assert fetched is not None
assert fetched.ServiceAvailabilityId == granted.ServiceAvailabilityId
# Revoke access
service_availability_repo.revoke(member_id=1, service_type_id=1)
# Verify it was revoked
assert service_availability_repo.get(member_id=1, service_type_id=1) is None
def test_grant_is_idempotent(service_availability_repo: ServiceAvailabilityRepository):
"""Test that grant is idempotent (multiple calls return same record)."""
# Grant access twice
first_grant = service_availability_repo.grant(member_id=1, service_type_id=1)
second_grant = service_availability_repo.grant(member_id=1, service_type_id=1)
# Should return the same record
assert first_grant.ServiceAvailabilityId == second_grant.ServiceAvailabilityId
assert first_grant.MemberId == second_grant.MemberId
assert first_grant.ServiceTypeId == second_grant.ServiceTypeId
def test_revoke_nonexistent_record(service_availability_repo: ServiceAvailabilityRepository):
"""Test revoking a nonexistent member/service type pair (should not raise error)."""
# This should not raise an exception
service_availability_repo.revoke(member_id=999, service_type_id=999)
# ----------------------------------------------------------------------
# 3⃣ List operations
# ----------------------------------------------------------------------
def test_list_by_member(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test listing all availabilities for a specific member."""
member_id = 1
service_types = [1, 2, 3]
# Grant access to multiple service types
created_records = []
for service_type_id in service_types:
record = service_availability_repo.grant(member_id, service_type_id)
created_records.append(record)
# List all availabilities for the member
member_availabilities = service_availability_repo.list_by_member(member_id)
# Should have all the records we created
assert len(member_availabilities) == 3
member_service_types = {a.ServiceTypeId for a in member_availabilities}
assert member_service_types == set(service_types)
# All should belong to the same member
for availability in member_availabilities:
assert availability.MemberId == member_id
def test_list_by_member_empty(service_availability_repo: ServiceAvailabilityRepository):
"""Test listing availabilities for a member with no records."""
availabilities = service_availability_repo.list_by_member(member_id=999)
assert availabilities == []
def test_list_by_service_type(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test listing all members available for a specific service type."""
service_type_id = 1
member_ids = [1, 2]
# Grant access to multiple members
created_records = []
for member_id in member_ids:
record = service_availability_repo.grant(member_id, service_type_id)
created_records.append(record)
# List all availabilities for the service type
type_availabilities = service_availability_repo.list_by_service_type(service_type_id)
# Should have all the records we created
assert len(type_availabilities) == 2
available_members = {a.MemberId for a in type_availabilities}
assert available_members == set(member_ids)
# All should be for the same service type
for availability in type_availabilities:
assert availability.ServiceTypeId == service_type_id
def test_list_by_service_type_empty(service_availability_repo: ServiceAvailabilityRepository):
"""Test listing availabilities for a service type with no records."""
availabilities = service_availability_repo.list_by_service_type(service_type_id=999)
assert availabilities == []
def test_list_all(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test listing all service availability records."""
# Create multiple records
test_data = [
(1, 1), (1, 2), (1, 3), # Member 1 available for types 1,2,3
(2, 1), (2, 2), # Member 2 available for types 1,2
]
created_records = []
for member_id, service_type_id in test_data:
record = service_availability_repo.grant(member_id, service_type_id)
created_records.append(record)
# List all records
all_records = service_availability_repo.list_all()
# Should have all the records we created
assert len(all_records) == len(test_data)
# Verify all our records are present
created_ids = {r.ServiceAvailabilityId for r in created_records}
fetched_ids = {r.ServiceAvailabilityId for r in all_records}
assert created_ids.issubset(fetched_ids)
def test_list_all_empty_table(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test list_all when table is empty."""
all_records = service_availability_repo.list_all()
assert all_records == []
# ----------------------------------------------------------------------
# 4⃣ members_for_type helper method
# ----------------------------------------------------------------------
def test_members_for_type(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test the members_for_type helper method."""
service_type_id = 1
member_ids = [1, 2] # Valid member IDs only
# Grant access to multiple members
for member_id in member_ids:
service_availability_repo.grant(member_id, service_type_id)
# Get member IDs for the service type
available_members = service_availability_repo.members_for_type(service_type_id)
# Should return the member IDs we granted access to
assert set(available_members) == set(member_ids)
# Should return integers (member IDs)
for member_id in available_members:
assert isinstance(member_id, int)
def test_members_for_type_empty(service_availability_repo: ServiceAvailabilityRepository):
"""Test members_for_type with no available members."""
member_ids = service_availability_repo.members_for_type(service_type_id=999)
assert member_ids == []
def test_members_for_type_ordering(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test that members_for_type returns consistent ordering."""
service_type_id = 1
member_ids = [2, 1] # Create in non-sequential order
# Grant access in the specified order
for member_id in member_ids:
service_availability_repo.grant(member_id, service_type_id)
# Get member IDs multiple times
results = []
for _ in range(3):
available_members = service_availability_repo.members_for_type(service_type_id)
results.append(available_members)
# All results should be identical (consistent ordering)
for i in range(1, len(results)):
assert results[0] == results[i]
# Should contain all our member IDs
assert set(results[0]) == set(member_ids)
# ----------------------------------------------------------------------
# 5⃣ Edge cases and error conditions
# ----------------------------------------------------------------------
def test_create_with_invalid_member_id(service_availability_repo: ServiceAvailabilityRepository):
"""Test creating availability with invalid member ID raises foreign key error."""
with pytest.raises(Exception): # SQLite IntegrityError for FK constraint
service_availability_repo.create(member_id=999, service_type_id=1)
def test_create_with_invalid_service_type_id(service_availability_repo: ServiceAvailabilityRepository):
"""Test creating availability with invalid service type ID raises foreign key error."""
with pytest.raises(Exception): # SQLite IntegrityError for FK constraint
service_availability_repo.create(member_id=1, service_type_id=999)
def test_create_with_negative_ids(service_availability_repo: ServiceAvailabilityRepository):
"""Test creating availability with negative IDs raises foreign key error."""
with pytest.raises(Exception):
service_availability_repo.create(member_id=-1, service_type_id=1)
with pytest.raises(Exception):
service_availability_repo.create(member_id=1, service_type_id=-1)
def test_create_with_zero_ids(service_availability_repo: ServiceAvailabilityRepository):
"""Test creating availability with zero IDs raises foreign key error."""
with pytest.raises(Exception):
service_availability_repo.create(member_id=0, service_type_id=1)
with pytest.raises(Exception):
service_availability_repo.create(member_id=1, service_type_id=0)
def test_get_with_negative_ids(service_availability_repo: ServiceAvailabilityRepository):
"""Test get with negative IDs returns None."""
assert service_availability_repo.get(member_id=-1, service_type_id=1) is None
assert service_availability_repo.get(member_id=1, service_type_id=-1) is None
def test_get_with_zero_ids(service_availability_repo: ServiceAvailabilityRepository):
"""Test get with zero IDs returns None."""
assert service_availability_repo.get(member_id=0, service_type_id=1) is None
assert service_availability_repo.get(member_id=1, service_type_id=0) is None
def test_delete_with_negative_id(service_availability_repo: ServiceAvailabilityRepository):
"""Test delete with negative ID (should not raise error)."""
service_availability_repo.delete(-1)
def test_delete_with_zero_id(service_availability_repo: ServiceAvailabilityRepository):
"""Test delete with zero ID (should not raise error)."""
service_availability_repo.delete(0)
# ----------------------------------------------------------------------
# 6⃣ Data integrity and consistency tests
# ----------------------------------------------------------------------
def test_unique_constraint_enforcement(service_availability_repo: ServiceAvailabilityRepository):
"""Test that the unique constraint on (MemberId, ServiceTypeId) is enforced."""
# Create first record
first = service_availability_repo.create(member_id=1, service_type_id=1)
# Try to create duplicate - should return existing record due to idempotent behavior
second = service_availability_repo.create(member_id=1, service_type_id=1)
# Should be the same record (idempotent behavior)
assert first.ServiceAvailabilityId == second.ServiceAvailabilityId
# Verify only one record exists
all_records = service_availability_repo.list_all()
matching_records = [r for r in all_records if r.MemberId == 1 and r.ServiceTypeId == 1]
assert len(matching_records) == 1
def test_service_availability_model_data_integrity(service_availability_repo: ServiceAvailabilityRepository):
"""Test that ServiceAvailability model preserves data integrity."""
original_member_id = 1
original_service_type_id = 2
availability = service_availability_repo.create(original_member_id, original_service_type_id)
original_id = availability.ServiceAvailabilityId
# Retrieve and verify data is preserved
retrieved = service_availability_repo.get(original_member_id, original_service_type_id)
assert retrieved is not None
assert retrieved.ServiceAvailabilityId == original_id
assert retrieved.MemberId == original_member_id
assert retrieved.ServiceTypeId == original_service_type_id
# Verify through list operations as well
by_member = service_availability_repo.list_by_member(original_member_id)
matching_by_member = [r for r in by_member if r.ServiceTypeId == original_service_type_id]
assert len(matching_by_member) == 1
assert matching_by_member[0].ServiceAvailabilityId == original_id
by_type = service_availability_repo.list_by_service_type(original_service_type_id)
matching_by_type = [r for r in by_type if r.MemberId == original_member_id]
assert len(matching_by_type) == 1
assert matching_by_type[0].ServiceAvailabilityId == original_id
def test_cross_method_consistency(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test consistency across different query methods."""
# Create a complex availability matrix
test_data = [
(1, 1), (1, 2), # Member 1: types 1,2
(2, 1), (2, 3), # Member 2: types 1,3
]
created_records = []
for member_id, service_type_id in test_data:
record = service_availability_repo.grant(member_id, service_type_id)
created_records.append(record)
# Verify consistency across methods
all_records = service_availability_repo.list_all()
# Check each member's records via list_by_member
for member_id in [1, 2]:
member_records = service_availability_repo.list_by_member(member_id)
member_records_from_all = [r for r in all_records if r.MemberId == member_id]
assert len(member_records) == len(member_records_from_all)
member_ids_direct = {r.ServiceAvailabilityId for r in member_records}
member_ids_from_all = {r.ServiceAvailabilityId for r in member_records_from_all}
assert member_ids_direct == member_ids_from_all
# Check each service type's records via list_by_service_type
for service_type_id in [1, 2, 3]:
type_records = service_availability_repo.list_by_service_type(service_type_id)
type_records_from_all = [r for r in all_records if r.ServiceTypeId == service_type_id]
assert len(type_records) == len(type_records_from_all)
type_ids_direct = {r.ServiceAvailabilityId for r in type_records}
type_ids_from_all = {r.ServiceAvailabilityId for r in type_records_from_all}
assert type_ids_direct == type_ids_from_all
# Verify members_for_type consistency
member_ids = service_availability_repo.members_for_type(service_type_id)
member_ids_from_records = [r.MemberId for r in type_records]
assert set(member_ids) == set(member_ids_from_records)
# ----------------------------------------------------------------------
# 7⃣ Parameterized tests for comprehensive coverage
# ----------------------------------------------------------------------
@pytest.mark.parametrize("member_id,service_type_id", [
(1, 1), (1, 2), (1, 3),
(2, 1), (2, 2), (2, 3),
])
def test_create_and_retrieve_valid_combinations(
service_availability_repo: ServiceAvailabilityRepository,
member_id: int,
service_type_id: int
):
"""Test creating and retrieving various valid member/service type combinations."""
# Create
created = service_availability_repo.create(member_id, service_type_id)
assert created.MemberId == member_id
assert created.ServiceTypeId == service_type_id
assert isinstance(created.ServiceAvailabilityId, int)
assert created.ServiceAvailabilityId > 0
# Retrieve
retrieved = service_availability_repo.get(member_id, service_type_id)
assert retrieved is not None
assert retrieved.ServiceAvailabilityId == created.ServiceAvailabilityId
assert retrieved.MemberId == member_id
assert retrieved.ServiceTypeId == service_type_id
@pytest.mark.parametrize("invalid_member_id,invalid_service_type_id", [
(999, 1), (1, 999), (999, 999),
(-1, 1), (1, -1), (-1, -1),
(0, 1), (1, 0), (0, 0),
])
def test_create_with_invalid_combinations_raises_error(
service_availability_repo: ServiceAvailabilityRepository,
invalid_member_id: int,
invalid_service_type_id: int
):
"""Test creating availability with invalid combinations raises foreign key errors."""
with pytest.raises(Exception): # SQLite IntegrityError for FK constraint
service_availability_repo.create(invalid_member_id, invalid_service_type_id)
@pytest.mark.parametrize("member_id", [1, 2])
def test_list_by_member_various_members(service_availability_repo: ServiceAvailabilityRepository, member_id: int):
"""Test list_by_member with various member IDs."""
# Grant access to a service type
service_availability_repo.grant(member_id, service_type_id=1)
# List availabilities
availabilities = service_availability_repo.list_by_member(member_id)
# Should have at least one record (the one we just granted)
assert len(availabilities) >= 1
# All records should belong to the specified member
for availability in availabilities:
assert availability.MemberId == member_id
@pytest.mark.parametrize("service_type_id", [1, 2, 3])
def test_list_by_service_type_various_types(service_availability_repo: ServiceAvailabilityRepository, service_type_id: int):
"""Test list_by_service_type with various service type IDs."""
# Grant access to a member
service_availability_repo.grant(member_id=1, service_type_id=service_type_id)
# List availabilities
availabilities = service_availability_repo.list_by_service_type(service_type_id)
# Should have at least one record (the one we just granted)
assert len(availabilities) >= 1
# All records should be for the specified service type
for availability in availabilities:
assert availability.ServiceTypeId == service_type_id
# ----------------------------------------------------------------------
# 8⃣ Integration and workflow tests
# ----------------------------------------------------------------------
def test_complete_availability_workflow(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test a complete workflow with multiple operations."""
member_id = 1
service_type_ids = [1, 2, 3]
initial_count = len(service_availability_repo.list_all())
# Step 1: Grant access to multiple service types
granted_records = []
for service_type_id in service_type_ids:
record = service_availability_repo.grant(member_id, service_type_id)
granted_records.append(record)
assert record.MemberId == member_id
assert record.ServiceTypeId == service_type_id
# Step 2: Verify records exist in list_all
all_records = service_availability_repo.list_all()
assert len(all_records) == initial_count + 3
granted_ids = {r.ServiceAvailabilityId for r in granted_records}
all_ids = {r.ServiceAvailabilityId for r in all_records}
assert granted_ids.issubset(all_ids)
# Step 3: Verify via list_by_member
member_records = service_availability_repo.list_by_member(member_id)
member_service_types = {r.ServiceTypeId for r in member_records}
assert set(service_type_ids) == member_service_types
# Step 4: Verify via list_by_service_type and members_for_type
for service_type_id in service_type_ids:
type_records = service_availability_repo.list_by_service_type(service_type_id)
type_member_ids = {r.MemberId for r in type_records}
assert member_id in type_member_ids
member_ids_for_type = service_availability_repo.members_for_type(service_type_id)
assert member_id in member_ids_for_type
# Step 5: Revoke access to one service type
revoked_type = service_type_ids[1] # Revoke access to type 2
service_availability_repo.revoke(member_id, revoked_type)
# Step 6: Verify revocation
assert service_availability_repo.get(member_id, revoked_type) is None
updated_member_records = service_availability_repo.list_by_member(member_id)
updated_service_types = {r.ServiceTypeId for r in updated_member_records}
expected_remaining = set(service_type_ids) - {revoked_type}
assert updated_service_types == expected_remaining
# Step 7: Clean up remaining records
for service_type_id in [1, 3]: # Types 1 and 3 should still exist
service_availability_repo.revoke(member_id, service_type_id)
# Step 8: Verify cleanup
final_member_records = service_availability_repo.list_by_member(member_id)
original_member_records = [r for r in final_member_records if r.ServiceAvailabilityId in granted_ids]
assert len(original_member_records) == 0
def test_complex_multi_member_scenario(service_availability_repo: ServiceAvailabilityRepository, clean_service_availability):
"""Test complex scenarios with multiple members and service types."""
# Create a realistic availability matrix:
# Member 1: Available for all service types (1,2,3)
# Member 2: Available for morning services (1,2)
# Member 3: Available for evening service only (3)
# Member 4: Not available for any services
availability_matrix = [
(1, 1), (1, 2), (1, 3), # Member 1: all services
(2, 1), (2, 2), # Member 2: morning only
# Member 3: no services (doesn't exist in seeded data)
]
# Grant all availabilities
for member_id, service_type_id in availability_matrix:
service_availability_repo.grant(member_id, service_type_id)
# Test service type 1 (should have members 1,2)
type1_members = service_availability_repo.members_for_type(1)
assert set(type1_members) == {1, 2}
# Test service type 2 (should have members 1,2)
type2_members = service_availability_repo.members_for_type(2)
assert set(type2_members) == {1, 2}
# Test service type 3 (should have member 1 only)
type3_members = service_availability_repo.members_for_type(3)
assert set(type3_members) == {1}
# Test member 1 (should have all service types)
member1_records = service_availability_repo.list_by_member(1)
member1_types = {r.ServiceTypeId for r in member1_records}
assert member1_types == {1, 2, 3}
# Test member 2 (should have types 1,2)
member2_records = service_availability_repo.list_by_member(2)
member2_types = {r.ServiceTypeId for r in member2_records}
assert member2_types == {1, 2}
# Test nonexistent member (should have no services)
member3_records = service_availability_repo.list_by_member(3)
assert len(member3_records) == 0
# Simulate removing member 1 from evening service
service_availability_repo.revoke(1, 3)
# Type 3 should now have no members
updated_type3_members = service_availability_repo.members_for_type(3)
assert set(updated_type3_members) == set()
# Member 1 should now only have types 1,2
updated_member1_records = service_availability_repo.list_by_member(1)
updated_member1_types = {r.ServiceTypeId for r in updated_member1_records}
assert updated_member1_types == {1, 2}
def test_service_availability_repository_consistency_under_operations(
service_availability_repo: ServiceAvailabilityRepository,
clean_service_availability
):
"""Test repository consistency under various operations."""
# Create, modify, and delete records while verifying consistency
operations = [
('grant', 1, 1),
('grant', 1, 2),
('grant', 2, 1),
('revoke', 1, 1),
('grant', 1, 3),
('revoke', 2, 1),
('grant', 2, 2),
]
expected_state = set() # Track expected (member_id, service_type_id) pairs
for operation, member_id, service_type_id in operations:
if operation == 'grant':
service_availability_repo.grant(member_id, service_type_id)
expected_state.add((member_id, service_type_id))
elif operation == 'revoke':
service_availability_repo.revoke(member_id, service_type_id)
expected_state.discard((member_id, service_type_id))
# Verify consistency after each operation
all_records = service_availability_repo.list_all()
actual_pairs = {(r.MemberId, r.ServiceTypeId) for r in all_records if (r.MemberId, r.ServiceTypeId) in expected_state or (r.MemberId, r.ServiceTypeId) not in expected_state}
# Filter to only the pairs we've been working with
relevant_actual_pairs = {(r.MemberId, r.ServiceTypeId) for r in all_records
if r.MemberId in [1, 2] and r.ServiceTypeId in [1, 2, 3]}
assert relevant_actual_pairs == expected_state, f"Inconsistency after {operation}({member_id}, {service_type_id})"
# Verify each record can be retrieved individually
for member_id_check, service_type_id_check in expected_state:
record = service_availability_repo.get(member_id_check, service_type_id_check)
assert record is not None, f"Could not retrieve ({member_id_check}, {service_type_id_check})"