Skip to content

Commit

Permalink
test: [M3-6169] - Account cancellation integration tests (#9952)
Browse files Browse the repository at this point in the history
* Add `mockCancelAccount`, `mockCancelAccountError`, and `mockWebpageUrl` mock utilities

* Make accordion UI helper yield contents to prevent selection conflicts between accordion headings and action buttons

* Add UI tests for account cancellation via Cloud Manager
  • Loading branch information
jdamore-linode authored Dec 4, 2023
1 parent 434917d commit 2bf58dd
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 4 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-9952-tests-1701457740855.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Add account cancellation UI tests ([#9952](https://github.com/linode/manager/pull/9952))
200 changes: 200 additions & 0 deletions packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* @file Integration tests for Cloud Manager account cancellation flows.
*/

import { profileFactory } from 'src/factories/profile';
import { accountFactory } from 'src/factories/account';
import {
mockGetAccount,
mockCancelAccount,
mockCancelAccountError,
} from 'support/intercepts/account';
import { mockGetProfile } from 'support/intercepts/profile';
import { ui } from 'support/ui';
import {
randomDomainName,
randomPhrase,
randomString,
} from 'support/util/random';
import type { CancelAccount } from '@linode/api-v4';
import { mockWebpageUrl } from 'support/intercepts/general';

// Data loss warning which is displayed in the account cancellation dialog.
const cancellationDataLossWarning =
'Please note this is an extremely destructive action. Closing your account \
means that all services Linodes, Volumes, DNS Records, etc will be lost and \
may not be able be restored.';

// Error message that appears when a payment failure occurs upon cancellation attempt.
const cancellationPaymentErrorMessage =
'We were unable to charge your credit card for services rendered. \
We cannot cancel this account until the balance has been paid.';

describe('Account cancellation', () => {
/*
* - Confirms that a user can cancel their account from the Account Settings page.
* - Confirms that user is warned that account cancellation is destructive.
* - Confirms that Cloud Manager displays a notice when an error occurs during cancellation.
* - Confirms that Cloud Manager includes user comments in cancellation request payload.
* - Confirms that Cloud Manager shows a survey CTA which directs the user to the expected URL.
*/
it('users can cancel account', () => {
const mockAccount = accountFactory.build();
const mockProfile = profileFactory.build({
username: 'mock-user',
restricted: false,
});
const mockCancellationResponse: CancelAccount = {
survey_link: `https://${randomDomainName()}/${randomString(5)}`,
};

const cancellationComments = randomPhrase();

mockGetAccount(mockAccount).as('getAccount');
mockGetProfile(mockProfile).as('getProfile');
mockCancelAccountError(cancellationPaymentErrorMessage, 409).as(
'cancelAccount'
);
mockWebpageUrl(
mockCancellationResponse.survey_link,
'This is a mock webpage to confirm Cloud Manager survey link behavior'
).as('getSurveyPage');

// Navigate to Account Settings page, click "Close Account" button.
cy.visitWithLogin('/account/settings');
cy.wait(['@getAccount', '@getProfile']);

ui.accordion
.findByTitle('Close Account')
.should('be.visible')
.within(() => {
ui.button
.findByTitle('Close Account')
.should('be.visible')
.should('be.enabled')
.click();
});

ui.dialog
.findByTitle('Are you sure you want to close your Linode account?')
.should('be.visible')
.within(() => {
cy.findByText(cancellationDataLossWarning, { exact: false }).should(
'be.visible'
);

// Confirm that submit button is disabled before entering required info.
ui.button
.findByTitle('Close Account')
.should('be.visible')
.should('be.disabled');

// Enter username, confirm that submit button becomes enabled, and click
// the submit button.
cy.findByLabelText(
`Please enter your Username (${mockProfile.username}) to confirm.`
)
.should('be.visible')
.should('be.enabled')
.type(mockProfile.username);

ui.button
.findByTitle('Close Account')
.should('be.visible')
.should('be.enabled')
.click();

// Confirm that request payload contains expected data and API error
// message is displayed in the dialog.
cy.wait('@cancelAccount').then((intercept) => {
expect(intercept.request.body['comments']).to.equal('');
});

cy.findByText(cancellationPaymentErrorMessage).should('be.visible');

// Enter account cancellation comments, click "Close Account" again,
// and this time mock a successful account cancellation response.
mockCancelAccount(mockCancellationResponse).as('cancelAccount');
cy.contains('Comments (optional)').click().type(cancellationComments);

ui.button
.findByTitle('Close Account')
.should('be.visible')
.should('be.enabled')
.click();

cy.wait('@cancelAccount').then((intercept) => {
expect(intercept.request.body['comments']).to.equal(
cancellationComments
);
});
});

// Confirm that Cloud presents account cancellation screen and prompts the
// user to complete the exit survey. Confirm that clicking survey button
// directs the user to the expected URL.
cy.findByText('It’s been our pleasure to serve you.').should('be.visible');
ui.button
.findByTitle('Take our exit survey')
.should('be.visible')
.should('be.enabled')
.click();

cy.wait('@getSurveyPage');
cy.url().should('equal', mockCancellationResponse.survey_link);
});

/*
* - Confirms Cloud Manager behavior when a restricted user attempts to close an account.
* - Confirms that API error response message is displayed in cancellation dialog.
*/
it('restricted users cannot cancel account', () => {
const mockAccount = accountFactory.build();
const mockProfile = profileFactory.build({
username: 'mock-restricted-user',
restricted: true,
});

mockGetAccount(mockAccount).as('getAccount');
mockGetProfile(mockProfile).as('getProfile');
mockCancelAccountError('Unauthorized', 403).as('cancelAccount');

// Navigate to Account Settings page, click "Close Account" button.
cy.visitWithLogin('/account/settings');
cy.wait(['@getAccount', '@getProfile']);

ui.accordion
.findByTitle('Close Account')
.should('be.visible')
.within(() => {
ui.button
.findByTitle('Close Account')
.should('be.visible')
.should('be.enabled')
.click();
});

// Fill out cancellation dialog and attempt submission.
ui.dialog
.findByTitle('Are you sure you want to close your Linode account?')
.should('be.visible')
.within(() => {
cy.findByLabelText(
`Please enter your Username (${mockProfile.username}) to confirm.`
)
.should('be.visible')
.should('be.enabled')
.type(mockProfile.username);

ui.button
.findByTitle('Close Account')
.should('be.visible')
.should('be.enabled')
.click();

// Confirm that API unauthorized error message is displayed.
cy.wait('@cancelAccount');
cy.findByText('Unauthorized').should('be.visible');
});
});
});
46 changes: 44 additions & 2 deletions packages/manager/cypress/support/intercepts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { makeResponse } from 'support/util/response';
import type {
Account,
AccountSettings,
CancelAccount,
EntityTransfer,
Invoice,
InvoiceItem,
Payment,
PaymentMethod,
User,
} from '@linode/api-v4';

/**
Expand All @@ -26,7 +28,7 @@ import type {
* @returns Cypress chainable.
*/
export const mockGetAccount = (account: Account): Cypress.Chainable<null> => {
return cy.intercept('GET', apiMatcher('account'), account);
return cy.intercept('GET', apiMatcher('account'), makeResponse(account));
};

/**
Expand All @@ -39,7 +41,11 @@ export const mockGetAccount = (account: Account): Cypress.Chainable<null> => {
export const mockUpdateAccount = (
updatedAccount: Account
): Cypress.Chainable<null> => {
return cy.intercept('PUT', apiMatcher('account'), updatedAccount);
return cy.intercept(
'PUT',
apiMatcher('account'),
makeResponse(updatedAccount)
);
};

/**
Expand Down Expand Up @@ -322,3 +328,39 @@ export const mockGetPayments = (
paginateResponse(payments)
);
};

/**
* Intercepts POST request to cancel account and mocks cancellation response.
*
* @param cancellation - Account cancellation object with which to mock response.
*
* @returns Cypress chainable.
*/
export const mockCancelAccount = (
cancellation: CancelAccount
): Cypress.Chainable<null> => {
return cy.intercept(
'POST',
apiMatcher('account/cancel'),
makeResponse(cancellation)
);
};

/**
* Intercepts POST request to cancel account and mocks an API error response.
*
* @param errorMessage - Error message to include in mock error response.
* @param status - HTTP status for mock error response.
*
* @returns Cypress chainable.
*/
export const mockCancelAccountError = (
errorMessage: string,
status: number = 400
): Cypress.Chainable<null> => {
return cy.intercept(
'POST',
apiMatcher('account/cancel'),
makeErrorResponse(errorMessage, status)
);
};
20 changes: 19 additions & 1 deletion packages/manager/cypress/support/intercepts/general.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { makeErrorResponse } from 'support/util/errors';
import { apiMatcher } from 'support/util/intercepts';
import { makeResponse } from 'support/util/response';

/**
* Intercepts GET request to given URL and mocks an HTTP 200 response with the given content.
*
* This can be used to mock visits to arbitrary webpages.
*
* @param url - Webpage URL for which to intercept GET request.
* @param content - Webpage content with which to mock response.
*
* @returns Cypress chainable.
*/
export const mockWebpageUrl = (
url: string,
content: string
): Cypress.Chainable<null> => {
return cy.intercept(url, makeResponse(content, 200));
};

/**
* Intercepts all Linode APIv4 requests and mocks maintenance mode response.
Expand All @@ -9,7 +27,7 @@ import { apiMatcher } from 'support/util/intercepts';
*
* @returns Cypress chainable.
*/
export const mockApiMaintenanceMode = () => {
export const mockApiMaintenanceMode = (): Cypress.Chainable<null> => {
const errorResponse = makeErrorResponse(
'Currently in maintenance mode.',
503
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/cypress/support/ui/accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export const accordion = {
* @returns Cypress chainable.
*/
findByTitle: (title: string) => {
return cy.get(`[data-qa-panel="${title}"]`);
return cy.get(`[data-qa-panel="${title}"]`).find('[data-qa-panel-details]');
},
};

0 comments on commit 2bf58dd

Please sign in to comment.