Files
lijiaoqiao/supply-api/internal/audit/service/metrics_service_test.go
Your Name 89104bd0db 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规范
2026-04-02 23:35:53 +08:00

376 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_token20个使用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)
}