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 @@ -32,6 +32,10 @@ test.describe('relay integration', () => {
).toBeVisible();

await signup.passwordTextbox.fill(password);
// keys_optional can't be sent in Firefox tests yet (FXA-13647), so this
// key-deriving desktop signup requires a password and renders the
// confirmation field.
await signup.verifyPasswordTextbox.fill(password);
await signup.createAccountButton.click();

await page.waitForURL(/confirm_signup_code/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ test.describe('smart window integration', () => {
await page.waitForURL(/signup/);

await signup.passwordTextbox.fill(password);
// keys_optional can't be sent in Firefox tests yet (FXA-13647), so this
// key-deriving desktop signup requires a password and renders the
// confirmation field.
await signup.verifyPasswordTextbox.fill(password);
await signup.createAccountButton.click();

await page.waitForURL(/confirm_signup_code/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ export const Default = storyWithProps();
export const WithCms = storyWithProps({
integration: createMockIntegrationWithCms(),
});

export const OtpWithCms = storyWithProps({
passwordCreationReason: 'otp',
integration: createMockIntegrationWithCms(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localiz
import { createMockIntegration, Subject } from './mocks';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MOCK_EMAIL } from '../../mocks';
import {
MOCK_EMAIL,
MOCK_CMS_INFO,
createMockIntegrationWithCms,
} from '../../mocks';
import { RelierCmsInfo } from '../../../models';
import GleanMetrics from '../../../lib/glean';

Expand Down Expand Up @@ -55,14 +59,14 @@ describe('SetPassword page', () => {
});

it('renders CMS overrides when PostVerifySetPasswordPage is set', () => {
const cmsInfo = {
shared: { buttonColor: '#333' },
const cmsInfo: RelierCmsInfo = {
...MOCK_CMS_INFO,
PostVerifySetPasswordPage: {
headline: 'Set a sync password',
description: 'Your password encrypts your synced data.',
primaryButtonText: 'Sync now',
},
} as unknown as RelierCmsInfo;
};

renderWithLocalizationProvider(
<Subject integration={createMockIntegration(cmsInfo)} />
Expand All @@ -80,6 +84,37 @@ describe('SetPassword page', () => {
).not.toBeInTheDocument();
});

it.each([['otp'], ['passkey']] as const)(
'renders the CMS description instead of the passwordless copy on the %s path',
(passwordCreationReason) => {
renderWithLocalizationProvider(
<Subject
passwordCreationReason={passwordCreationReason}
integration={createMockIntegrationWithCms()}
/>
);

expect(
screen.getByText(MOCK_CMS_INFO.PostVerifySetPasswordPage.description)
).toBeInTheDocument();
expect(
screen.queryByText(
'This password encrypts your synced data and keeps it secure.'
)
).not.toBeInTheDocument();
}
);

it('falls back to the passwordless info copy on the OTP path when no CMS description is set', () => {
renderWithLocalizationProvider(<Subject passwordCreationReason="otp" />);

expect(
screen.getByText(
'This password encrypts your synced data and keeps it secure.'
)
).toBeInTheDocument();
});

describe('Glean events', () => {
// First row asserts the default-arg behaviour (no prop passed); the
// others assert explicit pass-through for each non-default value.
Expand Down
22 changes: 11 additions & 11 deletions packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,17 @@ export const SetPassword = ({
<Banner type="error" content={{ localizedHeading: bannerErrorText }} />
)}

{passwordCreationReason === 'third_party_auth' ? (
cmsDescription ? (
<p className="text-sm mt-6 mb-5">{cmsDescription}</p>
) : (
<FtlMsg id="set-password-info-v2">
<p className="text-sm mt-6 mb-5">
This encrypts your data. It needs to be different from your Google
or Apple account password.
</p>
</FtlMsg>
)
{/* CMS copy is client-scoped, so it wins on every arrival path; the
hardcoded copy (which differs per path) is the no-CMS fallback. */}
{cmsDescription ? (
<p className="text-sm mt-6 mb-5">{cmsDescription}</p>
) : passwordCreationReason === 'third_party_auth' ? (
<FtlMsg id="set-password-info-v2">
<p className="text-sm mt-6 mb-5">
This encrypts your data. It needs to be different from your Google
or Apple account password.
</p>
</FtlMsg>
) : (
<FtlMsg id="set-password-passwordless-info">
<p className="text-sm mt-6 mb-5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,31 @@ describe('ConfirmSignupCode page', () => {
});
});

it('does not navigate to service_welcome when service=vpn on mobile (page automatically closes on fxaOAuthLogin)', async () => {
const integration = createMockOAuthNativeIntegration(
false,
OAuthNativeServices.Vpn
);
jest.spyOn(integration, 'isFirefoxMobileClient').mockReturnValue(true);
renderWithSession({
session,
integration,
finishOAuthFlowHandler: mockFinishOAuthFlowHandler,
});
submit();

await waitFor(() => {
expect(fxaOAuthLoginSpy).toHaveBeenCalledWith({
action: 'signup',
code: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.code,
redirect: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.redirect,
state: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.state,
scope: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.scope,
});
});
expect(mockNavigate).not.toHaveBeenCalled();
});

it('does not submit on paste when service=sync', async () => {
const user = userEvent.setup();
const integration = createMockOAuthNativeIntegration();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,15 @@ const ConfirmSignupCode = ({
scope,
});
if (integration.isFirefoxClientServiceVpn()) {
navigate('/post_verify/service_welcome', {
state: { origin: 'signup' },
});
// Don't navigate mobile users. The client controls the web view
// and users will see a "blip" of the service welcome page before
// the client closes the view in response to the fxaOAuthLogin web
// channel message. See FXA-11944.
if (!integration.isFirefoxMobileClient()) {
navigate('/post_verify/service_welcome', {
state: { origin: 'signup' },
});
}
} else {
goToSettingsWithAlertSuccess();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function mockIntegration() {
isSync: () => true,
requiresKeys: () => true,
wantsKeys: () => true,
requiresPasswordForLogin: () => true,
isFirefoxClientServiceRelay: () => false,
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
Expand Down
47 changes: 42 additions & 5 deletions packages/fxa-settings/src/pages/Signup/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ async function submit() {

const user = userEvent.setup();

// Note: we are removing the password confirmation requirement for signup
// except for Sync (in in FXA-10467). All tests by default will assume that
// password confirmation is not present or required except for Sync/Desktop Relay.
// Password confirmation is required only when the password derives encryption
// keys: Sync (keys mandatory), and non-Sync Firefox services (Relay/VPN/
// SmartWindow) when the browser lacks the keys-optional capability
// (`supportsKeysOptionalLogin` false). Plain web/OAuth-web signups omit it.

describe('Signup page', () => {
beforeEach(() => {
Expand Down Expand Up @@ -187,7 +188,7 @@ describe('Signup page', () => {
);
});

expect(screen.queryByLabelText('Repeat password')).not.toBeInTheDocument();
expect(screen.getByLabelText('Repeat password')).toBeVisible();

// newsletters and third party auth should not be displayed
expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
Expand Down Expand Up @@ -246,6 +247,42 @@ describe('Signup page', () => {
).toBeInTheDocument();
});

it('requires password confirmation for a service when keys-optional login is unsupported', async () => {
await act(() => {
renderWithLocalizationProvider(
<Subject
integration={createMockSignupOAuthNativeIntegration(
OAuthNativeServices.Vpn,
false
)}
supportsKeysOptionalLogin={false}
/>
);
});

expect(screen.getByLabelText('Repeat password')).toBeVisible();
});

it('does not require password confirmation for a service when keys-optional login is supported', async () => {
await act(() => {
renderWithLocalizationProvider(
<Subject
integration={createMockSignupOAuthNativeIntegration(
OAuthNativeServices.Relay,
false,
undefined,
// keys-optional login supported → the service doesn't require a
// password, so the confirmation field is dropped.
/* requiresPasswordForLogin */ false
)}
supportsKeysOptionalLogin={true}
/>
);
});

expect(screen.queryByLabelText('Repeat password')).not.toBeInTheDocument();
});

it('renders as expected when cms enabled', async () => {
renderWithLocalizationProvider(
<Subject
Expand Down Expand Up @@ -625,7 +662,7 @@ describe('Signup page', () => {
beginSignupHandler={mockBeginSignupHandler}
/>
);
await fillOutForm(false);
await fillOutForm(true);
await submit();

await waitFor(() => {
Expand Down
8 changes: 7 additions & 1 deletion packages/fxa-settings/src/pages/Signup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,13 @@ export const Signup = ({
handleSubmit,
onSubmit,
offeredSyncEngineConfigs,
requirePasswordConfirmation: isSync,
// Require password confirmation whenever the password is used for key
// derivation — Sync (keys mandatory) and non-Sync Firefox services
// that derive keys when the browser can't complete a keyless login
// (`supportsKeysOptionalLogin` false).
requirePasswordConfirmation:
integration.isSync() ||
integration.requiresPasswordForLogin(supportsKeysOptionalLogin),
setSelectedNewsletterSlugs,
cmsButton: {
text: cmsInfo?.SignupSetPasswordPage.primaryButtonText,
Expand Down
2 changes: 2 additions & 0 deletions packages/fxa-settings/src/pages/Signup/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type SignupOAuthIntegration = Pick<
| 'getWebChannelServices'
| 'requiresKeys'
| 'wantsKeys'
| 'requiresPasswordForLogin'
| 'getClientId'
| 'getCmsInfo'
| 'getLegalTerms'
Expand All @@ -71,6 +72,7 @@ export type SignupBaseIntegration = Pick<
| 'getWebChannelServices'
| 'requiresKeys'
| 'wantsKeys'
| 'requiresPasswordForLogin'
| 'getClientId'
| 'getCmsInfo'
| 'getLegalTerms'
Expand Down
7 changes: 6 additions & 1 deletion packages/fxa-settings/src/pages/Signup/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function createMockSignupWebIntegration(): SignupBaseIntegration {
getWebChannelServices: mockGetWebChannelServices(),
requiresKeys: () => false,
wantsKeys: () => false,
requiresPasswordForLogin: () => false,
getCmsInfo: () => undefined,
getLegalTerms: () => undefined,
};
Expand All @@ -57,6 +58,7 @@ export function createMockSignupSyncDesktopV3Integration(): SignupBaseIntegratio
getWebChannelServices: mockGetWebChannelServices({ isSync: true }),
requiresKeys: () => true,
wantsKeys: () => true,
requiresPasswordForLogin: () => true,
getCmsInfo: () => undefined,
getLegalTerms: () => undefined,
};
Expand All @@ -81,6 +83,7 @@ export function createMockSignupOAuthWebIntegration(
getWebChannelServices: mockGetWebChannelServices(),
requiresKeys: () => false,
wantsKeys: () => false,
requiresPasswordForLogin: () => false,
getCmsInfo: () => cmsInfo,
getLegalTerms: () => undefined,
};
Expand All @@ -89,7 +92,8 @@ export function createMockSignupOAuthWebIntegration(
export function createMockSignupOAuthNativeIntegration(
service?: string,
isSync = true,
cmsInfo?: RelierCmsInfo
cmsInfo?: RelierCmsInfo,
requiresPasswordForLogin = true
): SignupOAuthIntegration {
const isRelay = service === OAuthNativeServices.Relay;
const isSmartWindow = service === OAuthNativeServices.SmartWindow;
Expand All @@ -113,6 +117,7 @@ export function createMockSignupOAuthNativeIntegration(
}),
requiresKeys: () => true,
wantsKeys: () => true,
requiresPasswordForLogin: () => requiresPasswordForLogin,
getCmsInfo: () => cmsInfo,
Comment on lines 118 to 121

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had something like this originally, and then I opted to simplify it. I'd rather leave it as-is where you just pass in requiresPasswordForLogin depending on what you're testing.

getLegalTerms: () => undefined,
};
Expand Down
7 changes: 7 additions & 0 deletions packages/fxa-settings/src/pages/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ export const MOCK_CMS_INFO = {
},
},

PostVerifySetPasswordPage: {
headline: 'Create a password to sync',
description:
'A Mozilla account works with sync to help protect your browsing, passwords, and more.',
primaryButtonText: 'Continue',
pageTitle: 'Create a password to sync',
},
SigninPage: {
headline: 'Enter your password',
description: 'for your Mozilla account',
Expand Down
Loading