From a8a5046b0b3b430497889c9dcd98dee261ab85d4 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 24 Jan 2024 16:09:42 -0500 Subject: [PATCH 01/13] Add mock util to mock all APIv4 requests --- .../cypress/support/intercepts/general.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/manager/cypress/support/intercepts/general.ts b/packages/manager/cypress/support/intercepts/general.ts index 3cd13083bc5..09cd74e0167 100644 --- a/packages/manager/cypress/support/intercepts/general.ts +++ b/packages/manager/cypress/support/intercepts/general.ts @@ -2,6 +2,25 @@ import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; import { makeResponse } from 'support/util/response'; +/** + * Intercepts all requests to Linode API v4 and mocks an HTTP response. + * + * This is useful to apply a baseline mock on all Linode API v4 requests, e.g. + * to prevent 401 responses. More fine-grained mocking can be set up with + * subsequent calls to other mock utils. + * + * @param body - Body data with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockAllApiRequests = ( + body: any = {}, + statusCode: number = 200 +) => { + return cy.intercept(apiMatcher('**/*'), makeResponse(body, statusCode)); +}; + /** * Intercepts GET request to given URL and mocks an HTTP 200 response with the given content. * From 7004b21a5a3adb0dfcd30e059e64b804fd018b5b Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 24 Jan 2024 16:14:31 -0500 Subject: [PATCH 02/13] Add local storage assertion util --- .../cypress/support/util/local-storage.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/manager/cypress/support/util/local-storage.ts diff --git a/packages/manager/cypress/support/util/local-storage.ts b/packages/manager/cypress/support/util/local-storage.ts new file mode 100644 index 00000000000..311b9359613 --- /dev/null +++ b/packages/manager/cypress/support/util/local-storage.ts @@ -0,0 +1,30 @@ +/** + * @file Utilities to access and validate Local Storage data. + */ + +/** + * Asserts that a local storage item has a given value. + * + * @param key - Local storage item key. + * @param value - Local stroage item value to assert. + */ +export const assertLocalStorageValue = (key: string, value: any) => { + cy.getAllLocalStorage().then((localStorageData: any) => { + const origin = Cypress.config('baseUrl'); + if (!origin) { + // This should never happen in practice. + throw new Error('Unable to retrieve Cypress base URL configuration'); + } + if (!localStorageData[origin]) { + throw new Error( + `Unable to retrieve local storage data from origin '${origin}'` + ); + } + if (!localStorageData[origin][key]) { + throw new Error( + `No local storage data exists for key '${key}' and origin '${origin}'` + ); + } + expect(localStorageData[origin][key]).equals(value); + }); +}; From 051133c75012149b95839e4eecc88da753a82f99 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 24 Jan 2024 16:15:19 -0500 Subject: [PATCH 03/13] Add mock utils for child account endpoints --- .../cypress/support/intercepts/account.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/manager/cypress/support/intercepts/account.ts b/packages/manager/cypress/support/intercepts/account.ts index 41f092a814d..cd8fc1a0164 100644 --- a/packages/manager/cypress/support/intercepts/account.ts +++ b/packages/manager/cypress/support/intercepts/account.ts @@ -19,6 +19,7 @@ import type { InvoiceItem, Payment, PaymentMethod, + Token, User, } from '@linode/api-v4'; @@ -467,6 +468,7 @@ export const mockCancelAccountError = ( /** * Intercepts GET request to fetch the account agreements and mocks the response. * + * @param agreements - Agreements with which to mock response. * * @returns Cypress chainable. */ @@ -479,3 +481,39 @@ export const mockGetAccountAgreements = ( makeResponse(agreements) ); }; + +/** + * Intercepts GET request to fetch child accounts and mocks the response. + * + * @param childAccounts - Child account objects with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetChildAccounts = ( + childAccounts: Account[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('account/child-accounts*'), + paginateResponse(childAccounts) + ); +}; + +/** + * Intercepts POST request to create a child account token and mocks the response. + * + * @param childAccount - Child account for which to create a token. + * @param childAccountToken - Token object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateChildAccountToken = ( + childAccount: Account, + childAccountToken: Token +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`account/child-accounts/${childAccount.euuid}/token`), + makeResponse(childAccountToken) + ); +}; From 9bca65176fec678a81dbea19c1073fc5b6bccce1 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 24 Jan 2024 16:19:10 -0500 Subject: [PATCH 04/13] Add WIP account switching test --- .../parentChild/account-switching.spec.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts new file mode 100644 index 00000000000..c0fc2785063 --- /dev/null +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -0,0 +1,137 @@ +import { + accountFactory, + appTokenFactory, + profileFactory, +} from '@src/factories'; +import { accountUserFactory } from '@src/factories/accountUsers'; +import { DateTime } from 'luxon'; +import { + mockCreateChildAccountToken, + mockGetAccount, + mockGetChildAccounts, + mockGetUser, +} from 'support/intercepts/account'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { mockAllApiRequests } from 'support/intercepts/general'; +import { mockGetProfile } from 'support/intercepts/profile'; +import { ui } from 'support/ui'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { assertLocalStorageValue } from 'support/util/local-storage'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; + +describe('Parent/Child account switching', () => { + it('can switch from Parent account to Child account', () => { + const mockParentAccount = accountFactory.build({ + company: 'Parent Company', + }); + const mockParentProfile = profileFactory.build({ + username: randomLabel(), + user_type: 'parent', + }); + const mockParentUser = accountUserFactory.build({ + username: mockParentProfile.username, + user_type: 'parent', + }); + + const mockChildAccount = accountFactory.build({ + company: 'Child Company', + }); + + const mockChildAccountToken = appTokenFactory.build({ + id: randomNumber(), + created: DateTime.now().toISO(), + expiry: DateTime.now().plus({ hours: 1 }).toISO(), + label: `${mockChildAccount.company}_proxy`, + scopes: '*', + token: randomString(32), + website: undefined, + thumbnail_url: undefined, + }); + + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccounts([mockChildAccount]); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/'); + + // Confirm that Parent account username and company name are shown in user + // menu button, then click the button. + ui.userMenuButton + .find() + .should('be.visible') + .within(() => { + cy.findByText(mockParentProfile.username).should('be.visible'); + cy.findByText(mockParentAccount.company).should('be.visible'); + }) + .click(); + + // Click "Switch Account" button in user menu. + ui.userMenu + .find() + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Click mock company name in "Switch Account" drawer. + mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( + 'switchAccount' + ); + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText(mockChildAccount.company).should('be.visible').click(); + }); + + cy.wait('@switchAccount'); + + // Confirm that Cloud Manager updates local storage authentication values. + assertLocalStorageValue( + 'authentication/token', + mockChildAccountToken.token + ); + assertLocalStorageValue( + 'authentication/expire', + mockChildAccountToken.expiry + ); + assertLocalStorageValue( + 'authentication/scopes', + mockChildAccountToken.scopes + ); + + // From this point forward, we will not have a valid test account token stored in local storage, + // so all non-intercepted API requests will respond with a 401 status code and we will get booted to login. + // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the + // individual requests as needed. + mockAllApiRequests(); + mockGetAccount(mockChildAccount); + mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); + + // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. + // TODO Add assertions for toast upon account switch. + // This probably involves updating the Axios interceptor to use the new auth token, and possibly involves invalidating React Query cache. + cy.reload(); + + ui.userMenuButton + .find() + .should('be.visible') + .within(() => { + cy.findByText(mockParentProfile.username).should('be.visible'); + cy.findByText(mockChildAccount.company).should('be.visible'); + }); + }); +}); From 463a15a471335ed1ce42fda6921dd40528e9160f Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 25 Jan 2024 13:11:23 -0500 Subject: [PATCH 05/13] Add account switching API error mock utils --- .../cypress/support/intercepts/account.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/manager/cypress/support/intercepts/account.ts b/packages/manager/cypress/support/intercepts/account.ts index cd8fc1a0164..1ad2469f7dc 100644 --- a/packages/manager/cypress/support/intercepts/account.ts +++ b/packages/manager/cypress/support/intercepts/account.ts @@ -499,6 +499,25 @@ export const mockGetChildAccounts = ( ); }; +/** + * Intercepts GET request to fetch child accounts and mocks an error response. + * + * @param errorMessage - API error message with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetChildAccountsError = ( + errorMessage: string = 'An unknown error has occurred', + statusCode: number = 500 +) => { + return cy.intercept( + 'GET', + apiMatcher('account/child-accounts*'), + makeErrorResponse(errorMessage, statusCode) + ); +}; + /** * Intercepts POST request to create a child account token and mocks the response. * @@ -517,3 +536,24 @@ export const mockCreateChildAccountToken = ( makeResponse(childAccountToken) ); }; + +/** + * Intercepts POST request to create a child account token and mocks error response. + * + * @param childAccount - Child account for which to mock error response. + * @param errorMessage - API error message with which to mock response. + * @param statusCode - HTTP status code with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateChildAccountTokenError = ( + childAccount: Account, + errorMessage: string = 'An unknown error has occurred', + statusCode: number = 500 +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`account/child-accounts/${childAccount.euuid}/token`), + makeErrorResponse(errorMessage, statusCode) + ); +}; From bd630026e0357f3185808b402d70217d8803a14e Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 25 Jan 2024 13:12:00 -0500 Subject: [PATCH 06/13] Add account switching from billing page test, error flow test --- .../parentChild/account-switching.spec.ts | 337 ++++++++++++------ 1 file changed, 235 insertions(+), 102 deletions(-) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index c0fc2785063..7cd12191706 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -7,8 +7,10 @@ import { accountUserFactory } from '@src/factories/accountUsers'; import { DateTime } from 'luxon'; import { mockCreateChildAccountToken, + mockCreateChildAccountTokenError, mockGetAccount, mockGetChildAccounts, + mockGetChildAccountsError, mockGetUser, } from 'support/intercepts/account'; import { @@ -22,116 +24,247 @@ import { makeFeatureFlagData } from 'support/util/feature-flags'; import { assertLocalStorageValue } from 'support/util/local-storage'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; -describe('Parent/Child account switching', () => { - it('can switch from Parent account to Child account', () => { - const mockParentAccount = accountFactory.build({ - company: 'Parent Company', - }); - const mockParentProfile = profileFactory.build({ - username: randomLabel(), - user_type: 'parent', - }); - const mockParentUser = accountUserFactory.build({ - username: mockParentProfile.username, - user_type: 'parent', +/** + * Confirms expected username and company name are shown in user menu button and yields the button. + * + * @param username - Username to expect in user menu button. + * @param companyName - Company name to expect in user menu button. + * + * @returns Cypress chainable that yields the user menu button. + */ +const assertUserMenuButton = (username: string, companyName: string) => { + return ui.userMenuButton + .find() + .should('be.visible') + .within(() => { + cy.findByText(username).should('be.visible'); + cy.findByText(companyName).should('be.visible'); }); +}; + +/** + * Confirms that expected authentication values are set in Local Storage. + * + * @param token - Authentication token value to assert. + * @param expiry - Authentication expiry value to assert. + * @param scopes - Authentication scope value to assert. + */ +const assertAuthLocalStorage = ( + token: string, + expiry: string, + scopes: string +) => { + assertLocalStorageValue('authentication/token', token); + assertLocalStorageValue('authentication/expire', expiry); + assertLocalStorageValue('authentication/scopes', scopes); +}; + +const mockParentAccount = accountFactory.build({ + company: 'Parent Company', +}); + +const mockParentProfile = profileFactory.build({ + username: randomLabel(), + user_type: 'parent', +}); + +const mockParentUser = accountUserFactory.build({ + username: mockParentProfile.username, + user_type: 'parent', +}); + +const mockChildAccount = accountFactory.build({ + company: 'Child Company', +}); + +const mockChildAccountToken = appTokenFactory.build({ + id: randomNumber(), + created: DateTime.now().toISO(), + expiry: DateTime.now().plus({ hours: 1 }).toISO(), + label: `${mockChildAccount.company}_proxy`, + scopes: '*', + token: randomString(32), + website: undefined, + thumbnail_url: undefined, +}); - const mockChildAccount = accountFactory.build({ - company: 'Child Company', +const mockErrorMessage = 'An unknown error has occurred.'; + +describe('Parent/Child account switching', () => { + describe('From Parent to Child', () => { + beforeEach(() => { + // @TODO Remove feature flag mocks after feature launch and clean-up. + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); }); - const mockChildAccountToken = appTokenFactory.build({ - id: randomNumber(), - created: DateTime.now().toISO(), - expiry: DateTime.now().plus({ hours: 1 }).toISO(), - label: `${mockChildAccount.company}_proxy`, - scopes: '*', - token: randomString(32), - website: undefined, - thumbnail_url: undefined, + it('can switch from Parent account to Child account from Billing page', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccounts([mockChildAccount]); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/account/billing'); + + // Confirm that "Switch Account" button is present, then click it. + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + + mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( + 'switchAccount' + ); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText(mockChildAccount.company).should('be.visible').click(); + }); + + cy.wait('@switchAccount'); + + // Confirm that Cloud Manager updates local storage authentication values. + assertAuthLocalStorage( + mockChildAccountToken.token, + mockChildAccountToken.expiry, + mockChildAccountToken.scopes + ); + + // From this point forward, we will not have a valid test account token stored in local storage, + // so all non-intercepted API requests will respond with a 401 status code and we will get booted to login. + // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the + // individual requests as needed. + mockAllApiRequests(); + mockGetAccount(mockChildAccount); + mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); + + // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. + // TODO Add assertions for toast upon account switch. + cy.reload(); + + // Confirm expected username and company are shown in user menu button. + assertUserMenuButton( + mockParentProfile.username, + mockChildAccount.company + ); }); - mockAppendFeatureFlags({ - parentChildAccountAccess: makeFeatureFlagData(true), + it('can switch from Parent account to Child account using user menu', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccounts([mockChildAccount]); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/'); + + // Confirm that Parent account username and company name are shown in user + // menu button, then click the button. + assertUserMenuButton( + mockParentProfile.username, + mockParentAccount.company + ).click(); + + // Click "Switch Account" button in user menu. + ui.userMenu + .find() + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Click mock company name in "Switch Account" drawer. + mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( + 'switchAccount' + ); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText(mockChildAccount.company).should('be.visible').click(); + }); + + cy.wait('@switchAccount'); + + // Confirm that Cloud Manager updates local storage authentication values. + assertAuthLocalStorage( + mockChildAccountToken.token, + mockChildAccountToken.expiry, + mockChildAccountToken.scopes + ); + + // From this point forward, we will not have a valid test account token stored in local storage, + // so all non-intercepted API requests will respond with a 401 status code and we will get booted to login. + // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the + // individual requests as needed. + mockAllApiRequests(); + mockGetAccount(mockChildAccount); + mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); + + // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. + // TODO Add assertions for toast upon account switch. + cy.reload(); + + // Confirm expected username and company are shown in user menu button. + assertUserMenuButton( + mockParentProfile.username, + mockChildAccount.company + ); }); - mockGetFeatureFlagClientstream(); - mockGetProfile(mockParentProfile); - mockGetAccount(mockParentAccount); - mockGetChildAccounts([mockChildAccount]); - mockGetUser(mockParentUser); - - cy.visitWithLogin('/'); - - // Confirm that Parent account username and company name are shown in user - // menu button, then click the button. - ui.userMenuButton - .find() - .should('be.visible') - .within(() => { - cy.findByText(mockParentProfile.username).should('be.visible'); - cy.findByText(mockParentAccount.company).should('be.visible'); - }) - .click(); - - // Click "Switch Account" button in user menu. - ui.userMenu - .find() - .should('be.visible') - .within(() => { - ui.button - .findByTitle('Switch Account') - .should('be.visible') - .should('be.enabled') - .click(); - }); + }); - // Click mock company name in "Switch Account" drawer. - mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( - 'switchAccount' - ); - ui.drawer - .findByTitle('Switch Account') - .should('be.visible') - .within(() => { - cy.findByText(mockChildAccount.company).should('be.visible').click(); - }); + /* + * Tests to confirm that Cloud handles account switching errors gracefully. + */ + describe('Error flows', () => { + it('handles account switching API errors', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccountsError('An unknown error has occurred', 500); + mockGetUser(mockParentUser); - cy.wait('@switchAccount'); - - // Confirm that Cloud Manager updates local storage authentication values. - assertLocalStorageValue( - 'authentication/token', - mockChildAccountToken.token - ); - assertLocalStorageValue( - 'authentication/expire', - mockChildAccountToken.expiry - ); - assertLocalStorageValue( - 'authentication/scopes', - mockChildAccountToken.scopes - ); - - // From this point forward, we will not have a valid test account token stored in local storage, - // so all non-intercepted API requests will respond with a 401 status code and we will get booted to login. - // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the - // individual requests as needed. - mockAllApiRequests(); - mockGetAccount(mockChildAccount); - mockGetProfile(mockParentProfile); - mockGetUser(mockParentUser); - - // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. - // TODO Add assertions for toast upon account switch. - // This probably involves updating the Axios interceptor to use the new auth token, and possibly involves invalidating React Query cache. - cy.reload(); - - ui.userMenuButton - .find() - .should('be.visible') - .within(() => { - cy.findByText(mockParentProfile.username).should('be.visible'); - cy.findByText(mockChildAccount.company).should('be.visible'); - }); + cy.visitWithLogin('/account/billing'); + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + // Confirm error message upon failure to fetch child accounts. + cy.findByText('Unable to load data.').should('be.visible'); + cy.findByText( + 'Try again or contact support if the issue persists.' + ).should('be.visible'); + + // Click "Try Again" button and mock a successful response. + mockGetChildAccounts([mockChildAccount]); + ui.button + .findByTitle('Try again') + .should('be.visible') + .should('be.enabled') + .click(); + + // Click child company and mock an error. + // Confirm that Cloud Manager displays the error message in the drawer. + mockCreateChildAccountTokenError(mockChildAccount, mockErrorMessage); + cy.findByText(mockChildAccount.company).click(); + cy.findByText(mockErrorMessage).should('be.visible'); + }); + }); }); }); From 9fd9b396b1c98abfa35fa1f2fd50e6c2467886fd Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 25 Jan 2024 13:16:35 -0500 Subject: [PATCH 07/13] Improve test explanation comments --- .../parentChild/account-switching.spec.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index 7cd12191706..04d70f6e90c 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -91,15 +91,23 @@ const mockChildAccountToken = appTokenFactory.build({ const mockErrorMessage = 'An unknown error has occurred.'; describe('Parent/Child account switching', () => { + /* + * Tests to confirm that Parent account users can switch to Child accounts as expected. + */ describe('From Parent to Child', () => { beforeEach(() => { - // @TODO Remove feature flag mocks after feature launch and clean-up. + // @TODO M3-7554, M3-7559: Remove feature flag mocks after feature launch and clean-up. mockAppendFeatureFlags({ parentChildAccountAccess: makeFeatureFlagData(true), }); mockGetFeatureFlagClientstream(); }); + /* + * - Confirms that Parent account user can switch to Child account from Account Billing page. + * - Confirms that Child account information is displayed in user menu button after switch. + * - Confirms that Cloud updates local storage auth values upon account switch. + */ it('can switch from Parent account to Child account from Billing page', () => { mockGetProfile(mockParentProfile); mockGetAccount(mockParentAccount); @@ -155,6 +163,12 @@ describe('Parent/Child account switching', () => { ); }); + /* + * - Confirms that Parent account user can switch to Child account using the user menu. + * - Confirms that Parent account information is initially displayed in user menu button. + * - Confirms that Child account information is displayed in user menu button after switch. + * - Confirms that Cloud updates local storage auth values upon account switch. + */ it('can switch from Parent account to Child account using user menu', () => { mockGetProfile(mockParentProfile); mockGetAccount(mockParentAccount); @@ -228,6 +242,11 @@ describe('Parent/Child account switching', () => { * Tests to confirm that Cloud handles account switching errors gracefully. */ describe('Error flows', () => { + /* + * - Confirms error handling upon failure to fetch child accounts. + * - Confirms "Try Again" button can be used to re-fetch child accounts successfully. + * - Confirms error handling upon failure to create child account token. + */ it('handles account switching API errors', () => { mockGetProfile(mockParentProfile); mockGetAccount(mockParentAccount); From 2c48a53ef338d3ef0090f40cafe3ab20c2bbe568 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 25 Jan 2024 13:49:21 -0500 Subject: [PATCH 08/13] Added changeset: Add Cypress tests for account switching from Parent to Child --- packages/manager/.changeset/pr-10110-tests-1706208561568.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10110-tests-1706208561568.md diff --git a/packages/manager/.changeset/pr-10110-tests-1706208561568.md b/packages/manager/.changeset/pr-10110-tests-1706208561568.md new file mode 100644 index 00000000000..49a1409e99b --- /dev/null +++ b/packages/manager/.changeset/pr-10110-tests-1706208561568.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress tests for account switching from Parent to Child ([#10110](https://github.com/linode/manager/pull/10110)) From c57737b576d45e116eadf7aabf64792824da8622 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 5 Feb 2024 15:20:10 -0500 Subject: [PATCH 09/13] Use non-null assertion to satisfy TS error --- .../e2e/core/parentChild/account-switching.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index 04d70f6e90c..fe84abdcb79 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -137,9 +137,10 @@ describe('Parent/Child account switching', () => { cy.wait('@switchAccount'); // Confirm that Cloud Manager updates local storage authentication values. + // Satisfy TypeScript using non-null assertions since we know what the mock data contains. assertAuthLocalStorage( - mockChildAccountToken.token, - mockChildAccountToken.expiry, + mockChildAccountToken.token!, + mockChildAccountToken.expiry!, mockChildAccountToken.scopes ); @@ -211,9 +212,10 @@ describe('Parent/Child account switching', () => { cy.wait('@switchAccount'); // Confirm that Cloud Manager updates local storage authentication values. + // Satisfy TypeScript using non-null assertions since we know what the mock data contains. assertAuthLocalStorage( - mockChildAccountToken.token, - mockChildAccountToken.expiry, + mockChildAccountToken.token!, + mockChildAccountToken.expiry!, mockChildAccountToken.scopes ); From 6a1eb9f4ea4b4680d9792578f3de8893720367c6 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:08:42 -0500 Subject: [PATCH 10/13] Update packages/manager/cypress/support/util/local-storage.ts Fix doc comment typo Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> --- packages/manager/cypress/support/util/local-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/cypress/support/util/local-storage.ts b/packages/manager/cypress/support/util/local-storage.ts index 311b9359613..db748cb4105 100644 --- a/packages/manager/cypress/support/util/local-storage.ts +++ b/packages/manager/cypress/support/util/local-storage.ts @@ -6,7 +6,7 @@ * Asserts that a local storage item has a given value. * * @param key - Local storage item key. - * @param value - Local stroage item value to assert. + * @param value - Local storage item value to assert. */ export const assertLocalStorageValue = (key: string, value: any) => { cy.getAllLocalStorage().then((localStorageData: any) => { From ab83c026d1cc3fc172a5197653e188743915c280 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:09:17 -0500 Subject: [PATCH 11/13] Update packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts Update test title to be more accurate re: "child" vs "proxy" Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> --- .../cypress/e2e/core/parentChild/account-switching.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index fe84abdcb79..6719b705119 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -94,7 +94,7 @@ describe('Parent/Child account switching', () => { /* * Tests to confirm that Parent account users can switch to Child accounts as expected. */ - describe('From Parent to Child', () => { + describe('From Parent to Proxy', () => { beforeEach(() => { // @TODO M3-7554, M3-7559: Remove feature flag mocks after feature launch and clean-up. mockAppendFeatureFlags({ From af70d4a9abdbc33a160267b004e33fd3d7133e44 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 20 Feb 2024 14:56:09 -0500 Subject: [PATCH 12/13] Remove unneeded mock --- .../cypress/e2e/core/parentChild/account-switching.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index 6719b705119..ae16399d9ba 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -151,7 +151,6 @@ describe('Parent/Child account switching', () => { mockAllApiRequests(); mockGetAccount(mockChildAccount); mockGetProfile(mockParentProfile); - mockGetUser(mockParentUser); // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. // TODO Add assertions for toast upon account switch. @@ -226,7 +225,6 @@ describe('Parent/Child account switching', () => { mockAllApiRequests(); mockGetAccount(mockChildAccount); mockGetProfile(mockParentProfile); - mockGetUser(mockParentUser); // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. // TODO Add assertions for toast upon account switch. From 08d7b3028fc0d088a230465e75adade13c224809 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 20 Feb 2024 15:17:46 -0500 Subject: [PATCH 13/13] Add event and notification mocks to prevent Cloud crash upon account switch --- .../parentChild/account-switching.spec.ts | 18 ++++++++++++++++-- .../cypress/support/intercepts/events.ts | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index ae16399d9ba..60c6488ef30 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -13,12 +13,15 @@ import { mockGetChildAccountsError, mockGetUser, } from 'support/intercepts/account'; +import { mockGetEvents, mockGetNotifications } from 'support/intercepts/events'; import { mockAppendFeatureFlags, mockGetFeatureFlagClientstream, } from 'support/intercepts/feature-flags'; import { mockAllApiRequests } from 'support/intercepts/general'; +import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockGetProfile } from 'support/intercepts/profile'; +import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { makeFeatureFlagData } from 'support/util/feature-flags'; import { assertLocalStorageValue } from 'support/util/local-storage'; @@ -149,11 +152,17 @@ describe('Parent/Child account switching', () => { // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the // individual requests as needed. mockAllApiRequests(); + mockGetLinodes([]); + mockGetRegions([]); + mockGetEvents([]); + mockGetNotifications([]); + mockGetAccount(mockChildAccount); mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. - // TODO Add assertions for toast upon account switch. + // TODO Add assertions for toast upon account switch. This might involve improving mocks for events/notifications. cy.reload(); // Confirm expected username and company are shown in user menu button. @@ -223,11 +232,16 @@ describe('Parent/Child account switching', () => { // We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the // individual requests as needed. mockAllApiRequests(); + mockGetLinodes([]); + mockGetRegions([]); + mockGetEvents([]); + mockGetNotifications([]); mockGetAccount(mockChildAccount); mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); // TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching. - // TODO Add assertions for toast upon account switch. + // TODO Add assertions for toast upon account switch. This might involve improving mocks for events/notifications. cy.reload(); // Confirm expected username and company are shown in user menu button. diff --git a/packages/manager/cypress/support/intercepts/events.ts b/packages/manager/cypress/support/intercepts/events.ts index 2ae3685bd5e..eea7b5fac12 100644 --- a/packages/manager/cypress/support/intercepts/events.ts +++ b/packages/manager/cypress/support/intercepts/events.ts @@ -2,7 +2,7 @@ * @file Mocks and intercepts related to notification and event handling. */ -import { Event } from '@linode/api-v4'; +import type { Event, Notification } from '@linode/api-v4'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; @@ -20,3 +20,20 @@ export const mockGetEvents = (events: Event[]): Cypress.Chainable => { paginateResponse(events) ); }; + +/** + * Intercepts GET request to fetch notifications and mocks response. + * + * @param notifications - Notifications with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetNotifications = ( + notifications: Notification[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('account/notifications*'), + paginateResponse(notifications) + ); +};