27c9a81c0a
Bump workspace and backend module to Go 1.26 to satisfy module toolchain requirements and allow dependency tooling (Renovate) to run. Regenerated backend module checksums.
158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Phase 4 UAT: Emergency & Break-Glass Operations
|
|
*
|
|
* Purpose: Validate emergency recovery procedures and break-glass token usage
|
|
* Scenarios: Emergency token usage, system reset, WAF disable, encryption key reset
|
|
* Success: Emergency procedures work to recover system from locked state
|
|
*/
|
|
|
|
test.describe('UAT-008: Emergency & Break-Glass Operations', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForSelector('[data-testid="dashboard-container"], [role="main"]', { timeout: 5000 });
|
|
});
|
|
|
|
// UAT-701: Use emergency token
|
|
test('Emergency token enables break-glass access', async ({ page }) => {
|
|
await test.step('Verify emergency token available', async () => {
|
|
// Navigate to security settings where token is generated
|
|
await page.goto('/settings/security', { waitUntil: 'networkidle' }).catch(() => {
|
|
return page.goto('/settings');
|
|
});
|
|
|
|
const emergencySection = page.getByText(/emergency|break.?glass|recovery/i).first();
|
|
if (await emergencySection.isVisible()) {
|
|
await expect(emergencySection).toBeVisible();
|
|
}
|
|
});
|
|
|
|
await test.step('Simulate emergency token usage', async () => {
|
|
// In test, we'll verify the mechanism exists
|
|
// Actually using emergency token would require special header injection
|
|
const tokenField = page.locator('[data-testid*="emergency"], [class*="emergency"]').first();
|
|
if (await tokenField.isVisible()) {
|
|
await expect(tokenField).toBeVisible();
|
|
}
|
|
});
|
|
});
|
|
|
|
// UAT-702: Break-glass recovery
|
|
test('Break-glass recovery brings system to safe state', async ({ page }) => {
|
|
await test.step('Access break-glass procedures', async () => {
|
|
const emergencyLink = page.getByRole('link', { name: /emergency|break.?glass/i }).first();
|
|
if (await emergencyLink.isVisible()) {
|
|
await emergencyLink.click();
|
|
await page.waitForLoadState('networkidle');
|
|
} else {
|
|
// Check in settings
|
|
await page.goto('/settings/emergency', { waitUntil: 'networkidle' }).catch(() => {
|
|
return page.goto('/settings');
|
|
});
|
|
}
|
|
});
|
|
|
|
await test.step('Verify recovery procedures documented', async () => {
|
|
const procedures = page.getByText(/procedure|step|instruction|guide/i).first();
|
|
if (await procedures.isVisible()) {
|
|
await expect(procedures).toBeVisible();
|
|
}
|
|
});
|
|
|
|
await test.step('Verify recovery mechanism available', async () => {
|
|
const recoveryButton = page.getByRole('button', { name: /recover|reset|restore/i }).first();
|
|
if (await recoveryButton.isVisible()) {
|
|
await expect(recoveryButton).toBeVisible();
|
|
}
|
|
});
|
|
});
|
|
|
|
// UAT-703: Disable WAF in emergency
|
|
test('Emergency token can disable security modules', async ({ page }) => {
|
|
await test.step('Access emergency control panel', async () => {
|
|
await page.goto('/settings/emergency', { waitUntil: 'networkidle' }).catch(() => {
|
|
return page.goto('/settings');
|
|
});
|
|
});
|
|
|
|
await test.step('Verify WAF disable option exists', async () => {
|
|
const wafControl = page.getByText(/waf|coraza|disable|emergency/i).first();
|
|
if (await wafControl.isVisible()) {
|
|
await expect(wafControl).toBeVisible();
|
|
}
|
|
});
|
|
|
|
await test.step('Verify emergency procedures mention security disabling', async () => {
|
|
const controlsSection = page.locator('[data-testid="emergency-controls"], [class*="emergency"]').first();
|
|
if (await controlsSection.isVisible()) {
|
|
await expect(controlsSection).toBeVisible();
|
|
}
|
|
});
|
|
});
|
|
|
|
// UAT-704: Reset encryption key in emergency
|
|
test('Emergency token can reset encryption key', async ({ page }) => {
|
|
await test.step('Access emergency encryption settings', async () => {
|
|
await page.goto('/settings/emergency', { waitUntil: 'networkidle' }).catch(() => {
|
|
return page.goto('/settings');
|
|
});
|
|
|
|
const encryptionSection = page.getByText(/encrypt|key|cipher/i).first();
|
|
if (await encryptionSection.isVisible()) {
|
|
await expect(encryptionSection).toBeVisible();
|
|
}
|
|
});
|
|
|
|
await test.step('Verify encryption reset option available', async () => {
|
|
const resetButton = page.getByRole('button', { name: /reset|regenerate|new.*key/i }).first();
|
|
if (await resetButton.isVisible()) {
|
|
// Don't actually click - just verify it exists
|
|
await expect(resetButton).toBeVisible();
|
|
}
|
|
});
|
|
|
|
await test.step('Verify audit logging of emergency actions', async () => {
|
|
const auditNote = page.getByText(/audit|record|log/i).first();
|
|
if (await auditNote.isVisible()) {
|
|
await expect(auditNote).toBeVisible();
|
|
}
|
|
});
|
|
});
|
|
|
|
// UAT-705: Emergency token single-use or reusable
|
|
test('Emergency token usage logged and tracked', async ({ page }) => {
|
|
await test.step('View emergency token access log', async () => {
|
|
await page.goto('/audit', { waitUntil: 'networkidle' }).catch(() => {
|
|
return page.goto('/admin/audit');
|
|
});
|
|
|
|
// Search for emergency token usage logs
|
|
const searchInput = page.getByPlaceholder(/search|filter/i).first();
|
|
if (await searchInput.isVisible()) {
|
|
await searchInput.fill('emergency');
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
});
|
|
|
|
await test.step('Verify emergency actions are audited', async () => {
|
|
const auditEntries = page.locator('[role="row"], [class*="audit-entry"]');
|
|
const count = await auditEntries.count();
|
|
expect(count).toBeGreaterThanOrEqual(0);
|
|
|
|
// Should see emergency-related entries if token was used
|
|
const emergencyEntry = page.getByText(/emergency/i).first();
|
|
if (await emergencyEntry.isVisible()) {
|
|
await expect(emergencyEntry).toBeVisible();
|
|
}
|
|
});
|
|
|
|
await test.step('Verify emergency usage timestamp recorded', async () => {
|
|
const timestamp = page.getByText(/\d{1,2}\/\d{1,2}\/\d{2,4}/).first();
|
|
if (await timestamp.isVisible()) {
|
|
await expect(timestamp).toBeVisible();
|
|
}
|
|
});
|
|
});
|
|
});
|