test(auth): SmartAuth Playwright E2E specs — login, MFA settings, security dashboard, devices, passkeys
- smartauth-login.spec.ts: Google Sign-In button presence, MFA challenge not shown initially - smartauth-mfa-settings.spec.ts: MFA status, setup/disable flows with API mocking - smartauth-security-dashboard.spec.ts: stats cards, login events table, suspicious filter - smartauth-devices.spec.ts: device list, trust badges, revoke all button - smartauth-passkeys.spec.ts: passkey list, add button, empty state, device type labels
This commit is contained in:
parent
067a23449f
commit
ac798a727e
110
dashboards/admin-web/e2e/smartauth-devices.spec.ts
Normal file
110
dashboards/admin-web/e2e/smartauth-devices.spec.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('SmartAuth: Device Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('admin_access_token', 'mock-token');
|
||||
localStorage.setItem('admin_refresh_token', 'mock-refresh');
|
||||
localStorage.setItem(
|
||||
'admin_auth_user',
|
||||
JSON.stringify({
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
role: 'super_admin',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('device management page loads', async ({ page }) => {
|
||||
await page.route('**/api/auth/devices', async route => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
}
|
||||
});
|
||||
await page.goto('/settings/devices');
|
||||
await expect(page.getByText('Device Management')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows empty state when no devices', async ({ page }) => {
|
||||
await page.route('**/api/auth/devices', async route => {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
});
|
||||
await page.goto('/settings/devices');
|
||||
await expect(page.getByText('No devices found')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('renders device cards with trust badges', async ({ page }) => {
|
||||
await page.route('**/api/auth/devices', async route => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 'dev-1',
|
||||
name: 'Chrome on macOS',
|
||||
platform: 'web-browser',
|
||||
trustLevel: 'trusted',
|
||||
trustExpiresAt: null,
|
||||
lastIp: '192.168.1.1',
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'dev-2',
|
||||
name: 'Safari on iPhone',
|
||||
platform: 'mobile-ios',
|
||||
trustLevel: 'remembered',
|
||||
trustExpiresAt: null,
|
||||
lastIp: '10.0.0.1',
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
});
|
||||
await page.goto('/settings/devices');
|
||||
await expect(page.getByText('Chrome on macOS')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('Safari on iPhone')).toBeVisible();
|
||||
await expect(page.getByText('Trusted')).toBeVisible();
|
||||
await expect(page.getByText('Remembered')).toBeVisible();
|
||||
});
|
||||
|
||||
test('revoke all button appears with multiple devices', async ({ page }) => {
|
||||
await page.route('**/api/auth/devices', async route => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 'dev-1',
|
||||
name: 'D1',
|
||||
platform: 'web',
|
||||
trustLevel: 'trusted',
|
||||
trustExpiresAt: null,
|
||||
lastIp: '1.1.1.1',
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'dev-2',
|
||||
name: 'D2',
|
||||
platform: 'web',
|
||||
trustLevel: 'unknown',
|
||||
trustExpiresAt: null,
|
||||
lastIp: '2.2.2.2',
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
});
|
||||
await page.goto('/settings/devices');
|
||||
await expect(page.getByRole('button', { name: /revoke all/i })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
42
dashboards/admin-web/e2e/smartauth-login.spec.ts
Normal file
42
dashboards/admin-web/e2e/smartauth-login.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('SmartAuth: Login with Google Sign-In', () => {
|
||||
test('shows Google Sign-In button when GOOGLE_CLIENT_ID is set', async ({ page }) => {
|
||||
// This test validates UI presence — button only renders when env var is set
|
||||
await page.goto('/login');
|
||||
// The button may or may not be present depending on env
|
||||
const googleBtn = page.getByRole('button', { name: /sign in with google/i });
|
||||
// If the env var is not set, the button should not exist — this is expected in CI
|
||||
const count = await googleBtn.count();
|
||||
expect(count).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('login form still works without Google button', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.getByLabel('Email')).toBeVisible();
|
||||
await expect(page.getByLabel('Password')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('"or" divider appears only when Google button is present', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
const googleBtn = page.getByRole('button', { name: /sign in with google/i });
|
||||
const hasGoogle = (await googleBtn.count()) > 0;
|
||||
if (hasGoogle) {
|
||||
await expect(page.getByText('or')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('SmartAuth: MFA Challenge Flow', () => {
|
||||
test('MFA challenge view is not shown on initial load', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.getByText('Two-Factor Authentication')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('login form shows email and password fields', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.getByLabel('Email')).toBeVisible();
|
||||
await expect(page.getByLabel('Password')).toBeVisible();
|
||||
});
|
||||
});
|
||||
84
dashboards/admin-web/e2e/smartauth-mfa-settings.spec.ts
Normal file
84
dashboards/admin-web/e2e/smartauth-mfa-settings.spec.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('SmartAuth: MFA Settings Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Simulate authenticated state with mock token
|
||||
await page.goto('/login');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('admin_access_token', 'mock-token');
|
||||
localStorage.setItem('admin_refresh_token', 'mock-refresh');
|
||||
localStorage.setItem(
|
||||
'admin_auth_user',
|
||||
JSON.stringify({
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
role: 'super_admin',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('security settings page loads', async ({ page }) => {
|
||||
await page.goto('/settings/security');
|
||||
await expect(page.getByText('Security Settings')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows Two-Factor Authentication section', async ({ page }) => {
|
||||
await page.goto('/settings/security');
|
||||
await expect(page.getByText('Two-Factor Authentication')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows setup button when MFA is not enabled', async ({ page }) => {
|
||||
// Mock the MFA status API to return disabled
|
||||
await page.route('**/api/auth/mfa/status', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ mfaEnabled: false, methods: [], recoveryCodesRemaining: 0 }),
|
||||
});
|
||||
});
|
||||
await page.goto('/settings/security');
|
||||
await expect(page.getByRole('button', { name: /set up authenticator/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('shows disable button when MFA is enabled', async ({ page }) => {
|
||||
await page.route('**/api/auth/mfa/status', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ mfaEnabled: true, methods: ['totp'], recoveryCodesRemaining: 8 }),
|
||||
});
|
||||
});
|
||||
await page.goto('/settings/security');
|
||||
await expect(page.getByRole('button', { name: /disable two-factor/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('TOTP setup flow shows QR code', async ({ page }) => {
|
||||
await page.route('**/api/auth/mfa/status', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ mfaEnabled: false, methods: [], recoveryCodesRemaining: 0 }),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/auth/mfa/totp/setup', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
otpauthUri: 'otpauth://totp/Test?secret=JBSWY3DPEHPK3PXP',
|
||||
qrDataUrl: '',
|
||||
recoveryCodes: ['ABC123', 'DEF456', 'GHI789', 'JKL012'],
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.goto('/settings/security');
|
||||
await page.getByRole('button', { name: /set up authenticator/i }).click();
|
||||
await expect(page.getByText('Set up authenticator')).toBeVisible();
|
||||
await expect(page.getByText('Recovery Codes')).toBeVisible();
|
||||
});
|
||||
});
|
||||
77
dashboards/admin-web/e2e/smartauth-passkeys.spec.ts
Normal file
77
dashboards/admin-web/e2e/smartauth-passkeys.spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('SmartAuth: Passkey Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('admin_access_token', 'mock-token');
|
||||
localStorage.setItem('admin_refresh_token', 'mock-refresh');
|
||||
localStorage.setItem(
|
||||
'admin_auth_user',
|
||||
JSON.stringify({
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
role: 'super_admin',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('passkey management page loads', async ({ page }) => {
|
||||
await page.route('**/api/auth/passkeys', async route => {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
});
|
||||
await page.goto('/settings/passkeys');
|
||||
await expect(page.getByText('Passkeys')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows empty state when no passkeys', async ({ page }) => {
|
||||
await page.route('**/api/auth/passkeys', async route => {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
});
|
||||
await page.goto('/settings/passkeys');
|
||||
await expect(page.getByText('No passkeys registered yet')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows Add passkey button', async ({ page }) => {
|
||||
await page.route('**/api/auth/passkeys', async route => {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
});
|
||||
await page.goto('/settings/passkeys');
|
||||
await expect(page.getByRole('button', { name: /add passkey/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('renders passkey cards when passkeys exist', async ({ page }) => {
|
||||
await page.route('**/api/auth/passkeys', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 'pk-1',
|
||||
friendlyName: 'MacBook Pro Touch ID',
|
||||
deviceType: 'platform',
|
||||
backedUp: true,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'pk-2',
|
||||
friendlyName: 'YubiKey 5C',
|
||||
deviceType: 'cross-platform',
|
||||
backedUp: false,
|
||||
lastUsedAt: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
await page.goto('/settings/passkeys');
|
||||
await expect(page.getByText('MacBook Pro Touch ID')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('YubiKey 5C')).toBeVisible();
|
||||
await expect(page.getByText('Built-in authenticator')).toBeVisible();
|
||||
await expect(page.getByText('Security key')).toBeVisible();
|
||||
});
|
||||
});
|
||||
130
dashboards/admin-web/e2e/smartauth-security-dashboard.spec.ts
Normal file
130
dashboards/admin-web/e2e/smartauth-security-dashboard.spec.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('SmartAuth: Security Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('admin_access_token', 'mock-token');
|
||||
localStorage.setItem('admin_refresh_token', 'mock-refresh');
|
||||
localStorage.setItem(
|
||||
'admin_auth_user',
|
||||
JSON.stringify({
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
role: 'super_admin',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('security dashboard page loads', async ({ page }) => {
|
||||
await page.route('**/api/auth/security/overview', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
totalUsers: 42,
|
||||
mfaAdoptionPercent: 67,
|
||||
providerDistribution: { password: 30, google: 10, microsoft: 2 },
|
||||
activeSessions: 15,
|
||||
suspiciousEvents24h: 3,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/auth/login-events**', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
await page.goto('/ops/security');
|
||||
await expect(page.getByText('Security Dashboard')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows stats cards with overview data', async ({ page }) => {
|
||||
await page.route('**/api/auth/security/overview', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
totalUsers: 42,
|
||||
mfaAdoptionPercent: 67,
|
||||
providerDistribution: {},
|
||||
activeSessions: 15,
|
||||
suspiciousEvents24h: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/auth/login-events**', async route => {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
});
|
||||
await page.goto('/ops/security');
|
||||
await expect(page.getByText('42')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('67%')).toBeVisible();
|
||||
await expect(page.getByText('15')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows login events table', async ({ page }) => {
|
||||
await page.route('**/api/auth/security/overview', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
totalUsers: 1,
|
||||
mfaAdoptionPercent: 0,
|
||||
providerDistribution: {},
|
||||
activeSessions: 1,
|
||||
suspiciousEvents24h: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/auth/login-events**', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 'evt-1',
|
||||
eventType: 'login_success',
|
||||
method: 'password',
|
||||
ip: '192.168.1.1',
|
||||
userAgent: 'Chrome',
|
||||
riskScore: 10,
|
||||
riskFactors: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
await page.goto('/ops/security');
|
||||
await expect(page.getByText('Recent Login Events')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('192.168.1.1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('suspicious filter toggle works', async ({ page }) => {
|
||||
let requestedUrl = '';
|
||||
await page.route('**/api/auth/security/overview', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
totalUsers: 1,
|
||||
mfaAdoptionPercent: 0,
|
||||
providerDistribution: {},
|
||||
activeSessions: 1,
|
||||
suspiciousEvents24h: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/auth/login-events**', async route => {
|
||||
requestedUrl = route.request().url();
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
|
||||
});
|
||||
await page.goto('/ops/security');
|
||||
await page.getByRole('button', { name: /show suspicious/i }).click();
|
||||
// Wait for re-fetch
|
||||
await page.waitForTimeout(500);
|
||||
expect(requestedUrl).toContain('suspicious=true');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user