init push
This commit is contained in:
288
internal/service/audit_log_service_test.go
Normal file
288
internal/service/audit_log_service_test.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"wucher/internal/domain/audit"
|
||||
)
|
||||
|
||||
type mockAuditRepo struct {
|
||||
mu sync.Mutex
|
||||
entries []*audit.AuditLog
|
||||
createCalls int
|
||||
createErr error
|
||||
listRows []audit.AuditLog
|
||||
listTotal int64
|
||||
listErr error
|
||||
lastFilter string
|
||||
lastMethods []string
|
||||
lastModules []string
|
||||
lastActions []string
|
||||
lastSort string
|
||||
lastLimit int
|
||||
lastOffset int
|
||||
}
|
||||
|
||||
func (m *mockAuditRepo) Create(_ context.Context, entry *audit.AuditLog) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.createCalls++
|
||||
cp := *entry
|
||||
m.entries = append(m.entries, &cp)
|
||||
return m.createErr
|
||||
}
|
||||
|
||||
func (m *mockAuditRepo) 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.listErr != nil {
|
||||
return nil, 0, m.listErr
|
||||
}
|
||||
out := make([]audit.AuditLog, len(m.listRows))
|
||||
copy(out, m.listRows)
|
||||
return out, m.listTotal, nil
|
||||
}
|
||||
|
||||
func TestAuditLogService_LogAsync(t *testing.T) {
|
||||
repo := &mockAuditRepo{}
|
||||
svc := NewAuditLogService(repo, AuditLogServiceDependencies{QueueSize: 8, Workers: 1, IPEncryptionKey: "pepper"})
|
||||
defer close(svc.queue)
|
||||
|
||||
svc.Log(context.Background(), AuditLogInput{
|
||||
RequestID: "req-1",
|
||||
Layer: "service",
|
||||
Action: "auth.reset_password.consume",
|
||||
Success: true,
|
||||
StatusCode: 200,
|
||||
IP: "127.0.0.1",
|
||||
Message: "ok",
|
||||
Metadata: map[string]any{
|
||||
"k": "v",
|
||||
},
|
||||
})
|
||||
|
||||
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) != 1 {
|
||||
t.Fatalf("expected 1 audit entry, got %d", len(repo.entries))
|
||||
}
|
||||
if repo.entries[0].Action != "auth.reset_password.consume" {
|
||||
t.Fatalf("unexpected action: %s", repo.entries[0].Action)
|
||||
}
|
||||
if repo.entries[0].IP == "127.0.0.1" {
|
||||
t.Fatalf("expected encrypted ip to be persisted")
|
||||
}
|
||||
if plain := decryptStoredIP(repo.entries[0].IP, newIPSecretProtector("pepper")); plain != "127.0.0.1" {
|
||||
t.Fatalf("unexpected decrypted ip: %s", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditLogService_NewAuditLogService_Defaults(t *testing.T) {
|
||||
repo := &mockAuditRepo{}
|
||||
svc := NewAuditLogService(repo, AuditLogServiceDependencies{QueueSize: 0, Workers: 0})
|
||||
defer close(svc.queue)
|
||||
|
||||
if cap(svc.queue) != 1024 {
|
||||
t.Fatalf("expected default queue size 1024, got %d", cap(svc.queue))
|
||||
}
|
||||
|
||||
svc.Log(context.Background(), AuditLogInput{
|
||||
Layer: "service",
|
||||
Action: "audit.defaults",
|
||||
})
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Fatalf("expected default workers to process queued entry")
|
||||
}
|
||||
|
||||
func TestAuditLogService_Log_NoopAndQueueBehavior(t *testing.T) {
|
||||
t.Run("no-op when receiver is nil", func(t *testing.T) {
|
||||
var svc *AuditLogService
|
||||
svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "noop"})
|
||||
})
|
||||
|
||||
t.Run("no-op when repo is nil", func(t *testing.T) {
|
||||
svc := &AuditLogService{repo: nil, queue: make(chan *audit.AuditLog, 1)}
|
||||
svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "noop"})
|
||||
if len(svc.queue) != 0 {
|
||||
t.Fatalf("expected no queued entries when repo is nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("metadata marshal failure does not panic and leaves metadata empty", func(t *testing.T) {
|
||||
svc := &AuditLogService{
|
||||
repo: &mockAuditRepo{},
|
||||
queue: make(chan *audit.AuditLog, 1),
|
||||
}
|
||||
svc.Log(context.Background(), AuditLogInput{
|
||||
Layer: "service",
|
||||
Action: "audit.bad_metadata",
|
||||
Metadata: map[string]any{"bad": func() {}},
|
||||
})
|
||||
|
||||
if len(svc.queue) != 1 {
|
||||
t.Fatalf("expected one queued entry")
|
||||
}
|
||||
entry := <-svc.queue
|
||||
if len(entry.Metadata) != 0 {
|
||||
t.Fatalf("expected metadata to be empty when marshal fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("queue full branch drops extra entry", func(t *testing.T) {
|
||||
svc := &AuditLogService{
|
||||
repo: &mockAuditRepo{},
|
||||
queue: make(chan *audit.AuditLog, 1),
|
||||
}
|
||||
svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "first"})
|
||||
svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "second"})
|
||||
|
||||
if len(svc.queue) != 1 {
|
||||
t.Fatalf("expected queue to contain only first entry, got %d", len(svc.queue))
|
||||
}
|
||||
entry := <-svc.queue
|
||||
if entry.Action != "first" {
|
||||
t.Fatalf("expected first entry to be retained, got %q", entry.Action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuditLogService_runWorker_NilEntryAndPersistError(t *testing.T) {
|
||||
repo := &mockAuditRepo{createErr: errors.New("persist failed")}
|
||||
svc := &AuditLogService{
|
||||
repo: repo,
|
||||
queue: make(chan *audit.AuditLog, 2),
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
svc.runWorker()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
svc.queue <- nil
|
||||
svc.queue <- &audit.AuditLog{
|
||||
Layer: "service",
|
||||
Action: "audit.persist_error",
|
||||
}
|
||||
close(svc.queue)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("worker did not stop after queue close")
|
||||
}
|
||||
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
if repo.createCalls != 1 {
|
||||
t.Fatalf("expected exactly one repo.Create call for non-nil entry, got %d", repo.createCalls)
|
||||
}
|
||||
if len(repo.entries) != 1 {
|
||||
t.Fatalf("expected one persisted entry attempt, got %d", len(repo.entries))
|
||||
}
|
||||
if repo.entries[0].Action != "audit.persist_error" {
|
||||
t.Fatalf("unexpected persisted action: %s", repo.entries[0].Action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateBytes(t *testing.T) {
|
||||
original := []byte("abcdef")
|
||||
|
||||
t.Run("max less than or equal zero returns original bytes", func(t *testing.T) {
|
||||
got := truncateBytes(original, 0)
|
||||
if string(got) != string(original) {
|
||||
t.Fatalf("expected %q, got %q", string(original), string(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("len less than or equal max returns original bytes", func(t *testing.T) {
|
||||
got := truncateBytes(original, len(original))
|
||||
if string(got) != string(original) {
|
||||
t.Fatalf("expected %q, got %q", string(original), string(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("len greater than max returns truncated bytes", func(t *testing.T) {
|
||||
got := truncateBytes(original, 3)
|
||||
if string(got) != "abc" {
|
||||
t.Fatalf("expected %q, got %q", "abc", string(got))
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("expected len 3, got %d", len(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuditLogService_List(t *testing.T) {
|
||||
encIP := sanitizeIP("127.0.0.1", newIPSecretProtector("pepper"))
|
||||
repo := &mockAuditRepo{
|
||||
listRows: []audit.AuditLog{
|
||||
{Layer: "api", Action: "http.request", IP: encIP},
|
||||
},
|
||||
listTotal: 1,
|
||||
}
|
||||
svc := NewAuditLogService(repo, AuditLogServiceDependencies{QueueSize: 8, Workers: 1, IPEncryptionKey: "pepper"})
|
||||
defer close(svc.queue)
|
||||
|
||||
rows, total, err := svc.List(context.Background(), "http", []string{"GET", "update"}, []string{"helicopters"}, []string{"list"}, "-created_at", 20, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
if total != 1 || len(rows) != 1 {
|
||||
t.Fatalf("unexpected list result total=%d len=%d", total, len(rows))
|
||||
}
|
||||
if rows[0].IP != "127.0.0.1" {
|
||||
t.Fatalf("expected decrypted ip in list response, got %q", rows[0].IP)
|
||||
}
|
||||
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
if repo.lastSort != "created_at DESC" {
|
||||
t.Fatalf("expected normalized sort created_at DESC, got %q", repo.lastSort)
|
||||
}
|
||||
if repo.lastFilter != "http" {
|
||||
t.Fatalf("expected filter propagated, got %q", repo.lastFilter)
|
||||
}
|
||||
if len(repo.lastMethods) != 2 || repo.lastMethods[0] != "PUT" || repo.lastMethods[1] != "PATCH" {
|
||||
t.Fatalf("expected normalized methods [PUT PATCH], got %#v", repo.lastMethods)
|
||||
}
|
||||
if len(repo.lastModules) != 1 || repo.lastModules[0] != "helicopters" {
|
||||
t.Fatalf("expected modules [helicopters], got %#v", repo.lastModules)
|
||||
}
|
||||
if len(repo.lastActions) != 3 || repo.lastActions[0] != "create" || repo.lastActions[1] != "update" || repo.lastActions[2] != "delete" {
|
||||
t.Fatalf("expected actions [create update delete], got %#v", repo.lastActions)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user