287 lines
9.1 KiB
Go
287 lines
9.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
"wucher/internal/domain/audit"
|
|
"wucher/internal/service"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
type mockAuditListRepo struct {
|
|
mu sync.Mutex
|
|
rows []audit.AuditLog
|
|
total int64
|
|
err error
|
|
lastFilter string
|
|
lastMethods []string
|
|
lastModules []string
|
|
lastActions []string
|
|
lastSort string
|
|
lastLimit int
|
|
lastOffset int
|
|
}
|
|
|
|
func (m *mockAuditListRepo) Create(_ context.Context, _ *audit.AuditLog) error { return nil }
|
|
|
|
func (m *mockAuditListRepo) List(_ context.Context, filter string, methods []string, modules []string, actions []string, sort string, limit, offset int) ([]audit.AuditLog, int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.lastFilter = filter
|
|
m.lastMethods = append([]string(nil), methods...)
|
|
m.lastModules = append([]string(nil), modules...)
|
|
m.lastActions = append([]string(nil), actions...)
|
|
m.lastSort = sort
|
|
m.lastLimit = limit
|
|
m.lastOffset = offset
|
|
if m.err != nil {
|
|
return nil, 0, m.err
|
|
}
|
|
out := make([]audit.AuditLog, len(m.rows))
|
|
copy(out, m.rows)
|
|
return out, m.total, nil
|
|
}
|
|
|
|
func TestAuditHandler_List(t *testing.T) {
|
|
repo := &mockAuditListRepo{
|
|
rows: []audit.AuditLog{
|
|
{
|
|
ID: uuidv7.MustBytes(),
|
|
Layer: "api",
|
|
Module: "users",
|
|
Action: "list",
|
|
Method: "GET",
|
|
Path: "/api/v1/users/get-all",
|
|
StatusCode: 200,
|
|
Success: true,
|
|
IP: "127.0.0.1",
|
|
CreatedAt: time.Now().UTC(),
|
|
},
|
|
},
|
|
total: 1,
|
|
}
|
|
svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
|
|
|
|
h := NewAuditHandler(svc)
|
|
app := fiber.New()
|
|
app.Get("/api/v1/audit-logs/get-all", h.List)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?filter[search]=http&method=UPDATE&filter[module]=user&filter[action]=list&sort=-created_at&page[number]=1&page[size]=20", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
bodyStr := string(body)
|
|
if !containsAll(bodyStr, `"type":"audit_log"`, `"total":1`, `"ip":"127.0.0.1"`) {
|
|
t.Fatalf("unexpected body: %s", bodyStr)
|
|
}
|
|
|
|
repo.mu.Lock()
|
|
defer repo.mu.Unlock()
|
|
if repo.lastFilter != "http" {
|
|
t.Fatalf("expected filter to be propagated, got %q", repo.lastFilter)
|
|
}
|
|
if repo.lastSort != "created_at DESC" {
|
|
t.Fatalf("expected normalized sort, got %q", repo.lastSort)
|
|
}
|
|
if len(repo.lastMethods) != 2 || repo.lastMethods[0] != "PUT" || repo.lastMethods[1] != "PATCH" {
|
|
t.Fatalf("expected method filter [PUT PATCH], got %#v", repo.lastMethods)
|
|
}
|
|
if len(repo.lastModules) != 1 || repo.lastModules[0] != "user" {
|
|
t.Fatalf("expected module filter [user], got %#v", repo.lastModules)
|
|
}
|
|
if len(repo.lastActions) != 3 || repo.lastActions[0] != "create" || repo.lastActions[1] != "update" || repo.lastActions[2] != "delete" {
|
|
t.Fatalf("expected action filter [create update delete], got %#v", repo.lastActions)
|
|
}
|
|
if repo.lastLimit != 0 || repo.lastOffset != 0 {
|
|
t.Fatalf("unexpected paging limit=%d offset=%d", repo.lastLimit, repo.lastOffset)
|
|
}
|
|
}
|
|
|
|
func containsAll(v string, terms ...string) bool {
|
|
for i := range terms {
|
|
if !strings.Contains(v, terms[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestAuditHandler_List_Branches(t *testing.T) {
|
|
t.Run("fallback filter action and clamp page", func(t *testing.T) {
|
|
repo := &mockAuditListRepo{
|
|
rows: []audit.AuditLog{{ID: uuidv7.MustBytes(), Action: "create", CreatedAt: time.Now().UTC()}},
|
|
total: 1,
|
|
}
|
|
svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
|
|
h := NewAuditHandler(svc)
|
|
app := fiber.New()
|
|
app.Get("/api/v1/audit-logs/get-all", h.List)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?filter[action]=create&page[number]=0&page[size]=500&sort=action", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
repo.mu.Lock()
|
|
defer repo.mu.Unlock()
|
|
if repo.lastFilter != "create" {
|
|
t.Fatalf("expected action fallback filter, got %q", repo.lastFilter)
|
|
}
|
|
if repo.lastSort != "action ASC" {
|
|
t.Fatalf("expected normalized sort, got %q", repo.lastSort)
|
|
}
|
|
if repo.lastLimit != 0 || repo.lastOffset != 0 {
|
|
t.Fatalf("unexpected paging limit=%d offset=%d", repo.lastLimit, repo.lastOffset)
|
|
}
|
|
})
|
|
|
|
t.Run("fallback filter layer", func(t *testing.T) {
|
|
repo := &mockAuditListRepo{
|
|
rows: []audit.AuditLog{{ID: uuidv7.MustBytes(), Layer: "service", CreatedAt: time.Now().UTC()}},
|
|
total: 1,
|
|
}
|
|
svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
|
|
h := NewAuditHandler(svc)
|
|
app := fiber.New()
|
|
app.Get("/api/v1/audit-logs/get-all", h.List)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?filter[layer]=service&page[number]=1&page[size]=20", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
repo.mu.Lock()
|
|
defer repo.mu.Unlock()
|
|
if repo.lastFilter != "service" {
|
|
t.Fatalf("expected layer fallback filter, got %q", repo.lastFilter)
|
|
}
|
|
})
|
|
|
|
t.Run("list error", func(t *testing.T) {
|
|
repo := &mockAuditListRepo{err: errors.New("list failed")}
|
|
svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
|
|
h := NewAuditHandler(svc)
|
|
app := fiber.New()
|
|
app.Get("/api/v1/audit-logs/get-all", h.List)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("default mutation filter applied when query is empty", func(t *testing.T) {
|
|
repo := &mockAuditListRepo{
|
|
rows: []audit.AuditLog{{ID: uuidv7.MustBytes(), Action: "update", Method: "PATCH", CreatedAt: time.Now().UTC()}},
|
|
total: 1,
|
|
}
|
|
svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
|
|
h := NewAuditHandler(svc)
|
|
app := fiber.New()
|
|
app.Get("/api/v1/audit-logs/get-all", h.List)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
repo.mu.Lock()
|
|
defer repo.mu.Unlock()
|
|
if len(repo.lastMethods) != 4 || repo.lastMethods[0] != "POST" || repo.lastMethods[1] != "PUT" || repo.lastMethods[2] != "PATCH" || repo.lastMethods[3] != "DELETE" {
|
|
t.Fatalf("expected default method filter [POST PUT PATCH DELETE], got %#v", repo.lastMethods)
|
|
}
|
|
if len(repo.lastActions) != 3 || repo.lastActions[0] != "create" || repo.lastActions[1] != "update" || repo.lastActions[2] != "delete" {
|
|
t.Fatalf("expected default action filter [create update delete], got %#v", repo.lastActions)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid module query returns 422", func(t *testing.T) {
|
|
repo := &mockAuditListRepo{}
|
|
svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
|
|
h := NewAuditHandler(svc)
|
|
app := fiber.New()
|
|
app.Get("/api/v1/audit-logs/get-all", h.List)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?module=bad-module", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuditLogResource_Branches(t *testing.T) {
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
row := &audit.AuditLog{
|
|
ID: uuidv7.MustBytes(),
|
|
RequestID: " req-1 ",
|
|
ActorUserID: uuidv7.MustBytes(),
|
|
Layer: " service ",
|
|
Action: " create ",
|
|
Method: " POST ",
|
|
Path: " /api/v1/reserve-acs/get-all ",
|
|
StatusCode: 201,
|
|
Success: true,
|
|
IP: " 127.0.0.1 ",
|
|
UserAgent: " ua ",
|
|
Message: " ok ",
|
|
Metadata: []byte(`{"module":"auth","count":2}`),
|
|
CreatedAt: now,
|
|
}
|
|
|
|
res := auditLogResource(row)
|
|
if res.Type != "audit_log" || res.ID == "" {
|
|
t.Fatalf("expected audit resource type/id")
|
|
}
|
|
if res.Attributes.ActorUserID == "" {
|
|
t.Fatalf("expected actor_user_id to be encoded")
|
|
}
|
|
if res.Attributes.RequestID != "req-1" || res.Attributes.Layer != "service" || res.Attributes.Action != "create" {
|
|
t.Fatalf("expected trimmed string attributes")
|
|
}
|
|
if res.Attributes.Module != "reserve_ac" {
|
|
t.Fatalf("expected inferred module reserve_ac, got %q", res.Attributes.Module)
|
|
}
|
|
md, ok := res.Attributes.Metadata.(map[string]any)
|
|
if !ok || md["module"] != "auth" {
|
|
t.Fatalf("expected metadata map decoded, got %#v", res.Attributes.Metadata)
|
|
}
|
|
if res.Attributes.CreatedAt == "" {
|
|
t.Fatalf("expected created_at in resource")
|
|
}
|
|
}
|