import { test, expect } from '@playwright/test' /** * Multi-Sig Security E2E Test * =========================== * CISO-101: 資安驗收測試 * * 驗證項目: * 1. CRITICAL 授權需要 2 人簽核 * 2. 同一人不能重複簽核 (Identity Check) * 3. 第二人簽核後 → APPROVED * * ⚠️ 條件式執行說明: * - 此測試需要後端 API 連線 (localhost:8000 或 192.168.0.125:32334) * - CI/CD 環境無法連接生產 API,故標記為條件式執行 * - 本地開發環境可正常執行 * - 設定 SKIP_MULTISIG_TESTS=true 可強制跳過 * * P2 改進 (2026-03-29 首席架構師審查): * - 新增 SKIP_MULTISIG_TESTS 環境變數控制 * - 改善跳過訊息的可讀性 * * @version 1.1.0 * @date 2026-03-29 (台北時間) */ const API_BASE_URL = process.env.TEST_API_URL || 'http://localhost:8000' const FORCE_SKIP = process.env.SKIP_MULTISIG_TESTS === 'true' // 檢查 API 是否可用 async function isApiAvailable(): Promise { if (FORCE_SKIP) { console.log('⚠️ Multi-Sig tests skipped: SKIP_MULTISIG_TESTS=true') return false } try { const response = await fetch(`${API_BASE_URL}/api/v1/health`, { method: 'GET', signal: AbortSignal.timeout(5000), }) return response.ok } catch { return false } } test.describe('Multi-Sig Security Verification', () => { // 條件式跳過: 當 API 不可用時 test.beforeAll(async () => { const apiAvailable = await isApiAvailable() if (!apiAvailable) { console.log('┌────────────────────────────────────────────────┐') console.log('│ ⚠️ Multi-Sig tests SKIPPED │') console.log('├────────────────────────────────────────────────┤') console.log(`│ API URL: ${API_BASE_URL.padEnd(35)}│`) console.log('│ Reason: Backend API not available │') console.log('│ To run: Start API server first │') console.log('└────────────────────────────────────────────────┘') test.skip() } }) test.setTimeout(120000) // 輔助函數: 建立 CRITICAL 授權 async function createCriticalApproval(): Promise { const response = await fetch(`${API_BASE_URL}/api/v1/approvals`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'DROP TABLE user_sessions', description: 'E2E Security Test - Multi-Sig verification', risk_level: 'critical', blast_radius: { affected_pods: 0, estimated_downtime: '0', related_services: ['auth-service', 'api-gateway'], data_impact: 'destructive', }, dry_run_checks: [ { name: 'RBAC Check', passed: true, message: 'test-admin' }, { name: 'Syntax Check', passed: true }, ], requested_by: 'E2E-Test', }), }) expect(response.ok).toBeTruthy() const data = await response.json() expect(data.status).toBe('pending') expect(data.required_signatures).toBe(2) return data.id } // 輔助函數: 簽核 async function signApproval( approvalId: string, signerId: string, signerName: string ): Promise<{ success: boolean; status: number; data: any }> { const response = await fetch( `${API_BASE_URL}/api/v1/approvals/${approvalId}/sign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signer_id: signerId, signer_name: signerName, comment: `E2E Test sign by ${signerName}`, }), } ) const data = await response.json() return { success: response.ok, status: response.status, data, } } test('Step 1-4: Full Multi-Sig Security Flow', async ({ page }) => { // ======================================================================== // Step 1: 建立 CRITICAL 授權 // ======================================================================== console.log('Step 1: Creating CRITICAL approval...') const approvalId = await createCriticalApproval() console.log(`Created approval: ${approvalId}`) // 截圖: 初始狀態 await page.goto('/zh-TW/demo', { waitUntil: 'domcontentloaded' }) await page.waitForTimeout(3000) await page.screenshot({ path: 'test-results/screenshots/multisig-01-initial.png', fullPage: true, }) // ======================================================================== // Step 2: User A (admin-1) 第一次簽核 // ======================================================================== console.log('Step 2: User A (admin-1) signing...') const sign1Result = await signApproval(approvalId, 'admin-1', 'Admin A (CTO)') expect(sign1Result.success).toBeTruthy() expect(sign1Result.data.approval.status).toBe('pending') expect(sign1Result.data.approval.current_signatures).toBe(1) expect(sign1Result.data.approval.required_signatures).toBe(2) expect(sign1Result.data.execution_triggered).toBe(false) console.log('✅ First signature accepted: 1/2') // 截圖: 1/2 簽核狀態 await page.reload({ waitUntil: 'domcontentloaded' }) await page.waitForTimeout(2000) await page.screenshot({ path: 'test-results/screenshots/multisig-02-first-sign.png', fullPage: true, }) // ======================================================================== // Step 3: User A (admin-1) 嘗試重複簽核 → 應被拒絕 // ======================================================================== console.log('Step 3: User A attempting duplicate signature...') const duplicateResult = await signApproval(approvalId, 'admin-1', 'Admin A (CTO)') // 關鍵斷言: 重複簽核必須被拒絕 expect(duplicateResult.success).toBeFalsy() expect(duplicateResult.status).toBe(400) expect(duplicateResult.data.detail).toContain('already signed') console.log('✅ Duplicate signature REJECTED: 400 Bad Request') console.log(` Error: ${duplicateResult.data.detail}`) // 截圖: 重複簽核被拒 await page.screenshot({ path: 'test-results/screenshots/multisig-03-duplicate-rejected.png', fullPage: true, }) // ======================================================================== // Step 4: User B (admin-2) 簽核 → APPROVED // ======================================================================== console.log('Step 4: User B (admin-2) signing...') const sign2Result = await signApproval(approvalId, 'admin-2', 'Admin B (CISO)') expect(sign2Result.success).toBeTruthy() expect(sign2Result.data.approval.status).toBe('approved') expect(sign2Result.data.approval.current_signatures).toBe(2) expect(sign2Result.data.execution_triggered).toBe(true) console.log('✅ Second signature accepted: 2/2 → APPROVED') console.log('✅ Execution triggered!') // 截圖: 最終狀態 await page.reload({ waitUntil: 'domcontentloaded' }) await page.waitForTimeout(2000) await page.screenshot({ path: 'test-results/screenshots/multisig-04-approved.png', fullPage: true, }) // ======================================================================== // 驗證: 確認已從 pending 清單移除 // ======================================================================== const pendingResponse = await fetch(`${API_BASE_URL}/api/v1/approvals/pending`) const pendingData = await pendingResponse.json() const stillPending = pendingData.approvals.find( (a: any) => a.id === approvalId ) expect(stillPending).toBeUndefined() console.log('✅ Approval removed from pending list') // ======================================================================== // 最終報告 // ======================================================================== console.log('') console.log('═══════════════════════════════════════════════════════') console.log(' Multi-Sig Security Test: ALL PASSED') console.log('═══════════════════════════════════════════════════════') console.log('') console.log(' ✅ Step 1: CRITICAL approval created (0/2)') console.log(' ✅ Step 2: First signature accepted (1/2)') console.log(' ✅ Step 3: Duplicate signature REJECTED (400)') console.log(' ✅ Step 4: Second signature → APPROVED (2/2)') console.log('') console.log(' Identity Check: ENFORCED') console.log(' Multi-Sig Logic: VERIFIED') console.log('═══════════════════════════════════════════════════════') }) test('Duplicate signature returns 400 with correct error message', async () => { // 建立授權 const approvalId = await createCriticalApproval() // 第一次簽核 const first = await signApproval(approvalId, 'user-test-001', 'Test User') expect(first.success).toBeTruthy() // 重複簽核 const duplicate = await signApproval(approvalId, 'user-test-001', 'Test User') // 斷言 expect(duplicate.success).toBeFalsy() expect(duplicate.status).toBe(400) expect(duplicate.data.detail).toMatch(/already signed/i) }) test('Cannot sign after approval is completed', async () => { // 建立授權 const approvalId = await createCriticalApproval() // 兩人簽核完成 await signApproval(approvalId, 'signer-A', 'Signer A') const complete = await signApproval(approvalId, 'signer-B', 'Signer B') expect(complete.data.approval.status).toBe('approved') // 第三人嘗試簽核已完成的授權 const lateSign = await signApproval(approvalId, 'signer-C', 'Signer C') expect(lateSign.success).toBeFalsy() expect(lateSign.status).toBe(400) expect(lateSign.data.detail).toMatch(/cannot sign/i) }) })