Artifact 261f4d520b1fe3cbaa5f58e24feed2710c6636955298d5350c9a1e57405f7cb8:
- File
internal/ops/spec_test.go
— part of check-in
[cfd3f85ff3]
at
2026-08-14 22:29:35
on branch trunk
— OpenAPI reflector over the registry (dagnats 0.0.13)
quarry spec subcommand, RegisterAll with meta.version, required Op.Doc, validate rules feed schemas; make spec-lint via vacuum. (user: dmestas, size: 6705) [annotate] [blame] [check-ins using]
package ops import ( "context" "encoding/json" "strings" "testing" ) type specListIn struct { Repo string `json:"repo" validate:"required,max=64"` Limit int `json:"limit" validate:"min=1,max=500"` Sort string `json:"sort" validate:"oneof=asc|desc"` } type specListOut struct { Items []string `json:"items"` Total int `json:"total"` } type specPostIn struct { Repo string `json:"repo" validate:"required"` Body string `json:"body" validate:"required,max=4000"` } type specPostOut struct { ID string `json:"id"` } func specRegistry(t *testing.T) *Registry { t.Helper() r := NewRegistry() if err := Register(r, Op[specListIn, specListOut]{ Name: "timeline.list", Doc: "List a repository timeline.", Kind: Query, Auth: Anonymous, Route: "GET /api/v1/r/{repo}/timeline", Run: func(_ context.Context, _ specListIn) (specListOut, error) { return specListOut{}, nil }, }); err != nil { t.Fatal(err) } if err := Register(r, Op[specPostIn, specPostOut]{ Name: "chat.post", Doc: "Post a chat message.", Kind: Command, Auth: Token, Route: "POST /api/v1/r/{repo}/chat", Run: func(_ context.Context, _ specPostIn) (specPostOut, error) { return specPostOut{}, nil }, }); err != nil { t.Fatal(err) } return r } // specJSON renders the generated spec and re-parses it into a generic map // so tests assert on the document consumers actually receive. func specJSON(t *testing.T) map[string]any { t.Helper() spec, err := Spec(specRegistry(t), "quarry", "0.0.0-test") if err != nil { t.Fatal(err) } raw, err := json.Marshal(spec) if err != nil { t.Fatal(err) } var doc map[string]any if err := json.Unmarshal(raw, &doc); err != nil { t.Fatal(err) } return doc } func dig(t *testing.T, doc any, path ...string) any { t.Helper() for _, key := range path { m, ok := doc.(map[string]any) if !ok || m[key] == nil { t.Fatalf("missing %q in path %v", key, path) } doc = m[key] } return doc } func TestSpecTopLevel(t *testing.T) { doc := specJSON(t) if v := dig(t, doc, "openapi"); v != "3.1.0" { t.Fatalf("openapi = %v", v) } if v := dig(t, doc, "info", "version"); v != "0.0.0-test" { t.Fatalf("version = %v", v) } } func TestSpecQueryOpParameters(t *testing.T) { doc := specJSON(t) op := dig(t, doc, "paths", "/api/v1/r/{repo}/timeline", "get") if v := dig(t, op, "operationId"); v != "timeline.list" { t.Fatalf("operationId = %v", v) } params := dig(t, op, "parameters").([]any) byName := map[string]map[string]any{} for _, p := range params { pm := p.(map[string]any) byName[pm["name"].(string)] = pm } repo, ok := byName["repo"] if !ok || repo["in"] != "path" || repo["required"] != true { t.Fatalf("repo param wrong: %v", byName) } limit, ok := byName["limit"] if !ok || limit["in"] != "query" { t.Fatalf("limit param wrong: %v", byName) } var schema map[string]any if err := json.Unmarshal([]byte(mustJSON(t, limit["schema"])), &schema); err != nil { t.Fatal(err) } if schema["type"] != "integer" || schema["minimum"] != 1.0 || schema["maximum"] != 500.0 { t.Fatalf("limit schema = %v", schema) } var sortSchema map[string]any if err := json.Unmarshal([]byte(mustJSON(t, byName["sort"]["schema"])), &sortSchema); err != nil { t.Fatal(err) } enum, _ := sortSchema["enum"].([]any) if len(enum) != 2 || enum[0] != "asc" || enum[1] != "desc" { t.Fatalf("sort enum = %v", sortSchema) } } func TestSpecCommandOpBothContentTypes(t *testing.T) { doc := specJSON(t) content := dig(t, doc, "paths", "/api/v1/r/{repo}/chat", "post", "requestBody", "content").(map[string]any) if content["application/json"] == nil || content["application/x-www-form-urlencoded"] == nil { t.Fatalf("want both codec content types, got %v", content) } // Path param stays a parameter, not a body property. var body map[string]any schema := dig(t, content, "application/json", "schema") if err := json.Unmarshal([]byte(mustJSON(t, schema)), &body); err != nil { t.Fatal(err) } props := body["properties"].(map[string]any) if props["repo"] != nil { t.Fatal("path wildcard leaked into body schema") } if props["body"] == nil { t.Fatalf("body property missing: %v", props) } req, _ := body["required"].([]any) if len(req) != 1 || req[0] != "body" { t.Fatalf("required = %v", req) } } func TestSpecResponses(t *testing.T) { doc := specJSON(t) schema := dig(t, doc, "paths", "/api/v1/r/{repo}/timeline", "get", "responses", "200", "content", "application/json", "schema") var out map[string]any if err := json.Unmarshal([]byte(mustJSON(t, schema)), &out); err != nil { t.Fatal(err) } props := out["properties"].(map[string]any) if props["items"] == nil || props["total"] == nil { t.Fatalf("out schema = %v", out) } } func TestSpecAuth(t *testing.T) { doc := specJSON(t) // Anonymous op: no security. get := dig(t, doc, "paths", "/api/v1/r/{repo}/timeline", "get").(map[string]any) if get["security"] != nil { t.Fatalf("anonymous op has security: %v", get["security"]) } // Token op: security + scheme present. post := dig(t, doc, "paths", "/api/v1/r/{repo}/chat", "post").(map[string]any) if post["security"] == nil { t.Fatal("token op missing security") } if post["x-quarry-auth"] != "token" { t.Fatalf("x-quarry-auth = %v", post["x-quarry-auth"]) } if dig(t, doc, "components", "securitySchemes", "bearerToken", "scheme") != "bearer" { t.Fatal("bearerToken scheme missing") } } func TestSpecTags(t *testing.T) { doc := specJSON(t) tags := dig(t, doc, "paths", "/api/v1/r/{repo}/timeline", "get", "tags").([]any) if len(tags) != 1 || tags[0] != "timeline" { t.Fatalf("tags = %v", tags) } // Every op tag is declared globally (merged via the extension map). global := dig(t, doc, "tags").([]any) if len(global) != 2 { t.Fatalf("global tags = %v", global) } } func TestSpecSummariesAndInfoDescription(t *testing.T) { doc := specJSON(t) if dig(t, doc, "info", "description") == "" { t.Fatal("info.description missing") } if v := dig(t, doc, "paths", "/api/v1/r/{repo}/timeline", "get", "summary"); v != "List a repository timeline." { t.Fatalf("summary = %v", v) } } func mustJSON(t *testing.T, v any) string { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatal(err) } return string(b) } func TestSpecDeterministic(t *testing.T) { a, err := Spec(specRegistry(t), "quarry", "v") if err != nil { t.Fatal(err) } b, err := Spec(specRegistry(t), "quarry", "v") if err != nil { t.Fatal(err) } ja, jb := mustJSON(t, a), mustJSON(t, b) if ja != jb { t.Fatalf("spec not deterministic:\n%s\n%s", ja, jb) } if !strings.Contains(ja, "timeline.list") { t.Fatal("spec missing op") } }