Skip to content

Commit 54d6043

Browse files
committed
Enhance passkey login tests by refining error handling and improving test clarity; mock configurations for passkey enabled/disabled scenarios, and ensure proper UI rendering during authentication processes.
1 parent 1f45c06 commit 54d6043

File tree

3 files changed

+110
-127
lines changed

3 files changed

+110
-127
lines changed

packages/template-retail-react-app/app/hooks/use-auth-modal.test.js

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ describe('Passkey login', () => {
624624
// Clear all mocks
625625
jest.clearAllMocks()
626626

627+
// Override getConfig to return config with passkey enabled
627628
getConfig.mockReturnValue({
628629
...mockConfig,
629630
app: {
@@ -649,6 +650,12 @@ describe('Passkey login', () => {
649650
get: mockCredentialsGet
650651
}
651652

653+
// Mock parseRequestOptionsFromJSON to return mock options
654+
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue({
655+
challenge: 'mock-challenge',
656+
allowCredentials: []
657+
})
658+
652659
// Setup MSW handlers for WebAuthn API endpoints
653660
global.server.use(
654661
rest.post('*/oauth2/webauthn/authenticate/start', (req, res, ctx) => {
@@ -688,13 +695,6 @@ describe('Passkey login', () => {
688695
})
689696

690697
test('Triggers passkey login when modal opens with passkey enabled', async () => {
691-
const mockPublicKeyOptions = {
692-
challenge: 'mock-challenge',
693-
allowCredentials: []
694-
}
695-
696-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue(mockPublicKeyOptions)
697-
698698
// Mock credential that will be returned from navigator.credentials.get
699699
const mockCredential = {
700700
id: 'mock-credential-id',
@@ -745,14 +745,7 @@ describe('Passkey login', () => {
745745
)
746746
})
747747

748-
test('Falls back to other login methods when passkey login is cancelled', async () => {
749-
const mockPublicKeyOptions = {
750-
challenge: 'mock-challenge',
751-
allowCredentials: []
752-
}
753-
754-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue(mockPublicKeyOptions)
755-
748+
test('User can login with other method when passkey login is cancelled', async () => {
756749
// Simulate user cancelling passkey selection (NotAllowedError)
757750
const notAllowedError = new Error('User cancelled')
758751
notAllowedError.name = 'NotAllowedError'
@@ -768,7 +761,7 @@ describe('Passkey login', () => {
768761
const trigger = screen.getByText(/open modal/i)
769762
await user.click(trigger)
770763

771-
// Should not show error for cancelled passkey
764+
// Login form should be shown
772765
await waitFor(() => {
773766
expect(mockCredentialsGet).toHaveBeenCalled()
774767
expect(screen.getByText(/welcome back/i)).toBeInTheDocument()
@@ -779,13 +772,6 @@ describe('Passkey login', () => {
779772
})
780773

781774
test('Shows error when passkey authentication fails with error from the browser', async () => {
782-
const mockPublicKeyOptions = {
783-
challenge: 'mock-challenge',
784-
allowCredentials: []
785-
}
786-
787-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue(mockPublicKeyOptions)
788-
789775
// Simulate error in loginWithPasskey hook
790776
mockCredentialsGet.mockRejectedValue(new Error('Authentication failed'))
791777

@@ -870,13 +856,6 @@ describe('Passkey login', () => {
870856
})
871857

872858
test('Successfully logs in with passkey', async () => {
873-
const mockPublicKeyOptions = {
874-
challenge: 'mock-challenge',
875-
allowCredentials: []
876-
}
877-
878-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue(mockPublicKeyOptions)
879-
880859
const mockCredential = {
881860
id: 'mock-credential-id',
882861
rawId: new ArrayBuffer(32),

packages/template-retail-react-app/app/pages/login/index.jsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,10 @@ const Login = ({initialView = LOGIN_VIEW}) => {
211211
}, [isRegistered, redirectPath])
212212

213213
useEffect(() => {
214-
try {
215-
loginWithPasskey()
216-
} catch (error) {
214+
loginWithPasskey().catch((error) => {
217215
const message = formatMessage(getPasskeyErrorMessage(error))
218216
form.setError('global', {type: 'manual', message})
219-
}
217+
})
220218
}, [])
221219

222220
/**************** Einstein ****************/

packages/template-retail-react-app/app/pages/login/index.test.js

Lines changed: 99 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,26 @@ describe('Error while logging in', function () {
283283
describe('Passkey login', () => {
284284
let mockCredentialsGet
285285
let mockPublicKeyCredential
286+
let mockAppConfig
286287

287288
beforeEach(() => {
288289
// Clear all mocks
289290
jest.clearAllMocks()
290291

292+
// Override getConfig to return config with passkey enabled
293+
mockAppConfig = {
294+
...mockConfig.app,
295+
login: {
296+
...mockConfig.app.login,
297+
passkey: {enabled: true}
298+
}
299+
}
300+
301+
getConfig.mockReturnValue({
302+
...mockConfig,
303+
app: mockAppConfig
304+
})
305+
291306
// Mock WebAuthn API - default to never resolving (simulating no user action)
292307
mockCredentialsGet = jest.fn().mockImplementation(() => new Promise(() => {}))
293308
mockPublicKeyCredential = {
@@ -302,6 +317,12 @@ describe('Passkey login', () => {
302317
get: mockCredentialsGet
303318
}
304319

320+
// Mock parseRequestOptionsFromJSON to return mock options
321+
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue({
322+
challenge: 'mock-challenge',
323+
allowCredentials: []
324+
})
325+
305326
// Clear localStorage
306327
localStorage.clear()
307328

@@ -345,21 +366,6 @@ describe('Passkey login', () => {
345366
})
346367

347368
test('Sets up conditional mediation on page load when passkey enabled', async () => {
348-
const mockAppConfig = {
349-
...mockConfig.app,
350-
login: {
351-
...mockConfig.app.login,
352-
passkey: {enabled: true}
353-
}
354-
}
355-
356-
const mockPublicKeyOptions = {
357-
challenge: 'mock-challenge',
358-
allowCredentials: []
359-
}
360-
361-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue(mockPublicKeyOptions)
362-
363369
// Mock that conditional mediation starts but user doesn't select
364370
mockCredentialsGet.mockImplementation(
365371
() =>
@@ -395,23 +401,42 @@ describe('Passkey login', () => {
395401
)
396402
})
397403

398-
test('Successfully logs in with passkey in passwordless mode on login page', async () => {
404+
test('Does not trigger passkey when passkey is disabled', async () => {
399405
const mockAppConfig = {
400406
...mockConfig.app,
401407
login: {
402408
...mockConfig.app.login,
403-
passwordless: {enabled: true},
404-
passkey: {enabled: true}
409+
passkey: {enabled: false}
405410
}
406411
}
407412

408-
const mockPublicKeyOptions = {
409-
challenge: 'mock-challenge',
410-
allowCredentials: []
411-
}
413+
// Override getConfig to return config with passkey disabled
414+
getConfig.mockReturnValue({
415+
...mockConfig,
416+
app: mockAppConfig
417+
})
412418

413-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue(mockPublicKeyOptions)
419+
renderWithProviders(<MockedComponent />, {
420+
wrapperProps: {
421+
siteAlias: 'uk',
422+
locale: {id: 'en-GB'},
423+
appConfig: mockAppConfig,
424+
bypassAuth: false
425+
}
426+
})
427+
428+
await waitFor(() => {
429+
expect(screen.getByTestId('login-page')).toBeInTheDocument()
430+
})
431+
432+
// Give it a moment for any async effects to run
433+
await new Promise((resolve) => setTimeout(resolve, 100))
414434

435+
// Should not call credentials API when passkey is disabled
436+
expect(mockCredentialsGet).not.toHaveBeenCalled()
437+
})
438+
439+
test('Successfully logs in with passkey', async () => {
415440
const mockCredential = {
416441
id: 'mock-credential-id',
417442
rawId: new ArrayBuffer(32),
@@ -438,7 +463,7 @@ describe('Passkey login', () => {
438463

439464
mockCredentialsGet.mockResolvedValue(mockCredential)
440465

441-
// Mock successful auth after passkey
466+
// Mock customer as registered after passkey login
442467
global.server.use(
443468
rest.post('*/oauth2/token', (req, res, ctx) =>
444469
res(
@@ -456,7 +481,7 @@ describe('Passkey login', () => {
456481
)
457482
)
458483

459-
const {user} = renderWithProviders(<MockedComponent />, {
484+
renderWithProviders(<MockedComponent />, {
460485
wrapperProps: {
461486
siteAlias: 'uk',
462487
locale: {id: 'en-GB'},
@@ -465,41 +490,26 @@ describe('Passkey login', () => {
465490
}
466491
})
467492

468-
// Enter email (don't enter password for passwordless)
469-
await user.type(screen.getByLabelText('Email'), 'test@salesforce.com')
470-
await user.click(screen.getByRole('button', {name: /sign in/i}))
471-
472-
// Should trigger passkey authentication with credentials.get
493+
// Wait for passkey flow to be triggered when modal opens
473494
await waitFor(
474495
() => {
475496
expect(mockCredentialsGet).toHaveBeenCalled()
476497
},
477498
{timeout: 5000}
478499
)
479500

480-
// After successful passkey login, should redirect to account page
481-
await waitFor(
482-
() => {
483-
expect(window.location.pathname).toBe('/uk/en-GB/account')
484-
},
485-
{timeout: 5000}
486-
)
501+
// login successfully and navigate to account page
502+
await waitFor(() => {
503+
expect(window.location.pathname).toBe('/uk/en-GB/account')
504+
expect(screen.getByText(/My Profile/i)).toBeInTheDocument()
505+
})
487506
})
488507

489-
test('Does not trigger passkey when passkey is disabled', async () => {
490-
const mockAppConfig = {
491-
...mockConfig.app,
492-
login: {
493-
...mockConfig.app.login,
494-
passkey: {enabled: false}
495-
}
496-
}
497-
498-
// Override getConfig to return config with passkey disabled
499-
getConfig.mockReturnValue({
500-
...mockConfig,
501-
app: mockAppConfig
502-
})
508+
test('User can select other login method when passkey login is cancelled', async () => {
509+
// User cancels passkey selection
510+
const notAllowedError = new Error('User cancelled')
511+
notAllowedError.name = 'NotAllowedError'
512+
mockCredentialsGet.mockRejectedValue(notAllowedError)
503513

504514
renderWithProviders(<MockedComponent />, {
505515
wrapperProps: {
@@ -510,38 +520,50 @@ describe('Passkey login', () => {
510520
}
511521
})
512522

523+
// Login form should be shown
513524
await waitFor(() => {
525+
expect(mockCredentialsGet).toHaveBeenCalled()
526+
expect(screen.getByText(/welcome back/i)).toBeInTheDocument()
527+
expect(screen.getByLabelText('Email')).toBeInTheDocument()
528+
expect(screen.getByLabelText('Password')).toBeInTheDocument()
529+
expect(screen.getByRole('button', {name: /sign in/i})).toBeInTheDocument()
514530
expect(screen.getByTestId('login-page')).toBeInTheDocument()
515531
})
516-
517-
// Give it a moment for any async effects to run
518-
await new Promise((resolve) => setTimeout(resolve, 100))
519-
520-
// Should not call credentials API when passkey is disabled
521-
expect(mockCredentialsGet).not.toHaveBeenCalled()
522532
})
523533

524-
test('Handles passkey login cancellation gracefully', async () => {
525-
const mockAppConfig = {
526-
...mockConfig.app,
527-
login: {
528-
...mockConfig.app.login,
529-
passwordless: {enabled: true},
530-
passkey: {enabled: true}
534+
test('Shows error when passkey authentication fails with error from the browser', async () => {
535+
// Simulate error in navigator.credentials.get hook
536+
mockCredentialsGet.mockRejectedValue(new Error('Authentication failed'))
537+
538+
renderWithProviders(<MockedComponent />, {
539+
wrapperProps: {
540+
siteAlias: 'uk',
541+
locale: {id: 'en-GB'},
542+
appConfig: mockAppConfig,
543+
bypassAuth: false
531544
}
532-
}
545+
})
533546

534-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue({
535-
challenge: 'mock-challenge',
536-
allowCredentials: []
547+
// Should show error - passkey error should be caught and handled
548+
await waitFor(() => {
549+
expect(mockCredentialsGet).toHaveBeenCalled()
550+
expect(screen.getByText(/Something went wrong. Try again!/i)).toBeInTheDocument()
537551
})
552+
})
538553

539-
// User cancels passkey selection
540-
const notAllowedError = new Error('User cancelled')
541-
notAllowedError.name = 'NotAllowedError'
542-
mockCredentialsGet.mockRejectedValue(notAllowedError)
554+
test('Shows error when passkey authentication fails with error from the WebAuthn API', async () => {
555+
// Simulate error in WebAuthn API
556+
global.server.use(
557+
rest.post('*/oauth2/webauthn/authenticate/start', (req, res, ctx) => {
558+
return res(
559+
ctx.delay(0),
560+
ctx.status(401),
561+
ctx.json({message: 'Authentication failed'})
562+
)
563+
})
564+
)
543565

544-
const {user} = renderWithProviders(<MockedComponent />, {
566+
renderWithProviders(<MockedComponent />, {
545567
wrapperProps: {
546568
siteAlias: 'uk',
547569
locale: {id: 'en-GB'},
@@ -550,31 +572,15 @@ describe('Passkey login', () => {
550572
}
551573
})
552574

553-
// Enter email without password for passwordless
554-
await user.type(screen.getByLabelText('Email'), 'test@salesforce.com')
555-
await user.click(screen.getByRole('button', {name: /sign in/i}))
556-
557-
// Should not show error for cancelled passkey
558-
// Page should remain on login page
575+
// Should show error - 401 error from WebAuthn API should be caught and converted to user-friendly message
559576
await waitFor(() => {
560-
expect(screen.getByTestId('login-page')).toBeInTheDocument()
577+
expect(
578+
screen.getByText(/This feature is not currently available./i)
579+
).toBeInTheDocument()
561580
})
562581
})
563582

564583
test('Shows passkey registration prompt after successful login when passkey enabled and not registered', async () => {
565-
const mockAppConfig = {
566-
...mockConfig.app,
567-
login: {
568-
...mockConfig.app.login,
569-
passkey: {enabled: true}
570-
}
571-
}
572-
573-
mockPublicKeyCredential.parseRequestOptionsFromJSON.mockReturnValue({
574-
challenge: 'mock-challenge',
575-
allowCredentials: []
576-
})
577-
578584
const {user} = renderWithProviders(<MockedComponent />, {
579585
wrapperProps: {
580586
siteAlias: 'uk',

0 commit comments

Comments
 (0)