package openapi import ( "os" "testing" "go.yaml.in/yaml/v4" ) type document struct { OpenAPI string `yaml:"openapi"` Paths map[string]map[string]any `yaml:"paths"` Components map[string]any `yaml:"components"` } func TestDistributionContract(t *testing.T) { spec := readDocument(t, "proxy-pool.yaml") if spec.OpenAPI != "3.1.0" { t.Fatalf("openapi version = %q, want 3.1.0", spec.OpenAPI) } extraction, ok := spec.Paths["/api/v1/proxies/extract"] if !ok { t.Fatal("exclusive extraction path is missing") } post, ok := extraction["post"] if !ok { t.Fatal("exclusive extraction must use POST") } requireResponses(t, post, "200", "400", "409", "413", "415", "422", "429", "500", "503") for path := range spec.Paths { if path == "/api/v1/leases" || path == "/api/v1/proxies/release" || path == "/api/v1/proxies/renew" { t.Fatalf("lease/release path is forbidden: %s", path) } } } func TestAdminContract(t *testing.T) { spec := readDocument(t, "admin.yaml") for _, path := range []string{ "/api/v1/status", "/api/v1/audit", "/api/v1/upstreams/{name}/enable", "/api/v1/upstreams/{name}/disable", "/api/v1/routing/{name}/switch", "/api/v1/config/reload", } { if _, ok := spec.Paths[path]; !ok { t.Errorf("admin path is missing: %s", path) } } requireResponses(t, spec.Paths["/api/v1/routing/{name}/switch"]["post"], "200", "400", "401", "403", "404", "405", "409", "413", "415", "422", "500", "503") requireResponses(t, spec.Paths["/api/v1/audit"]["get"], "200", "400", "401", "403", "405", "500", "503") requireAuditPaginationContract(t, spec.Paths["/api/v1/audit"]["get"]) requireAuditSchemas(t, spec) } func requireAuditPaginationContract(t *testing.T, operation any) { t.Helper() operationMap, ok := operation.(map[string]any) if !ok { t.Fatalf("audit operation has type %T, want map", operation) } parameters, ok := operationMap["parameters"].([]any) if !ok { t.Fatalf("audit parameters has type %T, want array", operationMap["parameters"]) } afterID := parameterSchema(t, parameters, "afterId") if afterID["type"] != "integer" || afterID["minimum"] != 0 || afterID["default"] != 0 { t.Errorf("afterId schema = %#v, want integer with minimum/default 0", afterID) } limit := parameterSchema(t, parameters, "limit") if limit["type"] != "integer" || limit["minimum"] != 1 || limit["maximum"] != 1000 || limit["default"] != 100 { t.Errorf("limit schema = %#v, want integer [1, 1000] with default 100", limit) } } func parameterSchema(t *testing.T, parameters []any, name string) map[string]any { t.Helper() for _, raw := range parameters { parameter, ok := raw.(map[string]any) if !ok || parameter["name"] != name || parameter["in"] != "query" { continue } schema, ok := parameter["schema"].(map[string]any) if !ok { t.Fatalf("%s parameter schema has type %T, want map", name, parameter["schema"]) } return schema } t.Fatalf("query parameter %q is missing", name) return nil } func requireAuditSchemas(t *testing.T, spec document) { t.Helper() schemas, ok := spec.Components["schemas"].(map[string]any) if !ok { t.Fatal("components.schemas is missing") } for _, name := range []string{"AuditPage", "AuditRecord"} { if _, ok := schemas[name].(map[string]any); !ok { t.Errorf("schema %s is missing", name) } } record, _ := schemas["AuditRecord"].(map[string]any) properties, _ := record["properties"].(map[string]any) for _, forbidden := range []string{"proxy", "upstreamUrl", "credential", "activePool"} { if _, exists := properties[forbidden]; exists { t.Errorf("AuditRecord must not expose %q", forbidden) } } } func requireResponses(t *testing.T, operation any, codes ...string) { t.Helper() operationMap, ok := operation.(map[string]any) if !ok { t.Fatalf("operation has type %T, want map", operation) } responses, ok := operationMap["responses"].(map[string]any) if !ok { t.Fatalf("responses has type %T, want map", operationMap["responses"]) } for _, code := range codes { if _, ok := responses[code]; !ok { t.Errorf("response %s is missing", code) } } } func readDocument(t *testing.T, path string) document { t.Helper() content, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } var spec document if err := yaml.Unmarshal(content, &spec); err != nil { t.Fatalf("parse %s: %v", path, err) } if spec.OpenAPI != "3.1.0" { t.Fatalf("%s openapi version = %q, want 3.1.0", path, spec.OpenAPI) } return spec }