# 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})"