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"