From ac798a727eebafc7c2b49c2e1ef9e4004b97f297 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Mar 2026 11:13:41 -0700 Subject: [PATCH] =?UTF-8?q?test(auth):=20SmartAuth=20Playwright=20E2E=20sp?= =?UTF-8?q?ecs=20=E2=80=94=20login,=20MFA=20settings,=20security=20dashboa?= =?UTF-8?q?rd,=20devices,=20passkeys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../admin-web/e2e/smartauth-devices.spec.ts | 110 +++++++++++++++ .../admin-web/e2e/smartauth-login.spec.ts | 42 ++++++ .../e2e/smartauth-mfa-settings.spec.ts | 84 +++++++++++ .../admin-web/e2e/smartauth-passkeys.spec.ts | 77 +++++++++++ .../e2e/smartauth-security-dashboard.spec.ts | 130 ++++++++++++++++++ 5 files changed, 443 insertions(+) create mode 100644 dashboards/admin-web/e2e/smartauth-devices.spec.ts create mode 100644 dashboards/admin-web/e2e/smartauth-login.spec.ts create mode 100644 dashboards/admin-web/e2e/smartauth-mfa-settings.spec.ts create mode 100644 dashboards/admin-web/e2e/smartauth-passkeys.spec.ts create mode 100644 dashboards/admin-web/e2e/smartauth-security-dashboard.spec.ts diff --git a/dashboards/admin-web/e2e/smartauth-devices.spec.ts b/dashboards/admin-web/e2e/smartauth-devices.spec.ts new file mode 100644 index 00000000..003cf1d1 --- /dev/null +++ b/dashboards/admin-web/e2e/smartauth-devices.spec.ts @@ -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 }); + }); +}); diff --git a/dashboards/admin-web/e2e/smartauth-login.spec.ts b/dashboards/admin-web/e2e/smartauth-login.spec.ts new file mode 100644 index 00000000..5cac3bd7 --- /dev/null +++ b/dashboards/admin-web/e2e/smartauth-login.spec.ts @@ -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(); + }); +}); diff --git a/dashboards/admin-web/e2e/smartauth-mfa-settings.spec.ts b/dashboards/admin-web/e2e/smartauth-mfa-settings.spec.ts new file mode 100644 index 00000000..d99ba30e --- /dev/null +++ b/dashboards/admin-web/e2e/smartauth-mfa-settings.spec.ts @@ -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(); + }); +}); diff --git a/dashboards/admin-web/e2e/smartauth-passkeys.spec.ts b/dashboards/admin-web/e2e/smartauth-passkeys.spec.ts new file mode 100644 index 00000000..b5c8c926 --- /dev/null +++ b/dashboards/admin-web/e2e/smartauth-passkeys.spec.ts @@ -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(); + }); +}); diff --git a/dashboards/admin-web/e2e/smartauth-security-dashboard.spec.ts b/dashboards/admin-web/e2e/smartauth-security-dashboard.spec.ts new file mode 100644 index 00000000..4d43e9c4 --- /dev/null +++ b/dashboards/admin-web/e2e/smartauth-security-dashboard.spec.ts @@ -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'); + }); +});