473 lines
18 KiB
Python
473 lines
18 KiB
Python
# 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 |