|
7 | 7 | const {expect} = require('@playwright/test') |
8 | 8 | const config = require('../config') |
9 | 9 | const {getCreditCardExpiry, runAccessibilityTest} = require('../scripts/utils.js') |
| 10 | +const crypto = require('crypto') |
10 | 11 | /** |
11 | 12 | * Note: As a best practice, we should await the network call and assert on the network response rather than waiting for pageLoadState() |
12 | 13 | * to avoid race conditions from lock in pageLoadState being released before network call resolves. |
@@ -791,3 +792,85 @@ export const selectStoreFromPLP = async ({page}) => { |
791 | 792 | await page.getByRole('button', {name: 'Close'}).click() |
792 | 793 | } |
793 | 794 | } |
| 795 | + |
| 796 | +/** |
| 797 | + * Validates that a passkey login request is made to the /webAuthn/authenticate/finish endpoint. |
| 798 | + * We can't register an actual passkey in the E2E environment because registration requires a token verification. |
| 799 | + * Instead,we add a mock credential to the virtual authenticator to bypass the registration flow and verify the |
| 800 | + * request to the /webAuthn/authenticate/finish endpoint. |
| 801 | + * |
| 802 | + * @param {Object} options.page - Playwright page object representing a browser tab/window |
| 803 | + */ |
| 804 | +export const validatePasskeyLogin = async ({page}) => { |
| 805 | + // Start a CDP session to interact with WebAuthn |
| 806 | + const client = await page.context().newCDPSession(page) |
| 807 | + await client.send('WebAuthn.enable') |
| 808 | + // Create a virtual authenticator to simulate a hardware authenticator for testing |
| 809 | + const {authenticatorId} = await client.send('WebAuthn.addVirtualAuthenticator', { |
| 810 | + options: { |
| 811 | + protocol: 'ctap2', |
| 812 | + transport: 'internal', |
| 813 | + hasResidentKey: true, |
| 814 | + hasUserVerification: true, |
| 815 | + isUserVerified: true, |
| 816 | + // Enabling automaticPresenceSimulation automatically completes the device's passkey prompt without user interaction |
| 817 | + automaticPresenceSimulation: true |
| 818 | + } |
| 819 | + }) |
| 820 | + |
| 821 | + // Preload mock credential into the virtual authenticator |
| 822 | + const rpId = new URL(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME).hostname |
| 823 | + // Generate a valid EC key pair for WebAuthn (ES256/P-256) |
| 824 | + const {privateKey} = crypto.generateKeyPairSync('ec', {namedCurve: 'P-256'}) |
| 825 | + const privateKeyBase64 = privateKey.export({format: 'der', type: 'pkcs8'}).toString('base64') |
| 826 | + |
| 827 | + console.log('privateKeyBase64', privateKeyBase64) |
| 828 | + const credentialIdBuffer = Buffer.from('mock-credential-id-' + Date.now()) |
| 829 | + const credentialIdBase64 = credentialIdBuffer.toString('base64') // For mock credential |
| 830 | + const credentialId = credentialIdBuffer.toString('base64url') // For verifying the request |
| 831 | + await client.send('WebAuthn.addCredential', { |
| 832 | + authenticatorId, |
| 833 | + credential: { |
| 834 | + credentialId: credentialIdBase64, |
| 835 | + isResidentCredential: true, |
| 836 | + rpId, |
| 837 | + privateKey: privateKeyBase64, |
| 838 | + userHandle: Buffer.from('test-user-handle').toString('base64'), |
| 839 | + signCount: 0, |
| 840 | + transports: ['internal'] |
| 841 | + } |
| 842 | + }) |
| 843 | + |
| 844 | + let interceptedRequest = null |
| 845 | + |
| 846 | + // Intercept the WebAuthn authenticate/finish endpoint to verify the request |
| 847 | + await page.route( |
| 848 | + '**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/webauthn/authenticate/finish', |
| 849 | + (route) => { |
| 850 | + interceptedRequest = route.request() |
| 851 | + route.continue() |
| 852 | + } |
| 853 | + ) |
| 854 | + |
| 855 | + await page.goto(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME + '/login') |
| 856 | + |
| 857 | + // Wait for the WebAuthn authenticate/finish request |
| 858 | + await page.waitForResponse( |
| 859 | + '**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/webauthn/authenticate/finish' |
| 860 | + ) |
| 861 | + |
| 862 | + // Verify the /webAuthn/authenticate/finish request |
| 863 | + expect(interceptedRequest).toBeTruthy() |
| 864 | + expect(interceptedRequest.method()).toBe('POST') |
| 865 | + const postData = interceptedRequest.postData() |
| 866 | + expect(postData).toBeTruthy() |
| 867 | + const requestBody = JSON.parse(postData) |
| 868 | + expect(requestBody).toBeTruthy() |
| 869 | + |
| 870 | + // Verify the request body structure matches expected format |
| 871 | + expect(requestBody.client_id).toBeTruthy() |
| 872 | + expect(requestBody.channel_id).toBe(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME_SITE) |
| 873 | + expect(requestBody.credential.id).toBe(credentialId) |
| 874 | + expect(requestBody.credential.clientExtensionResults).toBeTruthy() |
| 875 | + expect(requestBody.credential.response).toBeTruthy() |
| 876 | +} |
0 commit comments