## 设计文档 - multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO) - audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO) - routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO) - sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO) - compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO) ## TDD开发成果 - IAM模块: supply-api/internal/iam/ (111个测试) - 审计日志模块: supply-api/internal/audit/ (40+测试) - 路由策略模块: gateway/internal/router/ (33+测试) - 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/ ## 规范文档 - parallel_agent_output_quality_standards: 并行Agent产出质量规范 - project_experience_summary: 项目经验总结 (v2) - 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划 ## 评审报告 - 5个CONDITIONAL GO设计文档评审报告 - fix_verification_report: 修复验证报告 - full_verification_report: 全面质量验证报告 - tdd_module_quality_verification: TDD模块质量验证 - tdd_execution_summary: TDD执行总结 依据: Superpowers执行框架 + TDD规范
403 lines
11 KiB
Go
403 lines
11 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"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)
|
||
} |