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:
saravanakumardb1 2026-03-12 11:13:41 -07:00
parent 067a23449f
commit ac798a727e
5 changed files with 443 additions and 0 deletions

View 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 });
});
});

View 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();
});
});

View 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();
});
});

View 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();
});
});

View 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');
});
});