Skip to content

Commit

Permalink
feat(auth): adding updating unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jjarvisp committed Oct 2, 2024
1 parent 7b860ef commit c3cd404
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 0 deletions.
94 changes: 94 additions & 0 deletions packages/auth/__tests__/mockData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
PasskeyCreateOptions,
PasskeyCreateOptionsJson,
PasskeyCreateResult,
PasskeyCreateResultJson,
} from '../src/utils/passkey/types';

// device tracking mock device data
export const mockDeviceArray = [
{
Expand Down Expand Up @@ -180,3 +187,90 @@ export const mockAuthConfigWithOAuth = {
},
},
};

export const passkeyCredentialCreateOptions =
'{"rp":{"id":"localhost","name":"localhost"},"user":{"id":"M2M0NjMyMGItYzYwZS00YTIxLTlkNjQtNTgyOWJmZWRlMWM0","name":"james","displayName":""},"challenge":"zsBch6DlNLUb6SgRdzHysw","pubKeyCredParams":[{"type":"public-key","alg":-7},{"type":"public-key","alg":-257}],"timeout":60000,"excludeCredentials":[{"type":"public-key","id":"VWxodmRFMUtjbEJZVWs1NE9IaHhOblZUTTBsUVJWSXRTbWhhUkdwZldHaDBSbVpmUmxKamFWRm5XUQ"},{"type":"public-key","id":"WDJnM1RrMWxaSGc0Y1ZWQmVsOTVTRXRvWjBoME56UlFNbFZ5VkZWZmNXTkNORjlVYjFWTWVqRXlUUQ"}],"authenticatorSelection":{"requireResidentKey":true,"residentKey":"required","userVerification":"required"}}';

export const passkeyRegistrationResultJson: PasskeyCreateResultJson = {
type: 'public-key',
id: 'vJCit9S2cglAvvW3txQ-OWRBb-NyhxaLOvRRisnr1aE',
rawId: 'vJCit9S2cglAvvW3txQ-OQ',
response: {
clientDataJSON: 'vJCit9S2cglAvvW3txQ-OQ',
attestationObject: 'vJCit9S2cglAvvW3txQ-OQ',
},
};
export const passkeyRegistrationResult: PasskeyCreateResult = {
type: 'public-key',
id: 'vJCit9S2cglAvvW3txQ-OWRBb-NyhxaLOvRRisnr1aE',
rawId: new Uint8Array([
188, 144, 162, 183, 212, 182, 114, 9, 64, 190, 245, 183, 183, 20, 62, 57,
]),
response: {
clientDataJSON: new Uint8Array([
188, 144, 162, 183, 212, 182, 114, 9, 64, 190, 245, 183, 183, 20, 62, 57,
]),
attestationObject: new Uint8Array([
188, 144, 162, 183, 212, 182, 114, 9, 64, 190, 245, 183, 183, 20, 62, 57,
]),
},
};

export const passkeyRegistrationRequest: PasskeyCreateOptions = {
rp: { id: 'localhost', name: 'localhost' },
user: {
id: new Uint8Array([
188, 144, 162, 183, 212, 182, 114, 9, 64, 190, 245, 183, 183, 20, 62, 57,
]),
name: 'james',
displayName: '',
},
challenge: new Uint8Array([
188, 144, 162, 183, 212, 182, 114, 9, 64, 190, 245, 183, 183, 20, 62, 57,
]),
pubKeyCredParams: [
{ type: 'public-key' as any, alg: -7 },
{ type: 'public-key' as any, alg: -257 },
],
timeout: 60000,
excludeCredentials: [
{
type: 'public-key' as any,
id: new Uint8Array([
188, 144, 162, 183, 212, 182, 114, 9, 64, 190, 245, 183, 183, 20, 62,
57,
]),
},
],
authenticatorSelection: {
requireResidentKey: true,
residentKey: 'required' as any,
userVerification: 'required' as any,
},
};

export const passkeyRegistrationRequestJson: PasskeyCreateOptionsJson = {
rp: { id: 'localhost', name: 'localhost' },
user: {
id: 'vJCit9S2cglAvvW3txQ-OQ',
name: 'james',
displayName: '',
},
challenge: 'vJCit9S2cglAvvW3txQ-OQ',
pubKeyCredParams: [
{ type: 'public-key', alg: -7 },
{ type: 'public-key', alg: -257 },
],
timeout: 60000,
excludeCredentials: [
{
type: 'public-key',
id: 'vJCit9S2cglAvvW3txQ-OQ',
},
],
authenticatorSelection: {
requireResidentKey: true,
residentKey: 'required',
userVerification: 'required',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Amplify, fetchAuthSession } from '@aws-amplify/core';
import { decodeJWT } from '@aws-amplify/core/internals/utils';

import {
createGetWebAuthnRegistrationOptionsClient,
createVerifyWebAuthnRegistrationResultClient,
} from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
import {
PasskeyError,
PasskeyErrorCode,
} from '../../../src/utils/passkey/errors';
import { associateWebAuthnCredential } from '../../../src/providers/cognito/apis/associateWebAuthnCredential';
import {
passkeyCredentialCreateOptions,
passkeyRegistrationResult,
} from '../../mockData';
import { serializePkcToJson } from '../../../src/utils/passkey/serde';
import * as utils from '../../../src/utils';

import { setUpGetConfig } from './testUtils/setUpGetConfig';
import { mockAccessToken } from './testUtils/data';

jest.mock('@aws-amplify/core', () => ({
...(jest.createMockFromModule('@aws-amplify/core') as object),
Amplify: { getConfig: jest.fn(() => ({})) },
}));
jest.mock('@aws-amplify/core/internals/utils', () => ({
...jest.requireActual('@aws-amplify/core/internals/utils'),
isBrowser: jest.fn(() => false),
}));
jest.mock(
'../../../src/foundation/factories/serviceClients/cognitoIdentityProvider',
);
jest.mock('../../../src/providers/cognito/factories');

Object.assign(navigator, {
credentials: {
create: jest.fn(),
},
});

describe('associateWebAuthnCredential', () => {
const navigatorCredentialsCreateSpy = jest.spyOn(
navigator.credentials,
'create',
);
const registerPasskeySpy = jest.spyOn(utils, 'registerPasskey');

const mockFetchAuthSession = jest.mocked(fetchAuthSession);

const mockGetWebAuthnRegistrationOptions = jest.fn();
const mockCreateGetWebAuthnRegistrationOptionsClient = jest.mocked(
createGetWebAuthnRegistrationOptionsClient,
);

const mockVerifyWebAuthnRegistrationResult = jest.fn();
const mockCreateVerifyWebAuthnRegistrationResultClient = jest.mocked(
createVerifyWebAuthnRegistrationResultClient,
);

beforeAll(() => {
setUpGetConfig(Amplify);
mockFetchAuthSession.mockResolvedValue({
tokens: { accessToken: decodeJWT(mockAccessToken) },
});
mockCreateGetWebAuthnRegistrationOptionsClient.mockReturnValue(
mockGetWebAuthnRegistrationOptions,
);
mockCreateVerifyWebAuthnRegistrationResultClient.mockReturnValue(
mockVerifyWebAuthnRegistrationResult,
);
mockVerifyWebAuthnRegistrationResult.mockImplementation(() => ({
CredentialId: '12345',
}));

navigatorCredentialsCreateSpy.mockResolvedValue(passkeyRegistrationResult);
});

afterEach(() => {
mockFetchAuthSession.mockClear();
mockGetWebAuthnRegistrationOptions.mockReset();
navigatorCredentialsCreateSpy.mockClear();
});

it('should pass the correct service options when retrieving credential creation options', async () => {
mockGetWebAuthnRegistrationOptions.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

await associateWebAuthnCredential();

expect(mockGetWebAuthnRegistrationOptions).toHaveBeenCalledWith(
{
region: 'us-west-2',
userAgentValue: expect.any(String),
},
{
AccessToken: mockAccessToken,
},
);
});

it('should pass the correct service options when verifying a credential', async () => {
mockGetWebAuthnRegistrationOptions.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

await associateWebAuthnCredential();

expect(mockVerifyWebAuthnRegistrationResult).toHaveBeenCalledWith(
{
region: 'us-west-2',
userAgentValue: expect.any(String),
},
{
AccessToken: mockAccessToken,
Credential: JSON.stringify(
serializePkcToJson(passkeyRegistrationResult),
),
},
);
});

it('should call the registerPasskey function with correct input', async () => {
mockGetWebAuthnRegistrationOptions.mockImplementation(() => ({
CredentialCreationOptions: passkeyCredentialCreateOptions,
}));

await associateWebAuthnCredential();

expect(registerPasskeySpy).toHaveBeenCalledWith(
JSON.parse(passkeyCredentialCreateOptions),
);

expect(navigatorCredentialsCreateSpy).toHaveBeenCalled();
});

it('should throw an error when service returns empty credential creation options', async () => {
expect.assertions(2);

mockGetWebAuthnRegistrationOptions.mockImplementation(() => ({
CredentialCreationOptions: undefined,
}));

try {
await associateWebAuthnCredential();
} catch (error: any) {
expect(error).toBeInstanceOf(PasskeyError);
expect(error.name).toBe(
PasskeyErrorCode.InvalidCredentialCreationOptions,
);
}
});
});
77 changes: 77 additions & 0 deletions packages/auth/__tests__/utils/passkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
convertArrayBufferToBase64Url,
convertBase64UrlToArrayBuffer,
} from '../../src/utils/passkey/base64Url';
import {
deserializeJsonToPkcCreationOptions,
serializePkcToJson,
} from '../../src/utils/passkey/serde';
import {
passkeyRegistrationRequest,
passkeyRegistrationRequestJson,
passkeyRegistrationResult,
passkeyRegistrationResultJson,
} from '../mockData';

describe('passkey', () => {
it('converts ArrayBuffer values to base64url', () => {
expect(convertArrayBufferToBase64Url(new Uint8Array([]))).toBe('');
expect(convertArrayBufferToBase64Url(new Uint8Array([0]))).toBe('AA');
expect(convertArrayBufferToBase64Url(new Uint8Array([1, 2, 3]))).toBe(
'AQID',
);
});
it('converts base64url values to ArrayBuffer', () => {
expect(
convertArrayBufferToBase64Url(convertBase64UrlToArrayBuffer('')),
).toBe(convertArrayBufferToBase64Url(new Uint8Array([])));
expect(
convertArrayBufferToBase64Url(convertBase64UrlToArrayBuffer('AA')),
).toBe(convertArrayBufferToBase64Url(new Uint8Array([0])));
expect(
convertArrayBufferToBase64Url(convertBase64UrlToArrayBuffer('AQID')),
).toBe(convertArrayBufferToBase64Url(new Uint8Array([1, 2, 3])));
});

it('converts base64url to ArrayBuffer and back without data loss', () => {
const input = '_h7NMedx8qUAz_yHKhgHt74P2UrTU_qcB4_ToULz12M';
expect(
convertArrayBufferToBase64Url(convertBase64UrlToArrayBuffer(input)),
).toBe(input);
});

it('serializes pkc into correct json format', () => {
expect(JSON.stringify(serializePkcToJson(passkeyRegistrationResult))).toBe(
JSON.stringify(passkeyRegistrationResultJson),
);
});

it('deserializes json into correct pkc format', () => {
const deserialized = deserializeJsonToPkcCreationOptions(
passkeyRegistrationRequestJson,
);

expect(deserialized.challenge.byteLength).toEqual(
passkeyRegistrationRequest.challenge.byteLength,
);
expect(deserialized.user.id.byteLength).toEqual(
passkeyRegistrationRequest.user.id.byteLength,
);

expect(deserialized).toEqual(
expect.objectContaining({
rp: expect.any(Object),
user: {
id: expect.any(ArrayBuffer),
name: expect.any(String),
displayName: expect.any(String),
},
challenge: expect.any(ArrayBuffer),
pubKeyCredParams: expect.any(Array),
timeout: expect.any(Number),
excludeCredentials: expect.any(Array),
authenticatorSelection: expect.any(Object),
}),
);
});
});
3 changes: 3 additions & 0 deletions packages/auth/src/utils/passkey/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
AmplifyError,
AmplifyErrorMap,
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-amplify/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ describe('aws-amplify Exports', () => {
'autoSignIn',
'fetchAuthSession',
'decodeJWT',
'associateWebAuthnCredential',
].sort(),
);
});
Expand Down Expand Up @@ -220,6 +221,7 @@ describe('aws-amplify Exports', () => {
'DefaultTokenStore',
'refreshAuthTokens',
'refreshAuthTokensWithoutDedupe',
'associateWebAuthnCredential',
].sort(),
);
});
Expand Down

0 comments on commit c3cd404

Please sign in to comment.