Skip to content
quarry Artifact [dee3fa8014]
Login

Artifact [dee3fa8014]

Artifact dee3fa8014bf0c5bb780e4fdee745f320275ac7241deb571a231d129d89a9616:


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", "basicAuth", "scheme") != "basic" {
		t.Fatal("basicAuth 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")
	}
}