148 lines
4.4 KiB
Go
148 lines
4.4 KiB
Go
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
|
|
}
|