You've already forked agentic-coding-workflow
137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type fakeResolver struct{ uid uuid.UUID }
|
|
|
|
func (f *fakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) {
|
|
if token == "" {
|
|
return uuid.Nil, false, nil
|
|
}
|
|
return f.uid, true, nil
|
|
}
|
|
|
|
type fakeAdmin struct{ admins map[uuid.UUID]bool }
|
|
|
|
func (f *fakeAdmin) IsAdmin(_ context.Context, id uuid.UUID) (bool, error) {
|
|
return f.admins[id], nil
|
|
}
|
|
|
|
type fakeReader struct {
|
|
lastFilter AuditFilter
|
|
entries []LogEntry
|
|
total int
|
|
}
|
|
|
|
func (f *fakeReader) List(_ context.Context, flt AuditFilter) ([]LogEntry, int, error) {
|
|
f.lastFilter = flt
|
|
return f.entries, f.total, nil
|
|
}
|
|
func (f *fakeReader) PurgeBefore(_ context.Context, _ time.Time) (int64, error) { return 0, nil }
|
|
|
|
func mount(reader Reader, uid uuid.UUID, admins map[uuid.UUID]bool) http.Handler {
|
|
r := chi.NewRouter()
|
|
NewHandler(reader, &fakeResolver{uid: uid}, &fakeAdmin{admins: admins}).Mount(r)
|
|
return r
|
|
}
|
|
|
|
func TestAuditHandlerNonAdminForbidden(t *testing.T) {
|
|
uid := uuid.New()
|
|
h := mount(&fakeReader{}, uid, map[uuid.UUID]bool{}) // not admin
|
|
req := httptest.NewRequest("GET", "/api/v1/admin/audit-logs/", nil)
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want 403", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuditHandlerUnauthenticated(t *testing.T) {
|
|
uid := uuid.New()
|
|
h := mount(&fakeReader{}, uid, map[uuid.UUID]bool{uid: true})
|
|
req := httptest.NewRequest("GET", "/api/v1/admin/audit-logs/", nil) // no token
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuditHandlerAdminListWithFilters(t *testing.T) {
|
|
uid := uuid.New()
|
|
filterUID := uuid.New()
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
reader := &fakeReader{
|
|
entries: []LogEntry{{
|
|
ID: 7,
|
|
UserID: &filterUID,
|
|
Action: "user.login",
|
|
CreatedAt: now,
|
|
}},
|
|
total: 1,
|
|
}
|
|
h := mount(reader, uid, map[uuid.UUID]bool{uid: true})
|
|
|
|
from := now.Add(-time.Hour).Format(time.RFC3339)
|
|
url := "/api/v1/admin/audit-logs/?action=user.login&target_type=user&user_id=" +
|
|
filterUID.String() + "&from=" + from + "&limit=10&offset=5"
|
|
req := httptest.NewRequest("GET", url, nil)
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
// Filter parsed and forwarded to reader.
|
|
if reader.lastFilter.Action != "user.login" {
|
|
t.Errorf("action filter = %q, want user.login", reader.lastFilter.Action)
|
|
}
|
|
if reader.lastFilter.TargetType != "user" {
|
|
t.Errorf("target_type filter = %q", reader.lastFilter.TargetType)
|
|
}
|
|
if reader.lastFilter.UserID == nil || *reader.lastFilter.UserID != filterUID {
|
|
t.Errorf("user_id filter not parsed: %v", reader.lastFilter.UserID)
|
|
}
|
|
if reader.lastFilter.From == nil {
|
|
t.Errorf("from filter not parsed")
|
|
}
|
|
if reader.lastFilter.Limit != 10 || reader.lastFilter.Offset != 5 {
|
|
t.Errorf("limit/offset = %d/%d, want 10/5", reader.lastFilter.Limit, reader.lastFilter.Offset)
|
|
}
|
|
|
|
var resp struct {
|
|
Items []auditLogDTO `json:"items"`
|
|
Total int `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Total != 1 || len(resp.Items) != 1 || resp.Items[0].ID != 7 {
|
|
t.Errorf("unexpected response: %+v", resp)
|
|
}
|
|
}
|
|
|
|
func TestAuditHandlerBadQueryParam(t *testing.T) {
|
|
uid := uuid.New()
|
|
h := mount(&fakeReader{}, uid, map[uuid.UUID]bool{uid: true})
|
|
req := httptest.NewRequest("GET", "/api/v1/admin/audit-logs/?from=not-a-date", nil)
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400; body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|