This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/openapi_test.go
Jeffrey Smith 680ec3b897
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m34s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / build-and-deploy (push) Successful in 1m55s
Feat rebrand armature (#43)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 23:25:37 +00:00

411 lines
10 KiB
Go

package handlers
import (
"context"
"encoding/json"
"testing"
"armature/database"
"armature/store"
postgres "armature/store/postgres"
sqlite "armature/store/sqlite"
)
// Minimal static spec for testing — valid OpenAPI 3.0 structure.
var testStaticSpec = []byte(`
openapi: "3.0.3"
info:
title: Armature
version: "${VERSION}"
paths:
/api/v1/health:
get:
summary: Health check
responses:
"200":
description: OK
tags:
- name: system
description: System endpoints
`)
func openapiTestStores(t *testing.T) store.Stores {
t.Helper()
database.RequireTestDB(t)
database.TruncateAll(t)
if database.IsSQLite() {
return sqlite.NewStores(database.TestDB)
}
return postgres.NewStores(database.TestDB)
}
// ── Test: Zero extensions → kernel spec only ──────────────────
func TestBuildOpenAPISpec_NoExtensions(t *testing.T) {
stores := openapiTestStores(t)
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "0.6.2")
if err != nil {
t.Fatalf("BuildOpenAPISpec failed: %v", err)
}
// Version should be substituted
info, _ := spec["info"].(map[string]any)
if info == nil {
t.Fatal("missing info field")
}
if v, _ := info["version"].(string); v != "0.6.2" {
t.Errorf("expected version 0.6.2, got %q", v)
}
// Paths should contain the kernel endpoint
paths, _ := spec["paths"].(map[string]any)
if paths == nil {
t.Fatal("missing paths field")
}
if _, ok := paths["/api/v1/health"]; !ok {
t.Error("expected /api/v1/health in paths")
}
// Tags should contain kernel tag
tags, _ := spec["tags"].([]any)
if len(tags) < 1 {
t.Error("expected at least 1 tag")
}
}
// ── Test: Extension with api_routes but no api_schema → stubs ────
func TestBuildOpenAPISpec_StubEntries(t *testing.T) {
stores := openapiTestStores(t)
// Install a test extension with api_routes
manifest := map[string]any{
"api_routes": []any{
map[string]any{"method": "GET", "path": "/items"},
map[string]any{"method": "POST", "path": "/items"},
},
}
manifestJSON, _ := json.Marshal(manifest)
err := stores.Packages.Create(context.Background(), &store.PackageRegistration{
ID: "test-ext",
Title: "Test Extension",
Type: "extension",
Version: "1.0.0",
Tier: "starlark",
Manifest: manifest,
Enabled: true,
Status: "active",
Source: "extension",
Scope: "global",
})
_ = manifestJSON
if err != nil {
t.Fatalf("failed to create test package: %v", err)
}
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "0.6.2")
if err != nil {
t.Fatalf("BuildOpenAPISpec failed: %v", err)
}
paths, _ := spec["paths"].(map[string]any)
// Check GET stub
getPath, ok := paths["/s/test-ext/api/items"]
if !ok {
t.Fatal("expected /s/test-ext/api/items in paths")
}
pathItem, _ := getPath.(map[string]any)
getOp, _ := pathItem["get"].(map[string]any)
if getOp == nil {
t.Fatal("expected GET operation")
}
summary, _ := getOp["summary"].(string)
if summary != "test-ext: GET /items" {
t.Errorf("unexpected stub summary: %q", summary)
}
// Check POST stub
postOp, _ := pathItem["post"].(map[string]any)
if postOp == nil {
t.Fatal("expected POST operation")
}
// Check tag
tags, _ := getOp["tags"].([]any)
if len(tags) != 1 || tags[0] != "extensions/test-ext" {
t.Errorf("unexpected tags: %v", tags)
}
}
// ── Test: Extension with api_schema → rich entries ────────
func TestBuildOpenAPISpec_SchemaEntries(t *testing.T) {
stores := openapiTestStores(t)
manifest := map[string]any{
"api_routes": []any{
map[string]any{"method": "GET", "path": "/items"},
map[string]any{"method": "POST", "path": "/items"},
map[string]any{"method": "DELETE", "path": "/items/:id"},
},
"api_schema": []any{
map[string]any{
"path": "/items",
"method": "GET",
"summary": "List items",
"params": map[string]any{
"limit": map[string]any{"type": "integer", "default": 50},
},
"response": map[string]any{
"type": "object",
"example": map[string]any{"data": []any{}},
},
},
map[string]any{
"path": "/items",
"method": "POST",
"summary": "Create item",
"body": map[string]any{
"name": map[string]any{"type": "string", "required": true},
"description": map[string]any{"type": "string"},
},
},
},
}
err := stores.Packages.Create(context.Background(), &store.PackageRegistration{
ID: "rich-ext",
Title: "Rich Extension",
Type: "extension",
Version: "1.0.0",
Tier: "starlark",
Manifest: manifest,
Enabled: true,
Status: "active",
Source: "extension",
Scope: "global",
})
if err != nil {
t.Fatalf("failed to create test package: %v", err)
}
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "0.6.2")
if err != nil {
t.Fatalf("BuildOpenAPISpec failed: %v", err)
}
paths, _ := spec["paths"].(map[string]any)
// GET /items should have rich schema (summary = "List items")
itemsPath, ok := paths["/s/rich-ext/api/items"]
if !ok {
t.Fatal("expected /s/rich-ext/api/items in paths")
}
pathItem, _ := itemsPath.(map[string]any)
getOp, _ := pathItem["get"].(map[string]any)
if getOp == nil {
t.Fatal("expected GET operation")
}
if s, _ := getOp["summary"].(string); s != "List items" {
t.Errorf("expected rich summary, got %q", s)
}
// Should have parameters
params, _ := getOp["parameters"].([]any)
if len(params) == 0 {
t.Error("expected parameters from api_schema")
}
// POST /items should have requestBody
postOp, _ := pathItem["post"].(map[string]any)
if postOp == nil {
t.Fatal("expected POST operation")
}
if _, ok := postOp["requestBody"]; !ok {
t.Error("expected requestBody from api_schema body")
}
// DELETE /items/:id should be a stub (no api_schema for it)
deletePath, ok := paths["/s/rich-ext/api/items/:id"]
if !ok {
t.Fatal("expected /s/rich-ext/api/items/:id in paths")
}
deleteItem, _ := deletePath.(map[string]any)
deleteOp, _ := deleteItem["delete"].(map[string]any)
if deleteOp == nil {
t.Fatal("expected DELETE operation")
}
deleteSummary, _ := deleteOp["summary"].(string)
if deleteSummary != "rich-ext: DELETE /items/:id" {
t.Errorf("expected stub summary for undeclared route, got %q", deleteSummary)
}
}
// ── Test: Multiple extensions → all appear ────────
func TestBuildOpenAPISpec_MultipleExtensions(t *testing.T) {
stores := openapiTestStores(t)
for _, ext := range []struct {
id string
title string
route string
}{
{"ext-a", "Extension A", "/status"},
{"ext-b", "Extension B", "/health"},
} {
err := stores.Packages.Create(context.Background(), &store.PackageRegistration{
ID: ext.id,
Title: ext.title,
Type: "extension",
Version: "1.0.0",
Tier: "starlark",
Manifest: map[string]any{
"api_routes": []any{
map[string]any{"method": "GET", "path": ext.route},
},
},
Enabled: true,
Status: "active",
Source: "extension",
Scope: "global",
})
if err != nil {
t.Fatalf("failed to create %s: %v", ext.id, err)
}
}
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "0.6.2")
if err != nil {
t.Fatalf("BuildOpenAPISpec failed: %v", err)
}
paths, _ := spec["paths"].(map[string]any)
if _, ok := paths["/s/ext-a/api/status"]; !ok {
t.Error("expected ext-a path")
}
if _, ok := paths["/s/ext-b/api/health"]; !ok {
t.Error("expected ext-b path")
}
// Verify tags
tags, _ := spec["tags"].([]any)
tagNames := make(map[string]bool)
for _, tag := range tags {
if m, ok := tag.(map[string]any); ok {
if n, ok := m["name"].(string); ok {
tagNames[n] = true
}
}
}
if !tagNames["extensions/ext-a"] {
t.Error("expected extensions/ext-a tag")
}
if !tagNames["extensions/ext-b"] {
t.Error("expected extensions/ext-b tag")
}
}
// ── Test: Malformed api_schema → stubs, no crash ────────
func TestBuildOpenAPISpec_MalformedSchema(t *testing.T) {
stores := openapiTestStores(t)
manifest := map[string]any{
"api_routes": []any{
map[string]any{"method": "GET", "path": "/data"},
},
"api_schema": []any{
"not-an-object", // malformed
map[string]any{"path": "/data"}, // missing method
map[string]any{"method": "GET"}, // missing path
map[string]any{"path": "/data", "method": "POST"}, // valid but no matching route
},
}
err := stores.Packages.Create(context.Background(), &store.PackageRegistration{
ID: "bad-schema",
Title: "Bad Schema",
Type: "extension",
Version: "1.0.0",
Tier: "starlark",
Manifest: manifest,
Enabled: true,
Status: "active",
Source: "extension",
Scope: "global",
})
if err != nil {
t.Fatalf("failed to create package: %v", err)
}
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "0.6.2")
if err != nil {
t.Fatalf("BuildOpenAPISpec should not error on malformed schema: %v", err)
}
paths, _ := spec["paths"].(map[string]any)
// GET /data should still appear as a stub
if _, ok := paths["/s/bad-schema/api/data"]; !ok {
t.Error("expected stub entry for /data despite malformed api_schema")
}
}
// ── Test: Disabled extension → excluded ────────
func TestBuildOpenAPISpec_DisabledExcluded(t *testing.T) {
stores := openapiTestStores(t)
err := stores.Packages.Create(context.Background(), &store.PackageRegistration{
ID: "disabled-ext",
Title: "Disabled",
Type: "extension",
Version: "1.0.0",
Tier: "starlark",
Manifest: map[string]any{
"api_routes": []any{
map[string]any{"method": "GET", "path": "/secret"},
},
},
Enabled: false,
Status: "active",
Source: "extension",
Scope: "global",
})
if err != nil {
t.Fatalf("failed to create package: %v", err)
}
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "0.6.2")
if err != nil {
t.Fatalf("BuildOpenAPISpec failed: %v", err)
}
paths, _ := spec["paths"].(map[string]any)
if _, ok := paths["/s/disabled-ext/api/secret"]; ok {
t.Error("disabled extension should not appear in spec")
}
}
// ── Test: Required OpenAPI 3.0 fields present ────────
func TestBuildOpenAPISpec_RequiredFields(t *testing.T) {
stores := openapiTestStores(t)
spec, err := BuildOpenAPISpec(stores, testStaticSpec, "1.0.0")
if err != nil {
t.Fatalf("BuildOpenAPISpec failed: %v", err)
}
if _, ok := spec["openapi"]; !ok {
t.Error("missing required 'openapi' field")
}
if _, ok := spec["info"]; !ok {
t.Error("missing required 'info' field")
}
if _, ok := spec["paths"]; !ok {
t.Error("missing required 'paths' field")
}
}