This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+136
View File
@@ -0,0 +1,136 @@
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())
}
}