//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, "