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-7053] - Add Cypress integration test for VPC create flow #9730

Merged
Merged
Show file tree
Hide file tree
Changes from 6 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
271 changes: 271 additions & 0 deletions packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/**
* @file Integration tests for VPC create flow.
*/

import type { Subnet, VPC } from '@linode/api-v4';
import { vpcFactory, subnetFactory, linodeFactory } from '@src/factories';
import {
mockAppendFeatureFlags,
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import {
mockCreateVPCError,
mockCreateVPC,
mockDeleteSubnet,
mockGetVPC,
mockGetSubnets,
} from 'support/intercepts/vpc';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import {
randomLabel,
randomPhrase,
randomIp,
randomNumber,
randomString,
} from 'support/util/random';
import { chooseRegion } from 'support/util/regions';
import { ui } from 'support/ui';
import { buildArray } from 'support/util/arrays';

/**
* Gets the "Add a Subnet" section with the given index.
*
* @returns Cypress chainable.
*/
const getSubnetNodeSection = (index: number) => {
return cy.get(`[data-qa-subnet-node="${index}"]`);
};

describe('VPC create flow', () => {
/*
* - Confirms VPC creation flow using mock API data.
* - Confirms that users can create and delete subnets.
* - Confirms client side validation when entering invalid IP ranges.
* - Confirms that UI handles API errors gracefully.
* - Confirms that UI redirects to created VPC page after creating a VPC.
*/
it('can create a VPC', () => {
const vpcRegion = chooseRegion();

const mockSubnets: Subnet[] = buildArray(3, (index: number) => {
return subnetFactory.build({
label: randomLabel(),
id: randomNumber(10000, 99999),
ipv4: `${randomIp()}/${randomNumber(0, 32)}`,
linodes: linodeFactory.buildList(index + 1),
});
});

const mockSubnetToDelete: Subnet = subnetFactory.build();
const mockInvalidIpRange = `${randomIp()}/${randomNumber(33, 100)}`;

const mockVpc: VPC = vpcFactory.build({
id: randomNumber(10000, 99999),
label: randomLabel(),
region: vpcRegion.id,
description: randomPhrase(),
subnets: mockSubnets,
});

const ipValidationErrorMessage = 'The IPv4 range must be in CIDR format';
const vpcCreationErrorMessage = 'An unknown error has occurred.';
const totalSubnetUniqueLinodes = mockSubnets.reduce(
(acc: number, cur: Subnet) => acc + cur.linodes.length,
0
);
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a function to calculate this called getUniqueLinodesFromSubnets

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 @hana-linode! I'll take a look at that and see if I can use it from the test


mockAppendFeatureFlags({
vpc: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientstream');

cy.visitWithLogin('/vpcs/create');
cy.wait(['@getFeatureFlags', '@getClientstream']);

cy.findByText('Region')
.should('be.visible')
.click()
.type(`${vpcRegion.label}{enter}`);

cy.findByText('VPC label').should('be.visible').click().type(mockVpc.label);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should "VPC label" be "VPC Label" to be more consistent with other resource create flows? (This question also applies to "Subnet label" vs "Subnet Label" later in the flow)

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense to me

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 makes sense to me too -- I'll ask UX to double check :D

Copy link
Contributor

Choose a reason for hiding this comment

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

confirmed that label should be capitalized :D

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 for confirming @coliu-akamai! I'll make those changes in this PR if that works for everyone 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

thank you!


cy.findByText('Description')
.should('be.visible')
.click()
.type(mockVpc.description);

// Fill out the first Subnet.
// Insert an invalid empty IP range to confirm client side validation.
getSubnetNodeSection(0)
.should('be.visible')
.within(() => {
cy.findByText('Subnet label')
.should('be.visible')
.click()
.type(mockSubnets[0].label);

cy.findByText('Subnet IP Address Range')
.should('be.visible')
.click()
.type(`{selectAll}{backspace}`);
});

ui.button
.findByTitle('Create VPC')
.should('be.visible')
.should('be.enabled')
.click();

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

// Enter a random non-IP address string to further test client side validation.
cy.findByText('Subnet IP Address Range')
.should('be.visible')
.click()
.type(`{selectAll}{backspace}`)
.type(randomString(18));

ui.button
.findByTitle('Create VPC')
.should('be.visible')
.should('be.enabled')
.click();

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

// Enter a valid IP address with an invalid network prefix to further test client side validation.
cy.findByText('Subnet IP Address Range')
.should('be.visible')
.click()
.type(`{selectAll}{backspace}`)
.type(mockInvalidIpRange);

ui.button
.findByTitle('Create VPC')
.should('be.visible')
.should('be.enabled')
.click();

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

// Replace invalid IP address range with valid range.
cy.findByText('Subnet IP Address Range')
.should('be.visible')
.click()
.type(`{selectAll}{backspace}`)
.type(mockSubnets[0].ipv4!);

// Add another subnet that we will remove later.
ui.button
.findByTitle('Add a Subnet')
.should('be.visible')
.should('be.enabled')
.click();

// Fill out subnet section, but leave label blank, then attempt to create
// VPC with missing subnet label.
getSubnetNodeSection(1)
.should('be.visible')
.within(() => {
cy.findByText('Subnet IP Address Range')
.should('be.visible')
.click()
.type(`{selectAll}{backspace}`)
.type(mockSubnetToDelete.ipv4!);
});

ui.button
.findByTitle('Create VPC')
.should('be.visible')
.should('be.enabled')
.click();

// Confirm that label validation message is displayed, then remove the
// subnet and confirm that UI responds accordingly.
getSubnetNodeSection(1)
.should('be.visible')
.within(() => {
cy.findByText('Label is required').should('be.visible');

// Delete subnet.
cy.findByLabelText('Remove Subnet')
.should('be.visible')
.should('be.enabled')
.click();
});

getSubnetNodeSection(1).should('not.exist');
cy.findByText(mockSubnetToDelete.label).should('not.exist');

// Continue adding remaining subnets.
mockSubnets.slice(1).forEach((mockSubnet: Subnet, index: number) => {
ui.button
.findByTitle('Add a Subnet')
.should('be.visible')
.should('be.enabled')
.click();

getSubnetNodeSection(index + 1)
.should('be.visible')
.within(() => {
cy.findByText('Subnet label')
.should('be.visible')
.click()
.type(mockSubnet.label);

cy.findByText('Subnet IP Address Range')
.should('be.visible')
.click()
.type(`{selectAll}{backspace}`)
.type(`${randomIp()}/${randomNumber(0, 32)}`);
});
});

// Click "Create VPC", mock an HTTP 500 error and confirm UI displays the message.
mockCreateVPCError(vpcCreationErrorMessage).as('createVPC');
ui.button
.findByTitle('Create VPC')
.should('be.visible')
.should('be.enabled')
.click();

cy.wait('@createVPC');
cy.findByText(vpcCreationErrorMessage).should('be.visible');

// Click "Create VPC", mock a successful response and confirm that Cloud
// redirects to the VPC details page for the new VPC.
mockCreateVPC(mockVpc).as('createVPC');
mockGetSubnets(mockVpc.id, mockVpc.subnets).as('getSubnets');
ui.button
.findByTitle('Create VPC')
.should('be.visible')
.should('be.enabled')
.click();

cy.wait('@createVPC');
cy.url().should('endWith', `/vpcs/${mockVpc.id}`);
cy.wait('@getSubnets');

// Confirm that new VPC information is displayed on details page as expected.
cy.findByText(mockVpc.label).should('be.visible');
cy.get('[data-qa-vpc-summary]')
.should('be.visible')
.within(() => {
cy.contains(`Subnets ${mockVpc.subnets.length}`).should('be.visible');
cy.contains(`Linodes ${totalSubnetUniqueLinodes}`).should('be.visible');
cy.contains(`VPC ID ${mockVpc.id}`).should('be.visible');
cy.contains(`Region ${vpcRegion.label}`).should('be.visible');
});

mockSubnets.forEach((mockSubnet: Subnet) => {
cy.findByText(mockSubnet.label)
.should('be.visible')
.closest('tr')
.within(() => {
cy.findByText(mockSubnet.id).should('be.visible');
cy.findByText(mockSubnet.ipv4!).should('be.visible');
cy.findByText(mockSubnet.linodes.length).should('be.visible');
});
});
});
});
33 changes: 33 additions & 0 deletions packages/manager/cypress/support/intercepts/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { paginateResponse } from 'support/util/paginate';

import type { Subnet, VPC } from '@linode/api-v4';
import { makeResponse } from 'support/util/response';
import { makeErrorResponse } from 'support/util/errors';

/**
* Intercepts GET request to fetch a VPC and mocks response.
Expand All @@ -30,6 +31,38 @@ export const mockGetVPCs = (vpcs: VPC[]): Cypress.Chainable<null> => {
return cy.intercept('GET', apiMatcher('vpcs*'), paginateResponse(vpcs));
};

/**
* Intercepts POST request to create a VPC and mocks the response.
*
* @param vpc - VPC object with which to mock response.
*
* @returns Cypress chainable.
*/
export const mockCreateVPC = (vpc: VPC): Cypress.Chainable<null> => {
return cy.intercept('POST', apiMatcher('vpcs'), makeResponse(vpc));
};

/**
* Intercepts POST request to create a VPC and mocks an HTTP error response.
*
* By default, a 500 response is mocked.
*
* @param errorMessage - Optional error message with which to mock response.
* @param errorCode - Optional error code with which to mock response. Default is `500`.
*
* @returns Cypress chainable.
*/
export const mockCreateVPCError = (
errorMessage: string = 'An error has occurred',
errorCode: number = 500
): Cypress.Chainable<null> => {
return cy.intercept(
'POST',
apiMatcher('vpcs'),
makeErrorResponse(errorMessage, errorCode)
);
};

/**
* Intercepts PUT request to update a VPC and mocks response.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const MultipleSubnetInput = (props: Props) => {
return (
<Grid>
{subnets.map((subnet, subnetIdx) => (
<Grid key={`subnet-${subnetIdx}`}>
<Grid key={`subnet-${subnetIdx}`} data-qa-subnet-node={subnetIdx}>
{subnetIdx !== 0 && <Divider sx={{ marginTop: theme.spacing(3) }} />}
<SubnetNode
onChange={(subnet, subnetIdx, removable) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const SubnetNode = (props: Props) => {
</Grid>
{isRemovable && !!idx && (
<Grid xs={1}>
<StyledButton onClick={removeSubnet}>
<StyledButton onClick={removeSubnet} aria-label="Remove Subnet">
<Close data-testid={`delete-subnet-${idx}`} />
</StyledButton>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const VPCDetail = () => {
</Box>
</EntityHeader>
<StyledPaper>
<StyledSummaryBox display="flex" flex={1}>
<StyledSummaryBox display="flex" flex={1} data-qa-vpc-summary>
{summaryData.map((col) => {
return (
<Box key={col[0].label} paddingRight={6}>
Expand Down