package openapi import ( "fmt" "os" "strings" "testing" "go.yaml.in/yaml/v4" ) var httpOperationMethods = map[string]struct{}{ "delete": {}, "get": {}, "head": {}, "options": {}, "patch": {}, "post": {}, "put": {}, "trace": {}, } func TestOpenAPIDocumentsHaveClosedContracts(t *testing.T) { for _, name := range []string{"proxy-pool.yaml", "admin.yaml"} { name := name t.Run(name, func(t *testing.T) { root := readOpenAPIRoot(t, name) validateLocalReferences(t, root) validateOperations(t, root) validateSecurityRequirements(t, root) }) } } func validateLocalReferences(t *testing.T, root map[string]any) { t.Helper() walkOpenAPI(root, func(path string, value any) { object, ok := value.(map[string]any) if !ok { return } reference, exists := object["$ref"] if !exists { return } text, ok := reference.(string) if !ok || !strings.HasPrefix(text, "#/") { t.Errorf("%s has unsupported reference %v", path, reference) return } if _, ok := resolveLocalReference(root, text); !ok { t.Errorf("%s points to missing local reference %q", path, text) } }) } func validateOperations(t *testing.T, root map[string]any) { t.Helper() paths, ok := root["paths"].(map[string]any) if !ok || len(paths) == 0 { t.Fatal("paths must be a non-empty object") } operationIDs := make(map[string]string) for path, rawItem := range paths { item, ok := rawItem.(map[string]any) if !ok { t.Errorf("path %q has type %T, want object", path, rawItem) continue } for method, rawOperation := range item { if _, ok := httpOperationMethods[strings.ToLower(method)]; !ok { continue } operation, ok := rawOperation.(map[string]any) if !ok { t.Errorf("%s %s has type %T, want object", method, path, rawOperation) continue } operationID, _ := operation["operationId"].(string) if strings.TrimSpace(operationID) == "" { t.Errorf("%s %s has no operationId", method, path) } else if previous, duplicate := operationIDs[operationID]; duplicate { t.Errorf("operationId %q is shared by %s and %s %s", operationID, previous, method, path) } else { operationIDs[operationID] = method + " " + path } responses, ok := operation["responses"].(map[string]any) if !ok || len(responses) == 0 { t.Errorf("%s %s has no responses", method, path) } } } } func validateSecurityRequirements(t *testing.T, root map[string]any) { t.Helper() components, _ := root["components"].(map[string]any) schemes, _ := components["securitySchemes"].(map[string]any) walkOpenAPI(root, func(path string, value any) { if !strings.HasSuffix(path, ".security") { return } requirements, ok := value.([]any) if !ok { t.Errorf("%s has type %T, want array", path, value) return } for _, rawRequirement := range requirements { requirement, ok := rawRequirement.(map[string]any) if !ok { t.Errorf("%s contains %T, want object", path, rawRequirement) continue } for name := range requirement { if _, exists := schemes[name]; !exists { t.Errorf("%s references unknown security scheme %q", path, name) } } } }) } func readOpenAPIRoot(t *testing.T, name string) map[string]any { t.Helper() payload, err := os.ReadFile(name) if err != nil { t.Fatalf("read %s: %v", name, err) } var root map[string]any if err := yaml.Unmarshal(payload, &root); err != nil { t.Fatalf("parse %s: %v", name, err) } return root } func walkOpenAPI(value any, visit func(string, any)) { var walk func(string, any) walk = func(path string, current any) { visit(path, current) switch item := current.(type) { case map[string]any: for key, child := range item { walk(joinOpenAPIPath(path, key), child) } case []any: for index, child := range item { walk(fmt.Sprintf("%s[%d]", path, index), child) } } } walk("$", value) } func resolveLocalReference(root map[string]any, reference string) (any, bool) { var current any = root for _, token := range strings.Split(strings.TrimPrefix(reference, "#/"), "/") { token = strings.ReplaceAll(strings.ReplaceAll(token, "~1", "/"), "~0", "~") object, ok := current.(map[string]any) if !ok { return nil, false } current, ok = object[token] if !ok { return nil, false } } return current, true } func joinOpenAPIPath(parent, child string) string { if parent == "" { return child } return parent + "." + child }