Files
awoooi/apps/web/tests/e2e/multisig-security.spec.ts
OG T 196d269b92 feat: add all application source code
- apps/api: FastAPI backend with Dockerfile
- apps/web: Next.js frontend with Dockerfile
- apps/sensor: Signal collection agent
- packages: shared packages

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-22 18:57:44 +08:00

225 lines
8.4 KiB
TypeScript

import { test, expect } from '@playwright/test'
/**
* Multi-Sig Security E2E Test
* ===========================
* CISO-101: 資安驗收測試
*
* 驗證項目:
* 1. CRITICAL 授權需要 2 人簽核
* 2. 同一人不能重複簽核 (Identity Check)
* 3. 第二人簽核後 → APPROVED
*/
const API_BASE_URL = 'http://localhost:8000'
test.describe('Multi-Sig Security Verification', () => {
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)
})
})