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") }