diff --git a/packages/manager/.changeset/pr-9807-tests-1697570775779.md b/packages/manager/.changeset/pr-9807-tests-1697570775779.md new file mode 100644 index 00000000000..fd6ce440327 --- /dev/null +++ b/packages/manager/.changeset/pr-9807-tests-1697570775779.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Refactor Cypress Firewall migration tests ([#9807](https://github.com/linode/manager/pull/9807)) diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index b20e44d9b1f..d4d76a052a7 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -1,52 +1,68 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { linodeFactory } from '@src/factories'; +import { + createLinodeRequestFactory, + firewallFactory, + linodeFactory, + regionFactory, +} from '@src/factories'; import { authenticate } from 'support/api/authentication'; -import { createLinode } from 'support/api/linodes'; +import { createLinode } from '@linode/api-v4'; +import { + interceptCreateFirewall, + interceptGetFirewalls, + mockGetFirewalls, +} from 'support/intercepts/firewalls'; import { - getClick, - containsClick, - fbtClick, - getVisible, - fbtVisible, - containsVisible, -} from 'support/helpers'; -import { mockGetLinodeDetails } from 'support/intercepts/linodes'; + interceptMigrateLinode, + mockGetLinodeDetails, + mockMigrateLinode, +} from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { selectRegionString } from 'support/ui/constants'; import { cleanUp } from 'support/util/cleanup'; -import { apiMatcher } from 'support/util/intercepts'; -import { randomString } from 'support/util/random'; - -const fakeRegionsData = { - data: [ - { - capabilities: ['Linodes', 'NodeBalancers', 'Block Storage'], - country: 'us', - id: 'us-central', - status: 'ok', - label: 'Dallas, TX', - }, - { - capabilities: ['Linodes', 'NodeBalancers', 'Block Storage'], - country: 'uk', - id: 'eu-west', - status: 'ok', - label: 'London, UK', - }, - { - capabilities: [ - 'Linodes', - 'NodeBalancers', - 'Block Storage', - 'Cloud Firewall', - ], - country: 'sg', - id: 'ap-south', - status: 'ok', - label: 'Singapore, SG', - }, - ], -}; +import { randomLabel, randomNumber } from 'support/util/random'; +import type { Linode, Region } from '@linode/api-v4'; +import { chooseRegions } from 'support/util/regions'; + +const mockRegions: Region[] = [ + regionFactory.build({ + capabilities: ['Linodes', 'NodeBalancers', 'Block Storage'], + id: 'us-central', + status: 'ok', + label: 'Dallas, TX', + }), + regionFactory.build({ + capabilities: ['Linodes', 'NodeBalancers', 'Block Storage'], + country: 'uk', + id: 'eu-west', + status: 'ok', + label: 'London, UK', + }), + regionFactory.build({ + capabilities: [ + 'Linodes', + 'NodeBalancers', + 'Block Storage', + 'Cloud Firewall', + ], + country: 'sg', + id: 'ap-south', + status: 'ok', + label: 'Singapore, SG', + }), +]; + +// Migration notes and warnings that are shown to the user. +// We want to confirm that these are displayed so that users are not surprised +// by migration side effects. +const migrationNoticeSubstrings = [ + 'assigned new IPv4 and IPv6 addresses', + 'existing backups with the Linode Backup Service will not be migrated', + 'DNS records (including Reverse DNS) will need to be updated', + 'attached VLANs will be inaccessible if the destination region does not support VLANs', + 'Your Linode will be powered off.', +]; authenticate(); describe('Migrate Linode With Firewall', () => { @@ -54,196 +70,131 @@ describe('Migrate Linode With Firewall', () => { cleanUp('firewalls'); }); + /* + * - Tests Linode migration flow for Linodes with Firewalls using mock API data. + * - Confirms that user is warned of migration consequences. + */ it('test migrate flow - mocking all data', () => { - const fakeLinodeId = 9999; - const fakeFirewallId = 6666; - - // modify incoming response - cy.intercept(apiMatcher('networking/firewalls*'), (req) => { - req.reply((res) => { - res.send({ - data: [ - { - id: fakeFirewallId, - label: 'test', - created: '2020-08-03T15:49:50', - updated: '2020-08-03T15:49:50', - status: 'enabled', - rules: { - inbound: [ - { - ports: '80', - protocol: 'TCP', - addresses: { ipv4: ['0.0.0.0/0'], ipv6: ['::/0'] }, - }, - ], - outbound: [ - { - ports: '80', - protocol: 'TCP', - addresses: { ipv4: ['0.0.0.0/0'], ipv6: ['::/0'] }, - }, - ], - }, - tags: [], - devices: { linodes: [fakeLinodeId] }, - }, - ], - page: 1, - pages: 1, - results: 1, - }); - }); - }).as('getFirewalls'); - - const fakeLinodeData = linodeFactory.build({ - id: fakeLinodeId, - label: 'debian-us-central', - group: '', - status: 'running', - created: '2020-06-23T16:02:14', - updated: '2020-06-23T16:05:23', - type: 'g6-standard-1', - ipv4: ['104.237.129.173'], - ipv6: '2600:3c00::f03c:92ff:feeb:98f9/64', - image: 'linode/debian10', + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), region: 'us-central', - specs: { - disk: 51200, - memory: 2048, - vcpus: 1, - gpus: 0, - transfer: 2000, - }, - alerts: { - cpu: 90, - network_in: 10, - network_out: 10, - transfer_quota: 80, - io: 10000, - }, - backups: { - enabled: true, - schedule: { day: 'Scheduling', window: 'Scheduling' }, - last_successful: '2020-08-02T22:26:19', - }, - hypervisor: 'kvm', - watchdog_enabled: true, - tags: [], }); - // modify incoming response - cy.intercept(apiMatcher('regions*'), (req) => { - req.reply((res) => { - res.send(fakeRegionsData); - }); - }).as('getRegions'); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + status: 'enabled', + devices: { + linodes: [mockLinode.id], + }, + }); - // intercept request and stub it, respond with 200 - cy.intercept( - 'POST', - apiMatcher(`linode/instances/${fakeLinodeId}/migrate`), - { - statusCode: 200, - } - ).as('migrateReq'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); + mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); + mockGetRegions(mockRegions).as('getRegions'); + mockMigrateLinode(mockLinode.id).as('migrateLinode'); - // modify incoming response - mockGetLinodeDetails(fakeLinodeId, fakeLinodeData).as('getLinode'); + cy.visitWithLogin(`/linodes/${mockLinode.id}/migrate`); + cy.wait(['@getLinode', '@getRegions']); + cy.findByText('Dallas, TX').should('be.visible'); - // modify incoming response - cy.intercept( - 'GET', - apiMatcher(`linode/instances/${fakeLinodeId}/migrate`), - (req) => { - req.reply((res) => { - res.send(fakeLinodeData); + ui.dialog + .findByTitle(`Migrate Linode ${mockLinode.label} to another region`) + .should('be.visible') + .within(() => { + // Confirm that 'Enter Migration Queue' button is disabled. + ui.button + .findByTitle('Enter Migration Queue') + .should('be.visible') + .should('be.disabled'); + + // Confirm that user is warned of Migration side effects. + cy.findByText('Caution:').should('be.visible'); + migrationNoticeSubstrings.forEach((noticeSubstring: string) => { + cy.contains(noticeSubstring).should('be.visible'); }); - } - ).as('getLinode'); - cy.visitWithLogin(`/linodes/${fakeLinodeId}/migrate`); - cy.wait('@getLinode'); - cy.wait('@getRegions'); - cy.findByText('Dallas, TX').should('be.visible'); - getClick('[data-qa-checked="false"]'); - cy.findByText(`North America: Dallas, TX`).should('be.visible'); - containsClick(selectRegionString); + // Click the "Accept" check box. + cy.findByText('Accept').should('be.visible').click(); - ui.regionSelect.findItemByRegionLabel('Singapore, SG').click(); + // Select migration region. + cy.findByText(`North America: Dallas, TX`).should('be.visible'); + cy.contains(selectRegionString).click(); + ui.regionSelect.findItemByRegionLabel('Singapore, SG').click(); - fbtClick('Enter Migration Queue'); - cy.wait('@migrateReq').its('response.statusCode').should('eq', 200); + ui.button + .findByTitle('Enter Migration Queue') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@migrateLinode').its('response.statusCode').should('eq', 200); }); - // create linode w/ firewall region then add firewall to it then attempt to migrate linode to non firewall region, should fail + /* + * - Uses real API data to create a Firewall, attach a Linode to it, then migrate the Linode. + */ it('migrates linode with firewall - real data', () => { - const validateMigration = () => { - ui.button - .findByTitle('Enter Migration Queue') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.wait('@migrateLinode').its('response.statusCode').should('eq', 200); - }; - - const firewallLabel = `cy-test-firewall-${randomString(5)}`; - // intercept create firewall request - cy.intercept('POST', apiMatcher('networking/firewalls')).as( - 'createFirewall' - ); - // modify incoming response - cy.intercept(apiMatcher('networking/firewalls*')).as('getFirewalls'); - - cy.visitWithLogin('/firewalls'); - - createLinode({ region: 'ap-southeast' }).then((linode) => { - // intercept migrate linode request - cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linode.id}/migrate`) - ).as('migrateLinode'); - - getVisible('[data-qa-header]').within(() => { - fbtVisible('Firewalls'); - }); - - cy.wait('@getFirewalls').then(({ response }) => { - const length = response?.body.data['length']; - console.log(`THIS: ${length}`); - getVisible('[data-qa-header]').within(() => { - fbtVisible('Firewalls'); - }); - fbtClick('Create Firewall'); - }); + const [migrationRegionStart, migrationRegionEnd] = chooseRegions(2); + const firewallLabel = randomLabel(); + const linodePayload = createLinodeRequestFactory.build({ + label: randomLabel(), + region: migrationRegionStart.id, + }); - cy.get('[data-testid="textfield-input"]:first') - .should('be.visible') - .type(firewallLabel); + interceptCreateFirewall().as('createFirewall'); + interceptGetFirewalls().as('getFirewalls'); - cy.get('[data-testid="textfield-input"]:last') - .should('be.visible') - .click() - .type(linode.label); + // Create a Linode, then navigate to the Firewalls landing page. + cy.defer(createLinode(linodePayload)).then((linode: Linode) => { + interceptMigrateLinode(linode.id).as('migrateLinode'); + cy.visitWithLogin('/firewalls'); + cy.wait('@getFirewalls'); - cy.get('[data-qa-autocomplete-popper]') - .findByText(linode.label) + ui.button + .findByTitle('Create Firewall') .should('be.visible') + .should('be.enabled') .click(); - cy.get('[data-testid="textfield-input"]:last') + ui.drawer + .findByTitle('Create Firewall') .should('be.visible') - .click(); - - cy.findByText(linode.label).should('be.visible'); + .within(() => { + cy.findByText('Label') + .should('be.visible') + .click() + .type(firewallLabel); + + cy.findByText('Linodes') + .should('be.visible') + .click() + .type(linode.label); + + ui.autocompletePopper + .findByTitle(linode.label) + .should('be.visible') + .click(); + + // Click on the Select again to dismiss the autocomplete popper. + cy.findByLabelText('Linodes').should('be.visible').click(); + + ui.buttonGroup + .findButtonByTitle('Create Firewall') + .should('be.visible') + .should('be.enabled') + .click(); + }); - getClick('[data-qa-submit="true"]'); cy.wait('@createFirewall'); - cy.visit(`/linodes/${linode.id}`); - getVisible('[data-qa-link-text="true"]').within(() => { - fbtVisible('linodes'); - }); + cy.visitWithLogin(`/linodes/${linode.id}`); + cy.get('[data-qa-link-text="true"]') + .should('be.visible') + .within(() => { + cy.findByText('linodes').should('be.visible'); + }); // Make sure Linode is running before attempting to migrate. cy.get('[data-qa-linode-status]').within(() => { @@ -257,56 +208,28 @@ describe('Migrate Linode With Firewall', () => { ui.actionMenuItem.findByTitle('Migrate').should('be.visible').click(); - containsVisible(`Migrate Linode ${linode.label}`); - getClick('[data-qa-checked="false"]'); - fbtClick(selectRegionString); - - ui.regionSelect.findItemByRegionLabel('Toronto, CA').click(); - validateMigration(); - }); - }); - - it('adds linode to firewall - real data', () => { - const firewallLabel = `cy-test-firewall-${randomString(5)}`; - // intercept firewall requests - cy.intercept('POST', apiMatcher('networking/firewalls')).as( - 'createFirewall' - ); - cy.intercept(apiMatcher('networking/firewalls*')).as('getFirewall'); - - cy.visitWithLogin('/firewalls'); - createLinode().then((linode) => { - const linodeLabel = linode.label; - cy.wait('@getFirewall').then(() => { - getVisible('[data-qa-header]').within(() => { - fbtVisible('Firewalls'); - }); - fbtClick('Create Firewall'); - }); - - cy.get('[data-testid="textfield-input"]:first') - .should('be.visible') - .type(firewallLabel); - - cy.get('[data-testid="textfield-input"]:last') - .should('be.visible') - .click() - .type(linodeLabel); - - cy.get('[data-qa-autocomplete-popper]') - .findByText(linode.label) + ui.dialog + .findByTitle(`Migrate Linode ${linode.label} to another region`) .should('be.visible') - .click(); - - cy.get('[data-testid="textfield-input"]:last') - .should('be.visible') - .click(); - - cy.findByText(linodeLabel).should('be.visible'); + .within(() => { + // Click "Accept" check box. + cy.findByText('Accept').should('be.visible').click(); + + // Select region for migration. + cy.findByText(selectRegionString).click(); + ui.regionSelect + .findItemByRegionLabel(migrationRegionEnd.label) + .click(); + + // Initiate migration. + ui.button + .findByTitle('Enter Migration Queue') + .should('be.visible') + .should('be.enabled') + .click(); + }); - getClick('[data-qa-submit="true"]'); - cy.wait('@createFirewall').its('response.statusCode').should('eq', 200); - fbtVisible(linodeLabel); + cy.wait('@migrateLinode').its('response.statusCode').should('eq', 200); }); }); }); diff --git a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts index 1f2af011634..a763eb5a833 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -1,10 +1,11 @@ -import { - createLinode, - createFirewall, +import type { Linode, Firewall, FirewallRuleType, + CreateLinodeRequest, + CreateFirewallPayload, } from '@linode/api-v4'; +import { createLinode, createFirewall } from '@linode/api-v4'; import { createLinodeRequestFactory, firewallFactory, @@ -12,9 +13,8 @@ import { linodeFactory, } from 'src/factories'; import { authenticate } from 'support/api/authentication'; -import { containsClick, getClick } from 'support/helpers'; +import { containsClick } from 'support/helpers'; import { - interceptCreateFirewall, interceptUpdateFirewallLinodes, interceptUpdateFirewallRules, } from 'support/intercepts/firewalls'; @@ -46,10 +46,6 @@ const outboundRule = firewallRuleFactory.build({ ports: randomItem(Object.keys(portPresetMap)), }); -const firewall = firewallFactory.build({ - label: randomLabel(), -}); - /** * Adds an inbound / outbound firewall rule. * @@ -154,8 +150,8 @@ const addLinodesToFirewall = (firewall: Firewall, linode: Linode) => { }; const createLinodeAndFirewall = async ( - linodeRequestPayload, - firewallRequestPayload + linodeRequestPayload: CreateLinodeRequest, + firewallRequestPayload: CreateFirewallPayload ) => { return Promise.all([ createLinode(linodeRequestPayload), diff --git a/packages/manager/cypress/support/intercepts/firewalls.ts b/packages/manager/cypress/support/intercepts/firewalls.ts index 2ac4d93c69b..6067e386c7b 100644 --- a/packages/manager/cypress/support/intercepts/firewalls.ts +++ b/packages/manager/cypress/support/intercepts/firewalls.ts @@ -1,8 +1,35 @@ /** - * @files Cypress intercepts and mocks for Firewall API requests. + * @file Cypress intercepts and mocks for Firewall API requests. */ - +import type { Firewall } from '@linode/api-v4'; import { apiMatcher } from 'support/util/intercepts'; +import { paginateResponse } from 'support/util/paginate'; + +/** + * Intercepts GET request to fetch Firewalls. + * + * @returns Cypress chainable. + */ +export const interceptGetFirewalls = (): Cypress.Chainable => { + return cy.intercept('GET', apiMatcher('networking/firewalls*')); +}; + +/** + * Intercepts GET request to fetch Firewalls and mocks response. + * + * @param firewalls - Array of Firewalls with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetFirewalls = ( + firewalls: Firewall[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('networking/firewalls*'), + paginateResponse(firewalls) + ); +}; /** * Intercepts POST request to create a Firewall. diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 18489f6ba96..4a873a8333a 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -299,6 +299,22 @@ export const interceptCreateLinodeSnapshot = ( ); }; +/** + * Intercepts POST request to migrate a Linode. + * + * @param linodeId - ID of Linode being migrated. + * + * @returns Cypress chainable. + */ +export const interceptMigrateLinode = ( + linodeId: number +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linodeId}/migrate`) + ); +}; + /** * Intercepts POST request to migrate a Linode. * diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index 72be7099e32..52d4af35180 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -80,9 +80,9 @@ export const getRegionByLabel = (label: string) => { * Returns a known Cloud Manager region at random, or returns a user-chosen * region if one was specified. * - * Region selection can be configured via the `CY_TEST_REGION_ID` and - * `CY_TEST_REGION_NAME` environment variables. Both must be specified in - * order to override the region that is returned by this function. + * Region selection can be configured via the `CY_TEST_REGION` environment + * variable. If defined, the region returned by this function will be + * overridden using the chosen region. * * @returns Object describing a Cloud Manager region to use during tests. */ @@ -91,6 +91,50 @@ export const chooseRegion = (): Region => { return overrideRegion ? overrideRegion : randomItem(regions); }; +/** + * Returns an array of unique Cloud Manager regions at random. + * + * If an override region is defined via the `CY_TEST_REGION` environment + * variable, the first item in the array will be the override region, and + * subsequent items will be chosen at random. + * + * @param count - Number of Regions to include in the returned array. + * + * @throws When `count` is less than 0. + * @throws When there are not enough regions to satisfy the given `count`. + * + * @returns Array of Cloud Manager Region objects. + */ +export const chooseRegions = (count: number): Region[] => { + if (count < 0) { + throw new Error( + 'Unable to choose regions. The desired number of regions must be 0 or greater' + ); + } + if (regions.length < count) { + throw new Error( + `Unable to choose regions. The desired number of regions exceeds the number of known regions (${regions.length})` + ); + } + const overrideRegion = getOverrideRegion(); + + return new Array(count).fill(null).reduce((acc: Region[], _cur, index) => { + const chosenRegion: Region = ((): Region => { + if (index === 0 && overrideRegion) { + return overrideRegion; + } + // Get an array of regions that have not already been selected. + const unusedRegions = regions.filter( + (regionA: Region) => + !!regions.find((regionB: Region) => regionA.id !== regionB.id) + ); + return randomItem(unusedRegions); + })(); + acc.push(chosenRegion); + return acc; + }, []); +}; + /** * Executes a test for each Linode region exposed to Cypress. */