"""SLICE-2 unit tests — collection field schema + central validation. Pure functions only (no DB / no Gitea). Per docs/design/2026-06-06-configurable-collection-metadata.md §7.2 (SLICE-2): `metadata_schema.parse_fields` (lenient schema parsing) and `metadata_schema.validate` (advisory at read / enforcement point at write). Honors INV-3 (never hard-fails), INV-5 (no-fields unchanged), INV-7 (undeclared keys ride along). """ from __future__ import annotations from app import metadata_schema as ms # ---- parse_fields: normalization + leniency ---- def test_parse_fields_each_type(): raw = { "priority": {"type": "enum", "values": ["P0", "P1", "P2"], "label": "Priority"}, "tags": {"type": "tags"}, "owner": {"type": "text"}, } fields = ms.parse_fields(raw) assert list(fields) == ["priority", "tags", "owner"] # order preserved assert fields["priority"] == { "type": "enum", "values": ["P0", "P1", "P2"], "label": "Priority", } assert fields["tags"] == {"type": "tags"} assert fields["owner"] == {"type": "text"} def test_parse_fields_controlled_tags_keeps_values(): fields = ms.parse_fields({"area": {"type": "tags", "values": ["a", "b"]}}) assert fields["area"] == {"type": "tags", "values": ["a", "b"]} def test_parse_fields_missing_block_is_empty(): assert ms.parse_fields(None) == {} assert ms.parse_fields({}) == {} def test_parse_fields_non_mapping_block_skipped(): assert ms.parse_fields(["not", "a", "mapping"]) == {} assert ms.parse_fields("nope") == {} def test_parse_fields_enum_without_values_skipped(): # enum requires a non-empty values list — skipped, not fatal. assert ms.parse_fields({"p": {"type": "enum"}}) == {} assert ms.parse_fields({"p": {"type": "enum", "values": []}}) == {} def test_parse_fields_unknown_type_skipped(): fields = ms.parse_fields( {"good": {"type": "text"}, "bad": {"type": "ref"}, "huh": {"type": "frob"}} ) assert list(fields) == ["good"] def test_parse_fields_non_mapping_def_skipped(): fields = ms.parse_fields({"good": {"type": "text"}, "bad": "scalar"}) assert list(fields) == ["good"] def test_parse_fields_values_coerced_to_str_list(): fields = ms.parse_fields({"p": {"type": "enum", "values": [0, 1, 2]}}) assert fields["p"]["values"] == ["0", "1", "2"] # ---- validate: advisory problem reporting ---- SCHEMA = ms.parse_fields( { "priority": {"type": "enum", "values": ["P0", "P1", "P2"]}, "tags": {"type": "tags"}, "area": {"type": "tags", "values": ["checkout", "cart"]}, "owner": {"type": "text"}, } ) def test_validate_happy(): values = { "priority": "P0", "tags": ["anything", "free"], "area": ["checkout"], "owner": "ben", } assert ms.validate(values, SCHEMA) == [] def test_validate_absent_fields_ok(): # A declared field that the entry omits is fine (no required fields in v1). assert ms.validate({}, SCHEMA) == [] def test_validate_enum_bad_value(): problems = ms.validate({"priority": "P9"}, SCHEMA) assert [p.field for p in problems] == ["priority"] assert problems[0].code == "not-in-values" def test_validate_enum_wrong_type(): problems = ms.validate({"priority": ["P0"]}, SCHEMA) assert problems[0].field == "priority" assert problems[0].code == "wrong-type" def test_validate_tags_free_form_ok(): assert ms.validate({"tags": ["x", "y", "z"]}, SCHEMA) == [] def test_validate_tags_wrong_type(): problems = ms.validate({"tags": "notalist"}, SCHEMA) assert problems[0].field == "tags" assert problems[0].code == "wrong-type" def test_validate_controlled_tags_bad_member(): problems = ms.validate({"area": ["checkout", "nope"]}, SCHEMA) assert problems[0].field == "area" assert problems[0].code == "not-in-values" def test_validate_text_wrong_type(): problems = ms.validate({"owner": ["a", "b"]}, SCHEMA) assert problems[0].field == "owner" assert problems[0].code == "wrong-type" def test_validate_undeclared_keys_ignored(): # INV-7: keys outside the schema ride along untouched, never flagged. assert ms.validate({"random": "value", "slug": "x", "title": "y"}, SCHEMA) == [] def test_validate_empty_schema_no_problems(): # INV-5: a collection with no fields validates everything as clean. assert ms.validate({"priority": "anything", "x": 1}, {}) == [] def test_problem_as_dict(): p = ms.Problem(field="priority", code="not-in-values", message="bad") assert p.as_dict() == { "field": "priority", "code": "not-in-values", "message": "bad", }