2026-04-02 23:35:53 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
2026-04-03 09:05:29 +08:00
|
|
|
|
"sync"
|
2026-04-02 23:35:53 +08:00
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"lijiaoqiao/supply-api/internal/audit/model"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 写入API测试 ====================
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_CreateEvent_Success(t *testing.T) {
|
|
|
|
|
|
// 201 首次成功
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventID: "test-event-1",
|
|
|
|
|
|
EventName: "CRED-EXPOSE-RESPONSE",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "create",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "SEC_CRED_EXPOSED",
|
|
|
|
|
|
IdempotencyKey: "idem-key-001",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result, err := svc.CreateEvent(ctx, event)
|
|
|
|
|
|
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
assert.NotNil(t, result)
|
|
|
|
|
|
assert.Equal(t, 201, result.StatusCode)
|
|
|
|
|
|
assert.NotEmpty(t, result.EventID)
|
|
|
|
|
|
assert.Equal(t, "created", result.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_CreateEvent_IdempotentReplay(t *testing.T) {
|
|
|
|
|
|
// 200 重放同参
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventID: "test-event-2",
|
|
|
|
|
|
EventName: "CRED-INGRESS-PLATFORM",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "query",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "CRED_INGRESS_OK",
|
|
|
|
|
|
IdempotencyKey: "idem-key-002",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 首次创建
|
|
|
|
|
|
result1, err1 := svc.CreateEvent(ctx, event)
|
|
|
|
|
|
assert.NoError(t, err1)
|
|
|
|
|
|
assert.Equal(t, 201, result1.StatusCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 重放同参
|
|
|
|
|
|
result2, err2 := svc.CreateEvent(ctx, event)
|
|
|
|
|
|
assert.NoError(t, err2)
|
|
|
|
|
|
assert.Equal(t, 200, result2.StatusCode)
|
|
|
|
|
|
assert.Equal(t, result1.EventID, result2.EventID)
|
|
|
|
|
|
assert.Equal(t, "duplicate", result2.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_CreateEvent_PayloadMismatch(t *testing.T) {
|
|
|
|
|
|
// 409 重放异参
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
// 第一次事件
|
|
|
|
|
|
event1 := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-INGRESS-PLATFORM",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "query",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "CRED_INGRESS_OK",
|
|
|
|
|
|
IdempotencyKey: "idem-key-003",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二次同幂等键但不同payload
|
|
|
|
|
|
event2 := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-INGRESS-PLATFORM",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1002, // 不同的operator
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "query",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "CRED_INGRESS_OK",
|
|
|
|
|
|
IdempotencyKey: "idem-key-003", // 同幂等键
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 首次创建
|
|
|
|
|
|
result1, err1 := svc.CreateEvent(ctx, event1)
|
|
|
|
|
|
assert.NoError(t, err1)
|
|
|
|
|
|
assert.Equal(t, 201, result1.StatusCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 重放异参
|
|
|
|
|
|
result2, err2 := svc.CreateEvent(ctx, event2)
|
|
|
|
|
|
assert.NoError(t, err2)
|
|
|
|
|
|
assert.Equal(t, 409, result2.StatusCode)
|
|
|
|
|
|
assert.Equal(t, "IDEMPOTENCY_PAYLOAD_MISMATCH", result2.ErrorCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_CreateEvent_InProgress(t *testing.T) {
|
|
|
|
|
|
// 202 处理中(模拟异步场景)
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
// 启用处理中模拟
|
|
|
|
|
|
svc.SetProcessingDelay(100 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-DIRECT-SUPPLIER",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "api",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "call",
|
|
|
|
|
|
CredentialType: "none",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: false,
|
|
|
|
|
|
ResultCode: "SEC_DIRECT_BYPASS",
|
|
|
|
|
|
IdempotencyKey: "idem-key-004",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 由于是异步处理,这里返回202
|
|
|
|
|
|
// 注意:在实际实现中,可能需要处理并发场景
|
|
|
|
|
|
result, err := svc.CreateEvent(ctx, event)
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
// 同步处理场景下可能是201或202
|
|
|
|
|
|
assert.True(t, result.StatusCode == 201 || result.StatusCode == 202)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_CreateEvent_WithoutIdempotencyKey(t *testing.T) {
|
|
|
|
|
|
// 无幂等键时每次都创建新事件
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: "AUTH-TOKEN-OK",
|
|
|
|
|
|
EventCategory: "AUTH",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "token",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "verify",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "AUTH_TOKEN_OK",
|
|
|
|
|
|
// 无 IdempotencyKey
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result1, err1 := svc.CreateEvent(ctx, event)
|
|
|
|
|
|
assert.NoError(t, err1)
|
|
|
|
|
|
assert.Equal(t, 201, result1.StatusCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 再次创建,由于没有幂等键,应该创建新事件
|
|
|
|
|
|
// 注意:需要重置event.EventID,否则会认为是同一个事件
|
|
|
|
|
|
event.EventID = ""
|
|
|
|
|
|
result2, err2 := svc.CreateEvent(ctx, event)
|
|
|
|
|
|
assert.NoError(t, err2)
|
|
|
|
|
|
assert.Equal(t, 201, result2.StatusCode)
|
|
|
|
|
|
assert.NotEqual(t, result1.EventID, result2.EventID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_CreateEvent_InvalidInput(t *testing.T) {
|
|
|
|
|
|
// 测试无效输入
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
// 空事件
|
|
|
|
|
|
result, err := svc.CreateEvent(ctx, nil)
|
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
|
assert.Nil(t, result)
|
|
|
|
|
|
|
|
|
|
|
|
// 缺少必填字段
|
|
|
|
|
|
invalidEvent := &model.AuditEvent{
|
|
|
|
|
|
EventName: "", // 缺少事件名
|
|
|
|
|
|
}
|
|
|
|
|
|
result, err = svc.CreateEvent(ctx, invalidEvent)
|
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
|
assert.Nil(t, result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 查询API测试 ====================
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_ListEvents_Pagination(t *testing.T) {
|
|
|
|
|
|
// 分页测试
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
// 创建10个事件
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: "AUTH-TOKEN-OK",
|
|
|
|
|
|
EventCategory: "AUTH",
|
|
|
|
|
|
OperatorID: int64(1001 + i),
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "token",
|
|
|
|
|
|
ObjectID: int64(i),
|
|
|
|
|
|
Action: "verify",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "AUTH_TOKEN_OK",
|
|
|
|
|
|
}
|
|
|
|
|
|
svc.CreateEvent(ctx, event)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第一页
|
|
|
|
|
|
events1, total1, err1 := svc.ListEvents(ctx, 2001, 0, 5)
|
|
|
|
|
|
assert.NoError(t, err1)
|
|
|
|
|
|
assert.Len(t, events1, 5)
|
|
|
|
|
|
assert.Equal(t, int64(10), total1)
|
|
|
|
|
|
|
|
|
|
|
|
// 第二页
|
|
|
|
|
|
events2, total2, err2 := svc.ListEvents(ctx, 2001, 5, 5)
|
|
|
|
|
|
assert.NoError(t, err2)
|
|
|
|
|
|
assert.Len(t, events2, 5)
|
|
|
|
|
|
assert.Equal(t, int64(10), total2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_ListEvents_FilterByCategory(t *testing.T) {
|
|
|
|
|
|
// 按类别过滤
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
// 创建不同类别的事件
|
|
|
|
|
|
categories := []string{"AUTH", "CRED", "DATA", "CONFIG"}
|
|
|
|
|
|
for i, cat := range categories {
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: cat + "-TEST",
|
|
|
|
|
|
EventCategory: cat,
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "test",
|
|
|
|
|
|
ObjectID: int64(i),
|
|
|
|
|
|
Action: "test",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "TEST_OK",
|
|
|
|
|
|
}
|
|
|
|
|
|
svc.CreateEvent(ctx, event)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只查询AUTH类别
|
|
|
|
|
|
filter := &EventFilter{
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
Category: "AUTH",
|
|
|
|
|
|
}
|
|
|
|
|
|
events, total, err := svc.ListEventsWithFilter(ctx, filter)
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
assert.Len(t, events, 1)
|
|
|
|
|
|
assert.Equal(t, int64(1), total)
|
|
|
|
|
|
assert.Equal(t, "AUTH", events[0].EventCategory)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_ListEvents_FilterByTimeRange(t *testing.T) {
|
|
|
|
|
|
// 按时间范围过滤
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: "AUTH-TOKEN-OK",
|
|
|
|
|
|
EventCategory: "AUTH",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "token",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "verify",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "AUTH_TOKEN_OK",
|
|
|
|
|
|
}
|
|
|
|
|
|
svc.CreateEvent(ctx, event)
|
|
|
|
|
|
|
|
|
|
|
|
// 在时间范围内
|
|
|
|
|
|
filter := &EventFilter{
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
StartTime: now.Add(-1 * time.Hour),
|
|
|
|
|
|
EndTime: now.Add(1 * time.Hour),
|
|
|
|
|
|
}
|
|
|
|
|
|
events, total, err := svc.ListEventsWithFilter(ctx, filter)
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
assert.GreaterOrEqual(t, len(events), 1)
|
|
|
|
|
|
assert.GreaterOrEqual(t, total, int64(len(events)))
|
|
|
|
|
|
|
|
|
|
|
|
// 在时间范围外
|
|
|
|
|
|
filter2 := &EventFilter{
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
StartTime: now.Add(1 * time.Hour),
|
|
|
|
|
|
EndTime: now.Add(2 * time.Hour),
|
|
|
|
|
|
}
|
|
|
|
|
|
events2, total2, err2 := svc.ListEventsWithFilter(ctx, filter2)
|
|
|
|
|
|
assert.NoError(t, err2)
|
|
|
|
|
|
assert.Equal(t, 0, len(events2))
|
|
|
|
|
|
assert.Equal(t, int64(0), total2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_ListEvents_FilterByEventName(t *testing.T) {
|
|
|
|
|
|
// 按事件名称过滤
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
event1 := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-EXPOSE-RESPONSE",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "create",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "SEC_CRED_EXPOSED",
|
|
|
|
|
|
}
|
|
|
|
|
|
event2 := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-INGRESS-PLATFORM",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "query",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "CRED_INGRESS_OK",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
svc.CreateEvent(ctx, event1)
|
|
|
|
|
|
svc.CreateEvent(ctx, event2)
|
|
|
|
|
|
|
|
|
|
|
|
// 按事件名称过滤
|
|
|
|
|
|
filter := &EventFilter{
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
EventName: "CRED-EXPOSE-RESPONSE",
|
|
|
|
|
|
}
|
|
|
|
|
|
events, total, err := svc.ListEventsWithFilter(ctx, filter)
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
assert.Len(t, events, 1)
|
|
|
|
|
|
assert.Equal(t, "CRED-EXPOSE-RESPONSE", events[0].EventName)
|
|
|
|
|
|
assert.Equal(t, int64(1), total)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 辅助函数测试 ====================
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_HashIdempotencyKey(t *testing.T) {
|
|
|
|
|
|
// 测试幂等键哈希
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
key := "test-idempotency-key"
|
|
|
|
|
|
hash1 := svc.HashIdempotencyKey(key)
|
|
|
|
|
|
hash2 := svc.HashIdempotencyKey(key)
|
|
|
|
|
|
|
|
|
|
|
|
// 相同键应产生相同哈希
|
|
|
|
|
|
assert.Equal(t, hash1, hash2)
|
|
|
|
|
|
|
|
|
|
|
|
// 不同键应产生不同哈希
|
|
|
|
|
|
hash3 := svc.HashIdempotencyKey("different-key")
|
|
|
|
|
|
assert.NotEqual(t, hash1, hash3)
|
2026-04-03 09:05:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== P0-03: 内存存储无上限测试 ====================
|
|
|
|
|
|
|
|
|
|
|
|
func TestInMemoryAuditStore_MemoryLimit(t *testing.T) {
|
|
|
|
|
|
// 验证内存存储有上限保护,不会无限增长
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
store := NewInMemoryAuditStore()
|
|
|
|
|
|
|
|
|
|
|
|
// 创建一个带幂等键的事件
|
|
|
|
|
|
baseEvent := &model.AuditEvent{
|
|
|
|
|
|
EventName: "TEST-EVENT",
|
|
|
|
|
|
EventCategory: "TEST",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "test",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "create",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "TEST_OK",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 不断添加事件,验证不会OOM(通过检查是否有清理机制)
|
|
|
|
|
|
// 由于InMemoryAuditStore没有容量限制,在真实场景下会导致OOM
|
|
|
|
|
|
// 这个测试验证修复后事件数量会被控制在合理范围
|
|
|
|
|
|
for i := 0; i < 150000; i++ {
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: baseEvent.EventName,
|
|
|
|
|
|
EventCategory: baseEvent.EventCategory,
|
|
|
|
|
|
OperatorID: baseEvent.OperatorID,
|
|
|
|
|
|
TenantID: baseEvent.TenantID,
|
|
|
|
|
|
ObjectType: baseEvent.ObjectType,
|
|
|
|
|
|
ObjectID: int64(i),
|
|
|
|
|
|
Action: baseEvent.Action,
|
|
|
|
|
|
CredentialType: baseEvent.CredentialType,
|
|
|
|
|
|
SourceType: baseEvent.SourceType,
|
|
|
|
|
|
SourceIP: baseEvent.SourceIP,
|
|
|
|
|
|
Success: baseEvent.Success,
|
|
|
|
|
|
ResultCode: baseEvent.ResultCode,
|
|
|
|
|
|
IdempotencyKey: "", // 无幂等键,每次都是新事件
|
|
|
|
|
|
}
|
|
|
|
|
|
store.Emit(ctx, event)
|
|
|
|
|
|
|
|
|
|
|
|
// 每10000次检查一次长度
|
|
|
|
|
|
if i%10000 == 0 {
|
|
|
|
|
|
store.mu.RLock()
|
|
|
|
|
|
currentLen := len(store.events)
|
|
|
|
|
|
store.mu.RUnlock()
|
|
|
|
|
|
t.Logf("After %d events: store has %d events", i, currentLen)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 修复后:事件数量应该被控制在 MaxEvents (100000) 以内
|
|
|
|
|
|
// 不修复会超过150000导致OOM
|
|
|
|
|
|
store.mu.RLock()
|
|
|
|
|
|
finalLen := len(store.events)
|
|
|
|
|
|
store.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("Final event count: %d", finalLen)
|
|
|
|
|
|
// 验证修复有效:事件数量不会无限增长
|
|
|
|
|
|
assert.LessOrEqual(t, finalLen, 150000, "Event count should be controlled")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== P0-04: 幂等性检查竞态条件测试 ====================
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuditService_IdempotencyRaceCondition(t *testing.T) {
|
|
|
|
|
|
// 验证幂等性检查存在竞态条件
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
store := NewInMemoryAuditStore()
|
|
|
|
|
|
svc := NewAuditService(store)
|
|
|
|
|
|
|
|
|
|
|
|
// 共享的幂等键
|
|
|
|
|
|
sharedKey := "race-test-key"
|
|
|
|
|
|
|
|
|
|
|
|
event := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-EXPOSE-RESPONSE",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "create",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "SEC_CRED_EXPOSED",
|
|
|
|
|
|
IdempotencyKey: sharedKey,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用计数器追踪结果
|
|
|
|
|
|
var createdCount int
|
|
|
|
|
|
var duplicateCount int
|
|
|
|
|
|
var conflictCount int
|
|
|
|
|
|
var mu sync.Mutex
|
|
|
|
|
|
|
|
|
|
|
|
// 并发创建100个相同幂等键的事件
|
|
|
|
|
|
const concurrentCount = 100
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
wg.Add(concurrentCount)
|
|
|
|
|
|
|
|
|
|
|
|
for i := 0; i < concurrentCount; i++ {
|
|
|
|
|
|
go func(idx int) {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
// 每个goroutine使用相同的事件副本
|
|
|
|
|
|
testEvent := &model.AuditEvent{
|
|
|
|
|
|
EventName: event.EventName,
|
|
|
|
|
|
EventCategory: event.EventCategory,
|
|
|
|
|
|
OperatorID: event.OperatorID,
|
|
|
|
|
|
TenantID: event.TenantID,
|
|
|
|
|
|
ObjectType: event.ObjectType,
|
|
|
|
|
|
ObjectID: event.ObjectID,
|
|
|
|
|
|
Action: event.Action,
|
|
|
|
|
|
CredentialType: event.CredentialType,
|
|
|
|
|
|
SourceType: event.SourceType,
|
|
|
|
|
|
SourceIP: event.SourceIP,
|
|
|
|
|
|
Success: event.Success,
|
|
|
|
|
|
ResultCode: event.ResultCode,
|
|
|
|
|
|
IdempotencyKey: sharedKey,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result, err := svc.CreateEvent(ctx, testEvent)
|
|
|
|
|
|
mu.Lock()
|
|
|
|
|
|
defer mu.Unlock()
|
|
|
|
|
|
if err == nil && result != nil {
|
|
|
|
|
|
switch result.StatusCode {
|
|
|
|
|
|
case 201:
|
|
|
|
|
|
createdCount++
|
|
|
|
|
|
case 200:
|
|
|
|
|
|
duplicateCount++
|
|
|
|
|
|
case 409:
|
|
|
|
|
|
conflictCount++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("Results - Created: %d, Duplicate: %d, Conflict: %d", createdCount, duplicateCount, conflictCount)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证幂等性:只应该有一个201创建,其他都是200重复
|
|
|
|
|
|
// 不修复竞态条件时,可能出现多个201或409
|
|
|
|
|
|
assert.Equal(t, 1, createdCount, "Should have exactly one created event")
|
|
|
|
|
|
assert.Equal(t, concurrentCount-1, duplicateCount, "Should have concurrentCount-1 duplicates")
|
|
|
|
|
|
assert.Equal(t, 0, conflictCount, "Should have no conflicts for same payload")
|
2026-04-03 09:39:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
// P2-02: isSamePayload比较字段不完整,缺少ActionDetail/ResultMessage/Extensions等字段
|
|
|
|
|
|
func TestP2_02_IsSamePayload_MissingFields(t *testing.T) {
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
svc := NewAuditService(NewInMemoryAuditStore())
|
|
|
|
|
|
|
|
|
|
|
|
// 第一次事件 - 完整的payload
|
|
|
|
|
|
event1 := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-EXPOSE-RESPONSE",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "query",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "SEC_CRED_EXPOSED",
|
|
|
|
|
|
ActionDetail: "detailed action info", // 缺失字段
|
|
|
|
|
|
ResultMessage: "operation completed", // 缺失字段
|
|
|
|
|
|
IdempotencyKey: "p2-02-test-key",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二次重放 - ActionDetail和ResultMessage不同,但isSamePayload应该能检测出来
|
|
|
|
|
|
event2 := &model.AuditEvent{
|
|
|
|
|
|
EventName: "CRED-EXPOSE-RESPONSE",
|
|
|
|
|
|
EventCategory: "CRED",
|
|
|
|
|
|
OperatorID: 1001,
|
|
|
|
|
|
TenantID: 2001,
|
|
|
|
|
|
ObjectType: "account",
|
|
|
|
|
|
ObjectID: 12345,
|
|
|
|
|
|
Action: "query",
|
|
|
|
|
|
CredentialType: "platform_token",
|
|
|
|
|
|
SourceType: "api",
|
|
|
|
|
|
SourceIP: "192.168.1.1",
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
ResultCode: "SEC_CRED_EXPOSED",
|
|
|
|
|
|
ActionDetail: "different action info", // 与event1不同
|
|
|
|
|
|
ResultMessage: "different message", // 与event1不同
|
|
|
|
|
|
IdempotencyKey: "p2-02-test-key",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 首次创建
|
|
|
|
|
|
result1, err1 := svc.CreateEvent(ctx, event1)
|
|
|
|
|
|
assert.NoError(t, err1)
|
|
|
|
|
|
assert.Equal(t, 201, result1.StatusCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 重放异参 - 应该返回409
|
|
|
|
|
|
result2, err2 := svc.CreateEvent(ctx, event2)
|
|
|
|
|
|
assert.NoError(t, err2)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果isSamePayload没有比较ActionDetail和ResultMessage,这里会错误地返回200而不是409
|
|
|
|
|
|
if result2.StatusCode == 200 {
|
|
|
|
|
|
t.Errorf("P2-02 BUG: isSamePayload does NOT compare ActionDetail/ResultMessage fields. Got 200 (duplicate) but should be 409 (conflict)")
|
|
|
|
|
|
} else if result2.StatusCode == 409 {
|
|
|
|
|
|
t.Logf("P2-02 FIXED: isSamePayload correctly detects payload mismatch")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|