AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
backends/mizan-fastapi/tests/test_forms.py
Normal file
145
backends/mizan-fastapi/tests/test_forms.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Forms behavior — the genuine capability behind the `forms` probe.
|
||||
|
||||
Proves the Pydantic binding exposes the same schema / validate / submit role
|
||||
contract as the Django adapter: subclassing `mizanForm` auto-registers
|
||||
`{name}.schema`, `{name}.validate`, `{name}.submit` with the matching
|
||||
`_meta["form_role"]`, the schema role emits typed field definitions, validate
|
||||
returns structured field errors, and submit validates then runs
|
||||
`on_submit_success` / `on_submit_failure`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mizan_core.registry import clear_registry, get_function
|
||||
|
||||
from mizan_fastapi.forms import (
|
||||
FormConfig,
|
||||
FormSubmitFail,
|
||||
FormSubmitPass,
|
||||
FormValidation,
|
||||
build_form_schema,
|
||||
get_forms,
|
||||
mizanForm,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean():
|
||||
clear_registry()
|
||||
yield
|
||||
clear_registry()
|
||||
|
||||
|
||||
def _make_contact_form():
|
||||
class ContactForm(mizanForm):
|
||||
mizan = FormConfig(name="contact", title="Contact Us", submit_label="Send")
|
||||
|
||||
name: str
|
||||
email: str
|
||||
message: str = ""
|
||||
|
||||
def on_submit_success(self, request) -> dict:
|
||||
return {"sent": True, "to": self.email}
|
||||
|
||||
return ContactForm
|
||||
|
||||
|
||||
def test_subclassing_registers_three_role_functions():
|
||||
_make_contact_form()
|
||||
for role in ("schema", "validate", "submit"):
|
||||
fn = get_function(f"contact.{role}")
|
||||
assert fn is not None, f"contact.{role} not registered"
|
||||
assert fn._meta["form"] is True
|
||||
assert fn._meta["form_name"] == "contact"
|
||||
assert fn._meta["form_role"] == role
|
||||
|
||||
|
||||
def test_schema_role_emits_field_definitions():
|
||||
form_cls = _make_contact_form()
|
||||
SchemaFn = get_function("contact.schema")
|
||||
schema = SchemaFn(request=None).call(None)
|
||||
assert schema.name == "contact"
|
||||
assert schema.title == "Contact Us"
|
||||
assert schema.submit_label == "Send"
|
||||
field_names = {f.name for f in schema.fields}
|
||||
assert field_names == {"name", "email", "message"}
|
||||
# `message` has a default → not required; `name`/`email` required
|
||||
by_name = {f.name: f for f in schema.fields}
|
||||
assert by_name["name"].required is True
|
||||
assert by_name["message"].required is False
|
||||
|
||||
|
||||
def test_build_form_schema_maps_types():
|
||||
class TypedForm(mizanForm):
|
||||
mizan = FormConfig(name="typed")
|
||||
count: int
|
||||
ratio: float
|
||||
active: bool
|
||||
label: str
|
||||
|
||||
schema = build_form_schema(TypedForm)
|
||||
by_name = {f.name: f for f in schema.fields}
|
||||
assert by_name["count"].type == "number"
|
||||
assert by_name["ratio"].type == "number"
|
||||
assert by_name["active"].type == "checkbox"
|
||||
assert by_name["label"].type == "text"
|
||||
|
||||
|
||||
def test_validate_role_passes_clean_data():
|
||||
_make_contact_form()
|
||||
ValidateFn = get_function("contact.validate")
|
||||
ValidateInput = ValidateFn.Input
|
||||
out = ValidateFn(request=None).call(ValidateInput(data={"name": "Ryth", "email": "r@x.com"}))
|
||||
assert isinstance(out, FormValidation)
|
||||
assert out.errors == []
|
||||
|
||||
|
||||
def test_validate_role_reports_field_errors():
|
||||
_make_contact_form()
|
||||
ValidateFn = get_function("contact.validate")
|
||||
ValidateInput = ValidateFn.Input
|
||||
out = ValidateFn(request=None).call(ValidateInput(data={"email": "r@x.com"})) # missing 'name'
|
||||
error_fields = {e.field for e in out.errors}
|
||||
assert "name" in error_fields
|
||||
|
||||
|
||||
def test_submit_role_runs_on_submit_success():
|
||||
_make_contact_form()
|
||||
SubmitFn = get_function("contact.submit")
|
||||
SubmitInput = SubmitFn.Input
|
||||
result = SubmitFn(request=None).call(
|
||||
SubmitInput(data={"name": "Ryth", "email": "ryth@example.com", "message": "hi"})
|
||||
)
|
||||
assert isinstance(result, FormSubmitPass)
|
||||
assert result.success is True
|
||||
assert result.data == {"sent": True, "to": "ryth@example.com"}
|
||||
|
||||
|
||||
def test_submit_role_returns_fail_on_invalid():
|
||||
captured = {}
|
||||
|
||||
class GuardedForm(mizanForm):
|
||||
mizan = FormConfig(name="guarded")
|
||||
name: str
|
||||
|
||||
def on_submit_failure(self, request, errors) -> None:
|
||||
captured["errors"] = errors
|
||||
|
||||
SubmitFn = get_function("guarded.submit")
|
||||
SubmitInput = SubmitFn.Input
|
||||
result = SubmitFn(request=None).call(SubmitInput(data={})) # missing required 'name'
|
||||
assert isinstance(result, FormSubmitFail)
|
||||
assert result.success is False
|
||||
assert any(e.field == "name" for e in result.errors.errors)
|
||||
# on_submit_failure hook fired with the validation
|
||||
assert "errors" in captured
|
||||
|
||||
|
||||
def test_get_forms_groups_by_form_name():
|
||||
_make_contact_form()
|
||||
forms = get_forms()
|
||||
assert set(forms.keys()) == {"contact"}
|
||||
assert len(forms["contact"]) == 3
|
||||
Reference in New Issue
Block a user