Files
ai-customer-service/internal/store/memory/audit_store_test.go
Your Name cf46b27610 fix: P0-1 RateLimiter并发写安全 + P0-2工单操作错误码区分 + P1 rows.Close修复
P0-1 (limits.go): Allow()方法改为全程使用写锁保护counters map读写,避免RLock写入时的data race
P0-2 (ticket_workflow.go+ticket_handler.go): Assign/Resolve/Close操作先查询ticket存在性和状态,返回明确的CS_TICKET_4001/CS_TKT_4002/CS_TICKET_4092/CS_TICKET_4093错误码,handler根据错误前缀路由HTTP状态码
P1-1 (ticket_store.go): 移除GetStats中3处手动rows.Close(),只保留defer Close()
2026-05-01 20:56:25 +08:00

146 lines
3.7 KiB
Go

package memory
import (
"context"
"slices"
"testing"
"time"
"github.com/bridge/ai-customer-service/internal/domain/audit"
)
func TestAuditStore_Add(t *testing.T) {
store := NewAuditStore()
ctx := context.Background()
now := time.Now().Truncate(time.Second)
t.Run("add single event", func(t *testing.T) {
event := audit.Event{
ID: "e1",
Type: "ticket.created",
SessionID: "sess1",
CreatedAt: now,
}
err := store.Add(ctx, event)
if err != nil {
t.Fatalf("Add() error = %v", err)
}
got := store.List()
if len(got) != 1 {
t.Errorf("List() len = %d, want 1", len(got))
}
})
t.Run("add multiple events", func(t *testing.T) {
for i := 2; i <= 3; i++ {
err := store.Add(ctx, audit.Event{
ID: "e" + string(rune('0'+i)),
Type: "ticket.updated",
CreatedAt: now,
})
if err != nil {
t.Fatalf("Add() error = %v", err)
}
}
got := store.List()
if len(got) != 3 {
t.Errorf("List() len = %d, want 3", len(got))
}
})
t.Run("zero time is set to now", func(t *testing.T) {
store2 := NewAuditStore()
before := time.Now().Add(-time.Second)
err := store2.Add(ctx, audit.Event{
ID: "zerotime",
Type: "test",
})
if err != nil {
t.Fatalf("Add() error = %v", err)
}
after := time.Now().Add(time.Second)
got := store2.List()
if len(got) != 1 {
t.Fatalf("List() len = %d, want 1", len(got))
}
if got[0].CreatedAt.Before(before) || got[0].CreatedAt.After(after) {
t.Errorf("Add() zero CreatedAt not set to now: got %v, want between %v and %v", got[0].CreatedAt, before, after)
}
})
t.Run("empty store", func(t *testing.T) {
emptyStore := NewAuditStore()
err := emptyStore.Add(ctx, audit.Event{ID: "first", Type: "init"})
if err != nil {
t.Fatalf("Add() error = %v", err)
}
if len(emptyStore.List()) != 1 {
t.Errorf("List() len = %d, want 1", len(emptyStore.List()))
}
})
}
func TestAuditStore_List(t *testing.T) {
store := NewAuditStore()
ctx := context.Background()
now := time.Now().Truncate(time.Second)
t.Run("empty store returns empty slice", func(t *testing.T) {
got := store.List()
if len(got) != 0 {
t.Errorf("List() len = %d, want 0", len(got))
}
})
t.Run("returns all events in order", func(t *testing.T) {
events := []audit.Event{
{ID: "l1", Type: "type1", CreatedAt: now.Add(-2 * time.Hour)},
{ID: "l2", Type: "type2", CreatedAt: now.Add(-1 * time.Hour)},
{ID: "l3", Type: "type3", CreatedAt: now},
}
for _, e := range events {
store.Add(ctx, e)
}
got := store.List()
if len(got) != 3 {
t.Errorf("List() len = %d, want 3", len(got))
}
// Verify order is preserved
ids := []string{got[0].ID, got[1].ID, got[2].ID}
if !slices.Equal(ids, []string{"l1", "l2", "l3"}) {
t.Errorf("List() order = %v, want [l1, l2, l3]", ids)
}
})
t.Run("returns copy not reference", func(t *testing.T) {
store2 := NewAuditStore()
store2.Add(ctx, audit.Event{ID: "orig", Type: "test", CreatedAt: now})
got := store2.List()
if len(got) > 0 {
got[0].ID = "mutated"
if store2.List()[0].ID == "mutated" {
t.Error("List() should return copies, not references")
}
}
})
t.Run("filters by session", func(t *testing.T) {
store3 := NewAuditStore()
store3.Add(ctx, audit.Event{ID: "sa1", SessionID: "sessA", Type: "a", CreatedAt: now})
store3.Add(ctx, audit.Event{ID: "sa2", SessionID: "sessB", Type: "b", CreatedAt: now})
store3.Add(ctx, audit.Event{ID: "sa3", SessionID: "sessA", Type: "c", CreatedAt: now})
got := store3.List()
sessionA := 0
for _, e := range got {
if e.SessionID == "sessA" {
sessionA++
}
}
if sessionA != 2 {
t.Errorf("List() sessA count = %d, want 2", sessionA)
}
})
}