feat(backend): create improved tests
This commit is contained in:
@@ -1,69 +1,652 @@
|
||||
# tests/test_service_availability.py
|
||||
# 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
|
||||
|
||||
def test_grant_and_revoke(
|
||||
service_availability_repo,
|
||||
member_repo,
|
||||
service_type_repo,
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 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
|
||||
):
|
||||
"""
|
||||
Verify that:
|
||||
• `grant` adds a new (member, service_type) pair idempotently.
|
||||
• `revoke` removes the pair.
|
||||
• The helper `members_for_type` returns the expected IDs.
|
||||
"""
|
||||
# ------------------------------------------------------------------
|
||||
# Arrange – fetch the IDs we know exist from the fixture.
|
||||
# ------------------------------------------------------------------
|
||||
# Alice is member_id 1, Bob is member_id 2 (AUTOINCREMENT order).
|
||||
alice_id = 1
|
||||
bob_id = 2
|
||||
|
||||
# Service type IDs correspond to the order we inserted them:
|
||||
# 9AM → 1, 11AM → 2, 6PM → 3
|
||||
nine_am_id = 1
|
||||
eleven_am_id = 2
|
||||
six_pm_id = 3
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Act – try granting a *new* availability that wasn't seeded.
|
||||
# We'll give Alice the 11AM slot (she didn't have it before).
|
||||
# ------------------------------------------------------------------
|
||||
new_pair = service_availability_repo.grant(alice_id, eleven_am_id)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Assert – the row exists and the helper returns the right member list.
|
||||
# ------------------------------------------------------------------
|
||||
assert new_pair.MemberId == alice_id
|
||||
assert new_pair.ServiceTypeId == eleven_am_id
|
||||
|
||||
# `members_for_type` should now contain Alice (1) **and** Bob (2) for 11AM.
|
||||
members_for_11am = service_availability_repo.members_for_type(eleven_am_id)
|
||||
assert set(members_for_11am) == {alice_id, bob_id}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revoke the newly added pair and ensure it disappears.
|
||||
# ------------------------------------------------------------------
|
||||
service_availability_repo.revoke(alice_id, eleven_am_id)
|
||||
|
||||
# After revocation the 11AM list should contain **only** Bob.
|
||||
members_after_revoke = service_availability_repo.members_for_type(eleven_am_id)
|
||||
assert members_after_revoke == [bob_id]
|
||||
|
||||
# Also verify that `get` returns None for the removed pair.
|
||||
assert service_availability_repo.get(alice_id, eleven_am_id) is None
|
||||
"""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
|
||||
|
||||
|
||||
def test_list_by_member(service_availability_repo):
|
||||
"""
|
||||
Validate that `list_by_member` returns exactly the slots we seeded.
|
||||
"""
|
||||
# Alice (member_id 1) should have 9AM (1) and 6PM (3)
|
||||
alice_slots = service_availability_repo.list_by_member(1)
|
||||
alice_type_ids = sorted([s.ServiceTypeId for s in alice_slots])
|
||||
assert alice_type_ids == [1, 3]
|
||||
@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)
|
||||
|
||||
# Bob (member_id 2) should have 11AM (2) and 6PM (3)
|
||||
bob_slots = service_availability_repo.list_by_member(2)
|
||||
bob_type_ids = sorted([s.ServiceTypeId for s in bob_slots])
|
||||
assert bob_type_ids == [2, 3]
|
||||
|
||||
@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})"
|
||||
Reference in New Issue
Block a user