Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the authorization Step for the Application Passwords #96891

Merged
merged 16 commits into from
Dec 4, 2024
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add the tests
  • Loading branch information
valterlorran committed Dec 3, 2024
commit fb0316c343c974de55a3d67023ace4b450b5fa36
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextButton } from '@automattic/onboarding';
import { check, Icon } from '@wordpress/icons';
import { useTranslate } from 'i18n-calypso';

const AuthorizationBenefits = ( { benefits }: { benefits: string[] } ) => {
return (
<div className="site-migration-application-password-authorization__benefits">
{ benefits.map( ( benefit, index ) => (
<div
className="site-migration-application-password-authorization__benefits-item"
key={ index }
>
<div className="site-migration-application-password-authorization__benefits-item-icon">
<Icon icon={ check } size={ 20 } />
</div>
<span>{ benefit }</span>
</div>
) ) }
</div>
);
};

interface AuthorizationProps {
onShareCredentialsClick: () => void;
onAuthorizationClick: () => void;
}

const Authorization = ( { onShareCredentialsClick, onAuthorizationClick }: AuthorizationProps ) => {
const translate = useTranslate();
return (
<div className="site-migration-application-password-authorization__authorization">
<div>
<NextButton onClick={ onAuthorizationClick }>{ translate( 'Authorize' ) }</NextButton>
</div>
<div>
<button
className="button navigation-link step-container__navigation-link has-underline is-borderless"
type="button"
onClick={ onShareCredentialsClick }
>
{ translate( 'Share credentials instead' ) }
</button>
</div>
<div className="site-migration-application-password-authorization__benefits-container">
<h3>{ translate( "Here's what else you're getting" ) }</h3>
<AuthorizationBenefits
benefits={ [
translate( 'Uninterrupted service throughout the entire migration experience.' ),
Copy link
Contributor

Choose a reason for hiding this comment

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

where this strings added to the string freeze PR we where using for having translations ready earliear?

translate( 'Unmatched reliability with 99.999% uptime and unmetered traffic.' ),
translate( 'Round-the-clock security monitoring and DDoS protection.' ),
] }
/>
</div>
</div>
);
};

export default Authorization;
Original file line number Diff line number Diff line change
@@ -1,67 +1,18 @@
import { StepContainer, NextButton } from '@automattic/onboarding';
import { check, Icon } from '@wordpress/icons';
import { StepContainer } from '@automattic/onboarding';
import { useTranslate } from 'i18n-calypso';
import { useEffect } from 'react';
import DocumentHead from 'calypso/components/data/document-head';
import FormattedHeader from 'calypso/components/formatted-header';
import { LoadingEllipsis } from 'calypso/components/loading-ellipsis';
import Notice from 'calypso/components/notice';
import { useQuery } from 'calypso/landing/stepper/hooks/use-query';
import { useSiteSlugParam } from 'calypso/landing/stepper/hooks/use-site-slug-param';
import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
import Authorization from './components/authorization';
import useStoreApplicationPassword from './hooks/use-store-application-password';
import type { Step } from '../../types';
import './style.scss';

const AuthorizationBenefits = ( { benefits }: { benefits: string[] } ) => {
return (
<div className="site-migration-application-password-authorization__benefits">
{ benefits.map( ( benefit, index ) => (
<div className="site-migration-application-password-authorization__benefits-item" key={ index }>
<div className="site-migration-application-password-authorization__benefits-item-icon">
<Icon icon={ check } size={ 20 } />
</div>
<span>{ benefit }</span>
</div>
) ) }
</div>
);
};

interface AuthorizationProps {
onShareCredentialsClick: () => void;
onAuthorizationClick: () => void;
}

const Authorization = ( { onShareCredentialsClick, onAuthorizationClick }: AuthorizationProps ) => {
const translate = useTranslate();
return (
<div className="site-migration-application-password-authorization__authorization">
<div>
<NextButton onClick={ onAuthorizationClick }>{ translate( 'Authorize' ) }</NextButton>
</div>
<div>
<button
className="button navigation-link step-container__navigation-link has-underline is-borderless"
type="button"
onClick={ onShareCredentialsClick }
>
{ translate( 'Share credentials instead' ) }
</button>
</div>
<div className="site-migration-application-password-authorization__benefits-container">
<h3>{ translate( "Here's what else you're getting" ) }</h3>
<AuthorizationBenefits
benefits={ [
translate( 'Uninterrupted service throughout the entire migration experience.' ),
translate( 'Unmatched reliability with 99.999% uptime and unmetered traffic.' ),
translate( 'Round-the-clock security monitoring and DDoS protection.' ),
] }
/>
</div>
</div>
);
};

const SiteMigrationApplicationPasswordsAuthorization: Step = function ( { navigation } ) {
const translate = useTranslate();
const siteSlug = useSiteSlugParam();
@@ -78,8 +29,11 @@ const SiteMigrationApplicationPasswordsAuthorization: Step = function ( { naviga
isError: isStoreApplicationPasswordError,
isPending: isStoreApplicationPasswordPending,
} = useStoreApplicationPassword( siteSlug as string );
const hasStoreApplicationPasswordResponse = isStoreApplicationPasswordSuccess || isStoreApplicationPasswordError;
const isLoading = isAuthorizationSuccessful && ( ! hasStoreApplicationPasswordResponse || isStoreApplicationPasswordPending );
const hasStoreApplicationPasswordResponse =
isStoreApplicationPasswordSuccess || isStoreApplicationPasswordError;
const isLoading =
isAuthorizationSuccessful &&
( ! hasStoreApplicationPasswordResponse || isStoreApplicationPasswordPending );

useEffect( () => {
if ( ! isAuthorizationSuccessful || ! siteSlug ) {
@@ -107,10 +61,35 @@ const SiteMigrationApplicationPasswordsAuthorization: Step = function ( { naviga
navigation?.submit?.( { action: 'authorization', authorizationUrl } );
};

if ( isLoading ) {
return <div>Loading...</div>;
let notice = undefined;
if ( isStoreApplicationPasswordError ) {
notice = (
<Notice status="is-error" showDismiss={ false }>
{ translate( "We couldn't complete the authorization." ) }
</Notice>
);
} else if ( isAuthorizationRejected ) {
notice = (
<Notice status="is-warning" showDismiss={ false }>
{ translate(
"We can't start your migration without your authorization. Please authorize WordPress.com in your WP Admin or share your credentials."
) }
</Notice>
);
}

const formattedHeader = ! isLoading ? (
<FormattedHeader
id="site-migration-credentials-header"
headerText={ translate( 'Get ready for blazing fast speeds' ) }
subHeaderAlign="center"
subHeaderText={ translate(
"We're ready to migrate longdomainname.com to WordPress.com. To make sure everything goes smoothly, we need you to authorize us for access in your WordPress admin."
) }
align="center"
/>
) : undefined;

return (
<>
<DocumentHead title={ translate( 'Get ready for blazing fast speeds' ) } />
@@ -121,30 +100,19 @@ const SiteMigrationApplicationPasswordsAuthorization: Step = function ( { naviga
goNext={ navigation?.submit }
hideSkip
isFullLayout
showNotice={ isAuthorizationRejected }
notice={
<Notice status="is-warning" showDismiss={ false }>
{ translate(
"We can't start your migration without your authorization. Please authorize WordPress.com in your WP Admin or share your credentials."
) }
</Notice>
}
formattedHeader={
<FormattedHeader
id="site-migration-credentials-header"
headerText={ translate( 'Get ready for blazing fast speeds' ) }
subHeaderAlign="center"
subHeaderText={ translate(
"We're ready to migrate longdomainname.com to WordPress.com. To make sure everything goes smoothly, we need you to authorize us for access in your WordPress admin."
) }
align="center"
/>
}
notice={ notice }
formattedHeader={ formattedHeader }
stepContent={
<Authorization
onAuthorizationClick={ startAuthorization }
onShareCredentialsClick={ navigateToFallbackCredentials }
/>
! isLoading ? (
<Authorization
onAuthorizationClick={ startAuthorization }
onShareCredentialsClick={ navigateToFallbackCredentials }
/>
) : (
<div data-testid="loading-ellipsis">
<LoadingEllipsis />
</div>
)
}
recordTracksEvent={ recordTracksEvent }
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* @jest-environment jsdom
*/
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import nock from 'nock';
import React from 'react';
import wpcomRequest from 'wpcom-proxy-request';
import { useSiteSlugParam } from 'calypso/landing/stepper/hooks/use-site-slug-param';
import SiteMigrationApplicationPasswordAuthorization from '..';
import { StepProps } from '../../../types';
import { RenderStepOptions, mockStepProps, renderStep } from '../../test/helpers';

const render = ( props?: Partial< StepProps >, renderOptions?: RenderStepOptions ) => {
const combinedProps = { ...mockStepProps( props ) };
return renderStep(
<SiteMigrationApplicationPasswordAuthorization { ...combinedProps } />,
renderOptions
);
};

jest.mock( 'wpcom-proxy-request', () => jest.fn() );
jest.mock( 'calypso/landing/stepper/hooks/use-site-slug-param' );

( useSiteSlugParam as jest.Mock ).mockImplementation( () => 'site-url.wordpress.com' );

const { getByRole, getByTestId, findByText } = screen;

describe( 'SiteMigrationApplicationPasswordAuthorization', () => {
beforeAll( () => nock.disableNetConnect() );
it( 'renders the loading state when the authorization is successful and the application password is not yet stored', async () => {
( wpcomRequest as jest.Mock ).mockImplementation( () => new Promise( () => {} ) );

const initialEntry =
'/step?authorization_url=https://example.com&user_login=test&password=test';
render( {}, { initialEntry } );

await waitFor( () => {
expect( getByTestId( 'loading-ellipsis' ) ).toBeVisible();
} );
} );

it( 'redirects to the next step when the application password is stored', async () => {
const submit = jest.fn();
const initialEntry =
'/step?authorization_url=https://example.com&user_login=test&password=test';

( wpcomRequest as jest.Mock ).mockResolvedValue( {
status: 200,
body: {
data: { status: 200 },
},
} );

render( { navigation: { submit } }, { initialEntry } );
await waitFor( () => {
expect( submit ).toHaveBeenCalledWith( { action: 'migration-started' } );
} );
} );

it( 'renders the error state when the application password fails to be stored', async () => {
( wpcomRequest as jest.Mock ).mockRejectedValue( {
status: 500,
body: {
code: 'no_ticket_found',
message: 'No migration ticket found.',
data: { status: 500 },
},
} );

const initialEntry =
'/step?authorization_url=https://example.com&user_login=test&password=test';
render( {}, { initialEntry } );

const errorMessage = await findByText( /We couldn't complete the authorization./ );

await waitFor( () => {
expect( errorMessage ).toBeVisible();
} );
} );

it( 'renders the alert notice when the authorization is rejected', async () => {
const submit = jest.fn();
const initialEntry = '/step?authorization_url=https://example.com&success=false';
render( { navigation: { submit } }, { initialEntry } );

const errorMessage = await findByText(
/We can't start your migration without your authorization. Please authorize WordPress.com in your WP Admin or share your credentials./
);

await waitFor( () => {
expect( errorMessage ).toBeVisible();
} );
} );

it( 'the authorization button redirects to the source URL when clicked', async () => {
const submit = jest.fn();
const authorizationUrl = 'https://example.com/authorization.php';
const initialEntry = `/step?authorization_url=${ authorizationUrl }`;
render( { navigation: { submit } }, { initialEntry } );

await userEvent.click( getByRole( 'button', { name: 'Authorize' } ) );

expect( submit ).toHaveBeenCalledWith( { action: 'authorization', authorizationUrl } );
} );

it( 'the share credentials button redirects to the fallback credentials step', async () => {
const submit = jest.fn();
const initialEntry = '/step?authorization_url=https://example.com';
render( { navigation: { submit } }, { initialEntry } );

await userEvent.click( getByRole( 'button', { name: 'Share credentials instead' } ) );

expect( submit ).toHaveBeenCalledWith( { action: 'fallback-credentials' } );
} );
} );
4 changes: 1 addition & 3 deletions packages/onboarding/src/step-container/index.tsx
Original file line number Diff line number Diff line change
@@ -21,7 +21,6 @@ interface Props {
backLabelText?: TranslateResult;
skipLabelText?: TranslateResult;
nextLabelText?: TranslateResult;
showNotice?: boolean;
notice?: ReactElement;
formattedHeader?: ReactElement;
hideFormattedHeader?: boolean;
@@ -63,7 +62,6 @@ const StepContainer: React.FC< Props > = ( {
skipHeadingText,
hideNext = true,
nextLabelText,
showNotice = false,
notice,
formattedHeader,
headerImageUrl,
@@ -198,7 +196,7 @@ const StepContainer: React.FC< Props > = ( {
</ActionButtons>
{ ! hideFormattedHeader && (
<div className="step-container__header">
{ showNotice && notice }
{ notice }
{ formattedHeader }
{ headerImageUrl && (
<div className="step-container__header-image">