From 744f1d63dbc983143209e6ff5cb33a7ff0506714 Mon Sep 17 00:00:00 2001 From: JoshuaHarris391 Date: Wed, 12 Aug 2026 15:05:33 +1000 Subject: [PATCH] chore: update gen3-validator to 2.2.0 and add real-resolver integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2.2.0 resolver is bundle-aware: node-level $refs into _terms.yaml (the official Gen3 dictionary's shape) now resolve instead of silently dropping the node from the resolved schema. The existing suite mocks gen3_validator entirely, so upstream resolution regressions were invisible to CI — the new integration tests run the real resolver and validator over a small bundle covering the previously-broken shapes. --- poetry.lock | 6 +- tests/test_validator_integration.py | 125 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/test_validator_integration.py diff --git a/poetry.lock b/poetry.lock index b2ff7f6..3208a25 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1539,14 +1539,14 @@ typer = ">=0.12,<1.0" [[package]] name = "gen3-validator" -version = "2.1.0" +version = "2.2.0" description = "Comprehensive toolkit for resolving Gen3 JSON schemas, validating JSON metadata against schemas, and verifying linkage integrity between data nodes. Includes utilities for parsing Excel metadata templates, generating linkage configuration maps, orchestrating schema validation, and producing detailed validation reports and statistics" optional = false python-versions = ">=3.9.5" groups = ["main", "synth"] files = [ - {file = "gen3_validator-2.1.0-py3-none-any.whl", hash = "sha256:a0211f5ed24465d44eceeb9dc554e5c366383abc07ec4d8ff7f566120b7f75db"}, - {file = "gen3_validator-2.1.0.tar.gz", hash = "sha256:65915becbf98e8b38e1df1bfad3ffe45e932698cec1b47306d8d1c24b8c60993"}, + {file = "gen3_validator-2.2.0-py3-none-any.whl", hash = "sha256:cb69124e9f1ae7e23fe46b115b72a9596fe74f8dbcb3dd8ec7c1662491968cb7"}, + {file = "gen3_validator-2.2.0.tar.gz", hash = "sha256:7c29032e2e3a4ec753586203d7c69f5a3ac70a228f120ed2e90f7b02a291ea61"}, ] [package.dependencies] diff --git a/tests/test_validator_integration.py b/tests/test_validator_integration.py new file mode 100644 index 0000000..e06fde1 --- /dev/null +++ b/tests/test_validator_integration.py @@ -0,0 +1,125 @@ +"""Integration tests exercising the real gen3_validator dependency. + +Background +---------- +The toolkit's validation pipeline (``g3dt.validate.validate``) delegates schema +resolution and record validation to the ``gen3_validator`` package: +``load_and_resolve_schema`` constructs ``gen3_validator.ResolveSchema`` over a +downloaded schema bundle, and ``validate_pipeline`` feeds records through +``gen3_validator.validate.validate_list_dict``. + +Every other test in this suite mocks gen3_validator away, which means an +upstream regression in reference resolution would sail through the toolkit's +CI unnoticed — exactly what happened when the resolver could not handle +node-level ``$ref`` entries into ``_terms.yaml`` (the shape the official Gen3 +dictionary uses): affected nodes were silently dropped and their data was +never validated. + +These tests run the REAL resolver and validator over a small in-repo bundle +so the toolkit's suite fails if the upstream contract breaks. They need +gen3-validator >= 2.2.0 (bundle-aware resolution). +""" + +import json + +import gen3_validator +import pytest + + +@pytest.fixture +def schema_bundle_path(tmp_path): + """Write a minimal Gen3-style schema bundle to disk and return its path. + + The bundle deliberately includes the two shapes that broke the old + resolver: a node-level ``$ref`` into ``_terms.yaml``, and the ``UUID`` key + defined in both ``_terms.yaml`` and ``_definitions.yaml`` with different + content. + """ + bundle = { + "_settings.yaml": {"_dict_version": "1.0.0"}, + "_terms.yaml": { + "sample_note": {"description": "Documentation about samples"}, + "UUID": {"description": "terms-flavoured UUID"}, + }, + "_definitions.yaml": { + "UUID": {"type": "string", "pattern": "^[a-f0-9-]{36}$"}, + }, + "sample.yaml": { + "id": "sample", + "type": "object", + "required": ["sample_id"], + "properties": { + "type": {"type": "string"}, + "sample_id": {"$ref": "_definitions.yaml#/UUID"}, + "volume": { + "type": "number", + "term": {"$ref": "_terms.yaml#/sample_note"}, + }, + }, + }, + } + path = tmp_path / "schema.json" + path.write_text(json.dumps(bundle)) + return str(path) + + +def test_resolve_schema_handles_node_level_terms_refs(schema_bundle_path): + """The real resolver resolves a bundle shaped like the official dictionary. + + This is the exact call ``load_and_resolve_schema`` makes + (``g3dt/validate/validate.py``). Before gen3-validator 2.2.0 the + node-level ``_terms.yaml`` ref made the resolver drop ``sample.yaml`` + entirely, so downstream validation would raise "key not found in resolved + schema" for every sample record. + + Expected: sample.yaml survives resolution, its term block carries the + _terms.yaml content, and the collision key resolved to the + _definitions.yaml flavour (a pattern, not a description). + """ + resolver = gen3_validator.ResolveSchema(schema_path=schema_bundle_path) + resolver.resolve_schema() + + assert "sample.yaml" in resolver.schema_resolved + properties = resolver.schema_resolved["sample.yaml"]["properties"] + assert properties["volume"]["term"] == { + "description": "Documentation about samples" + } + assert properties["sample_id"]["pattern"] == "^[a-f0-9-]{36}$" + + +def test_validate_list_dict_over_really_resolved_schema(schema_bundle_path): + """Records validate end to end through the resolved schema, no mocks. + + Mirrors validate_pipeline's usage: records carry a ``type`` field naming + their node, and validate_list_dict pulls each node's resolved schema by + that name. A structurally valid record must produce no FAIL rows; a + record violating the resolved ``_definitions.yaml#/UUID`` pattern must + produce one — proving the $ref actually resolved into an enforceable + constraint rather than being dropped. + """ + resolver = gen3_validator.ResolveSchema(schema_path=schema_bundle_path) + resolver.resolve_schema() + + valid_record = { + "type": "sample", + "sample_id": "0f8fad5b-d9cb-469f-a165-70867728950e", + "volume": 1.5, + } + invalid_record = { + "type": "sample", + "sample_id": "NOT-A-UUID", + "volume": 1.5, + } + + valid_results = gen3_validator.validate.validate_list_dict( + [valid_record], resolver.schema_resolved + ) + invalid_results = gen3_validator.validate.validate_list_dict( + [invalid_record], resolver.schema_resolved + ) + + assert [r for r in valid_results if r["validation_result"] == "FAIL"] == [] + failures = [r for r in invalid_results if r["validation_result"] == "FAIL"] + assert len(failures) == 1 + assert failures[0]["invalid_key"] == "sample_id" + assert failures[0]["validator"] == "pattern"