mail-address-semantics/tests/test_parser_conformance.py
tegwick 0a2d811444 MAS-WP-0005-T02: SCP-1 quoted local-parts equivalent on receipt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 252470@bnt-lap001
Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241
2026-10-18 18:16:59 +02:00

134 lines
4.7 KiB
Python

import json
import socket
from pathlib import Path
import pytest
from mas_conformance import address_fixtures, property_fixtures
from mas_conformance.runner import check_address_fixture, run_parser_corpus
from mas_core.generator import generate_local_part, resource_local_part
from mas_core.model import RequiredProfile
from mas_core.parser import (
MasSyntaxError,
parse_address,
parse_local_part,
parse_received_address,
validate_profile,
)
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = address_fixtures() + property_fixtures()
def test_vendored_corpus_matches_spec_bundle():
spec = json.loads((ROOT / "mas-v0.1-spec/conformance/addresses.json").read_text())
assert spec == address_fixtures()
def test_vendored_schema_matches_spec_bundle():
a = (ROOT / "mas-v0.1-spec/schema/mas-statute-v0.1.schema.json").read_bytes()
b = (ROOT / "mas_core/schema/mas-statute-v0.1.schema.json").read_bytes()
assert a == b
@pytest.mark.parametrize("fixture", FIXTURES, ids=[f["id"] for f in FIXTURES])
def test_fixture(fixture):
assert check_address_fixture(fixture) is None
def test_whole_corpus_passes():
assert run_parser_corpus().ok
@pytest.fixture
def no_network(monkeypatch):
def deny(*a, **k):
raise AssertionError("parser attempted network I/O")
monkeypatch.setattr(socket, "socket", deny)
monkeypatch.setattr(socket, "create_connection", deny)
monkeypatch.setattr(socket, "getaddrinfo", deny)
def test_parser_performs_no_io(no_network, monkeypatch):
monkeypatch.setattr("builtins.open", lambda *a, **k: (_ for _ in ()).throw(AssertionError("file I/O")))
for f in FIXTURES:
try:
parse_local_part(f["local_part"])
except MasSyntaxError:
pass
parse_address("anna.mueller@acme.coulomb.social")
def test_maximal_parser_reports_required_profile():
p = parse_local_part("ticket!callback")
assert p.required_profile is RequiredProfile.P2
assert not validate_profile(p, "p1")
assert validate_profile(p, "P2")
assert parse_local_part("ticket=PR42+comments").required_profile is RequiredProfile.P1
def test_combined_form_model():
p = parse_local_part("ticket=PR42+comments!callback")
assert p.target.resource_type == "ticket" and p.target.resource_id == "PR42"
assert p.facet.value == "comments" and p.interaction.mode == "callback"
assert p.required_profile is RequiredProfile.P2
assert not p.generator_allowed
def test_no_kind_inference_from_punctuation():
for lp in ("bernd.worsch", "research-companion", "invoice_ingest"):
d = parse_local_part(lp).to_dict()
assert d["target_kind"] == "opaque"
assert not {"kind", "person", "actor", "function"} & set(d)
def test_facet_loss_degrades_to_base():
p = parse_local_part("ticket=GS-41872+comments")
assert p.base == "ticket=GS-41872"
stripped = p.without_facet()
assert stripped.raw == "ticket=GS-41872" and stripped.target == p.target
def test_parse_address_domain():
a = parse_address("ticket=GS-41872@Great-Service.Example")
assert a.domain == "great-service.example"
with pytest.raises(MasSyntaxError):
parse_address("no-domain")
def test_generator_rules():
assert generate_local_part("ticket", interaction="callback") == "ticket!callback"
assert resource_local_part("ticket", "GS-41872") == "ticket=GS-41872"
with pytest.raises(MasSyntaxError):
generate_local_part("ticket=PR42", facet="comments", interaction="callback")
with pytest.raises(MasSyntaxError):
generate_local_part("ticket", facet="a+b")
with pytest.raises(MasSyntaxError):
generate_local_part("ticket", facet="")
with pytest.raises(MasSyntaxError):
resource_local_part("ticket", "x" * 60)
for f in FIXTURES:
if f["valid"] and f.get("generator_allowed") is False:
p = parse_local_part(f["local_part"])
with pytest.raises(MasSyntaxError):
generate_local_part(str(p.target), facet=p.facet.value, interaction=p.interaction.mode)
@pytest.mark.parametrize("raw, expected", [
('"ticket=PR42"@example.com', "ticket=PR42"), # SCP-1: unnecessary quotes
('"ticket!callback"@example.com', "ticket!callback"),
("ticket=PR42@example.com", "ticket=PR42"),
])
def test_received_quoted_local_part_is_equivalent(raw, expected):
assert parse_received_address(raw).local.raw == expected
@pytest.mark.parametrize("raw", ['"a\\"b"@example.com', '"a b"@example.com', '"x@y"@example.com'])
def test_received_quoted_non_mas_stays_invalid(raw):
with pytest.raises(MasSyntaxError):
parse_received_address(raw)
def test_canonical_parser_still_rejects_quotes():
with pytest.raises(MasSyntaxError):
parse_address('"ticket=PR42"@example.com')