Some checks failed
CI / build-and-test (push) Failing after -31m53s
* Implement maintenance page with task and log display * Add backfill and metadata retry functionality * Integrate grid component for project display in thoughts page * Update types for maintenance tasks and logs * Enhance sidebar and shell for new maintenance navigation
173 lines
5.0 KiB
Go
173 lines
5.0 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: "thoughts update", entity: "thoughts", operation: "update"},
|
|
{name: "thoughts delete", entity: "thoughts", operation: "delete"},
|
|
{name: "agent_skills delete", entity: "agent_skills", operation: "delete"},
|
|
{name: "agent_guardrails delete", entity: "agent_guardrails", 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: "create not allowed on thoughts",
|
|
entity: "thoughts",
|
|
operation: "create",
|
|
wantCode: http.StatusForbidden,
|
|
wantMessageIn: `operation "create" is not allowed for public.thoughts`,
|
|
},
|
|
{
|
|
name: "delete not allowed on projects",
|
|
entity: "projects",
|
|
operation: "delete",
|
|
wantCode: http.StatusForbidden,
|
|
wantMessageIn: `operation "delete" is not allowed for public.projects`,
|
|
},
|
|
{
|
|
name: "mutations blocked for non-allowlisted entity",
|
|
entity: "stored_files",
|
|
operation: "delete",
|
|
wantCode: http.StatusForbidden,
|
|
wantMessageIn: `operation "delete" is not allowed for public.stored_files`,
|
|
},
|
|
{
|
|
name: "mutations blocked for learnings",
|
|
entity: "learnings",
|
|
operation: "delete",
|
|
wantCode: http.StatusForbidden,
|
|
wantMessageIn: `operation "delete" is not allowed for public.learnings`,
|
|
},
|
|
{
|
|
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")
|
|
}
|