Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('PasskeyRepository (Integration)', () => {
});

describe('update operations', () => {
it('should update counter and lastUsed after authentication', async () => {
it('should update counter and lastUsedAt after authentication', async () => {
const uid = await createTestAccount();
const passkey = PasskeyFactory({ uid });
await PasskeyRepository.insertPasskey(db, passkey);
Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-auth-server/lib/routes/account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4819,7 +4819,7 @@ describe('/account', () => {
const result: any = await runTest(route, request);

expect(mockService.listPasskeysForUser).toHaveBeenCalledWith(
Buffer.from(uid)
Buffer.from(uid, 'hex')
);
expect(result.passkeys).toHaveLength(1);
expect(result.passkeys[0]).toEqual({
Expand Down
4 changes: 3 additions & 1 deletion packages/fxa-auth-server/lib/routes/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2457,7 +2457,9 @@ export class AccountHandler {
this.db.devices(uid),
listAuthorizedClients(uid),
this.config.passkeys?.enabled
? Container.get(PasskeyService).listPasskeysForUser(Buffer.from(uid))
? Container.get(PasskeyService).listPasskeysForUser(
Buffer.from(uid, 'hex')
)
: Promise.resolve([]),
]);

Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-auth-server/lib/routes/passkeys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('passkeys routes', () => {
mockPasskeyService: any,
mockFxaMailer: any;

const UID = 'uid-123';
const UID = 'f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6';
const SESSION_TOKEN_ID = 'session-token-456';
const TEST_EMAIL = 'test@example.com';
const CREDENTIAL_ID_B64 =
Expand Down
5 changes: 4 additions & 1 deletion packages/fxa-content-server/server/config/local.json-dist
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@
"featureFlags": {
"recoveryCodeSetupOnSyncSignIn": true,
"showLocaleToggle": true,
"paymentsNextSubscriptionManagement": true
"paymentsNextSubscriptionManagement": true,
"passkeysEnabled": true,
"passkeyRegistrationEnabled": true,
"passkeyAuthenticationEnabled": false
},
"darkMode": {
"enabled": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const mockCredential = {
function renderPage() {
const account = {
getCachedJwtByScope: jest.fn(() => 'mock-jwt'),
refresh: jest.fn(),
} as unknown as Account;

const authClient = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const PagePasskeyAdd = () => {
credential,
challenge
);
await account.refresh('passkeys');

if (!isMounted.current) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { withLocalization, withLocation } from 'fxa-react/lib/storybooks';
import { CodeIcon } from '../../Icons';
import { MOCK_NATIONAL_FORMAT_PHONE_NUMBER } from '../../../pages/mocks';
import { AppContext } from '../../../models';
import { mockAppContext } from '../../../models/mocks';
import { MOCK_ACCOUNT, mockAppContext } from '../../../models/mocks';
import { initLocalAccount, mockAuthClient } from './mock';

export default {
Expand All @@ -25,9 +25,16 @@ export default {
withLocation(),
(Story) => {
initLocalAccount();
const mockAccount = {
...MOCK_ACCOUNT,
deletePasskey: async () => {},
};
return (
<AppContext.Provider
value={mockAppContext({ authClient: mockAuthClient } as any)}
value={mockAppContext({
authClient: mockAuthClient,
account: mockAccount,
} as any)}
>
{/* @container/unitRow on the container div allows sub row to adjust based on the size of the parent container
instead of the viewport. This fixes issues with the subrow and CTAs overflowing their parent container in mobileLandscape
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { Passkey } from 'fxa-auth-client/browser';
import { AppContext } from '../../../models';
import { mockAppContext } from '../../../models/mocks';
import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors';
import { mockAuthClient } from './mock';
import { LocationProvider } from '@reach/router';

Expand All @@ -28,6 +29,10 @@ const mockAlertBar = {
info: jest.fn(),
};

let mockAccount = {
deletePasskey: jest.fn(),
};

jest.mock('../../../lib/cache', () => ({
...jest.requireActual('../../../lib/cache'),
JwtTokenCache: {
Expand All @@ -42,6 +47,7 @@ jest.mock('../../../models', () => ({
...jest.requireActual('../../../models'),
useAuthClient: () => mockAuthClient,
useAlertBar: () => mockAlertBar,
useAccount: () => mockAccount,
}));

describe('SubRow', () => {
Expand Down Expand Up @@ -280,22 +286,17 @@ describe('PasskeySubRow', () => {
prfEnabled: true,
};

const mockDeletePasskey = jest.fn();

beforeEach(() => {
mockDeletePasskey.mockClear();
mockAccount.deletePasskey.mockClear();
mockAlertBar.success.mockClear();
mockAlertBar.error.mockClear();
});

const renderPasskeySubRow = (
passkey: Passkey = mockPasskey,
deletePasskey = mockDeletePasskey
) => {
const renderPasskeySubRow = (passkey: Passkey = mockPasskey) => {
return render(
<LocationProvider>
<AppContext.Provider value={mockAppContext()}>
<PasskeySubRow passkey={passkey} deletePasskey={deletePasskey} />
<PasskeySubRow passkey={passkey} />
</AppContext.Provider>
</LocationProvider>
);
Expand Down Expand Up @@ -350,7 +351,7 @@ describe('PasskeySubRow', () => {
});

it('calls deletePasskey when confirm button is clicked', async () => {
mockDeletePasskey.mockResolvedValue(undefined);
mockAccount.deletePasskey.mockResolvedValue(undefined);
renderPasskeySubRow();

const deleteButtons = screen.getAllByTitle(/Delete passkey/);
Expand All @@ -361,13 +362,11 @@ describe('PasskeySubRow', () => {
const confirmButton = screen.getByTestId('confirm-delete-passkey-button');
await userEvent.click(confirmButton);

await waitFor(() => {
expect(mockDeletePasskey).toHaveBeenCalledWith('passkey-1');
});
expect(mockAccount.deletePasskey).toHaveBeenCalledWith('passkey-1');
});

it('shows success banner when deletion succeeds', async () => {
mockDeletePasskey.mockResolvedValue(undefined);
mockAccount.deletePasskey.mockResolvedValue(undefined);
renderPasskeySubRow();

const deleteButtons = screen.getAllByTitle(/Delete passkey/);
Expand All @@ -389,8 +388,8 @@ describe('PasskeySubRow', () => {
});
});

it('shows error banner when deletion fails', async () => {
mockDeletePasskey.mockRejectedValue(new Error('Some error'));
it('shows generic error banner when deletion fails with an unexpected error', async () => {
mockAccount.deletePasskey.mockRejectedValue(new Error('Some error'));
renderPasskeySubRow();

const deleteButtons = screen.getAllByTitle(/Delete passkey/);
Expand All @@ -413,4 +412,27 @@ describe('PasskeySubRow', () => {
).not.toBeInTheDocument();
});
});

it('shows "Passkey not found" error when passkey no longer exists', async () => {
mockAccount.deletePasskey.mockRejectedValue(AuthUiErrors.PASSKEY_NOT_FOUND);
renderPasskeySubRow();

const deleteButtons = screen.getAllByTitle(/Delete passkey/);
await userEvent.click(deleteButtons[0]);

expect(await screen.findByText('Delete your passkey?')).toBeInTheDocument();

const confirmButton = screen.getByTestId('confirm-delete-passkey-button');
await userEvent.click(confirmButton);

await waitFor(() => {
expect(mockAlertBar.error).toHaveBeenCalledWith('Passkey not found');
});

await waitFor(() => {
expect(
screen.queryByText('Delete your passkey?')
).not.toBeInTheDocument();
});
});
});
Loading
Loading