Skip to content
Open
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
126 changes: 124 additions & 2 deletions src/__tests__/connect-mail-redirect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// Component-level: ConnectMail builds redirectUri via relativeUrl so magic-link return
// keeps personal-iban and other query params from redirectPath / current search.
// Also covers the house-brand wallet default when no `?wallet=` URL param is present.

const mockSignInWithMail = jest.fn();
const mockRedirectPath = jest.fn();
const mockNavigate = jest.fn();
const mockUseAppParams = jest.fn();
const mockOnCancel = jest.fn();
const mockLocationSearch = jest.fn(() => '?user=user@example.com');

jest.mock('@dfx.swiss/react', () => ({
Utils: { createRules: () => ({}) },
Expand All @@ -27,7 +30,7 @@ jest.mock('@dfx.swiss/react-components', () => ({
}));

jest.mock('react-router-dom', () => ({
useLocation: () => ({ search: '?user=user@example.com' }),
useLocation: () => ({ search: mockLocationSearch() }),
}));

jest.mock('../contexts/app-handling.context', () => ({
Expand All @@ -51,6 +54,10 @@ jest.mock('../hooks/navigation.hook', () => ({
useNavigation: () => ({ navigate: mockNavigate }),
}));

jest.mock('../components/home/connect-shared', () => ({
ConnectError: ({ error }: { error: string }) => <div data-testid="connect-error">{error}</div>,
}));

import { act, render, screen, waitFor } from '@testing-library/react';
import { createRef } from 'react';
import ConnectMail from '../components/home/wallet/connect-mail';
Expand All @@ -63,6 +70,8 @@ describe('ConnectMail login redirect', () => {
jest.clearAllMocks();
mockSignInWithMail.mockResolvedValue(undefined);
mockUseAppParams.mockReturnValue({ wallet: undefined, recommendationCode: undefined });
mockRedirectPath.mockReturnValue('/buy');
mockLocationSearch.mockReturnValue('?user=user@example.com');

locationStub = {
href: 'http://localhost/login',
Expand All @@ -86,7 +95,7 @@ describe('ConnectMail login redirect', () => {
blockchain={undefined}
isConnect={false}
onLogin={jest.fn()}
onCancel={jest.fn()}
onCancel={mockOnCancel}
onSwitch={jest.fn()}
/>,
);
Expand Down Expand Up @@ -142,4 +151,117 @@ describe('ConnectMail login redirect', () => {
expect(redirectUri).toBe('http://localhost/buy');
expect(redirectUri).not.toContain('?');
});

it('sends wallet=DFX Wallet when no wallet URL param is present', async () => {
mockUseAppParams.mockReturnValue({ wallet: undefined, recommendationCode: undefined });

renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() => expect(mockSignInWithMail).toHaveBeenCalled());

expect(mockSignInWithMail).toHaveBeenCalledWith(
'user@example.com',
'http://localhost/buy',
undefined,
'DFX Wallet',
);
});

it('forwards an explicit wallet URL param unchanged', async () => {
mockUseAppParams.mockReturnValue({ wallet: 'RealUnit', recommendationCode: 'ref-1' });

renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() => expect(mockSignInWithMail).toHaveBeenCalled());

expect(mockSignInWithMail).toHaveBeenCalledWith(
'user@example.com',
'http://localhost/buy',
'ref-1',
'RealUnit',
);
});

it('shows the sent confirmation and Back returns to home', async () => {
renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() =>
expect(
screen.getByText('We have sent an email with further instructions to the address provided.'),
).toBeInTheDocument(),
);

await act(async () => {
screen.getByRole('button', { name: 'Back' }).click();
});

expect(mockOnCancel).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith({ pathname: '/' }, { clearParams: ['user'] });
});

it('surfaces the API error message when sign-in fails', async () => {
mockSignInWithMail.mockRejectedValue({ message: 'Mail service down' });

renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() => expect(screen.getByTestId('connect-error')).toHaveTextContent('Mail service down'));
});

it('falls back to Unknown error when the rejection has no message', async () => {
mockSignInWithMail.mockRejectedValue({});

renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() => expect(screen.getByTestId('connect-error')).toHaveTextContent('Unknown error'));
});

it('omits redirectUri when redirectPath is unset', async () => {
mockRedirectPath.mockReturnValue(undefined);

renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() => expect(mockSignInWithMail).toHaveBeenCalled());

expect(mockSignInWithMail.mock.calls[0][1]).toBeFalsy();
});

it('submits without a prefilled mail when the user query param is absent', async () => {
mockLocationSearch.mockReturnValue('');

renderConnectMail();

await act(async () => {
screen.getByRole('button', { name: 'Next' }).click();
});

await waitFor(() => expect(mockSignInWithMail).toHaveBeenCalled());

// No `user` query → RHF default is undefined; the form still submits under the test mock.
expect(mockSignInWithMail.mock.calls[0][0]).toBeUndefined();
expect(mockSignInWithMail.mock.calls[0][3]).toBe('DFX Wallet');
});
});
7 changes: 6 additions & 1 deletion src/components/home/wallet/connect-mail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ interface FormData {
mail: string;
}

// Resolvable name of the backend default wallet (wallet table: "DFX Wallet", Config.defaultWalletId = 1).
// Sent when no explicit `wallet` URL param is present, so POST /v1/auth/mail resolves the login wallet by
// name rather than via getDefault(). Partner embeds that pass `?wallet=` keep their value (??).
const DFX_WALLET_NAME = 'DFX Wallet';

export default function ConnectMail({ onCancel }: ConnectProps): JSX.Element {
const { translate, translateError } = useSettingsContext();
const { signInWithMail } = useAuth();
Expand Down Expand Up @@ -62,7 +67,7 @@ export default function ConnectMail({ onCancel }: ConnectProps): JSX.Element {
async function submit({ mail }: FormData): Promise<void> {
setIsLoading(true);
setError(undefined);
signInWithMail(mail, redirectUri, recommendationCode, wallet)
signInWithMail(mail, redirectUri, recommendationCode, wallet ?? DFX_WALLET_NAME)
.then(() => setMailSent(true))
.catch((error: ApiError) => setError(error.message ?? 'Unknown error'))
.finally(() => setIsLoading(false));
Expand Down
Loading