feat(09-12): add the phase 9 acceptance gate
- Document every D-09 admin route for the OpenAPI contract. - Fail the gate on skipped tests, zero-test runs, and a missing admin path.
This commit is contained in:
91
cabana/phase09_contract_test.go
Normal file
91
cabana/phase09_contract_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPhase09ContractInventory fails when the committed OpenAPI document
|
||||
// drops a D-09 route or a protected route's 401 response.
|
||||
func TestPhase09ContractInventory(t *testing.T) {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("caller")
|
||||
}
|
||||
specPath := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "fonoteka.go", "docs", "openapi.json"))
|
||||
raw, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", specPath, err)
|
||||
}
|
||||
var spec struct {
|
||||
Paths map[string]map[string]struct {
|
||||
Responses map[string]json.RawMessage `json:"responses"`
|
||||
Security []map[string]json.RawMessage `json:"security"`
|
||||
} `json:"paths"`
|
||||
Components struct {
|
||||
SecuritySchemes map[string]json.RawMessage `json:"securitySchemes"`
|
||||
} `json:"components"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &spec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := spec.Components.SecuritySchemes["BackendBearer"]; !ok {
|
||||
t.Fatal("openapi is missing the BackendBearer scheme")
|
||||
}
|
||||
public := map[string]bool{
|
||||
"POST /_admin/api/v1/auth/login": true,
|
||||
"POST /_admin/api/v1/auth/refresh": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, route := range phase09Routes {
|
||||
method, path, ok := splitRoute(route.key)
|
||||
if !ok {
|
||||
t.Fatalf("bad route key %s", route.key)
|
||||
}
|
||||
method = strings.ToLower(method)
|
||||
ops, ok := spec.Paths[path]
|
||||
if !ok {
|
||||
t.Fatalf("openapi missing %s", path)
|
||||
}
|
||||
op, ok := ops[method]
|
||||
if !ok {
|
||||
t.Fatalf("openapi missing %s %s", method, path)
|
||||
}
|
||||
key := route.key
|
||||
if seen[key] {
|
||||
t.Fatalf("duplicate contract route %s", key)
|
||||
}
|
||||
seen[key] = true
|
||||
if _, ok := op.Responses["200"]; !ok {
|
||||
t.Fatalf("%s has no 200 response", key)
|
||||
}
|
||||
if public[key] {
|
||||
if _, ok := op.Responses["401"]; !ok {
|
||||
t.Fatalf("%s has no 401 response", key)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(op.Security) == 0 {
|
||||
t.Fatalf("%s has no BackendBearer security requirement", key)
|
||||
}
|
||||
if _, ok := op.Responses["401"]; !ok {
|
||||
t.Fatalf("%s has no 401 response", key)
|
||||
}
|
||||
}
|
||||
if len(seen) != len(phase09Routes) {
|
||||
t.Fatalf("contract routes=%d want %d", len(seen), len(phase09Routes))
|
||||
}
|
||||
}
|
||||
|
||||
func splitRoute(key string) (method, path string, ok bool) {
|
||||
for i := 0; i < len(key); i++ {
|
||||
if key[i] == ' ' {
|
||||
return key[:i], key[i+1:], key[i+1:] != ""
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
Reference in New Issue
Block a user