Files
amcs/internal/app/resolvespec_admin_test.go
Hein 9e6d05e055
Some checks failed
CI / build-and-test (push) Failing after -31m24s
feat(ui): add content editor components for skills and thoughts
* Implement ContentEditorField for inline editing of content
* Create ContentEditorModal for editing content in a modal
* Introduce FormerShell for managing forms related to skills and thoughts
* Enhance SkillsPage and ThoughtsPage with new components for better content management
2026-05-02 19:35:27 +02:00

174 lines
5.7 KiB
Go

package app
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.warky.dev/wdevs/amcs/internal/auth"
"git.warky.dev/wdevs/amcs/internal/config"
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
)
func TestResolveSpecAuthRequiresValidCredentials(t *testing.T) {
keyring, err := auth.NewKeyring([]config.APIKey{{ID: "operator", Value: "secret"}})
if err != nil {
t.Fatalf("NewKeyring() error = %v", err)
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
protected := auth.Middleware(config.AuthConfig{}, keyring, nil, nil, nil, logger)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/rs/public/projects" {
t.Fatalf("path = %q, want /api/rs/public/projects", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
t.Run("missing credentials are rejected", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/rs/public/projects", strings.NewReader(`{"operation":"read"}`))
rec := httptest.NewRecorder()
protected.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
})
t.Run("valid API key is accepted", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/rs/public/projects", strings.NewReader(`{"operation":"read"}`))
req.Header.Set("x-brain-key", "secret")
rec := httptest.NewRecorder()
protected.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
}
})
}
func TestResolveSpecGuardAllowsSupportedMutations(t *testing.T) {
rs := resolvespec.NewHandler(nil, nil)
registerResolveSpecGuards(rs)
cases := []struct {
name string
entity string
operation string
}{
{name: "learnings read", entity: "learnings", operation: "read"},
{name: "projects create", entity: "projects", operation: "create"},
{name: "projects update", entity: "projects", operation: "update"},
{name: "projects delete", entity: "projects", operation: "delete"},
{name: "plans create", entity: "plans", operation: "create"},
{name: "plans update", entity: "plans", operation: "update"},
{name: "plans delete", entity: "plans", operation: "delete"},
{name: "learnings create", entity: "learnings", operation: "create"},
{name: "learnings update", entity: "learnings", operation: "update"},
{name: "learnings delete", entity: "learnings", operation: "delete"},
{name: "thoughts create", entity: "thoughts", operation: "create"},
{name: "thoughts update", entity: "thoughts", operation: "update"},
{name: "thoughts delete", entity: "thoughts", operation: "delete"},
{name: "agent_skills create", entity: "agent_skills", operation: "create"},
{name: "agent_skills update", entity: "agent_skills", operation: "update"},
{name: "agent_skills delete", entity: "agent_skills", operation: "delete"},
{name: "agent_guardrails create", entity: "agent_guardrails", operation: "create"},
{name: "agent_guardrails update", entity: "agent_guardrails", operation: "update"},
{name: "agent_guardrails delete", entity: "agent_guardrails", operation: "delete"},
{name: "stored_files update", entity: "stored_files", operation: "update"},
{name: "stored_files delete", entity: "stored_files", operation: "delete"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
hookCtx := &resolvespec.HookContext{
Schema: "public",
Entity: tc.entity,
Operation: tc.operation,
}
err := rs.Hooks().Execute(resolvespec.BeforeHandle, hookCtx)
if err != nil {
t.Fatalf("Execute() error = %v, want nil", err)
}
if hookCtx.Abort {
t.Fatalf("Abort = true, want false (code=%d message=%q)", hookCtx.AbortCode, hookCtx.AbortMessage)
}
})
}
}
func TestResolveSpecGuardBlocksUnsupportedMutations(t *testing.T) {
rs := resolvespec.NewHandler(nil, nil)
registerResolveSpecGuards(rs)
cases := []struct {
name string
entity string
operation string
wantCode int
wantMessageIn string
}{
{
name: "mutations blocked for non-allowlisted operation",
entity: "stored_files",
operation: "create",
wantCode: http.StatusForbidden,
wantMessageIn: `operation "create" is not allowed for public.stored_files`,
},
{
name: "mutations blocked for non-allowlisted entity",
entity: "maintenance_logs",
operation: "delete",
wantCode: http.StatusForbidden,
wantMessageIn: `operation "delete" is not allowed for public.maintenance_logs`,
},
{
name: "unknown operation is rejected",
entity: "projects",
operation: "scan",
wantCode: http.StatusBadRequest,
wantMessageIn: `unsupported operation "scan"`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
hookCtx := &resolvespec.HookContext{
Schema: "public",
Entity: tc.entity,
Operation: tc.operation,
}
err := rs.Hooks().Execute(resolvespec.BeforeHandle, hookCtx)
if err == nil {
t.Fatal("Execute() error = nil, want non-nil")
}
if !hookCtx.Abort {
t.Fatal("Abort = false, want true")
}
if hookCtx.AbortCode != tc.wantCode {
t.Fatalf("AbortCode = %d, want %d", hookCtx.AbortCode, tc.wantCode)
}
if !strings.Contains(hookCtx.AbortMessage, tc.wantMessageIn) {
t.Fatalf("AbortMessage = %q, want substring %q", hookCtx.AbortMessage, tc.wantMessageIn)
}
})
}
}
func TestResolveSpecModelsIncludeLearnings(t *testing.T) {
models := resolveSpecModels()
for _, model := range models {
if model.schema == "public" && model.entity == "learnings" {
return
}
}
t.Fatal("resolveSpecModels() missing public.learnings")
}