//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{
"",
"javascript: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, "