mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-18 11:16:30 +01:00
340 lines
12 KiB
Python
340 lines
12 KiB
Python
"""Tests for validation framework (models, validators, card names)."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from code.web.validation.models import (
|
|
BuildRequest,
|
|
CommanderSearchRequest,
|
|
ThemeValidationRequest,
|
|
OwnedCardsImportRequest,
|
|
BatchBuildRequest,
|
|
CardReplacementRequest,
|
|
PowerBracket,
|
|
OwnedMode,
|
|
CommanderPartnerType,
|
|
)
|
|
from code.web.validation.card_names import CardNameValidator
|
|
from code.web.validation.validators import (
|
|
ThemeValidator,
|
|
PowerBracketValidator,
|
|
ColorIdentityValidator,
|
|
)
|
|
from code.web.validation.messages import ValidationMessages, MSG
|
|
|
|
|
|
class TestBuildRequest:
|
|
"""Test BuildRequest Pydantic model."""
|
|
|
|
def test_minimal_valid_request(self):
|
|
"""Test minimal valid build request."""
|
|
req = BuildRequest(commander="Atraxa, Praetors' Voice")
|
|
|
|
assert req.commander == "Atraxa, Praetors' Voice"
|
|
assert req.themes == []
|
|
assert req.power_bracket == PowerBracket.BRACKET_2
|
|
assert req.owned_mode == OwnedMode.OFF
|
|
|
|
def test_full_valid_request(self):
|
|
"""Test fully populated build request."""
|
|
req = BuildRequest(
|
|
commander="Kess, Dissident Mage",
|
|
themes=["Spellslinger", "Graveyard"],
|
|
power_bracket=PowerBracket.BRACKET_3,
|
|
owned_mode=OwnedMode.PREFER,
|
|
must_include=["Counterspell", "Lightning Bolt"],
|
|
must_exclude=["Armageddon"]
|
|
)
|
|
|
|
assert req.commander == "Kess, Dissident Mage"
|
|
assert len(req.themes) == 2
|
|
assert req.power_bracket == PowerBracket.BRACKET_3
|
|
assert len(req.must_include) == 2
|
|
|
|
def test_commander_whitespace_stripped(self):
|
|
"""Test commander name whitespace is stripped."""
|
|
req = BuildRequest(commander=" Atraxa ")
|
|
assert req.commander == "Atraxa"
|
|
|
|
def test_commander_empty_fails(self):
|
|
"""Test empty commander name fails validation."""
|
|
with pytest.raises(ValidationError):
|
|
BuildRequest(commander="")
|
|
|
|
with pytest.raises(ValidationError):
|
|
BuildRequest(commander=" ")
|
|
|
|
def test_themes_deduplicated(self):
|
|
"""Test themes are deduplicated case-insensitively."""
|
|
req = BuildRequest(
|
|
commander="Test",
|
|
themes=["Spellslinger", "spellslinger", "SPELLSLINGER", "Tokens"]
|
|
)
|
|
|
|
assert len(req.themes) == 2
|
|
assert "Spellslinger" in req.themes
|
|
assert "Tokens" in req.themes
|
|
|
|
def test_partner_validation_requires_name(self):
|
|
"""Test partner mode requires partner name."""
|
|
with pytest.raises(ValidationError, match="Partner mode requires partner_name"):
|
|
BuildRequest(
|
|
commander="Kydele, Chosen of Kruphix",
|
|
partner_mode=CommanderPartnerType.PARTNER
|
|
)
|
|
|
|
def test_partner_valid_with_name(self):
|
|
"""Test partner mode valid with name."""
|
|
req = BuildRequest(
|
|
commander="Kydele, Chosen of Kruphix",
|
|
partner_mode=CommanderPartnerType.PARTNER,
|
|
partner_name="Thrasios, Triton Hero"
|
|
)
|
|
|
|
assert req.partner_mode == CommanderPartnerType.PARTNER
|
|
assert req.partner_name == "Thrasios, Triton Hero"
|
|
|
|
def test_background_requires_name(self):
|
|
"""Test background mode requires background name."""
|
|
with pytest.raises(ValidationError, match="Background mode requires background_name"):
|
|
BuildRequest(
|
|
commander="Erinis, Gloom Stalker",
|
|
partner_mode=CommanderPartnerType.BACKGROUND
|
|
)
|
|
|
|
def test_custom_theme_requires_both(self):
|
|
"""Test custom theme requires both name and tags."""
|
|
with pytest.raises(ValidationError, match="Custom theme requires both name and tags"):
|
|
BuildRequest(
|
|
commander="Test",
|
|
custom_theme_name="My Theme"
|
|
)
|
|
|
|
with pytest.raises(ValidationError, match="Custom theme tags require theme name"):
|
|
BuildRequest(
|
|
commander="Test",
|
|
custom_theme_tags=["Tag1", "Tag2"]
|
|
)
|
|
|
|
|
|
class TestCommanderSearchRequest:
|
|
"""Test CommanderSearchRequest model."""
|
|
|
|
def test_valid_search(self):
|
|
"""Test valid search request."""
|
|
req = CommanderSearchRequest(query="Atraxa")
|
|
assert req.query == "Atraxa"
|
|
assert req.limit == 10
|
|
|
|
def test_custom_limit(self):
|
|
"""Test custom limit."""
|
|
req = CommanderSearchRequest(query="Test", limit=25)
|
|
assert req.limit == 25
|
|
|
|
def test_empty_query_fails(self):
|
|
"""Test empty query fails."""
|
|
with pytest.raises(ValidationError):
|
|
CommanderSearchRequest(query="")
|
|
|
|
def test_limit_bounds(self):
|
|
"""Test limit must be within bounds."""
|
|
with pytest.raises(ValidationError):
|
|
CommanderSearchRequest(query="Test", limit=0)
|
|
|
|
with pytest.raises(ValidationError):
|
|
CommanderSearchRequest(query="Test", limit=101)
|
|
|
|
|
|
class TestCardNameValidator:
|
|
"""Test card name validation and normalization."""
|
|
|
|
def test_normalize_lowercase(self):
|
|
"""Test normalization converts to lowercase."""
|
|
assert CardNameValidator.normalize("Atraxa, Praetors' Voice") == "atraxa, praetors' voice"
|
|
|
|
def test_normalize_removes_diacritics(self):
|
|
"""Test normalization removes diacritics."""
|
|
assert CardNameValidator.normalize("Dánitha Capashen") == "danitha capashen"
|
|
assert CardNameValidator.normalize("Gisela, the Broken Blade") == "gisela, the broken blade"
|
|
|
|
def test_normalize_standardizes_apostrophes(self):
|
|
"""Test normalization standardizes apostrophes."""
|
|
assert CardNameValidator.normalize("Atraxa, Praetors' Voice") == CardNameValidator.normalize("Atraxa, Praetors' Voice")
|
|
assert CardNameValidator.normalize("Atraxa, Praetors` Voice") == CardNameValidator.normalize("Atraxa, Praetors' Voice")
|
|
|
|
def test_normalize_collapses_whitespace(self):
|
|
"""Test normalization collapses whitespace."""
|
|
assert CardNameValidator.normalize("Test Card") == "test card"
|
|
assert CardNameValidator.normalize(" Test ") == "test"
|
|
|
|
def test_validator_caches_normalization(self):
|
|
"""Test validator caches normalized lookups."""
|
|
validator = CardNameValidator()
|
|
validator._card_names = {"Atraxa, Praetors' Voice"}
|
|
validator._normalized_map = {
|
|
"atraxa, praetors' voice": "Atraxa, Praetors' Voice"
|
|
}
|
|
validator._loaded = True
|
|
|
|
# Should find exact match
|
|
assert validator.is_valid("Atraxa, Praetors' Voice")
|
|
|
|
|
|
class TestThemeValidator:
|
|
"""Test theme validation."""
|
|
|
|
def test_validate_themes_separates_valid_invalid(self):
|
|
"""Test validation separates valid from invalid themes."""
|
|
validator = ThemeValidator()
|
|
validator._themes = {"Spellslinger", "spellslinger", "Tokens", "tokens"}
|
|
validator._loaded = True
|
|
|
|
valid, invalid = validator.validate_themes(["Spellslinger", "Invalid", "Tokens"])
|
|
|
|
assert "Spellslinger" in valid
|
|
assert "Tokens" in valid
|
|
assert "Invalid" in invalid
|
|
|
|
|
|
class TestPowerBracketValidator:
|
|
"""Test power bracket validation."""
|
|
|
|
def test_valid_brackets(self):
|
|
"""Test valid bracket values (1-4)."""
|
|
assert PowerBracketValidator.is_valid_bracket(1)
|
|
assert PowerBracketValidator.is_valid_bracket(2)
|
|
assert PowerBracketValidator.is_valid_bracket(3)
|
|
assert PowerBracketValidator.is_valid_bracket(4)
|
|
|
|
def test_invalid_brackets(self):
|
|
"""Test invalid bracket values."""
|
|
assert not PowerBracketValidator.is_valid_bracket(0)
|
|
assert not PowerBracketValidator.is_valid_bracket(5)
|
|
assert not PowerBracketValidator.is_valid_bracket(-1)
|
|
|
|
|
|
class TestColorIdentityValidator:
|
|
"""Test color identity validation."""
|
|
|
|
def test_parse_comma_separated(self):
|
|
"""Test parsing comma-separated colors."""
|
|
colors = ColorIdentityValidator.parse_colors("W,U,B")
|
|
assert colors == {"W", "U", "B"}
|
|
|
|
def test_parse_concatenated(self):
|
|
"""Test parsing concatenated colors."""
|
|
colors = ColorIdentityValidator.parse_colors("WUB")
|
|
assert colors == {"W", "U", "B"}
|
|
|
|
def test_parse_empty(self):
|
|
"""Test parsing empty string."""
|
|
colors = ColorIdentityValidator.parse_colors("")
|
|
assert colors == set()
|
|
|
|
def test_colorless_subset_any(self):
|
|
"""Test colorless cards valid in any deck."""
|
|
validator = ColorIdentityValidator()
|
|
assert validator.is_subset({"C"}, {"W", "U"})
|
|
assert validator.is_subset(set(), {"R", "G"})
|
|
|
|
def test_subset_validation(self):
|
|
"""Test subset validation."""
|
|
validator = ColorIdentityValidator()
|
|
|
|
# Valid: card colors subset of commander
|
|
assert validator.is_subset({"W", "U"}, {"W", "U", "B"})
|
|
|
|
# Invalid: card has colors not in commander
|
|
assert not validator.is_subset({"W", "U", "B"}, {"W", "U"})
|
|
|
|
|
|
class TestValidationMessages:
|
|
"""Test validation message formatting."""
|
|
|
|
def test_format_commander_invalid(self):
|
|
"""Test commander invalid message formatting."""
|
|
msg = MSG.format_commander_invalid("Test Commander")
|
|
assert "Test Commander" in msg
|
|
assert "not found" in msg
|
|
|
|
def test_format_themes_invalid(self):
|
|
"""Test multiple invalid themes formatting."""
|
|
msg = MSG.format_themes_invalid(["Theme1", "Theme2"])
|
|
assert "Theme1" in msg
|
|
assert "Theme2" in msg
|
|
|
|
def test_format_bracket_exceeded(self):
|
|
"""Test bracket exceeded message formatting."""
|
|
msg = MSG.format_bracket_exceeded("Mana Crypt", 4, 2)
|
|
assert "Mana Crypt" in msg
|
|
assert "4" in msg
|
|
assert "2" in msg
|
|
|
|
def test_format_color_mismatch(self):
|
|
"""Test color mismatch message formatting."""
|
|
msg = MSG.format_color_mismatch("Card", "WUB", "WU")
|
|
assert "Card" in msg
|
|
assert "WUB" in msg
|
|
assert "WU" in msg
|
|
|
|
|
|
class TestBatchBuildRequest:
|
|
"""Test batch build request validation."""
|
|
|
|
def test_valid_batch(self):
|
|
"""Test valid batch request."""
|
|
base = BuildRequest(commander="Test")
|
|
req = BatchBuildRequest(base_config=base, count=5)
|
|
|
|
assert req.count == 5
|
|
assert req.base_config.commander == "Test"
|
|
|
|
def test_count_limit(self):
|
|
"""Test batch count limit."""
|
|
base = BuildRequest(commander="Test")
|
|
|
|
with pytest.raises(ValidationError):
|
|
BatchBuildRequest(base_config=base, count=11)
|
|
|
|
|
|
class TestCardReplacementRequest:
|
|
"""Test card replacement request validation."""
|
|
|
|
def test_valid_replacement(self):
|
|
"""Test valid replacement request."""
|
|
req = CardReplacementRequest(card_name="Sol Ring", reason="Too powerful")
|
|
|
|
assert req.card_name == "Sol Ring"
|
|
assert req.reason == "Too powerful"
|
|
|
|
def test_whitespace_stripped(self):
|
|
"""Test whitespace is stripped."""
|
|
req = CardReplacementRequest(card_name=" Sol Ring ")
|
|
assert req.card_name == "Sol Ring"
|
|
|
|
def test_empty_name_fails(self):
|
|
"""Test empty card name fails."""
|
|
with pytest.raises(ValidationError):
|
|
CardReplacementRequest(card_name="")
|
|
|
|
|
|
class TestOwnedCardsImportRequest:
|
|
"""Test owned cards import validation."""
|
|
|
|
def test_valid_import(self):
|
|
"""Test valid import request."""
|
|
req = OwnedCardsImportRequest(format_type="csv", content="Name\nSol Ring\n")
|
|
|
|
assert req.format_type == "csv"
|
|
assert "Sol Ring" in req.content
|
|
|
|
def test_invalid_format(self):
|
|
"""Test invalid format fails."""
|
|
with pytest.raises(ValidationError):
|
|
OwnedCardsImportRequest(format_type="invalid", content="test")
|
|
|
|
def test_empty_content_fails(self):
|
|
"""Test empty content fails."""
|
|
with pytest.raises(ValidationError):
|
|
OwnedCardsImportRequest(format_type="csv", content="")
|