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

test: [M3-6169] - Account cancellation integration tests #9952

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
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))
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
);
});
Comment on lines +126 to +130
Copy link
Contributor Author

@jdamore-linode jdamore-linode Dec 1, 2023

Choose a reason for hiding this comment

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

@cliu-akamai, calling this out since you'll probably have to do something similar to confirm the Add User request payload contains the expected data for your M3-7482 ticket.

});

// 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]');
},
};