feat(P1/P2): 完成TDD开发及P1/P2设计文档
## 设计文档 - 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规范
This commit is contained in:
376
supply-api/internal/audit/service/metrics_service_test.go
Normal file
376
supply-api/internal/audit/service/metrics_service_test.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/supply-api/internal/audit/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuditMetrics_M013_CredentialExposure(t *testing.T) {
|
||||
// M-013: supplier_credential_exposure_events = 0
|
||||
ctx := context.Background()
|
||||
svc := NewAuditService(NewInMemoryAuditStore())
|
||||
metricsSvc := NewMetricsService(svc)
|
||||
|
||||
// 创建一些事件,包括CRED-EXPOSE事件
|
||||
events := []*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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
svc.CreateEvent(ctx, e)
|
||||
}
|
||||
|
||||
// 计算M-013指标
|
||||
now := time.Now()
|
||||
metric, err := metricsSvc.CalculateM013(ctx, now.Add(-24*time.Hour), now)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, metric)
|
||||
assert.Equal(t, "M-013", metric.MetricID)
|
||||
assert.Equal(t, "supplier_credential_exposure_events", metric.MetricName)
|
||||
assert.Equal(t, float64(1), metric.Value) // 有1个暴露事件
|
||||
assert.Equal(t, "FAIL", metric.Status) // 暴露事件数 > 0,应该是FAIL
|
||||
}
|
||||
|
||||
func TestAuditMetrics_M014_IngressCoverage(t *testing.T) {
|
||||
// M-014: platform_credential_ingress_coverage_pct = 100%
|
||||
// 分母定义:经平台凭证校验的入站请求(credential_type = 'platform_token'),不含被拒绝的无效请求
|
||||
ctx := context.Background()
|
||||
svc := NewAuditService(NewInMemoryAuditStore())
|
||||
metricsSvc := NewMetricsService(svc)
|
||||
|
||||
// 创建入站凭证事件
|
||||
events := []*model.AuditEvent{
|
||||
// 合规的platform_token请求
|
||||
{
|
||||
EventName: "CRED-INGRESS-PLATFORM",
|
||||
EventCategory: "CRED",
|
||||
EventSubCategory: "INGRESS",
|
||||
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",
|
||||
},
|
||||
{
|
||||
EventName: "CRED-INGRESS-PLATFORM",
|
||||
EventCategory: "CRED",
|
||||
EventSubCategory: "INGRESS",
|
||||
OperatorID: 1002,
|
||||
TenantID: 2001,
|
||||
ObjectType: "account",
|
||||
ObjectID: 12346,
|
||||
Action: "query",
|
||||
CredentialType: "platform_token",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.2",
|
||||
Success: true,
|
||||
ResultCode: "CRED_INGRESS_OK",
|
||||
},
|
||||
// 非合规的query_key请求 - 不应该计入M-014的分母
|
||||
{
|
||||
EventName: "CRED-INGRESS-SUPPLIER",
|
||||
EventCategory: "CRED",
|
||||
EventSubCategory: "INGRESS",
|
||||
OperatorID: 1003,
|
||||
TenantID: 2001,
|
||||
ObjectType: "account",
|
||||
ObjectID: 12347,
|
||||
Action: "query",
|
||||
CredentialType: "query_key",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.3",
|
||||
Success: false,
|
||||
ResultCode: "AUTH_QUERY_REJECT",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
svc.CreateEvent(ctx, e)
|
||||
}
|
||||
|
||||
// 计算M-014指标
|
||||
now := time.Now()
|
||||
metric, err := metricsSvc.CalculateM014(ctx, now.Add(-24*time.Hour), now)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, metric)
|
||||
assert.Equal(t, "M-014", metric.MetricID)
|
||||
assert.Equal(t, "platform_credential_ingress_coverage_pct", metric.MetricName)
|
||||
// 2个platform_token / 2个总入站请求 = 100%
|
||||
assert.Equal(t, 100.0, metric.Value)
|
||||
assert.Equal(t, "PASS", metric.Status)
|
||||
}
|
||||
|
||||
func TestAuditMetrics_M015_DirectCall(t *testing.T) {
|
||||
// M-015: direct_supplier_call_by_consumer_events = 0
|
||||
ctx := context.Background()
|
||||
svc := NewAuditService(NewInMemoryAuditStore())
|
||||
metricsSvc := NewMetricsService(svc)
|
||||
|
||||
// 创建直连事件
|
||||
events := []*model.AuditEvent{
|
||||
{
|
||||
EventName: "CRED-DIRECT-SUPPLIER",
|
||||
EventCategory: "CRED",
|
||||
EventSubCategory: "DIRECT",
|
||||
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",
|
||||
TargetDirect: true,
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
svc.CreateEvent(ctx, e)
|
||||
}
|
||||
|
||||
// 计算M-015指标
|
||||
now := time.Now()
|
||||
metric, err := metricsSvc.CalculateM015(ctx, now.Add(-24*time.Hour), now)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, metric)
|
||||
assert.Equal(t, "M-015", metric.MetricID)
|
||||
assert.Equal(t, "direct_supplier_call_by_consumer_events", metric.MetricName)
|
||||
assert.Equal(t, float64(1), metric.Value) // 有1个直连事件
|
||||
assert.Equal(t, "FAIL", metric.Status) // 直连事件数 > 0,应该是FAIL
|
||||
}
|
||||
|
||||
func TestAuditMetrics_M016_QueryKeyRejectRate(t *testing.T) {
|
||||
// M-016: query_key_external_reject_rate_pct = 100%
|
||||
// 分母:所有query key请求(不含被拒绝的无效请求)
|
||||
ctx := context.Background()
|
||||
svc := NewAuditService(NewInMemoryAuditStore())
|
||||
metricsSvc := NewMetricsService(svc)
|
||||
|
||||
// 创建query key事件
|
||||
events := []*model.AuditEvent{
|
||||
// 被拒绝的query key请求
|
||||
{
|
||||
EventName: "AUTH-QUERY-REJECT",
|
||||
EventCategory: "AUTH",
|
||||
OperatorID: 1001,
|
||||
TenantID: 2001,
|
||||
ObjectType: "query_key",
|
||||
ObjectID: 12345,
|
||||
Action: "query",
|
||||
CredentialType: "query_key",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.1",
|
||||
Success: false,
|
||||
ResultCode: "QUERY_KEY_NOT_ALLOWED",
|
||||
},
|
||||
{
|
||||
EventName: "AUTH-QUERY-REJECT",
|
||||
EventCategory: "AUTH",
|
||||
OperatorID: 1002,
|
||||
TenantID: 2001,
|
||||
ObjectType: "query_key",
|
||||
ObjectID: 12346,
|
||||
Action: "query",
|
||||
CredentialType: "query_key",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.2",
|
||||
Success: false,
|
||||
ResultCode: "QUERY_KEY_EXPIRED",
|
||||
},
|
||||
// query key请求
|
||||
{
|
||||
EventName: "AUTH-QUERY-KEY",
|
||||
EventCategory: "AUTH",
|
||||
OperatorID: 1003,
|
||||
TenantID: 2001,
|
||||
ObjectType: "query_key",
|
||||
ObjectID: 12347,
|
||||
Action: "query",
|
||||
CredentialType: "query_key",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.3",
|
||||
Success: false,
|
||||
ResultCode: "QUERY_KEY_EXPIRED",
|
||||
},
|
||||
// 非query key事件
|
||||
{
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
svc.CreateEvent(ctx, e)
|
||||
}
|
||||
|
||||
// 计算M-016指标
|
||||
now := time.Now()
|
||||
metric, err := metricsSvc.CalculateM016(ctx, now.Add(-24*time.Hour), now)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, metric)
|
||||
assert.Equal(t, "M-016", metric.MetricID)
|
||||
assert.Equal(t, "query_key_external_reject_rate_pct", metric.MetricName)
|
||||
// 2个拒绝 / 3个query key总请求 = 66.67%
|
||||
assert.InDelta(t, 66.67, metric.Value, 0.01)
|
||||
assert.Equal(t, "FAIL", metric.Status) // 拒绝率 < 100%,应该是FAIL
|
||||
}
|
||||
|
||||
func TestAuditMetrics_M016_DifferentFromM014(t *testing.T) {
|
||||
// M-014与M-016边界清晰:分母不同,无重叠
|
||||
// M-014 分母:经平台凭证校验的入站请求(platform_token)
|
||||
// M-016 分母:检测到的所有query key请求
|
||||
|
||||
ctx := context.Background()
|
||||
svc := NewAuditService(NewInMemoryAuditStore())
|
||||
metricsSvc := NewMetricsService(svc)
|
||||
|
||||
// 场景:100个请求,80个使用platform_token,20个使用query key(被拒绝)
|
||||
// M-014 = 80/80 = 100%(分母只计算platform_token请求)
|
||||
// M-016 = 20/20 = 100%(分母计算所有query key请求)
|
||||
|
||||
// 创建80个platform_token请求
|
||||
for i := 0; i < 80; i++ {
|
||||
svc.CreateEvent(ctx, &model.AuditEvent{
|
||||
EventName: "CRED-INGRESS-PLATFORM",
|
||||
EventCategory: "CRED",
|
||||
EventSubCategory: "INGRESS",
|
||||
OperatorID: int64(1000 + i),
|
||||
TenantID: 2001,
|
||||
ObjectType: "account",
|
||||
ObjectID: int64(i),
|
||||
Action: "query",
|
||||
CredentialType: "platform_token",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.1",
|
||||
Success: true,
|
||||
ResultCode: "CRED_INGRESS_OK",
|
||||
})
|
||||
}
|
||||
|
||||
// 创建20个query key请求(全部被拒绝)
|
||||
for i := 0; i < 20; i++ {
|
||||
svc.CreateEvent(ctx, &model.AuditEvent{
|
||||
EventName: "AUTH-QUERY-REJECT",
|
||||
EventCategory: "AUTH",
|
||||
OperatorID: int64(2000 + i),
|
||||
TenantID: 2001,
|
||||
ObjectType: "query_key",
|
||||
ObjectID: int64(1000 + i),
|
||||
Action: "query",
|
||||
CredentialType: "query_key",
|
||||
SourceType: "api",
|
||||
SourceIP: "192.168.1.1",
|
||||
Success: false,
|
||||
ResultCode: "QUERY_KEY_NOT_ALLOWED",
|
||||
})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 计算M-014
|
||||
m014, err := metricsSvc.CalculateM014(ctx, now.Add(-24*time.Hour), now)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100.0, m014.Value) // 80/80 = 100%
|
||||
|
||||
// 计算M-016
|
||||
m016, err := metricsSvc.CalculateM016(ctx, now.Add(-24*time.Hour), now)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100.0, m016.Value) // 20/20 = 100%
|
||||
}
|
||||
|
||||
func TestAuditMetrics_M013_ZeroExposure(t *testing.T) {
|
||||
// M-013: 当没有凭证暴露事件时,应该为0,状态PASS
|
||||
ctx := context.Background()
|
||||
svc := NewAuditService(NewInMemoryAuditStore())
|
||||
metricsSvc := NewMetricsService(svc)
|
||||
|
||||
// 创建一些正常事件,没有CRED-EXPOSE
|
||||
svc.CreateEvent(ctx, &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",
|
||||
})
|
||||
|
||||
now := time.Now()
|
||||
metric, err := metricsSvc.CalculateM013(ctx, now.Add(-24*time.Hour), now)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(0), metric.Value)
|
||||
assert.Equal(t, "PASS", metric.Status)
|
||||
}
|
||||
Reference in New Issue
Block a user