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

473 lines
18 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# backend/tests/repositories/test_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