Files
Meister-Eder/tests/test_storage.py
T
Claude b2e04fa07b implement registration-confirmation-email
- Add qrbill and pillow dependencies for Swiss QR-bill PNG generation
- Add _STRINGS_DE / _STRINGS_EN bilingual string tables to AdminNotifier
- Add _generate_qr_bill_png(): Swiss QR code with cross overlay (PNG bytes)
- Add _build_parent_html(): HTML confirmation with inline cid:qrbill image
- Add _build_parent_text(): plain-text fallback with IBAN in full
- Add notify_parent(): multipart/mixed MIME email to parent on completion
- Persist metadata.language in _build_record() in json_store.py
- Wire notify_parent() into src/agent/core.py and chat_app.py completion events
- Add 9 tests for notify_parent, _generate_qr_bill_png, language fallback
- Update tasks.md: tasks 1–5 complete; task 6 (smoke test) remains manual

112 tests passing.

https://claude.ai/code/session_01LjjK7RjKVnC8bETtccfgna
2026-02-22 21:23:32 +00:00

177 lines
7.1 KiB
Python

"""Tests for ConversationStore and storage helpers."""
import json
from pathlib import Path
import pytest
from src.storage.json_store import (
ConversationStore,
normalize_email,
_diff_registrations,
)
from src.models.conversation import ConversationState
# ---------------------------------------------------------------------------
# normalize_email
# ---------------------------------------------------------------------------
class TestNormalizeEmail:
def test_lowercases(self):
assert normalize_email("Anna.Muster@Example.COM") == "anna.muster@example.com"
def test_strips_whitespace(self):
assert normalize_email(" user@example.com ") == "user@example.com"
def test_already_normalized(self):
assert normalize_email("user@example.com") == "user@example.com"
# ---------------------------------------------------------------------------
# _diff_registrations
# ---------------------------------------------------------------------------
class TestDiffRegistrations:
def test_detects_changed_field(self):
old = {"child": {"fullName": "Lena"}}
new = {"child": {"fullName": "Lena Muster"}}
diff = _diff_registrations(old, new)
assert "child.fullName" in diff
assert diff["child.fullName"] == ("Lena", "Lena Muster")
def test_unchanged_fields_not_included(self):
old = {"child": {"fullName": "Lena", "dateOfBirth": "2022-01-01"}}
new = {"child": {"fullName": "Lena", "dateOfBirth": "2022-01-01"}}
assert _diff_registrations(old, new) == {}
def test_nested_change_detected(self):
old = {"parentGuardian": {"email": "old@example.com"}}
new = {"parentGuardian": {"email": "new@example.com"}}
diff = _diff_registrations(old, new)
assert "parentGuardian.email" in diff
# ---------------------------------------------------------------------------
# ConversationStore — CRUD
# ---------------------------------------------------------------------------
@pytest.fixture
def store(tmp_path) -> ConversationStore:
return ConversationStore(tmp_path)
class TestConversationStoreCRUD:
def test_load_returns_none_for_unknown_email(self, store):
assert store.load("nobody@example.com") is None
def test_save_and_load_round_trip(self, store, fresh_state):
store.save(fresh_state)
loaded = store.load(fresh_state.parent_email)
assert loaded is not None
assert loaded.conversation_id == fresh_state.conversation_id
def test_save_overwrites_existing(self, store, fresh_state):
store.save(fresh_state)
fresh_state.language = "en"
store.save(fresh_state)
loaded = store.load(fresh_state.parent_email)
assert loaded.language == "en"
def test_delete_removes_conversation(self, store, fresh_state):
store.save(fresh_state)
store.delete(fresh_state.parent_email)
assert store.load(fresh_state.parent_email) is None
def test_delete_nonexistent_is_silent(self, store):
store.delete("ghost@example.com") # should not raise
def test_list_incomplete_returns_non_completed(self, store, fresh_state):
store.save(fresh_state)
incomplete = store.list_incomplete()
assert any(s.conversation_id == fresh_state.conversation_id for s in incomplete)
def test_list_incomplete_excludes_completed(self, store, fresh_state):
fresh_state.completed = True
store.save(fresh_state)
incomplete = store.list_incomplete()
assert all(not s.completed for s in incomplete)
def test_find_by_email_is_alias_for_load(self, store, fresh_state):
store.save(fresh_state)
assert store.find_by_email(fresh_state.parent_email) is not None
# ---------------------------------------------------------------------------
# ConversationStore — registration versioning
# ---------------------------------------------------------------------------
class TestRegistrationVersioning:
def test_save_registration_creates_version_1(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
email_key, version = store.save_registration(fresh_state)
assert version == 1
# email_key is the filesystem-safe form (@ → _at_)
assert email_key == "anna.muster_at_example.com"
def test_save_registration_writes_current_json(self, store, fresh_state, complete_registration, tmp_path):
fresh_state.registration = complete_registration
fresh_state.completed = True
email_key, _ = store.save_registration(fresh_state)
current = tmp_path / "registrations" / email_key / "current.json"
assert current.exists()
def test_save_registration_version_increments(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
store.save_registration(fresh_state)
_, v2 = store.save_registration_version(
fresh_state, {"child.fullName": ("Old", "New")}
)
assert v2 == 2
def test_get_current_registration_returns_latest(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
store.save_registration(fresh_state)
current = store.get_current_registration(fresh_state.parent_email)
assert current is not None
assert current["metadata"]["version"] == 1
def test_get_registration_history_returns_all_versions(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
store.save_registration(fresh_state)
store.save_registration_version(fresh_state, {"child.fullName": ("A", "B")})
history = store.get_registration_history(fresh_state.parent_email)
assert len(history) == 2
def test_list_registrations_includes_saved(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
store.save_registration(fresh_state)
registrations = store.list_registrations()
assert len(registrations) == 1
def test_save_registration_persists_language(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
fresh_state.language = "en"
store.save_registration(fresh_state)
current = store.get_current_registration(fresh_state.parent_email)
assert current is not None
assert current["metadata"]["language"] == "en"
def test_save_registration_defaults_language_to_de(self, store, fresh_state, complete_registration):
fresh_state.registration = complete_registration
fresh_state.completed = True
# language defaults to "de" in ConversationState
store.save_registration(fresh_state)
current = store.get_current_registration(fresh_state.parent_email)
assert current["metadata"]["language"] == "de"