package httpapi import ( "encoding/json" "os" "regexp" "strings" "testing" ) func TestOpenAPIContractMatchesRegisteredRoutesAndOmitsSecrets(t *testing.T) { contract, err := os.ReadFile("../../api/openapi.yaml") if err != nil { t.Fatalf("read OpenAPI contract: %v", err) } var document struct { OpenAPI string `json:"openapi"` Info map[string]any `json:"info"` Paths map[string]map[string]any `json:"paths"` Components map[string]map[string]any `json:"components"` } if err := json.Unmarshal(contract, &document); err != nil { t.Fatalf("OpenAPI contract must use JSON-compatible YAML: %v", err) } if document.OpenAPI != "3.1.0" || document.Info["title"] != "Knowledge Governance API" || len(document.Paths) == 0 { t.Fatalf("invalid OpenAPI header: %#v", document) } routes, err := os.ReadFile("server.go") if err != nil { t.Fatalf("read registered routes: %v", err) } matcher := regexp.MustCompile(`mux\.HandleFunc\("(GET|POST|PUT|DELETE) ([^"]+)"`) for _, match := range matcher.FindAllStringSubmatch(string(routes), -1) { method, path := strings.ToLower(match[1]), match[2] operation, ok := document.Paths[path][method].(map[string]any) if !ok { t.Fatalf("OpenAPI is missing %s %s", strings.ToUpper(method), path) } if strings.TrimSpace(stringValue(operation["operationId"])) == "" { t.Fatalf("OpenAPI operation %s %s has no operationId", strings.ToUpper(method), path) } responses, ok := operation["responses"].(map[string]any) if !ok || responses["400"] == nil || responses["500"] == nil { t.Fatalf("OpenAPI operation %s %s must document 400 and 500 responses", strings.ToUpper(method), path) } } var guardBaseURL func(any, string) guardBaseURL = func(node any, path string) { switch value := node.(type) { case map[string]any: for key, child := range value { if key == "base_url" && path != "components.schemas.QAAISafeSettings.properties" && path != "components.schemas.QAAISaveSettings.properties" { t.Fatalf("unapproved endpoint exposure at %s", path) } next := key if path != "" { next = path + "." + key } guardBaseURL(child, next) } case []any: for _, child := range value { guardBaseURL(child, path) } } } var tree any _ = json.Unmarshal(contract, &tree) guardBaseURL(tree, "") lower := strings.ToLower(string(contract)) for _, forbidden := range []string{"secret_ref", "authorization_code", "manifest_sha256", "knowledge_id", "test_kb_id", "download_code", "cookie"} { if strings.Contains(lower, forbidden) { t.Fatalf("OpenAPI contract must not expose %q", forbidden) } } } func TestOpenAPICatalogMutationsRequireIdempotencyKeyHeader(t *testing.T) { contract, err := os.ReadFile("../../api/openapi.yaml") if err != nil { t.Fatal(err) } var document struct { Paths map[string]map[string]any `json:"paths"` Components struct { Parameters map[string]map[string]any `json:"parameters"` } `json:"components"` } if err := json.Unmarshal(contract, &document); err != nil { t.Fatal(err) } parameter, ok := document.Components.Parameters["IdempotencyKey"] if !ok || parameter["name"] != "Idempotency-Key" || parameter["in"] != "header" || parameter["required"] != true { t.Fatalf("IdempotencyKey header must be required: %#v", parameter) } schema, ok := parameter["schema"].(map[string]any) if !ok || schema["type"] != "string" || schema["minLength"] != float64(1) || schema["maxLength"] != float64(128) { t.Fatalf("IdempotencyKey header schema is unsafe: %#v", parameter["schema"]) } for _, path := range []string{"/api/v1/qa-ops/organization/sync", "/api/v1/source-catalog/route", "/api/v1/source-catalog/sources/{id}/refresh"} { operation, ok := document.Paths[path]["post"].(map[string]any) if !ok { t.Fatalf("missing POST %s", path) } parameters, ok := operation["parameters"].([]any) if !ok { t.Fatalf("POST %s must declare the idempotency header", path) } found := false for _, value := range parameters { parameterRef, _ := value.(map[string]any) if parameterRef["$ref"] == "#/components/parameters/IdempotencyKey" { found = true } } if !found { t.Fatalf("POST %s must reference IdempotencyKey", path) } responses, _ := operation["responses"].(map[string]any) accepted, _ := responses["202"].(map[string]any) if accepted["$ref"] != "#/components/responses/OperationEnvelope" { t.Fatalf("POST %s must document created/replayed operation response: %#v", path, accepted) } } } func TestQAAIContractDescribesAllAdministratorPathsAndWriteOnlyCredential(t *testing.T) { raw, err := os.ReadFile("../../api/openapi.yaml") if err != nil { t.Fatal(err) } var document struct { Paths map[string]map[string]any `json:"paths"` Components struct { Schemas map[string]map[string]any `json:"schemas"` } `json:"components"` } if json.Unmarshal(raw, &document) != nil { t.Fatal("invalid API document") } for _, route := range []struct{ path, method string }{{"/settings", "get"}, {"/settings", "post"}, {"/settings/test", "post"}, {"/automation", "post"}, {"/previews", "post"}, {"/batches", "post"}, {"/batches/{id}/actions", "post"}, {"/status", "get"}, {"/records/{id}", "get"}} { path := "/api/v1/qa-ops/ai" + route.path op, ok := document.Paths[path][route.method].(map[string]any) if !ok { t.Fatalf("missing %s %s", route.method, path) } responses, _ := op["responses"].(map[string]any) for _, status := range []string{"400", "401", "403", "500", "503"} { if responses[status] == nil { t.Fatalf("missing %s for %s", status, path) } } } safe := document.Components.Schemas["QAAISafeSettings"] properties, _ := safe["properties"].(map[string]any) if properties["api_key"] != nil || properties["key_ciphertext"] != nil || properties["key_configured"] == nil || safe["additionalProperties"] != false { t.Fatal("safe settings contract exposes secrets or accepts unknown fields") } input := document.Components.Schemas["QAAISaveSettings"] properties, _ = input["properties"].(map[string]any) key, _ := properties["api_key"].(map[string]any) if key["writeOnly"] != true { t.Fatal("API key request property is not write-only") } } func stringValue(value any) string { result, _ := value.(string) return result }