Updated E2E tests.
This commit is contained in:
+1
-1
@@ -86,7 +86,7 @@ e2e/
|
|||||||
- WebSocket challenge-response with virtual authenticator
|
- WebSocket challenge-response with virtual authenticator
|
||||||
- Session token creation and validation
|
- Session token creation and validation
|
||||||
|
|
||||||
### Authentication Flow
|
### Authentication Flow
|
||||||
- Passkey authentication via WebSocket
|
- Passkey authentication via WebSocket
|
||||||
- Credential verification
|
- Credential verification
|
||||||
- Session management
|
- Session management
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { defineConfig, devices } from '@playwright/test'
|
|||||||
/**
|
/**
|
||||||
* Playwright configuration for PasskeyAuth E2E tests.
|
* Playwright configuration for PasskeyAuth E2E tests.
|
||||||
* Uses Chrome's Virtual Authenticator for automated passkey testing.
|
* Uses Chrome's Virtual Authenticator for automated passkey testing.
|
||||||
*
|
*
|
||||||
* Run with: bun run test
|
* Run with: bun run test
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -17,18 +17,18 @@ export default defineConfig({
|
|||||||
['html', { open: 'never' }],
|
['html', { open: 'never' }],
|
||||||
['list']
|
['list']
|
||||||
],
|
],
|
||||||
|
|
||||||
// Global setup/teardown for test database and server
|
// Global setup/teardown for test database and server
|
||||||
globalSetup: './tests/global-setup.ts',
|
globalSetup: './tests/global-setup.ts',
|
||||||
globalTeardown: './tests/global-teardown.ts',
|
globalTeardown: './tests/global-teardown.ts',
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
// Base URL for the passkey-auth server
|
// Base URL for the passkey-auth server
|
||||||
baseURL: process.env.BASE_URL || 'http://localhost:4401',
|
baseURL: process.env.BASE_URL || 'http://localhost:4401',
|
||||||
|
|
||||||
// Collect trace on failure for debugging
|
// Collect trace on failure for debugging
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
// Screenshot on failure
|
// Screenshot on failure
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-26
@@ -57,34 +57,34 @@ export async function registerPasskey(
|
|||||||
|
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
const ws = new WebSocket(wsUrl)
|
const ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('WebSocket connected for registration')
|
console.log('WebSocket connected for registration')
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onmessage = async (event) => {
|
ws.onmessage = async (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
|
|
||||||
// Check for error response
|
// Check for error response
|
||||||
if (data.detail) {
|
if (data.detail) {
|
||||||
ws.close()
|
ws.close()
|
||||||
reject(new Error(data.detail))
|
reject(new Error(data.detail))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is the final success response
|
// Check if this is the final success response
|
||||||
if (data.session_token) {
|
if (data.session_token) {
|
||||||
ws.close()
|
ws.close()
|
||||||
resolve(data)
|
resolve(data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should be the registration options from server
|
// This should be the registration options from server
|
||||||
// Use the native WebAuthn API with the virtual authenticator
|
// Use the native WebAuthn API with the virtual authenticator
|
||||||
try {
|
try {
|
||||||
// Convert base64url challenge to ArrayBuffer
|
// Convert base64url challenge to ArrayBuffer
|
||||||
const challenge = Uint8Array.from(atob(data.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
|
const challenge = Uint8Array.from(atob(data.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
|
||||||
|
|
||||||
// Build the credential creation options
|
// Build the credential creation options
|
||||||
const publicKeyCredentialCreationOptions: CredentialCreationOptions = {
|
const publicKeyCredentialCreationOptions: CredentialCreationOptions = {
|
||||||
publicKey: {
|
publicKey: {
|
||||||
@@ -108,16 +108,16 @@ export async function registerPasskey(
|
|||||||
})) || [],
|
})) || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the credential using native WebAuthn API (virtual authenticator handles it)
|
// Create the credential using native WebAuthn API (virtual authenticator handles it)
|
||||||
const credential = await navigator.credentials.create(publicKeyCredentialCreationOptions) as PublicKeyCredential
|
const credential = await navigator.credentials.create(publicKeyCredentialCreationOptions) as PublicKeyCredential
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
throw new Error('Failed to create credential')
|
throw new Error('Failed to create credential')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = credential.response as AuthenticatorAttestationResponse
|
const response = credential.response as AuthenticatorAttestationResponse
|
||||||
|
|
||||||
// Convert response to JSON format expected by server
|
// Convert response to JSON format expected by server
|
||||||
const registrationResponse = {
|
const registrationResponse = {
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
@@ -131,18 +131,18 @@ export async function registerPasskey(
|
|||||||
clientExtensionResults: credential.getClientExtensionResults(),
|
clientExtensionResults: credential.getClientExtensionResults(),
|
||||||
authenticatorAttachment: (credential as any).authenticatorAttachment,
|
authenticatorAttachment: (credential as any).authenticatorAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.send(JSON.stringify(registrationResponse))
|
ws.send(JSON.stringify(registrationResponse))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ws.close()
|
ws.close()
|
||||||
reject(new Error(error.message || 'Registration failed'))
|
reject(new Error(error.message || 'Registration failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
reject(new Error('WebSocket error during registration'))
|
reject(new Error('WebSocket error during registration'))
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
if (!event.wasClean && event.code !== 1000) {
|
if (!event.wasClean && event.code !== 1000) {
|
||||||
reject(new Error(`WebSocket closed unexpectedly: ${event.code}`))
|
reject(new Error(`WebSocket closed unexpectedly: ${event.code}`))
|
||||||
@@ -165,33 +165,33 @@ export async function authenticatePasskey(
|
|||||||
|
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
const ws = new WebSocket(wsUrl)
|
const ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('WebSocket connected for authentication')
|
console.log('WebSocket connected for authentication')
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onmessage = async (event) => {
|
ws.onmessage = async (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
|
|
||||||
// Check for error response
|
// Check for error response
|
||||||
if (data.detail) {
|
if (data.detail) {
|
||||||
ws.close()
|
ws.close()
|
||||||
reject(new Error(data.detail))
|
reject(new Error(data.detail))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is the final success response
|
// Check if this is the final success response
|
||||||
if (data.session_token) {
|
if (data.session_token) {
|
||||||
ws.close()
|
ws.close()
|
||||||
resolve(data)
|
resolve(data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should be the authentication options from server
|
// This should be the authentication options from server
|
||||||
try {
|
try {
|
||||||
// Convert base64url challenge to ArrayBuffer
|
// Convert base64url challenge to ArrayBuffer
|
||||||
const challenge = Uint8Array.from(atob(data.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
|
const challenge = Uint8Array.from(atob(data.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
|
||||||
|
|
||||||
// Build the credential request options
|
// Build the credential request options
|
||||||
const publicKeyCredentialRequestOptions: CredentialRequestOptions = {
|
const publicKeyCredentialRequestOptions: CredentialRequestOptions = {
|
||||||
publicKey: {
|
publicKey: {
|
||||||
@@ -206,16 +206,16 @@ export async function authenticatePasskey(
|
|||||||
})) || [],
|
})) || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the credential using native WebAuthn API (virtual authenticator handles it)
|
// Get the credential using native WebAuthn API (virtual authenticator handles it)
|
||||||
const credential = await navigator.credentials.get(publicKeyCredentialRequestOptions) as PublicKeyCredential
|
const credential = await navigator.credentials.get(publicKeyCredentialRequestOptions) as PublicKeyCredential
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
throw new Error('Failed to get credential')
|
throw new Error('Failed to get credential')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = credential.response as AuthenticatorAssertionResponse
|
const response = credential.response as AuthenticatorAssertionResponse
|
||||||
|
|
||||||
// Convert response to JSON format expected by server
|
// Convert response to JSON format expected by server
|
||||||
const authenticationResponse = {
|
const authenticationResponse = {
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
@@ -230,18 +230,18 @@ export async function authenticatePasskey(
|
|||||||
clientExtensionResults: credential.getClientExtensionResults(),
|
clientExtensionResults: credential.getClientExtensionResults(),
|
||||||
authenticatorAttachment: (credential as any).authenticatorAttachment,
|
authenticatorAttachment: (credential as any).authenticatorAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.send(JSON.stringify(authenticationResponse))
|
ws.send(JSON.stringify(authenticationResponse))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ws.close()
|
ws.close()
|
||||||
reject(new Error(error.message || 'Authentication failed'))
|
reject(new Error(error.message || 'Authentication failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
reject(new Error('WebSocket error during authentication'))
|
reject(new Error('WebSocket error during authentication'))
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
if (!event.wasClean && event.code !== 1000) {
|
if (!event.wasClean && event.code !== 1000) {
|
||||||
reject(new Error(`WebSocket closed unexpectedly: ${event.code}`))
|
reject(new Error(`WebSocket closed unexpectedly: ${event.code}`))
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,7 @@ export async function createVirtualAuthenticator(
|
|||||||
options: VirtualAuthenticatorOptions = {}
|
options: VirtualAuthenticatorOptions = {}
|
||||||
): Promise<VirtualAuthenticator> {
|
): Promise<VirtualAuthenticator> {
|
||||||
const cdpSession = await page.context().newCDPSession(page)
|
const cdpSession = await page.context().newCDPSession(page)
|
||||||
|
|
||||||
// Enable WebAuthn in CDP
|
// Enable WebAuthn in CDP
|
||||||
await cdpSession.send('WebAuthn.enable', {
|
await cdpSession.send('WebAuthn.enable', {
|
||||||
enableUI: false, // Suppress any UI prompts
|
enableUI: false, // Suppress any UI prompts
|
||||||
@@ -81,10 +81,10 @@ export const test = base.extend<{
|
|||||||
virtualAuthenticator: async ({ page }, use) => {
|
virtualAuthenticator: async ({ page }, use) => {
|
||||||
// Create virtual authenticator before test
|
// Create virtual authenticator before test
|
||||||
const authenticator = await createVirtualAuthenticator(page)
|
const authenticator = await createVirtualAuthenticator(page)
|
||||||
|
|
||||||
// Run the test
|
// Run the test
|
||||||
await use(authenticator)
|
await use(authenticator)
|
||||||
|
|
||||||
// Cleanup after test
|
// Cleanup after test
|
||||||
await removeVirtualAuthenticator(authenticator)
|
await removeVirtualAuthenticator(authenticator)
|
||||||
},
|
},
|
||||||
|
|||||||
+18
-18
@@ -15,34 +15,34 @@ interface TestState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Global setup for E2E tests.
|
* Global setup for E2E tests.
|
||||||
*
|
*
|
||||||
* This creates a fresh test database and starts the server,
|
* This creates a fresh test database and starts the server,
|
||||||
* capturing the bootstrap reset token for initial user registration.
|
* capturing the bootstrap reset token for initial user registration.
|
||||||
*/
|
*/
|
||||||
export default async function globalSetup() {
|
export default async function globalSetup() {
|
||||||
console.log('\n🔧 Setting up E2E test environment...\n')
|
console.log('\n🔧 Setting up E2E test environment...\n')
|
||||||
|
|
||||||
// Create test data directory
|
// Create test data directory
|
||||||
if (!existsSync(testDataDir)) {
|
if (!existsSync(testDataDir)) {
|
||||||
mkdirSync(testDataDir, { recursive: true })
|
mkdirSync(testDataDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old database for clean state
|
// Remove old database for clean state
|
||||||
if (existsSync(dbPath)) {
|
if (existsSync(dbPath)) {
|
||||||
console.log(' Removing old test database...')
|
console.log(' Removing old test database...')
|
||||||
rmSync(dbPath)
|
rmSync(dbPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any wal/shm files too
|
// Remove any wal/shm files too
|
||||||
for (const ext of ['-wal', '-shm']) {
|
for (const ext of ['-wal', '-shm']) {
|
||||||
const file = dbPath + ext
|
const file = dbPath + ext
|
||||||
if (existsSync(file)) rmSync(file)
|
if (existsSync(file)) rmSync(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(' Starting server with fresh database...')
|
console.log(' Starting server with fresh database...')
|
||||||
|
|
||||||
const state: TestState = {}
|
const state: TestState = {}
|
||||||
|
|
||||||
// Start the server using Node's spawn
|
// Start the server using Node's spawn
|
||||||
const serverProcess = spawn('uv', [
|
const serverProcess = spawn('uv', [
|
||||||
'run', 'passkey-auth', 'serve', ':4401',
|
'run', 'passkey-auth', 'serve', ':4401',
|
||||||
@@ -55,22 +55,22 @@ export default async function globalSetup() {
|
|||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
})
|
})
|
||||||
|
|
||||||
state.serverPid = serverProcess.pid
|
state.serverPid = serverProcess.pid
|
||||||
|
|
||||||
// Capture output to find reset token
|
// Capture output to find reset token
|
||||||
const resetTokenPromise = new Promise<string>((resolve, reject) => {
|
const resetTokenPromise = new Promise<string>((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reject(new Error('Timed out waiting for server bootstrap (30s)'))
|
reject(new Error('Timed out waiting for server bootstrap (30s)'))
|
||||||
}, 30000)
|
}, 30000)
|
||||||
|
|
||||||
let output = ''
|
let output = ''
|
||||||
|
|
||||||
const handleData = (data: Buffer) => {
|
const handleData = (data: Buffer) => {
|
||||||
const text = data.toString()
|
const text = data.toString()
|
||||||
output += text
|
output += text
|
||||||
process.stdout.write(text) // Echo to console
|
process.stdout.write(text) // Echo to console
|
||||||
|
|
||||||
// Look for the reset token URL in the output
|
// Look for the reset token URL in the output
|
||||||
// Format: http://localhost:4401/auth/{token} where token is word.word.word.word.word (dot separated)
|
// Format: http://localhost:4401/auth/{token} where token is word.word.word.word.word (dot separated)
|
||||||
const match = output.match(/http:\/\/localhost:\d+\/auth\/([a-z]+(?:\.[a-z]+)+)/)
|
const match = output.match(/http:\/\/localhost:\d+\/auth\/([a-z]+(?:\.[a-z]+)+)/)
|
||||||
@@ -80,15 +80,15 @@ export default async function globalSetup() {
|
|||||||
setTimeout(() => resolve(match[1]), 1000)
|
setTimeout(() => resolve(match[1]), 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverProcess.stdout?.on('data', handleData)
|
serverProcess.stdout?.on('data', handleData)
|
||||||
serverProcess.stderr?.on('data', handleData)
|
serverProcess.stderr?.on('data', handleData)
|
||||||
|
|
||||||
serverProcess.on('error', (err) => {
|
serverProcess.on('error', (err) => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
serverProcess.on('exit', (code) => {
|
serverProcess.on('exit', (code) => {
|
||||||
if (code !== 0 && code !== null) {
|
if (code !== 0 && code !== null) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
@@ -96,7 +96,7 @@ export default async function globalSetup() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state.resetToken = await resetTokenPromise
|
state.resetToken = await resetTokenPromise
|
||||||
console.log(`\n ✅ Captured reset token: ${state.resetToken}\n`)
|
console.log(`\n ✅ Captured reset token: ${state.resetToken}\n`)
|
||||||
@@ -105,9 +105,9 @@ export default async function globalSetup() {
|
|||||||
serverProcess.kill()
|
serverProcess.kill()
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save state for tests
|
// Save state for tests
|
||||||
writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||||
|
|
||||||
console.log(' ✅ E2E test environment ready\n')
|
console.log(' ✅ E2E test environment ready\n')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ interface TestState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Global teardown for E2E tests.
|
* Global teardown for E2E tests.
|
||||||
*
|
*
|
||||||
* This cleans up the test server and optionally removes the test database.
|
* This cleans up the test server and optionally removes the test database.
|
||||||
*/
|
*/
|
||||||
export default async function globalTeardown() {
|
export default async function globalTeardown() {
|
||||||
console.log('\n🧹 Cleaning up E2E test environment...\n')
|
console.log('\n🧹 Cleaning up E2E test environment...\n')
|
||||||
|
|
||||||
// Read state file to get server PID
|
// Read state file to get server PID
|
||||||
if (existsSync(stateFile)) {
|
if (existsSync(stateFile)) {
|
||||||
try {
|
try {
|
||||||
const state: TestState = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
const state: TestState = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
||||||
|
|
||||||
if (state.serverPid) {
|
if (state.serverPid) {
|
||||||
console.log(` Stopping server (PID: ${state.serverPid})...`)
|
console.log(` Stopping server (PID: ${state.serverPid})...`)
|
||||||
try {
|
try {
|
||||||
@@ -40,11 +40,11 @@ export default async function globalTeardown() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(' Warning: Could not read state file')
|
console.warn(' Warning: Could not read state file')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up state file
|
// Clean up state file
|
||||||
rmSync(stateFile, { force: true })
|
rmSync(stateFile, { force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally clean up test database (keep it for debugging by default)
|
// Optionally clean up test database (keep it for debugging by default)
|
||||||
if (process.env.CLEANUP_TEST_DB === 'true') {
|
if (process.env.CLEANUP_TEST_DB === 'true') {
|
||||||
const dbPath = join(testDataDir, 'test.sqlite')
|
const dbPath = join(testDataDir, 'test.sqlite')
|
||||||
@@ -58,6 +58,6 @@ export default async function globalTeardown() {
|
|||||||
if (existsSync(file)) rmSync(file)
|
if (existsSync(file)) rmSync(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(' ✅ Cleanup complete\n')
|
console.log(' ✅ Cleanup complete\n')
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-20
@@ -11,21 +11,21 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* E2E tests for PasskeyAuth using Chrome's Virtual Authenticator.
|
* E2E tests for PasskeyAuth using Chrome's Virtual Authenticator.
|
||||||
*
|
*
|
||||||
* These tests exercise the complete WebAuthn flow:
|
* These tests exercise the complete WebAuthn flow:
|
||||||
* 1. Registration via WebSocket using bootstrap reset token
|
* 1. Registration via WebSocket using bootstrap reset token
|
||||||
* 2. Authentication via WebSocket
|
* 2. Authentication via WebSocket
|
||||||
* 3. Session validation
|
* 3. Session validation
|
||||||
* 4. User info retrieval
|
* 4. User info retrieval
|
||||||
* 5. Logout
|
* 5. Logout
|
||||||
*
|
*
|
||||||
* The virtual authenticator simulates a hardware passkey device,
|
* The virtual authenticator simulates a hardware passkey device,
|
||||||
* allowing fully automated testing without physical hardware.
|
* allowing fully automated testing without physical hardware.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
test.describe('Passkey Authentication E2E', () => {
|
test.describe('Passkey Authentication E2E', () => {
|
||||||
const baseUrl = process.env.BASE_URL || 'http://localhost:4401'
|
const baseUrl = process.env.BASE_URL || 'http://localhost:4401'
|
||||||
|
|
||||||
test.describe.configure({ mode: 'serial' })
|
test.describe.configure({ mode: 'serial' })
|
||||||
|
|
||||||
// Shared state across tests in this describe block
|
// Shared state across tests in this describe block
|
||||||
@@ -48,20 +48,20 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
// Navigate to auth page to establish origin for WebAuthn
|
// Navigate to auth page to establish origin for WebAuthn
|
||||||
await page.goto('/auth/')
|
await page.goto('/auth/')
|
||||||
await expect(page).toHaveTitle(/.*/)
|
await expect(page).toHaveTitle(/.*/)
|
||||||
|
|
||||||
// Page should load - 401 errors are expected since user is not logged in
|
// Page should load - 401 errors are expected since user is not logged in
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
|
|
||||||
// Just verify the page loaded without JS errors (network 401s are OK)
|
// Just verify the page loaded without JS errors (network 401s are OK)
|
||||||
console.log('✓ Auth page loaded successfully')
|
console.log('✓ Auth page loaded successfully')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should register admin passkey via WebSocket using reset token', async ({ page, virtualAuthenticator }) => {
|
test('should register admin passkey via WebSocket using reset token', async ({ page, virtualAuthenticator }) => {
|
||||||
test.skip(!resetToken, 'No reset token available from bootstrap')
|
test.skip(!resetToken, 'No reset token available from bootstrap')
|
||||||
|
|
||||||
// Must visit the page first to establish origin
|
// Must visit the page first to establish origin
|
||||||
await page.goto('/auth/')
|
await page.goto('/auth/')
|
||||||
|
|
||||||
// Perform registration via WebSocket with virtual authenticator
|
// Perform registration via WebSocket with virtual authenticator
|
||||||
// Using the bootstrap reset token for the admin user
|
// Using the bootstrap reset token for the admin user
|
||||||
const result = await registerPasskey(page, baseUrl, {
|
const result = await registerPasskey(page, baseUrl, {
|
||||||
@@ -91,10 +91,10 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
test.skip(!sessionToken, 'Requires successful registration')
|
test.skip(!sessionToken, 'Requires successful registration')
|
||||||
|
|
||||||
const validation = await validateSession(page, baseUrl, sessionToken)
|
const validation = await validateSession(page, baseUrl, sessionToken)
|
||||||
|
|
||||||
expect(validation.valid).toBe(true)
|
expect(validation.valid).toBe(true)
|
||||||
expect(validation.user_uuid).toBe(userUuid)
|
expect(validation.user_uuid).toBe(userUuid)
|
||||||
|
|
||||||
console.log(`✓ Session validated for user: ${validation.user_uuid}`)
|
console.log(`✓ Session validated for user: ${validation.user_uuid}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -102,12 +102,12 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
test.skip(!sessionToken, 'Requires successful registration')
|
test.skip(!sessionToken, 'Requires successful registration')
|
||||||
|
|
||||||
const userInfo = await getUserInfo(page, baseUrl, sessionToken)
|
const userInfo = await getUserInfo(page, baseUrl, sessionToken)
|
||||||
|
|
||||||
expect(userInfo.user.user_uuid).toBe(userUuid)
|
expect(userInfo.user.user_uuid).toBe(userUuid)
|
||||||
expect(userInfo.user.user_name).toBe('Admin User')
|
expect(userInfo.user.user_name).toBe('Admin User')
|
||||||
expect(userInfo.credentials).toBeDefined()
|
expect(userInfo.credentials).toBeDefined()
|
||||||
expect(userInfo.credentials.length).toBeGreaterThanOrEqual(1)
|
expect(userInfo.credentials.length).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
console.log(`✓ User info retrieved: ${userInfo.user.user_name}`)
|
console.log(`✓ User info retrieved: ${userInfo.user.user_name}`)
|
||||||
console.log(`✓ Credentials count: ${userInfo.credentials.length}`)
|
console.log(`✓ Credentials count: ${userInfo.credentials.length}`)
|
||||||
})
|
})
|
||||||
@@ -128,9 +128,9 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
resetToken: deviceLink.token,
|
resetToken: deviceLink.token,
|
||||||
displayName: 'Admin User (test device)'
|
displayName: 'Admin User (test device)'
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`✓ Added test credential: ${regResult.credential_uuid}`)
|
console.log(`✓ Added test credential: ${regResult.credential_uuid}`)
|
||||||
|
|
||||||
// Now logout and authenticate with the fresh credential
|
// Now logout and authenticate with the fresh credential
|
||||||
await logout(page, baseUrl, regResult.session_token)
|
await logout(page, baseUrl, regResult.session_token)
|
||||||
console.log('✓ Logged out')
|
console.log('✓ Logged out')
|
||||||
@@ -153,10 +153,10 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
test.skip(!sessionToken, 'Requires successful authentication')
|
test.skip(!sessionToken, 'Requires successful authentication')
|
||||||
|
|
||||||
const validation = await validateSession(page, baseUrl, sessionToken)
|
const validation = await validateSession(page, baseUrl, sessionToken)
|
||||||
|
|
||||||
expect(validation.valid).toBe(true)
|
expect(validation.valid).toBe(true)
|
||||||
expect(validation.user_uuid).toBe(userUuid)
|
expect(validation.user_uuid).toBe(userUuid)
|
||||||
|
|
||||||
console.log(`✓ New session validated`)
|
console.log(`✓ New session validated`)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
test.skip(!sessionToken, 'Requires valid session')
|
test.skip(!sessionToken, 'Requires valid session')
|
||||||
|
|
||||||
await logout(page, baseUrl, sessionToken)
|
await logout(page, baseUrl, sessionToken)
|
||||||
|
|
||||||
// Session should no longer be valid
|
// Session should no longer be valid
|
||||||
const response = await page.request.post(`${baseUrl}/auth/api/validate`, {
|
const response = await page.request.post(`${baseUrl}/auth/api/validate`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -172,7 +172,7 @@ test.describe('Passkey Authentication E2E', () => {
|
|||||||
},
|
},
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response.status()).toBe(401)
|
expect(response.status()).toBe(401)
|
||||||
console.log(`✓ Logout successful, session invalidated`)
|
console.log(`✓ Logout successful, session invalidated`)
|
||||||
})
|
})
|
||||||
@@ -188,7 +188,7 @@ test.describe('Session Management', () => {
|
|||||||
},
|
},
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Server may return 400 (bad format) or 401 (unauthorized)
|
// Server may return 400 (bad format) or 401 (unauthorized)
|
||||||
expect([400, 401]).toContain(response.status())
|
expect([400, 401]).toContain(response.status())
|
||||||
console.log(`✓ Invalid token correctly rejected`)
|
console.log(`✓ Invalid token correctly rejected`)
|
||||||
@@ -198,7 +198,7 @@ test.describe('Session Management', () => {
|
|||||||
const response = await page.request.post(`${baseUrl}/auth/api/validate`, {
|
const response = await page.request.post(`${baseUrl}/auth/api/validate`, {
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response.status()).toBe(401)
|
expect(response.status()).toBe(401)
|
||||||
console.log(`✓ Missing token correctly rejected`)
|
console.log(`✓ Missing token correctly rejected`)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user