From 62231704911595f579b81a83b4b2abe580256159 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:05:54 -0400 Subject: [PATCH] test: [M3-7517 & M3-7519] - Cypress integration tests for Child -> Parent and Child -> Child account switching (#10288) * Add account payment intercept utils * Add tests for Child to Parent and Child to Child switching flows --- .../pr-10288-tests-1710450875663.md | 5 + .../parentChild/account-switching.spec.ts | 267 +++++++++++++++++- .../cypress/support/intercepts/account.ts | 27 ++ 3 files changed, 289 insertions(+), 10 deletions(-) create mode 100644 packages/manager/.changeset/pr-10288-tests-1710450875663.md diff --git a/packages/manager/.changeset/pr-10288-tests-1710450875663.md b/packages/manager/.changeset/pr-10288-tests-1710450875663.md new file mode 100644 index 00000000000..ebfd27e5e30 --- /dev/null +++ b/packages/manager/.changeset/pr-10288-tests-1710450875663.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Parent/Child account switching UI tests for Child->Parent and Child->Child flows ([#10288](https://github.com/linode/manager/pull/10288)) 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 8877d0b79c6..d0a0e550dec 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -1,16 +1,23 @@ import { accountFactory, appTokenFactory, + paymentMethodFactory, profileFactory, } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { DateTime } from 'luxon'; import { + interceptGetInvoices, + interceptGetPayments, + interceptGetPaymentMethods, mockCreateChildAccountToken, mockCreateChildAccountTokenError, mockGetAccount, mockGetChildAccounts, mockGetChildAccountsError, + mockGetInvoices, + mockGetPaymentMethods, + mockGetPayments, mockGetUser, } from 'support/intercepts/account'; import { mockGetEvents, mockGetNotifications } from 'support/intercepts/events'; @@ -81,7 +88,34 @@ const mockParentUser = accountUserFactory.build({ }); const mockChildAccount = accountFactory.build({ - company: 'Child Company', + company: 'Partner Company', +}); + +// Used for testing flows involving multiple children (e.g. switching child -> child). +const mockAlternateChildAccount = accountFactory.build({ + company: 'Other Partner Company', +}); + +const mockChildAccountProxyUser = accountUserFactory.build({ + username: mockParentProfile.username, + user_type: 'proxy', +}); + +// Used for testing flows involving multiple children (e.g. switching child -> child). +const mockAlternateChildAccountProxyUser = accountUserFactory.build({ + username: mockParentProfile.username, + user_type: 'proxy', +}); + +const mockChildAccountProfile = profileFactory.build({ + username: mockChildAccountProxyUser.username, + user_type: 'proxy', +}); + +// Used for testing flows involving multiple children (e.g. switching child -> child). +const mockAlternateChildAccountProfile = profileFactory.build({ + username: mockAlternateChildAccountProxyUser.username, + user_type: 'proxy', }); const childAccountAccessGrantEnabled = grantsFactory.build({ @@ -95,7 +129,7 @@ const childAccountAccessGrantDisabled = grantsFactory.build({ const mockChildAccountToken = appTokenFactory.build({ id: randomNumber(), created: DateTime.now().toISO(), - expiry: DateTime.now().plus({ hours: 1 }).toISO(), + expiry: DateTime.now().plus({ minutes: 15 }).toISO(), label: `${mockChildAccount.company}_proxy`, scopes: '*', token: randomString(32), @@ -103,13 +137,25 @@ const mockChildAccountToken = appTokenFactory.build({ thumbnail_url: undefined, }); +// Used for testing flows involving multiple children (e.g. switching child -> child). +const mockAlternateChildAccountToken = appTokenFactory.build({ + id: randomNumber(), + created: DateTime.now().toISO(), + expiry: DateTime.now().plus({ minutes: 15 }).toISO(), + label: `${mockAlternateChildAccount.company}_proxy`, + scopes: '*', + token: randomString(32), + website: undefined, + thumbnail_url: undefined, +}); + 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 Proxy', () => { + describe('From Parent to Child', () => { beforeEach(() => { // @TODO M3-7554, M3-7559: Remove feature flag mocks after feature launch and clean-up. mockAppendFeatureFlags({ @@ -123,13 +169,17 @@ describe('Parent/Child account switching', () => { * - 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', () => { + it('can switch from Parent account user to Proxy account user from Billing page', () => { mockGetProfile(mockParentProfile); mockGetAccount(mockParentAccount); mockGetChildAccounts([mockChildAccount]); mockGetUser(mockParentUser); + interceptGetPayments().as('getPayments'); + interceptGetPaymentMethods().as('getPaymentMethods'); + interceptGetInvoices().as('getInvoices'); cy.visitWithLogin('/account/billing'); + cy.wait(['@getPayments', '@getInvoices', '@getPaymentMethods']); // Confirm that "Switch Account" button is present, then click it. ui.button @@ -150,8 +200,11 @@ describe('Parent/Child account switching', () => { mockGetEvents([]); mockGetNotifications([]); mockGetAccount(mockChildAccount); - mockGetProfile(mockParentProfile); - mockGetUser(mockParentUser); + mockGetProfile(mockChildAccountProfile); + mockGetUser(mockChildAccountProxyUser); + mockGetPaymentMethods(paymentMethodFactory.buildList(1)); + mockGetInvoices([]); + mockGetPayments([]); // Mock the account switch itself -- we have to do this after the mocks above // to ensure that it is applied. @@ -178,9 +231,13 @@ describe('Parent/Child account switching', () => { // Confirm expected username and company are shown in user menu button. assertUserMenuButton( - mockParentProfile.username, + mockChildAccountProxyUser.username, mockChildAccount.company ); + + ui.toast.assertMessage( + `Account switched to ${mockChildAccount.company}.` + ); }); /* @@ -189,7 +246,7 @@ describe('Parent/Child account switching', () => { * - 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', () => { + it('can switch from Parent account user to Proxy account user using user menu', () => { mockGetProfile(mockParentProfile); mockGetAccount(mockParentAccount); mockGetChildAccounts([mockChildAccount]); @@ -228,8 +285,8 @@ describe('Parent/Child account switching', () => { mockGetEvents([]); mockGetNotifications([]); mockGetAccount(mockChildAccount); - mockGetProfile(mockParentProfile); - mockGetUser(mockParentUser); + mockGetProfile(mockChildAccountProfile); + mockGetUser(mockChildAccountProxyUser); // Click mock company name in "Switch Account" drawer. mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as( @@ -261,6 +318,196 @@ describe('Parent/Child account switching', () => { }); }); + /** + * Tests to confirm that Parent account users can switch back from Child accounts as expected. + */ + describe('From Child to Parent', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms that a Child account Proxy user can switch back to a Parent account from Billing page. + * - Confirms that Parent 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 Proxy user back to Parent account user from Billing page', () => { + const mockParentToken = randomString(32); + const mockParentExpiration = DateTime.now().plus({ minutes: 15 }).toISO(); + + mockGetAccount(mockChildAccount); + mockGetProfile(mockChildAccountProfile); + mockGetChildAccounts([]); + mockGetUser(mockChildAccountProxyUser); + interceptGetPayments().as('getPayments'); + interceptGetPaymentMethods().as('getPaymentMethods'); + interceptGetInvoices().as('getInvoices'); + + // Visit billing page with `authentication/parent_token/*` local storage + // data set to mock values. + cy.visitWithLogin('/account/billing', { + localStorageOverrides: { + proxy_user: true, + 'authentication/parent_token/token': `Bearer ${mockParentToken}`, + 'authentication/parent_token/expire': mockParentExpiration, + 'authentication/parent_token/scopes': '*', + }, + }); + + // Wait for page to finish loading before proceeding with account switch. + cy.wait(['@getPayments', '@getPaymentMethods', '@getInvoices']); + + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + + // Prepare mocks in advance of the account switch. As soon as the switch back link is clicked, + // Cloud will replace its stored token with the token provided by the API and then reload. + // From that 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(); + mockGetLinodes([]); + mockGetRegions([]); + mockGetEvents([]); + mockGetNotifications([]); + mockGetAccount(mockParentAccount); + mockGetProfile(mockParentProfile); + mockGetUser(mockParentUser); + mockGetPaymentMethods(paymentMethodFactory.buildList(1)).as( + 'getPaymentMethods' + ); + mockGetInvoices([]).as('getInvoices'); + mockGetPayments([]).as('getPayments'); + + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText('There are no indirect customer accounts.').should( + 'be.visible' + ); + cy.findByText('switch back to your account') + .should('be.visible') + .click(); + }); + + // Allow page to load before asserting user menu, ensuring no app crash, etc. + cy.wait(['@getInvoices', '@getPayments', '@getPaymentMethods']); + + assertUserMenuButton( + mockParentProfile.username, + mockParentAccount.company + ); + + assertAuthLocalStorage(mockParentToken, mockParentExpiration, '*'); + }); + }); + + /** + * Tests to confirm that Proxy users can switch to other Child accounts as expected. + */ + describe('From Child to Child', () => { + /* + * - Confirms that a Child account Proxy user can switch to another Child account from Billing page. + * - Confirms that alternate 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 to another Child account as a Proxy user', () => { + const mockParentToken = randomString(32); + const mockParentExpiration = DateTime.now().plus({ minutes: 15 }).toISO(); + + mockGetAccount(mockChildAccount); + mockGetProfile(mockChildAccountProfile); + mockGetChildAccounts([mockAlternateChildAccount]); + mockGetUser(mockChildAccountProxyUser); + interceptGetPayments().as('getPayments'); + interceptGetPaymentMethods().as('getPaymentMethods'); + interceptGetInvoices().as('getInvoices'); + + // Visit billing page with `authentication/parent_token/*` local storage + // data set to mock values. + cy.visitWithLogin('/account/billing', { + localStorageOverrides: { + proxy_user: true, + 'authentication/parent_token/token': `Bearer ${mockParentToken}`, + 'authentication/parent_token/expire': mockParentExpiration, + 'authentication/parent_token/scopes': '*', + }, + }); + + // Wait for page to finish loading before proceeding with account switch. + cy.wait(['@getPayments', '@getPaymentMethods', '@getInvoices']); + + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + + // Prepare mocks in advance of the account switch. As soon as the child account is clicked, + // Cloud will replace its stored token with the token provided by the API and then reload. + // From that 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(); + mockGetLinodes([]); + mockGetRegions([]); + mockGetEvents([]); + mockGetNotifications([]); + mockGetAccount(mockAlternateChildAccount); + mockGetProfile(mockAlternateChildAccountProfile); + mockGetUser(mockAlternateChildAccountProxyUser); + mockGetPaymentMethods(paymentMethodFactory.buildList(1)).as( + 'getPaymentMethods' + ); + mockGetInvoices([]).as('getInvoices'); + mockGetPayments([]).as('getPayments'); + + // Set up account switch mock. + mockCreateChildAccountToken( + mockAlternateChildAccount, + mockAlternateChildAccountToken + ).as('switchAccount'); + + // Click mock company name in "Switch Account" drawer. + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + cy.findByText(mockAlternateChildAccount.company) + .should('be.visible') + .click(); + }); + + // Allow page to load before asserting user menu, ensuring no app crash, etc. + cy.wait('@switchAccount'); + cy.wait(['@getInvoices', '@getPayments', '@getPaymentMethods']); + + assertUserMenuButton( + mockAlternateChildAccountProfile.username, + mockAlternateChildAccount.company + ); + + assertAuthLocalStorage( + mockAlternateChildAccountToken.token!, + mockAlternateChildAccountToken.expiry!, + mockAlternateChildAccountToken.scopes + ); + // TODO Confirm whether toast is intended here. + // ui.toast.assertMessage( + // `Account switched to ${mockAlternateChildAccount.company}.` + // ); + }); + }); + describe('Child Account Access', () => { /* * - Smoke test to confirm that restricted parent users with the child_account_access grant can switch accounts. diff --git a/packages/manager/cypress/support/intercepts/account.ts b/packages/manager/cypress/support/intercepts/account.ts index 5d4fb4d5cd7..4d51f05b620 100644 --- a/packages/manager/cypress/support/intercepts/account.ts +++ b/packages/manager/cypress/support/intercepts/account.ts @@ -363,6 +363,15 @@ export const mockUpdateUsername = ( }); }; +/** + * Intercepts GET request to retrieve account payment methods. + * + * @returns Cypress chainable. + */ +export const interceptGetPaymentMethods = (): Cypress.Chainable => { + return cy.intercept('GET', apiMatcher('account/payment-methods*')); +}; + /** * Intercepts GET request to retrieve account payment methods and mocks response. * @@ -412,6 +421,15 @@ export const mockGetInvoice = (invoice: Invoice): Cypress.Chainable => { ); }; +/** + * Intercepts GET request to fetch account invoices. + * + * @returns Cypress chainable. + */ +export const interceptGetInvoices = (): Cypress.Chainable => { + return cy.intercept('GET', apiMatcher('account/invoices*')); +}; + /** * Intercepts GET request to fetch account invoices and mocks response. * @@ -448,6 +466,15 @@ export const mockGetInvoiceItems = ( ); }; +/** + * Intercepts GET request to fetch account payments. + * + * @returns Cypress chainable. + */ +export const interceptGetPayments = (): Cypress.Chainable => { + return cy.intercept('GET', apiMatcher('account/payments*')); +}; + /** * Intercepts GET request to fetch account payments and mocks response. *