package api import ( "context" "net/http" "sync" "testing" "time" "github.com/gofiber/fiber/v2" "wucher/internal/domain/audit" "wucher/internal/service" sharedconst "wucher/internal/shared/const" ) type mockAuditMiddlewareRepo struct { mu sync.Mutex entries []*audit.AuditLog } func (m *mockAuditMiddlewareRepo) Create(_ context.Context, entry *audit.AuditLog) error { m.mu.Lock() defer m.mu.Unlock() cp := *entry m.entries = append(m.entries, &cp) return nil } func TestAuditMiddleware_SkipsGETRequests(t *testing.T) { repo := &mockAuditMiddlewareRepo{} auditSvc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 16, Workers: 1}) app := fiber.New() app.Use(AuditMiddleware(auditSvc)) app.Get("/api/v1/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusCreated) }) req, _ := http.NewRequest(http.MethodGet, "/api/v1/ping", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } deadline := time.Now().Add(500 * time.Millisecond) for time.Now().Before(deadline) { repo.mu.Lock() n := len(repo.entries) repo.mu.Unlock() if n > 0 { break } time.Sleep(10 * time.Millisecond) } repo.mu.Lock() defer repo.mu.Unlock() if len(repo.entries) != 0 { t.Fatalf("expected no audit log entry for GET, got=%d", len(repo.entries)) } } func TestAuditMiddleware_LogsMutationRequests(t *testing.T) { repo := &mockAuditMiddlewareRepo{} auditSvc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 16, Workers: 1}) app := fiber.New() app.Use(AuditMiddleware(auditSvc)) app.Post("/api/v1/helicopters/create", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusCreated) }) req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } deadline := time.Now().Add(500 * time.Millisecond) for time.Now().Before(deadline) { repo.mu.Lock() n := len(repo.entries) repo.mu.Unlock() if n > 0 { break } time.Sleep(10 * time.Millisecond) } repo.mu.Lock() defer repo.mu.Unlock() if len(repo.entries) == 0 { t.Fatalf("expected audit log entry for mutation request") } got := repo.entries[0] if got.Layer != "api" || got.Module != sharedconst.AuditModuleHelicopter || got.Action != sharedconst.AuditActionCreate { t.Fatalf("unexpected audit entry: layer=%s module=%s action=%s", got.Layer, got.Module, got.Action) } if got.StatusCode != http.StatusCreated { t.Fatalf("expected status 201, got %d", got.StatusCode) } } func TestModuleFromPathSegment(t *testing.T) { cases := []struct { segment string want string }{ {segment: "helicopters", want: sharedconst.AuditModuleHelicopter}, {segment: "bases", want: sharedconst.AuditModuleBase}, {segment: "health-insurance-companies", want: sharedconst.AuditModuleHealthInsuranceCompany}, {segment: "hems-roster", want: sharedconst.AuditModuleUnknown}, {segment: "dul", want: sharedconst.AuditModuleDUL}, {segment: "reserve-acs", want: sharedconst.AuditModuleReserveAC}, {segment: "not-registered", want: sharedconst.AuditModuleUnknown}, } for _, tc := range cases { t.Run(tc.segment, func(t *testing.T) { got := moduleFromPathSegment(tc.segment) if got != tc.want { t.Fatalf("moduleFromPathSegment(%q) = %q, want %q", tc.segment, got, tc.want) } }) } } func TestResolveAuditModuleAction(t *testing.T) { cases := []struct { name string method string target string wantModule string wantAction string }{ { name: "datatable list", method: http.MethodGet, target: "/api/v1/helicopters/get-all/dt", wantModule: sharedconst.AuditModuleHelicopter, wantAction: sharedconst.AuditActionList, }, { name: "explicit create", method: http.MethodPost, target: "/api/v1/helicopters/create", wantModule: sharedconst.AuditModuleHelicopter, wantAction: sharedconst.AuditActionCreate, }, { name: "fallback method update", method: http.MethodPatch, target: "/api/v1/helicopters/019dbeb6-4c37-7495-b0d0-e02ed134f87d", wantModule: sharedconst.AuditModuleHelicopter, wantAction: sharedconst.AuditActionUpdate, }, { name: "base regular variant from query", method: http.MethodGet, target: "/api/v1/bases/get-all?category_type=regular", wantModule: sharedconst.AuditModuleBaseRegular, wantAction: sharedconst.AuditActionList, }, { name: "base hems variant from query alias", method: http.MethodGet, target: "/api/v1/bases/get-all?base_category=hems", wantModule: sharedconst.AuditModuleBaseHEMS, wantAction: sharedconst.AuditActionList, }, { name: "unknown module", method: http.MethodGet, target: "/api/v1/ping", wantModule: sharedconst.AuditModuleUnknown, wantAction: sharedconst.AuditActionGet, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { app := fiber.New() app.All("/*", func(c *fiber.Ctx) error { gotModule, gotAction := resolveAuditModuleAction(c) if gotModule != tc.wantModule || gotAction != tc.wantAction { t.Fatalf("resolveAuditModuleAction(%q, %q) = (%q, %q), want (%q, %q)", tc.method, tc.target, gotModule, gotAction, tc.wantModule, tc.wantAction) } return c.SendStatus(http.StatusNoContent) }) req, _ := http.NewRequest(tc.method, tc.target, nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } if resp.StatusCode != http.StatusNoContent { t.Fatalf("expected 204, got %d", resp.StatusCode) } }) } }