Files
awoooi/apps/web/tests/e2e/multisig-security.spec.ts
OG T ee2bceefff feat(monitoring): Phase 19.6 測試文檔 + P1-P3 改進 + 首席架構師審查
Phase 19.6 測試文檔收尾:
- E2E 測試擴充至 18 項 (Terminal/GenUI 驗證)
- 新增 PHASE19-VERIFICATION-CHECKLIST.md (完整驗證清單)

P1 驗證:
- ArgoCD Metrics NodePort 監控 (30883/30884)
- TLS 證書監控 (Blackbox Exporter 9115)

P2 改進:
- waitForTimeout → waitForLoadState('networkidle')
- 跨平台快捷鍵 (Meta+J / Control+J)
- SKIP_MULTISIG_TESTS 環境變數控制
- Prometheus GitOps 部署腳本

P3 改進:
- HPA maxReplicas 4 → 6 (API/Web)

首席架構師審查: 47/50 OUTSTANDING (94%)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 01:19:26 +08:00

270 lines
10 KiB
TypeScript
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.
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<boolean> {
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<string> {
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)
})
})