Files
fm_be/internal/app/api/audit_middleware_test.go
2026-07-16 22:16:45 +07:00

209 lines
5.8 KiB
Go

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