← Files Verified by NPCARCHIVED FILE

tests/test_openapi_spec.py

6.47 KB · Nov 3, 2026 · 18:59 UTC

↓ Download file

"""OpenAPI spec tests: openapi-plugin.yaml validity and operation mapping.

The spec is parsed with the package's restricted-YAML subset parser
(stdlib only). Tests assert: the spec is well-formed, the five plugin
operations map onto the correct API v1 endpoints, every operation is
read-only (no attestation issuance), and the API-backed operations are a
subset of the API's own openapi.yaml (same paths, same methods, same
operation semantics).
"""
import os
import unittest

from tests._yaml_subset import parse, YAMLError
from tests.helpers import OpenAITestCase  # noqa: F401  (path setup only)

BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
API_SPEC = os.path.normpath(os.path.join(BASE, "..", "..", "api",
                                         "openapi.yaml"))

EXPECTED_OPS = {
    # operationId: (path, method, api_operationId)
    "verify_product": ("/v1/products/{tag_id}", "get", "getProduct"),
    "verify_credential": ("/v1/credentials/{id}", "get", "getCredential"),
    "verify_claim": ("/v1/claims/verify", "post", "verifyClaim"),
    "verify_agent": ("/v1/agents/{agent_id}/attestation", "get",
                     "getAgentAttestation"),
    "explain_verification": ("/explain/verification", "get", None),
}


def load_plugin_spec():
    with open(os.path.join(BASE, "openapi-plugin.yaml"),
              encoding="utf-8") as f:
        return parse(f.read())


def load_api_spec():
    with open(API_SPEC, encoding="utf-8") as f:
        return parse(f.read())


class TestSpecValidity(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.spec = load_plugin_spec()

    def test_parses_within_subset(self):
        self.assertIsInstance(self.spec, dict)

    def test_openapi_version(self):
        self.assertEqual(self.spec["openapi"], "3.1.0")

    def test_info(self):
        info = self.spec["info"]
        self.assertIn("Verified by NPC", info["title"])
        self.assertTrue(info["version"])

    def test_servers(self):
        urls = [s["url"] for s in self.spec["servers"]]
        self.assertIn("https://api.npclabs.xyz/v1", urls)
        self.assertIn("http://127.0.0.1:8787/v1", urls)

    def test_security_bearer(self):
        self.assertEqual(self.spec["security"], [{"bearerAuth": []}])
        self.assertEqual(
            self.spec["components"]["securitySchemes"]["bearerAuth"]["type"],
            "http")

    def test_all_schemas_referenced_resolve(self):
        schemas = self.spec["components"]["schemas"]
        blob = str(self.spec["paths"])
        for line in blob.split("#/components/schemas/")[1:]:
            name = line.split("'")[0].split('"')[0].split("}")[0].strip()
            if name:
                self.assertIn(name, schemas, f"unresolved schema {name!r}")

    def test_explain_verification_is_client_side(self):
        op = self.spec["paths"]["/explain/verification"]["get"]
        self.assertEqual(op["operationId"], "explain_verification")
        self.assertTrue(op.get("x-npc-client-side"),
                        "explain_verification must be marked client-side: "
                        "it performs no API call")

    def test_no_em_dashes(self):
        with open(os.path.join(BASE, "openapi-plugin.yaml"),
                  encoding="utf-8") as f:
            text = f.read()
        self.assertNotIn("—", text)


class TestOperationMapping(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.spec = load_plugin_spec()
        cls.api = load_api_spec()
        cls.ops = {}
        for path, methods in cls.spec["paths"].items():
            for method, op in methods.items():
                cls.ops[op["operationId"]] = (path, method, op)

    def test_five_operations_present(self):
        self.assertEqual(sorted(self.ops),
                         sorted(EXPECTED_OPS))

    def test_correct_endpoints(self):
        for op_id, (path, method, _) in EXPECTED_OPS.items():
            got_path, got_method, _ = self.ops[op_id]
            self.assertEqual((got_path, got_method), (path, method),
                             f"{op_id} mapped to wrong endpoint")

    def test_trigger_phrasing_in_descriptions(self):
        triggers = ["is this real?", "verify this", "is this authentic?"]
        for op_id in ("verify_product", "verify_credential", "verify_claim"):
            desc = self.ops[op_id][2].get("description", "")
            self.assertTrue(any(p in desc for p in triggers),
                            f"{op_id} description lacks trigger phrasing")

    def test_matches_api_spec_subset(self):
        # Every API-backed plugin operation must exist in the API's own
        # openapi.yaml with the same path, method, and semantics. The API
        # spec puts the /v1 prefix in its servers' base URL, so normalize.
        api_paths = self.api["paths"]
        for op_id, (path, method, api_op) in EXPECTED_OPS.items():
            if api_op is None:
                continue
            api_path = path[3:] if path.startswith("/v1") else path
            self.assertIn(api_path, api_paths,
                          f"{op_id}: path missing in API spec")
            self.assertIn(method, api_paths[api_path],
                          f"{op_id}: method missing in API spec")
            self.assertEqual(api_paths[api_path][method]["operationId"],
                             api_op)

    def test_read_only_no_issuance(self):
        # The plugin must not expose attestation issuance: the only POST
        # allowed anywhere in the spec is /v1/claims/verify (a read-only
        # verification call), and /v1/attestations must be absent.
        self.assertNotIn("/v1/attestations", self.spec["paths"])
        post_paths = [p for p, ms in self.spec["paths"].items()
                      if "post" in ms]
        self.assertEqual(post_paths, ["/v1/claims/verify"],
                         "the only POST allowed is the read-only claim verify")

    def test_404_shapes_are_fail_closed(self):
        for op_id in ("verify_product", "verify_credential", "verify_agent"):
            path, method, _ = self.ops[op_id]
            responses = self.spec["paths"][path][method]["responses"]
            ref = str(responses.get("404", ""))
            self.assertIn("UnknownVerdict", ref,
                          f"{op_id}: 404 must use the UnknownVerdict shape")


class TestYamlSubsetParser(unittest.TestCase):
    def test_rejects_tabs(self):
        with self.assertRaises(YAMLError):
            parse("a:\n\tb: 1\n")

    def test_rejects_garbage(self):
        with self.assertRaises(YAMLError):
            parse("just a string\n: broken\n")

SHA-256: 7ef1fb2124a48c1274fd59ccd8418680767596e20e8dabe875d040f109613287