mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat: Add include/exclude card lists feature with web UI, validation, fuzzy matching, and JSON persistence (ALLOW_MUST_HAVES=1)
This commit is contained in:
parent
7ef45252f7
commit
0516260304
39 changed files with 3672 additions and 626 deletions
181
code/tests/test_exclude_cards_integration.py
Normal file
181
code/tests/test_exclude_cards_integration.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
"""
|
||||
Exclude Cards Integration Test
|
||||
|
||||
Comprehensive end-to-end test demonstrating all exclude card features
|
||||
working together: parsing, validation, deck building, export/import,
|
||||
performance, and backward compatibility.
|
||||
"""
|
||||
import time
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
def test_exclude_cards_complete_integration():
|
||||
"""Comprehensive test demonstrating all exclude card features working together."""
|
||||
# Set up test client with feature enabled
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure project root is in sys.path for reliable imports
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# Ensure feature flag is enabled
|
||||
original_value = os.environ.get('ALLOW_MUST_HAVES')
|
||||
os.environ['ALLOW_MUST_HAVES'] = '1'
|
||||
|
||||
try:
|
||||
# Fresh import to pick up environment
|
||||
try:
|
||||
del importlib.sys.modules['code.web.app']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
app_module = importlib.import_module('code.web.app')
|
||||
client = TestClient(app_module.app)
|
||||
|
||||
print("\n=== EXCLUDE CARDS INTEGRATION TEST ===")
|
||||
|
||||
# 1. Test file upload simulation (parsing multi-line input)
|
||||
print("\n1. Testing exclude card parsing (file upload simulation):")
|
||||
exclude_cards_content = """Sol Ring
|
||||
Rhystic Study
|
||||
Smothering Tithe
|
||||
Lightning Bolt
|
||||
Counterspell"""
|
||||
|
||||
from deck_builder.include_exclude_utils import parse_card_list_input
|
||||
parsed_cards = parse_card_list_input(exclude_cards_content)
|
||||
print(f" Parsed {len(parsed_cards)} cards from input")
|
||||
assert len(parsed_cards) == 5
|
||||
assert "Sol Ring" in parsed_cards
|
||||
assert "Rhystic Study" in parsed_cards
|
||||
|
||||
# 2. Test live validation endpoint
|
||||
print("\\n2. Testing live validation API:")
|
||||
start_time = time.time()
|
||||
response = client.post('/build/validate/exclude_cards',
|
||||
data={'exclude_cards': exclude_cards_content})
|
||||
validation_time = time.time() - start_time
|
||||
|
||||
assert response.status_code == 200
|
||||
validation_data = response.json()
|
||||
print(f" Validation response time: {validation_time*1000:.1f}ms")
|
||||
print(f" Validated {validation_data['count']}/{validation_data['limit']} excludes")
|
||||
assert validation_data["count"] == 5
|
||||
assert validation_data["limit"] == 15
|
||||
assert validation_data["over_limit"] is False
|
||||
|
||||
# 3. Test complete deck building workflow with excludes
|
||||
print("\\n3. Testing complete deck building with excludes:")
|
||||
|
||||
# Start session and create deck with excludes
|
||||
r1 = client.get('/build')
|
||||
assert r1.status_code == 200
|
||||
|
||||
form_data = {
|
||||
"name": "Exclude Cards Integration Test",
|
||||
"commander": "Inti, Seneschal of the Sun",
|
||||
"primary_tag": "discard",
|
||||
"bracket": 3,
|
||||
"ramp": 10, "lands": 36, "basic_lands": 18, "creatures": 28,
|
||||
"removal": 10, "wipes": 3, "card_advantage": 8, "protection": 4,
|
||||
"exclude_cards": exclude_cards_content
|
||||
}
|
||||
|
||||
build_start = time.time()
|
||||
r2 = client.post('/build/new', data=form_data)
|
||||
build_time = time.time() - build_start
|
||||
|
||||
assert r2.status_code == 200
|
||||
print(f" Deck build completed in {build_time*1000:.0f}ms")
|
||||
|
||||
# 4. Test JSON export/import (permalinks)
|
||||
print("\\n4. Testing JSON export/import:")
|
||||
|
||||
# Get session cookie and export permalink
|
||||
session_cookie = r2.cookies.get('sid')
|
||||
r3 = client.get('/build/permalink', cookies={'sid': session_cookie})
|
||||
assert r3.status_code == 200
|
||||
|
||||
export_data = r3.json()
|
||||
assert export_data["ok"] is True
|
||||
assert "exclude_cards" in export_data["state"]
|
||||
|
||||
# Verify excluded cards are preserved
|
||||
exported_excludes = export_data["state"]["exclude_cards"]
|
||||
print(f" Exported {len(exported_excludes)} exclude cards in JSON")
|
||||
for card in ["Sol Ring", "Rhystic Study", "Smothering Tithe"]:
|
||||
assert card in exported_excludes
|
||||
|
||||
# Test import (round-trip)
|
||||
token = export_data["permalink"].split("state=")[1]
|
||||
r4 = client.get(f'/build/from?state={token}')
|
||||
assert r4.status_code == 200
|
||||
print(" JSON import successful - round-trip verified")
|
||||
|
||||
# 5. Test performance benchmarks
|
||||
print("\\n5. Testing performance benchmarks:")
|
||||
|
||||
# Parsing performance
|
||||
parse_times = []
|
||||
for _ in range(10):
|
||||
start = time.time()
|
||||
parse_card_list_input(exclude_cards_content)
|
||||
parse_times.append((time.time() - start) * 1000)
|
||||
|
||||
avg_parse_time = sum(parse_times) / len(parse_times)
|
||||
print(f" Average parse time: {avg_parse_time:.2f}ms (target: <10ms)")
|
||||
assert avg_parse_time < 10.0
|
||||
|
||||
# Validation API performance
|
||||
validation_times = []
|
||||
for _ in range(5):
|
||||
start = time.time()
|
||||
client.post('/build/validate/exclude_cards', data={'exclude_cards': exclude_cards_content})
|
||||
validation_times.append((time.time() - start) * 1000)
|
||||
|
||||
avg_validation_time = sum(validation_times) / len(validation_times)
|
||||
print(f" Average validation time: {avg_validation_time:.1f}ms (target: <100ms)")
|
||||
assert avg_validation_time < 100.0
|
||||
|
||||
# 6. Test backward compatibility
|
||||
print("\\n6. Testing backward compatibility:")
|
||||
|
||||
# Legacy config without exclude_cards
|
||||
legacy_payload = {
|
||||
"commander": "Inti, Seneschal of the Sun",
|
||||
"tags": ["discard"],
|
||||
"bracket": 3,
|
||||
"ideals": {"ramp": 10, "lands": 36, "basic_lands": 18, "creatures": 28,
|
||||
"removal": 10, "wipes": 3, "card_advantage": 8, "protection": 4},
|
||||
"tag_mode": "AND",
|
||||
"flags": {"owned_only": False, "prefer_owned": False},
|
||||
"locks": [],
|
||||
}
|
||||
|
||||
import base64
|
||||
import json
|
||||
raw = json.dumps(legacy_payload, separators=(",", ":")).encode('utf-8')
|
||||
legacy_token = base64.urlsafe_b64encode(raw).decode('ascii').rstrip('=')
|
||||
|
||||
r5 = client.get(f'/build/from?state={legacy_token}')
|
||||
assert r5.status_code == 200
|
||||
print(" Legacy config import works without exclude_cards")
|
||||
|
||||
print("\n=== ALL EXCLUDE CARD FEATURES VERIFIED ===")
|
||||
print("✅ File upload parsing (simulated)")
|
||||
print("✅ Live validation API with performance targets met")
|
||||
print("✅ Complete deck building workflow with exclude filtering")
|
||||
print("✅ JSON export/import with exclude_cards preservation")
|
||||
print("✅ Performance benchmarks under targets")
|
||||
print("✅ Backward compatibility with legacy configs")
|
||||
print("\n🎉 EXCLUDE CARDS IMPLEMENTATION COMPLETE! 🎉")
|
||||
|
||||
finally:
|
||||
# Restore environment
|
||||
if original_value is not None:
|
||||
os.environ['ALLOW_MUST_HAVES'] = original_value
|
||||
else:
|
||||
os.environ.pop('ALLOW_MUST_HAVES', None)
|
||||
Loading…
Add table
Add a link
Reference in a new issue