# backend/tests/repositories/test_classification.py # ------------------------------------------------------------ # Comprehensive pytest suite for the ClassificationRepository. # ------------------------------------------------------------ import pytest from typing import List from backend.models import Classification as ClassificationModel from backend.repositories import ClassificationRepository # ---------------------------------------------------------------------- # 1️⃣ Basic CRUD – create & get_by_id # ---------------------------------------------------------------------- def test_create_and_get_by_id(classification_repo): new = classification_repo.create("Countertenor") assert isinstance(new.ClassificationId, int) and new.ClassificationId > 0 assert new.ClassificationName == "Countertenor" fetched = classification_repo.get_by_id(new.ClassificationId) assert fetched is not None assert fetched.ClassificationId == new.ClassificationId assert fetched.ClassificationName == "Countertenor" # ---------------------------------------------------------------------- # 2️⃣ Lookup by name (exact match) # ---------------------------------------------------------------------- def test_find_by_name_existing(classification_repo): soprano = classification_repo.find_by_name("Soprano") assert soprano is not None assert soprano.ClassificationName == "Soprano" def test_find_by_name_missing(classification_repo): missing = classification_repo.find_by_name("Bass") assert missing is None # ---------------------------------------------------------------------- # 3️⃣ List all classifications (ordered alphabetically) # ---------------------------------------------------------------------- def test_list_all(classification_repo): all_rows: list[ClassificationModel] = classification_repo.list_all() assert len(all_rows) == 4 # the four seeded rows names = [row.ClassificationName for row in all_rows] assert names == sorted(names) # ---------------------------------------------------------------------- # 4️⃣ Idempotent helper – ensure_exists # ---------------------------------------------------------------------- def test_ensure_exists_creates_when_missing(classification_repo): before = classification_repo.find_by_name("Bass") assert before is None bass = classification_repo.ensure_exists("Bass") assert isinstance(bass, ClassificationModel) assert bass.ClassificationName == "Bass" # second call returns the same row again = classification_repo.ensure_exists("Bass") assert again.ClassificationId == bass.ClassificationId # total rows = 4 seeded + the new “Bass” all_rows = classification_repo.list_all() assert len(all_rows) == 5 def test_ensure_exists_returns_existing(classification_repo): existing = classification_repo.ensure_exists("Alto / Mezzo") assert existing is not None assert existing.ClassificationName == "Alto / Mezzo" # no extra rows added all_rows = classification_repo.list_all() assert len(all_rows) == 4 # ---------------------------------------------------------------------- # 5️⃣ Delete (optional – demonstrates that the method works) # ---------------------------------------------------------------------- def test_delete(classification_repo): temp = classification_repo.create("TempVoice") assert classification_repo.find_by_name("TempVoice") is not None classification_repo.delete(temp.ClassificationId) assert classification_repo.find_by_name("TempVoice") is None remaining = classification_repo.list_all() remaining_names = {r.ClassificationName for r in remaining} assert "TempVoice" not in remaining_names # the original four seeded names must still be present assert {"Soprano", "Alto / Mezzo", "Tenor", "Baritone"} <= remaining_names # ---------------------------------------------------------------------- # 6️⃣ Edge cases and error conditions # ---------------------------------------------------------------------- def test_get_by_id_returns_none_when_missing(classification_repo: ClassificationRepository): """Test that get_by_id returns None for nonexistent IDs.""" result = classification_repo.get_by_id(99999) assert result is None def test_get_by_id_with_negative_id(classification_repo: ClassificationRepository): """Test get_by_id with negative ID (should return None).""" result = classification_repo.get_by_id(-1) assert result is None def test_get_by_id_with_zero_id(classification_repo: ClassificationRepository): """Test get_by_id with zero ID (should return None).""" result = classification_repo.get_by_id(0) assert result is None def test_find_by_name_case_sensitivity(classification_repo: ClassificationRepository): """Test that find_by_name is case-sensitive.""" # Exact case should work exact = classification_repo.find_by_name("Soprano") assert exact is not None assert exact.ClassificationName == "Soprano" # Different cases should return None assert classification_repo.find_by_name("soprano") is None assert classification_repo.find_by_name("SOPRANO") is None assert classification_repo.find_by_name("SoPrAnO") is None def test_find_by_name_with_whitespace(classification_repo: ClassificationRepository): """Test find_by_name behavior with whitespace variations.""" # Exact name with spaces should work exact = classification_repo.find_by_name("Alto / Mezzo") assert exact is not None # Names with extra whitespace should return None (no trimming) assert classification_repo.find_by_name(" Alto / Mezzo") is None assert classification_repo.find_by_name("Alto / Mezzo ") is None assert classification_repo.find_by_name(" Alto / Mezzo ") is None def test_find_by_name_with_empty_string(classification_repo: ClassificationRepository): """Test find_by_name with empty string.""" result = classification_repo.find_by_name("") assert result is None def test_create_with_empty_string_name(classification_repo: ClassificationRepository): """Test creating a classification with empty string name.""" # This should work - empty string is a valid name empty_name = classification_repo.create("") assert empty_name.ClassificationName == "" assert isinstance(empty_name.ClassificationId, int) assert empty_name.ClassificationId > 0 # Should be able to find it back found = classification_repo.find_by_name("") assert found is not None assert found.ClassificationId == empty_name.ClassificationId def test_create_with_whitespace_only_name(classification_repo: ClassificationRepository): """Test creating a classification with whitespace-only name.""" whitespace_name = classification_repo.create(" ") assert whitespace_name.ClassificationName == " " assert isinstance(whitespace_name.ClassificationId, int) # Should be findable found = classification_repo.find_by_name(" ") assert found is not None assert found.ClassificationId == whitespace_name.ClassificationId def test_create_with_very_long_name(classification_repo: ClassificationRepository): """Test creating a classification with a very long name.""" long_name = "A" * 1000 # 1000 character name long_classification = classification_repo.create(long_name) assert long_classification.ClassificationName == long_name assert isinstance(long_classification.ClassificationId, int) # Should be findable found = classification_repo.find_by_name(long_name) assert found is not None assert found.ClassificationId == long_classification.ClassificationId def test_create_with_special_characters(classification_repo: ClassificationRepository): """Test creating classifications with special characters.""" special_names = [ "Alto/Soprano", "Bass-Baritone", "Counter-tenor (High)", "Mezzo@Soprano", "Coloratura Soprano (1st)", "Basso Profondo & Cantante", "Soprano (🎵)", "Tenor - Lyric/Dramatic", ] created_ids = [] for name in special_names: classification = classification_repo.create(name) assert classification.ClassificationName == name assert isinstance(classification.ClassificationId, int) created_ids.append(classification.ClassificationId) # Should be findable found = classification_repo.find_by_name(name) assert found is not None assert found.ClassificationId == classification.ClassificationId # All IDs should be unique assert len(set(created_ids)) == len(created_ids) def test_delete_nonexistent_classification(classification_repo: ClassificationRepository): """Test deleting a classification that doesn't exist (should not raise error).""" initial_count = len(classification_repo.list_all()) # This should not raise an exception classification_repo.delete(99999) # Count should remain the same final_count = len(classification_repo.list_all()) assert final_count == initial_count def test_delete_with_negative_id(classification_repo: ClassificationRepository): """Test delete with negative ID (should not raise error).""" initial_count = len(classification_repo.list_all()) # This should not raise an exception classification_repo.delete(-1) # Count should remain the same final_count = len(classification_repo.list_all()) assert final_count == initial_count # ---------------------------------------------------------------------- # 7️⃣ Data integrity and consistency tests # ---------------------------------------------------------------------- def test_list_all_ordering_consistency(classification_repo: ClassificationRepository): """Test that list_all always returns results in consistent alphabetical order.""" # Add some classifications with names that test alphabetical ordering test_names = ["Zebra", "Alpha", "Beta", "Zulu", "Apple", "Banana"] created = [] for name in test_names: created.append(classification_repo.create(name)) # Get all classifications multiple times for _ in range(3): all_classifications = classification_repo.list_all() names = [c.ClassificationName for c in all_classifications] # Should be in alphabetical order assert names == sorted(names) # Should contain our test names for name in test_names: assert name in names def test_ensure_exists_idempotency_stress_test(classification_repo: ClassificationRepository): """Test that ensure_exists is truly idempotent under multiple calls.""" name = "StressTestClassification" # Call ensure_exists multiple times results = [] for _ in range(10): result = classification_repo.ensure_exists(name) results.append(result) # All results should be the same object (same ID) first_id = results[0].ClassificationId for result in results: assert result.ClassificationId == first_id assert result.ClassificationName == name # Should only exist once in the database all_classifications = classification_repo.list_all() matching = [c for c in all_classifications if c.ClassificationName == name] assert len(matching) == 1 def test_classification_model_data_integrity(classification_repo: ClassificationRepository): """Test that Classification model preserves data integrity.""" original_name = "DataIntegrityTest" classification = classification_repo.create(original_name) # Verify original data assert classification.ClassificationName == original_name original_id = classification.ClassificationId # Retrieve and verify data is preserved retrieved = classification_repo.get_by_id(original_id) assert retrieved is not None assert retrieved.ClassificationId == original_id assert retrieved.ClassificationName == original_name # Verify through find_by_name as well found_by_name = classification_repo.find_by_name(original_name) assert found_by_name is not None assert found_by_name.ClassificationId == original_id assert found_by_name.ClassificationName == original_name # ---------------------------------------------------------------------- # 8️⃣ Parameterized tests for comprehensive coverage # ---------------------------------------------------------------------- @pytest.mark.parametrize( "test_name,expected_found", [ ("Soprano", True), ("Alto / Mezzo", True), ("Tenor", True), ("Baritone", True), ("Bass", False), ("Countertenor", False), ("Mezzo-Soprano", False), ("", False), ("soprano", False), # case sensitivity ("SOPRANO", False), # case sensitivity ] ) def test_find_by_name_comprehensive( classification_repo: ClassificationRepository, test_name: str, expected_found: bool ): """Comprehensive test of find_by_name with various inputs.""" result = classification_repo.find_by_name(test_name) if expected_found: assert result is not None assert result.ClassificationName == test_name assert isinstance(result.ClassificationId, int) assert result.ClassificationId > 0 else: assert result is None @pytest.mark.parametrize( "test_name", [ "NewClassification1", "Test With Spaces", "Special-Characters!@#", "123NumbersFirst", "Mixed123Characters", "Très_French_Ñame", "Multi\nLine\nName", "Tab\tSeparated", "Quote'Name", 'Double"Quote"Name', ] ) def test_create_and_retrieve_various_names(classification_repo: ClassificationRepository, test_name: str): """Test creating and retrieving classifications with various name formats.""" # Create created = classification_repo.create(test_name) assert created.ClassificationName == test_name assert isinstance(created.ClassificationId, int) assert created.ClassificationId > 0 # Retrieve by ID by_id = classification_repo.get_by_id(created.ClassificationId) assert by_id is not None assert by_id.ClassificationName == test_name assert by_id.ClassificationId == created.ClassificationId # Retrieve by name by_name = classification_repo.find_by_name(test_name) assert by_name is not None assert by_name.ClassificationName == test_name assert by_name.ClassificationId == created.ClassificationId # ---------------------------------------------------------------------- # 9️⃣ Integration and workflow tests # ---------------------------------------------------------------------- def test_complete_classification_workflow(classification_repo: ClassificationRepository): """Test a complete workflow with multiple operations.""" initial_count = len(classification_repo.list_all()) # Step 1: Create a new classification new_name = "WorkflowTest" created = classification_repo.create(new_name) assert created.ClassificationName == new_name # Step 2: Verify it exists in list_all all_classifications = classification_repo.list_all() assert len(all_classifications) == initial_count + 1 assert new_name in [c.ClassificationName for c in all_classifications] # Step 3: Find by name found = classification_repo.find_by_name(new_name) assert found is not None assert found.ClassificationId == created.ClassificationId # Step 4: Get by ID by_id = classification_repo.get_by_id(created.ClassificationId) assert by_id is not None assert by_id.ClassificationName == new_name # Step 5: Use ensure_exists (should return existing) ensured = classification_repo.ensure_exists(new_name) assert ensured.ClassificationId == created.ClassificationId # Step 6: Delete it classification_repo.delete(created.ClassificationId) # Step 7: Verify it's gone assert classification_repo.get_by_id(created.ClassificationId) is None assert classification_repo.find_by_name(new_name) is None final_all = classification_repo.list_all() assert len(final_all) == initial_count assert new_name not in [c.ClassificationName for c in final_all] def test_multiple_classifications_with_similar_names(classification_repo: ClassificationRepository): """Test handling of classifications with similar but distinct names.""" base_name = "TestSimilar" similar_names = [ base_name, base_name + " ", # with trailing space " " + base_name, # with leading space base_name.upper(), # different case base_name.lower(), # different case base_name + "2", # with number base_name + "_Alt", # with suffix ] created_classifications = [] for name in similar_names: classification = classification_repo.create(name) created_classifications.append(classification) assert classification.ClassificationName == name # All should have unique IDs ids = [c.ClassificationId for c in created_classifications] assert len(set(ids)) == len(ids) # All should be findable by their exact names for i, name in enumerate(similar_names): found = classification_repo.find_by_name(name) assert found is not None assert found.ClassificationId == created_classifications[i].ClassificationId assert found.ClassificationName == name def test_classification_repository_thread_safety_simulation(classification_repo: ClassificationRepository): """Simulate concurrent operations to test repository consistency.""" # This simulates what might happen if multiple threads/processes were accessing the repo base_name = "ConcurrencyTest" # Simulate multiple "threads" trying to ensure the same classification exists results = [] for i in range(5): result = classification_repo.ensure_exists(base_name) results.append(result) # All should return the same classification first_id = results[0].ClassificationId for result in results: assert result.ClassificationId == first_id assert result.ClassificationName == base_name # Should only exist once in the database all_matches = [c for c in classification_repo.list_all() if c.ClassificationName == base_name] assert len(all_matches) == 1