go-api/export_test.go
Snider e94283b06c feat: add spec export helpers for JSON and YAML output
ExportSpec writes the OpenAPI spec to any io.Writer in JSON or YAML
format. ExportSpecToFile is a convenience wrapper that creates the
parent directory and writes to a file path. Adds gopkg.in/yaml.v3
for YAML marshalling.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:51:58 +00:00

166 lines
4.5 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api_test
import (
"bytes"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
api "forge.lthn.ai/core/go-api"
)
// ── ExportSpec tests ─────────────────────────────────────────────────────
func TestExportSpec_Good_JSON(t *testing.T) {
builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"}
var buf bytes.Buffer
if err := api.ExportSpec(&buf, "json", builder, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
var spec map[string]any
if err := json.Unmarshal(buf.Bytes(), &spec); err != nil {
t.Fatalf("output is not valid JSON: %v", err)
}
if spec["openapi"] != "3.1.0" {
t.Fatalf("expected openapi=3.1.0, got %v", spec["openapi"])
}
info := spec["info"].(map[string]any)
if info["title"] != "Test" {
t.Fatalf("expected title=Test, got %v", info["title"])
}
}
func TestExportSpec_Good_YAML(t *testing.T) {
builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"}
var buf bytes.Buffer
if err := api.ExportSpec(&buf, "yaml", builder, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := buf.String()
if !strings.Contains(output, "openapi:") {
t.Fatalf("expected YAML output to contain 'openapi:', got:\n%s", output)
}
var spec map[string]any
if err := yaml.Unmarshal(buf.Bytes(), &spec); err != nil {
t.Fatalf("output is not valid YAML: %v", err)
}
if spec["openapi"] != "3.1.0" {
t.Fatalf("expected openapi=3.1.0, got %v", spec["openapi"])
}
}
func TestExportSpec_Bad_InvalidFormat(t *testing.T) {
builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"}
var buf bytes.Buffer
err := api.ExportSpec(&buf, "xml", builder, nil)
if err == nil {
t.Fatal("expected error for unsupported format, got nil")
}
if !strings.Contains(err.Error(), "unsupported format") {
t.Fatalf("expected error to contain 'unsupported format', got: %v", err)
}
}
func TestExportSpecToFile_Good_CreatesFile(t *testing.T) {
builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"}
dir := t.TempDir()
path := filepath.Join(dir, "subdir", "spec.json")
if err := api.ExportSpecToFile(path, "json", builder, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
var spec map[string]any
if err := json.Unmarshal(data, &spec); err != nil {
t.Fatalf("file content is not valid JSON: %v", err)
}
if spec["openapi"] != "3.1.0" {
t.Fatalf("expected openapi=3.1.0, got %v", spec["openapi"])
}
}
func TestExportSpec_Good_WithToolBridge(t *testing.T) {
gin.SetMode(gin.TestMode)
builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"}
bridge := api.NewToolBridge("/tools")
bridge.Add(api.ToolDescriptor{
Name: "file_read",
Description: "Read a file",
Group: "files",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"path": map[string]any{"type": "string"},
},
},
}, func(c *gin.Context) {
c.JSON(http.StatusOK, api.OK("ok"))
})
bridge.Add(api.ToolDescriptor{
Name: "metrics_query",
Description: "Query metrics",
Group: "metrics",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
},
},
}, func(c *gin.Context) {
c.JSON(http.StatusOK, api.OK("ok"))
})
var buf bytes.Buffer
if err := api.ExportSpec(&buf, "json", builder, []api.RouteGroup{bridge}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := buf.String()
if !strings.Contains(output, "/tools/file_read") {
t.Fatalf("expected output to contain /tools/file_read, got:\n%s", output)
}
if !strings.Contains(output, "/tools/metrics_query") {
t.Fatalf("expected output to contain /tools/metrics_query, got:\n%s", output)
}
// Verify it's valid JSON.
var spec map[string]any
if err := json.Unmarshal(buf.Bytes(), &spec); err != nil {
t.Fatalf("output is not valid JSON: %v", err)
}
// Verify paths exist.
paths := spec["paths"].(map[string]any)
if _, ok := paths["/tools/file_read"]; !ok {
t.Fatal("expected /tools/file_read path in spec")
}
if _, ok := paths["/tools/metrics_query"]; !ok {
t.Fatal("expected /tools/metrics_query path in spec")
}
}