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