package adminapi import ( "context" "net/http" "net/http/httptest" "slices" "strings" "testing" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/guard" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) type memGuard struct{ att []guard.Attachment } func (m *memGuard) GuardAttachments(context.Context) ([]guard.Attachment, error) { return m.att, nil } func (m *memGuard) AttachGuard(_ context.Context, a guard.Attachment, _ string) error { if !slices.Contains(m.att, a) { m.att = append(m.att, a) } return nil } func (m *memGuard) DetachGuard(_ context.Context, a guard.Attachment) (bool, error) { n := len(m.att) m.att = slices.DeleteFunc(m.att, func(x guard.Attachment) bool { return x == a }) return len(m.att) < n, nil } func TestGuardPoliciesAreAttachedByTarget(t *testing.T) { a, _ := setup(t, testModels()) if code := call(t, a, "GET", "/api/v1/guard", "", nil); code != 501 { t.Errorf("without policies: %d", code) } st := &memGuard{} invalidated := 0 a.SetGuard(&GuardAdmin{ Library: guard.Config{Default: []string{"base"}, Policies: map[string]guard.Policy{"base": {}, "finance": {}}}, Store: st, Invalidate: func() { invalidated++ }, }) if code := call(t, a, "POST", "/api/v1/guard/attachments", `{"target":"key:k1","policy":"finance"}`, nil); code != 200 { t.Fatalf("attach: %d", code) } if invalidated != 1 || len(st.att) != 1 || st.att[0] != (guard.Attachment{Scope: "key", ScopeID: "k1", Policy: "finance"}) { t.Errorf("stored %v, invalidated %d", st.att, invalidated) } for _, bad := range []string{`{"target":"key:k1","policy":"nope"}`, `{"target":"galaxy:x","policy":"finance"}`, `{"target":"key:","policy":"finance"}`} { if code := call(t, a, "POST", "/api/v1/guard/attachments", bad, nil); code != 400 { t.Errorf("%s: %d", bad, code) } } var v GuardView if code := call(t, a, "GET", "/api/v1/guard", "", &v); code != 200 || len(v.Attachments) != 1 || !slices.Equal(v.Policies, []string{"base", "finance"}) || !slices.Equal(v.Default, []string{"base"}) { t.Errorf("%d %+v", code, v) } if code := call(t, a, "DELETE", "/api/v1/guard/attachments", `{"target":"key:k1","policy":"finance"}`, nil); code != 200 || len(st.att) != 0 { t.Errorf("detach: %d %v", code, st.att) } if code := call(t, a, "DELETE", "/api/v1/guard/attachments", `{"target":"key:k1","policy":"finance"}`, nil); code != 404 { t.Errorf("detach again: %d", code) } } type memAudit struct{ f store.AuditFilter } func (m *memAudit) AuditEvents(_ context.Context, f store.AuditFilter) ([]store.AuditEvent, error) { m.f = f return []store.AuditEvent{{Actor: "ilker", Action: "POST /api/v1/models"}}, nil } func TestTheAuditLogIsReadWithFilters(t *testing.T) { a, _ := setup(t, testModels()) m := &memAudit{} a.SetAudit(m) var ev []store.AuditEvent if code := call(t, a, "GET", "/api/v1/audit?since=2d&actor=ilker&contains=models&limit=5", "", &ev); code != 200 || len(ev) != 1 { t.Fatalf("%d %v", code, ev) } if m.f.Actor != "ilker" || m.f.Contains != "models" || m.f.Limit != 5 || time.Since(m.f.Since) < 47*time.Hour { t.Errorf("filter %+v", m.f) } if code := call(t, a, "GET", "/api/v1/audit?since=soon", "", nil); code != 400 { t.Errorf("a bad since: %d", code) } } type memUsageExport struct { memAudit by []string period string } func (m *memUsageExport) UsageByScope(context.Context, store.Scope, time.Time) ([]store.ScopeUsage, error) { return nil, nil } func (m *memUsageExport) UsageExport(_ context.Context, from, to time.Time, period string, by []string) ([]store.ExportRow, error) { m.by, m.period = by, period return []store.ExportRow{{Period: from, Project: "=HYPERLINK(evil)", Model: "m", Requests: 3, CostMicros: 1500000}}, nil } func TestUsageIsExportedAsCSVThatASpreadsheetCannotRun(t *testing.T) { a, _ := setup(t, testModels()) m := &memUsageExport{} a.usage = m mux := http.NewServeMux() a.Register(mux) rec := httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/v1/usage/export?from=2026-09-01&to=2026-10-01&period=month&by=project,model", nil)) body := rec.Body.String() if rec.Code != 200 || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/csv") || !strings.Contains(body, "period_start,org,team,project") || !strings.Contains(body, "'=HYPERLINK") || !strings.Contains(body, "1.500000") || m.period != "month" || !slices.Equal(m.by, []string{"project", "model"}) { t.Errorf("%d %s %+v", rec.Code, body, m) } }