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-7516] - Cypress tests for Parent β†’ Child account switching flows #10110

Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import {
accountFactory,
appTokenFactory,
profileFactory,
} from '@src/factories';
import { accountUserFactory } from '@src/factories/accountUsers';
import { DateTime } from 'luxon';
import {
mockCreateChildAccountToken,
mockCreateChildAccountTokenError,
mockGetAccount,
mockGetChildAccounts,
mockGetChildAccountsError,
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';

/**
* 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);
};
Comment on lines +48 to +63
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question for @jaalah-akamai or @mjac0bs: does the token value returned by the API already include the Bearer prefix, or do we have logic somewhere that prepends it before saving it in Local Storage?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe it includes the Bearer prefix. It looks like the token first gets set when a session begins and Bearer comes from token_type, which is how it ends up with Bearer in local storage.

And from the API spec:

image

On a related bearer-in-token-related note, I'm not sure that we're supposed to have Bearer when calling getChildAccountPersonalAccessToken here in the switch account drawer. Maybe it was a oversight or I'm missing something and @jaalah-akamai can clarify.

Copy link
Contributor

Choose a reason for hiding this comment

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

On a related bearer-in-token-related note, I'm not sure that we're supposed to have Bearer when calling getChildAccountPersonalAccessToken here in the switch account drawer. Maybe it was a oversight or I'm missing something and @jaalah-akamai can clarify.

This is true - left over probably from one of my iterations βœ…

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @jaalah-akamai and @mjac0bs -- sorry for the slow follow up here.

If I'm understanding your explanation @mjac0bs, it sounds like we're supposed to have the Bearer prefix in local storage but the token that gets stored during the account switching flow does not contain the prefix.

Is there a ticket for this? I can update this test to expect the Bearer prefix but it will fail in the meantime.


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 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', () => {
jdamore-linode marked this conversation as resolved.
Show resolved Hide resolved
beforeEach(() => {
// @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);
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
);
Copy link
Contributor

Choose a reason for hiding this comment

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

We also want to ensure that parent tokens are being created here too

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point, the Switch Account drawer does that here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going to hold off on this for now -- the Cypress tests deal with a real token, so asserting its value will cause it to be logged and recorded.


// 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);
Comment on lines +160 to +162
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also hoping for an extra set of eyes here -- do these mocks accurately reflect what the account, profile, and user API request will look like in this situation?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that mockGetUser is now left over from when we previously had to call */users/:user to retrieve the user_type and it can now be removed.

Otherwise, this seems correct. We'll have the child account once logged in as a proxy user and we'll also need to fetch the parent's username from */profile.


// 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();
Copy link
Contributor Author

@jdamore-linode jdamore-linode Jan 25, 2024

Choose a reason for hiding this comment

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

cc @jaalah-akamai @mjac0bs -- I think this relates to this discussion in the account switching PR.

Cloud definitely isn't responding as expected upon updating the Local Storage auth values, so a page reload is currently necessary on the Cypress (i.e. user) side to trigger the change. I'm guessing if we want to avoid an automatic page reload, it'll involve at least resetting Cloud's Axios interceptors, resetting React Query, and maybe doing something with our Redux state?

Either way, this test can be used to develop/troubleshoot this functionality without having to wait for anything from the API -- just remove these calls to cy.reload()


// Confirm expected username and company are shown in user menu button.
assertUserMenuButton(
mockParentProfile.username,
mockChildAccount.company
);
});

/*
* - 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);
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
);
});
});

/*
* 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);
mockGetChildAccountsError('An unknown error has occurred', 500);
mockGetUser(mockParentUser);

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');
});
});
});
});
78 changes: 78 additions & 0 deletions packages/manager/cypress/support/intercepts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
InvoiceItem,
Payment,
PaymentMethod,
Token,
User,
} from '@linode/api-v4';

Expand Down Expand Up @@ -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.
*/
Expand All @@ -479,3 +481,79 @@ 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<null> => {
return cy.intercept(
'GET',
apiMatcher('account/child-accounts*'),
paginateResponse(childAccounts)
);
};

/**
* 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.
*
* @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<null> => {
return cy.intercept(
'POST',
apiMatcher(`account/child-accounts/${childAccount.euuid}/token`),
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<null> => {
return cy.intercept(
'POST',
apiMatcher(`account/child-accounts/${childAccount.euuid}/token`),
makeErrorResponse(errorMessage, statusCode)
);
};
Loading
Loading