Files
lijiaoqiao/supply-api/e2e/production_flow_test.go
Your Name a2f042f1c2 test(supply-api): expand e2e coverage and support unix socket dsn
Add broader e2e coverage for account, package, billing, tracing, and reliability scenarios.\nSupport Unix socket DSN formatting in config and cover it with unit tests.\nIgnore local assistant metadata and generated gate artifacts to reduce workspace noise.
2026-04-13 18:53:35 +08:00

472 lines
18 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.
//go:build e2e
// +build e2e
package e2e
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"lijiaoqiao/supply-api/internal/audit"
)
// ============================================================================
// 生产级 E2E 测试:完整用户流程
// ============================================================================
// TestProductionFlow_SupplierOnboarding 测试供应商完整入驻流程
func TestProductionFlow_SupplierOnboarding(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
tenantID := int64(90001)
token := system.tokenForTenant(t, "tok-onboard", tenantID)
// Step 1: 验证账户凭证
verifyBody := `{"provider":"openai","account_type":"resource","credential_input":"sk-prod-test-key-12345678901234567890","min_quota_threshold":1000}`
req1 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts/verify", strings.NewReader(verifyBody))
req1.Header.Set("Authorization", "Bearer "+token)
req1.Header.Set("Content-Type", "application/json")
req1.Header.Set("X-Request-Id", "onboard-step1-verify")
recorder1 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder1, req1)
if recorder1.Code != http.StatusOK {
t.Fatalf("步骤1失败: 期望 200, 实际 %d, body=%s", recorder1.Code, recorder1.Body.String())
}
t.Log("✅ Step 1: 账户验证成功")
// Step 2: 创建账户
createBody := `{"provider":"openai","account_type":"resource","credential_input":"sk-prod-test-key-12345678901234567890"}`
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts", strings.NewReader(createBody))
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("X-Request-Id", "onboard-step2-create")
recorder2 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder2, req2)
if recorder2.Code != http.StatusCreated {
t.Fatalf("步骤2失败: 期望 201, 实际 %d, body=%s", recorder2.Code, recorder2.Body.String())
}
t.Log("✅ Step 2: 账户创建成功")
// Step 3: 创建套餐草稿
draftBody := `{"model":"gpt-4-turbo","display_name":"GPT-4 Turbo 生产版","price_input":0.03,"quota_per_batch":10000}`
req3 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages/draft", strings.NewReader(draftBody))
req3.Header.Set("Authorization", "Bearer "+token)
req3.Header.Set("Content-Type", "application/json")
req3.Header.Set("X-Request-Id", "onboard-step3-draft")
recorder3 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder3, req3)
if recorder3.Code != http.StatusCreated {
t.Fatalf("步骤3失败: 期望 201, 实际 %d, body=%s", recorder3.Code, recorder3.Body.String())
}
t.Log("✅ Step 3: 套餐草稿创建成功")
// Step 4: 发布套餐
req4 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages/1/publish", nil)
req4.Header.Set("Authorization", "Bearer "+token)
req4.Header.Set("X-Request-Id", "onboard-step4-publish")
recorder4 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder4, req4)
if recorder4.Code != http.StatusOK {
t.Fatalf("步骤4失败: 期望 200, 实际 %d, body=%s", recorder4.Code, recorder4.Body.String())
}
t.Log("✅ Step 4: 套餐发布成功")
t.Log("🎉 供应商入驻流程完成")
}
// TestProductionFlow_CompleteSettlementCycle 测试完整结算周期
func TestProductionFlow_CompleteSettlementCycle(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
tenantID := int64(90002)
token := system.tokenForTenant(t, "tok-settlement", tenantID)
// Step 1: 查看账单汇总
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/supply/billing?start_date=2026-04-01&end_date=2026-04-30", nil)
req1.Header.Set("Authorization", "Bearer "+token)
req1.Header.Set("X-Request-Id", "settlement-step1-billing")
recorder1 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder1, req1)
if recorder1.Code != http.StatusOK {
t.Fatalf("步骤1失败: 期望 200, 实际 %d", recorder1.Code)
}
payload1 := decodeJSONBody(t, recorder1)
data1, ok := payload1["data"].(map[string]any)
if !ok {
t.Fatal("期望账单数据结构")
}
summary1, ok := data1["summary"].(map[string]any)
if !ok {
t.Fatal("期望账单汇总数据")
}
t.Logf("✅ Step 1: 账单汇总 - 总收入: %v", summary1["total_revenue"])
t.Logf(" 总订单: %v, 总使用量: %v", summary1["total_orders"], summary1["total_usage"])
// Step 2: 查看收益记录
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/supply/earnings/records?start_date=2026-04-01&end_date=2026-04-30&page=1&page_size=10", nil)
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("X-Request-Id", "settlement-step2-earnings")
recorder2 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder2, req2)
if recorder2.Code != http.StatusOK {
t.Fatalf("步骤2失败: 期望 200, 实际 %d", recorder2.Code)
}
t.Log("✅ Step 2: 收益记录查询成功")
// Step 3: 验证提现功能状态
req3 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/settlements/withdraw", strings.NewReader(`{"withdraw_amount":100,"payment_method":"bank","payment_account":"13800000000","sms_code":"123456"}`))
req3.Header.Set("Authorization", "Bearer "+token)
req3.Header.Set("Content-Type", "application/json")
req3.Header.Set("X-Request-Id", "settlement-step3-withdraw-check")
recorder3 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder3, req3)
if recorder3.Code != http.StatusServiceUnavailable {
t.Fatalf("步骤3失败: 提现功能应禁用, 期望 503, 实际 %d", recorder3.Code)
}
t.Log("✅ Step 3: 提现功能正确禁用 (SMS 未集成)")
t.Log("🎉 结算周期流程完成")
}
// TestProductionFlow_AuditTrailCompliance 测试审计追溯合规
func TestProductionFlow_AuditTrailCompliance(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
tenantID := int64(90003)
token := system.tokenForTenant(t, "tok-audit-compliance", tenantID)
// 执行多个操作以生成审计事件
operations := []struct {
name string
method string
path string
body string
}{
{"验证账户", "POST", "/api/v1/supply/accounts/verify", `{"provider":"openai","account_type":"resource","credential_input":"sk-test-key-1234567890"}`},
{"创建套餐", "POST", "/api/v1/supply/packages/draft", `{"model":"gpt-4","display_name":"GPT-4","price_input":0.03,"quota_per_batch":1000}`},
{"发布套餐", "POST", "/api/v1/supply/packages/1/publish", ""},
{"查询账单", "GET", "/api/v1/supply/billing?start_date=2026-04-01&end_date=2026-04-30", ""},
}
// 执行操作
for i, op := range operations {
var req *http.Request
if op.body != "" {
req = httptest.NewRequest(op.method, op.path, strings.NewReader(op.body))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(op.method, op.path, nil)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Request-Id", "audit-op-"+string(rune('a'+i)))
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, req)
if recorder.Code >= 400 && recorder.Code != 404 {
t.Logf("操作 %s 返回: %d (可能是预期的)", op.name, recorder.Code)
} else {
t.Logf("✅ 操作 %s 执行", op.name)
}
}
// 验证审计存储包含事件
events, err := system.auditStore.Query(context.Background(), audit.EventFilter{
TenantID: tenantID,
Limit: 10,
})
if err != nil {
t.Fatalf("查询审计事件失败: %v", err)
}
// 验证事件元数据
for _, event := range events {
if event.EventID == "" {
t.Error("审计事件缺少 EventID")
}
if event.TenantID != tenantID {
t.Errorf("审计事件 TenantID 不匹配: 期望 %d, 实际 %d", tenantID, event.TenantID)
}
if event.CreatedAt.IsZero() {
t.Error("审计事件缺少 CreatedAt 时间戳")
}
t.Logf("✅ 审计事件: type=%s, action=%s, result=%s", event.ObjectType, event.Action, event.ResultCode)
}
t.Log("🎉 审计追溯合规检查完成")
}
// ============================================================================
// 生产级 E2E 测试:安全边界测试
// ============================================================================
// TestSecurityBoundary_TokenReplayPrevention 测试 Token 重放防护
func TestSecurityBoundary_TokenReplayPrevention(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-replay-test", 90010)
// 第一次请求(应该成功)- 使用 POST /api/v1/supply/accounts/verify
body := `{"provider":"openai","account_type":"resource","credential_input":"sk-test"}`
req1 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts/verify", strings.NewReader(body))
req1.Header.Set("Authorization", "Bearer "+token)
req1.Header.Set("Content-Type", "application/json")
recorder1 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder1, req1)
if recorder1.Code != http.StatusOK {
t.Fatalf("第一次请求失败: %d, body=%s", recorder1.Code, recorder1.Body.String())
}
t.Log("✅ 第一次请求成功")
// 模拟重复请求(幂等性测试)
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts/verify", strings.NewReader(body))
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("Idempotency-Key", "replay-test-key")
recorder2 := httptest.NewRecorder()
system.handler.ServeHTTP(recorder2, req2)
t.Logf("Token 重放测试: 状态码=%d", recorder2.Code)
t.Log("✅ 重放防护测试完成")
}
// TestSecurityBoundary_SQLInjectionPrevention 测试 SQL 注入防护
func TestSecurityBoundary_SQLInjectionPrevention(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-sql-inj", 90011)
// 测试各种 SQL 注入 payload
injectionPayloads := []string{
"'; DROP TABLE supply_accounts; --",
"1' OR '1'='1",
"1; DELETE FROM supply_accounts WHERE 1=1; --",
"NULL",
}
for _, payload := range injectionPayloads {
body := `{"provider":"` + payload + `","account_type":"resource","credential_input":"sk-test"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts/verify", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, req)
// 应该返回 400/401/403但绝对不应该是 200 并执行了注入
if recorder.Code == http.StatusOK && strings.Contains(recorder.Body.String(), "DROP TABLE") {
t.Errorf("SQL 注入漏洞: payload=%s", payload)
}
t.Logf("SQL 注入测试: payload=%q, status=%d", payload, recorder.Code)
}
}
// TestSecurityBoundary_XSSPrevention 测试 XSS 防护
func TestSecurityBoundary_XSSPrevention(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-xss", 90012)
xssPayloads := []string{
"<script>alert('xss')</script>",
"javascript:alert('xss')",
"<img src=x onerror=alert('xss')>",
}
for _, payload := range xssPayloads {
body := `{"provider":"openai","account_type":"` + payload + `","credential_input":"sk-test"}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts/verify", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, req)
// 验证响应中没有未经处理的 XSS payload
responseBody := recorder.Body.String()
if strings.Contains(responseBody, "<script>") || strings.Contains(responseBody, "javascript:") {
t.Errorf("XSS 漏洞: payload=%s", payload)
}
t.Logf("XSS 测试: payload=%q, status=%d", payload, recorder.Code)
}
}
// TestSecurityBoundary_RateLimitingBoundary 测试限流边界
func TestSecurityBoundary_RateLimitingBoundary(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-rate-limit", 90013)
// 发送大量请求(模拟限流测试)
successCount := 0
rateLimitedCount := 0
for i := 0; i < 100; i++ {
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
req.Header.Set("Authorization", "Bearer "+token)
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, req)
if recorder.Code == http.StatusOK {
successCount++
} else if recorder.Code == http.StatusTooManyRequests {
rateLimitedCount++
}
}
t.Logf("限流测试结果: 成功=%d, 限流=%d, 总请求=100", successCount, rateLimitedCount)
// 验证系统有某种限流机制(要么成功数 < 100要么有明确的限流响应
if successCount == 100 && rateLimitedCount == 0 {
t.Log("⚠️ 警告: 100 个请求全部成功,可能缺少限流保护")
}
}
// ============================================================================
// 生产级 E2E 测试:性能与可靠性
// ============================================================================
// TestReliability_ResponseTimeUnderLoad 测试响应时间
func TestReliability_ResponseTimeUnderLoad(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-perf", 90020)
// 测试关键端点响应时间
endpoints := []struct {
name string
path string
}{
{"健康检查", "/actuator/health"},
{"账单查询", "/api/v1/supply/billing?start_date=2026-04-01&end_date=2026-04-30"},
{"收益记录", "/api/v1/supply/earnings/records?start_date=2026-04-01&end_date=2026-04-30"},
}
for _, ep := range endpoints {
start := time.Now()
req := httptest.NewRequest(http.MethodGet, ep.path, nil)
req.Header.Set("Authorization", "Bearer "+token)
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, req)
duration := time.Since(start)
t.Logf("端点 %s: 状态码=%d, 耗时=%v", ep.name, recorder.Code, duration)
// 生产级 SLA: P99 < 200ms
if duration > 200*time.Millisecond {
t.Logf("⚠️ 警告: %s 响应时间超过 200ms SLA", ep.name)
}
}
}
// TestReliability_CircuitBreakerBehavior 测试断路器行为
func TestReliability_CircuitBreakerBehavior(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-circuit", 90021)
// 模拟连续失败场景
for i := 0; i < 5; i++ {
// 使用无效路径触发错误
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts/invalid-id", nil)
req.Header.Set("Authorization", "Bearer "+token)
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, req)
t.Logf("错误请求 %d: 状态码=%d", i+1, recorder.Code)
}
t.Log("断路器测试完成")
}
// ============================================================================
// 生产级 E2E 测试:数据完整性
// ============================================================================
// TestDataIntegrity_AccountStateTransitions 测试账户状态转换
func TestDataIntegrity_AccountStateTransitions(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
token := system.tokenForTenant(t, "tok-state", 90030)
// 创建账户
createBody := `{"provider":"openai","account_type":"resource","credential_input":"sk-test-key-123456789012"}`
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts", strings.NewReader(createBody))
reqCreate.Header.Set("Authorization", "Bearer "+token)
reqCreate.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
system.handler.ServeHTTP(recorder, reqCreate)
if recorder.Code != http.StatusCreated {
t.Fatalf("创建账户失败: %d", recorder.Code)
}
t.Log("✅ 账户创建成功")
// 暂停账户
reqSuspend := httptest.NewRequest(http.MethodPost, "/api/v1/supply/accounts/1/suspend", nil)
reqSuspend.Header.Set("Authorization", "Bearer "+token)
recorderSuspend := httptest.NewRecorder()
system.handler.ServeHTTP(recorderSuspend, reqSuspend)
if recorderSuspend.Code != http.StatusOK {
t.Fatalf("暂停账户失败: %d", recorderSuspend.Code)
}
payload := decodeJSONBody(t, recorderSuspend)
data := payload["data"].(map[string]any)
if data["status"] != "suspended" {
t.Errorf("期望状态 suspended, 实际 %v", data["status"])
} else {
t.Log("✅ 账户暂停成功,状态正确")
}
}
// TestDataIntegrity_AuditLogImmutability 测试审计日志不可篡改
func TestDataIntegrity_AuditLogImmutability(t *testing.T) {
system := newE2ESystem(t, e2eOptions{withdrawEnabled: false})
tenantID := int64(90031)
// 直接向审计存储插入事件
if err := system.auditStore.Emit(context.Background(), audit.Event{
TenantID: tenantID,
ObjectType: "supply_account",
ObjectID: 1,
Action: "verify",
ResultCode: "OK",
RequestID: "immutable-req-001",
SourceIP: "127.0.0.1",
}); err != nil {
t.Fatalf("插入审计事件失败: %v", err)
}
// 查询事件
events, err := system.auditStore.Query(context.Background(), audit.EventFilter{
TenantID: tenantID,
Limit: 10,
})
if err != nil {
t.Fatalf("查询审计事件失败: %v", err)
}
if len(events) == 0 {
t.Fatal("未找到审计事件")
}
// 验证事件不可变字段
for _, event := range events {
if event.EventID == "" {
t.Error("EventID 不应为空")
}
if event.CreatedAt.IsZero() {
t.Error("CreatedAt 时间戳不应为零")
}
if event.ResultCode == "" {
t.Error("ResultCode 不应为空")
}
t.Logf("✅ 审计事件验证: EventID=%s, ObjectType=%s, Action=%s, ResultCode=%s",
event.EventID, event.ObjectType, event.Action, event.ResultCode)
}
}