From 78a9cbc707b06852fcd739e0e655e3ec8e49cd32 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 21 May 2024 15:19:17 -0400 Subject: [PATCH 01/12] Disable booting of test Linodes by default --- packages/manager/cypress/support/util/linodes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index 68d33007a7b..b5436c386b6 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -51,6 +51,7 @@ export const createTestLinode = async ( label: randomLabel(), image: 'linode/debian11', region: chooseRegion().id, + booted: false, }), ...(createRequestPayload || {}), }; From d81fe87dbcab8d16ba86d21052a67002afea1cbf Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 21 May 2024 15:21:14 -0400 Subject: [PATCH 02/12] Use `createTestLinode` to create Linodes, remove deprecated util --- .../e2e/core/account/service-transfer.spec.ts | 5 +- .../core/firewalls/create-firewall.spec.ts | 80 ++++----- .../migrate-linode-with-firewall.spec.ts | 9 +- .../e2e/core/linodes/backup-linode.spec.ts | 6 +- .../e2e/core/linodes/clone-linode.spec.ts | 6 +- .../e2e/core/linodes/linode-config.spec.ts | 6 +- .../e2e/core/linodes/linode-storage.spec.ts | 12 +- .../e2e/core/linodes/rebuild-linode.spec.ts | 9 +- .../e2e/core/linodes/rescue-linode.spec.ts | 4 +- .../e2e/core/linodes/resize-linode.spec.ts | 164 +++++++++--------- .../core/linodes/smoke-delete-linode.spec.ts | 12 +- .../core/linodes/switch-linode-state.spec.ts | 8 +- .../core/linodes/update-linode-labels.spec.ts | 11 +- .../e2e/core/longview/longview.spec.ts | 5 +- .../smoke-create-nodebal.spec.ts | 63 ++++--- .../stackscripts/create-stackscripts.spec.ts | 7 +- .../smoke-community-stackscrips.spec.ts | 2 +- .../e2e/core/volumes/attach-volume.spec.ts | 7 +- .../e2e/core/volumes/create-volume.spec.ts | 98 ++++++----- .../create-machine-image-from-linode.spec.ts | 35 +--- .../e2e/region/linodes/delete-linode.spec.ts | 5 +- .../e2e/region/linodes/update-linode.spec.ts | 9 +- .../manager/cypress/support/api/linodes.ts | 49 +----- 23 files changed, 290 insertions(+), 322 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 7a71dfe0fb9..60f73cd436e 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -2,7 +2,6 @@ * @file Tests for service transfer functionality between accounts. */ -import { createLinode } from '@linode/api-v4/lib/linodes'; import { getProfile } from '@linode/api-v4/lib/profile'; import { EntityTransfer, Linode, Profile } from '@linode/api-v4'; import { entityTransferFactory } from 'src/factories/entityTransfers'; @@ -19,6 +18,7 @@ import { } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; +import { createTestLinode } from 'support/util/linodes'; import { pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomUuid } from 'support/util/random'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; @@ -249,9 +249,10 @@ describe('Account service transfers', () => { const payload = createLinodeRequestFactory.build({ label: randomLabel(), region: chooseRegion().id, + booted: true, }); - const linode: Linode = await createLinode(payload); + const linode: Linode = await createTestLinode(payload); await pollLinodeStatus(linode.id, 'running', { initialDelay: 15000, }); diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index 43a92c8c3ca..fdf73697611 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -1,7 +1,6 @@ -import { createLinode } from '@linode/api-v4/lib/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { createLinodeRequestFactory } from 'src/factories/linodes'; import { authenticate } from 'support/api/authentication'; -import { containsClick, getClick } from 'support/helpers'; import { interceptCreateFirewall } from 'support/intercepts/firewalls'; import { randomString, randomLabel } from 'support/util/random'; import { ui } from 'support/ui'; @@ -33,10 +32,10 @@ describe('create firewall', () => { .should('be.visible') .within(() => { // An error message appears when attempting to create a Firewall without a label - getClick('[data-testid="submit"]'); + cy.get('[data-testid="submit"]').click(); cy.findByText('Label is required.'); // Fill out and submit firewall create form. - containsClick('Label').type(firewall.label); + cy.contains('Label').click().type(firewall.label); ui.buttonGroup .findButtonByTitle('Create Firewall') .should('be.visible') @@ -69,52 +68,55 @@ describe('create firewall', () => { label: randomLabel(), region: region.id, root_pass: randomString(16), + booted: false, }); const firewall = { label: randomLabel(), }; - cy.defer(createLinode(linodeRequest), 'creating Linode').then((linode) => { - interceptCreateFirewall().as('createFirewall'); - cy.visitWithLogin('/firewalls/create'); + cy.defer(createTestLinode(linodeRequest), 'creating Linode').then( + (linode) => { + interceptCreateFirewall().as('createFirewall'); + cy.visitWithLogin('/firewalls/create'); - ui.drawer - .findByTitle('Create Firewall') - .should('be.visible') - .within(() => { - // Fill out and submit firewall create form. - containsClick('Label').type(firewall.label); - cy.findByLabelText('Linodes') - .should('be.visible') - .click() - .type(linode.label); + ui.drawer + .findByTitle('Create Firewall') + .should('be.visible') + .within(() => { + // Fill out and submit firewall create form. + cy.contains('Label').click().type(firewall.label); + cy.findByLabelText('Linodes') + .should('be.visible') + .click() + .type(linode.label); - ui.autocompletePopper - .findByTitle(linode.label) - .should('be.visible') - .click(); + ui.autocompletePopper + .findByTitle(linode.label) + .should('be.visible') + .click(); - cy.findByLabelText('Linodes').should('be.visible').click(); + cy.findByLabelText('Linodes').should('be.visible').click(); - ui.buttonGroup - .findButtonByTitle('Create Firewall') - .should('be.visible') - .should('be.enabled') - .click(); - }); + ui.buttonGroup + .findButtonByTitle('Create Firewall') + .should('be.visible') + .should('be.enabled') + .click(); + }); - cy.wait('@createFirewall'); + cy.wait('@createFirewall'); - // Confirm that firewall is listed on landing page with expected configuration. - cy.findByText(firewall.label) - .closest('tr') - .within(() => { - cy.findByText(firewall.label).should('be.visible'); - cy.findByText('Enabled').should('be.visible'); - cy.findByText('No rules').should('be.visible'); - cy.findByText(linode.label).should('be.visible'); - }); - }); + // Confirm that firewall is listed on landing page with expected configuration. + cy.findByText(firewall.label) + .closest('tr') + .within(() => { + cy.findByText(firewall.label).should('be.visible'); + cy.findByText('Enabled').should('be.visible'); + cy.findByText('No rules').should('be.visible'); + cy.findByText(linode.label).should('be.visible'); + }); + } + ); }); }); 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 8763f5bd8fe..711649a88d0 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 @@ -6,7 +6,6 @@ import { regionFactory, } from '@src/factories'; import { authenticate } from 'support/api/authentication'; -import { createLinode } from '@linode/api-v4'; import { interceptCreateFirewall, interceptGetFirewalls, @@ -23,6 +22,7 @@ import { cleanUp } from 'support/util/cleanup'; import { randomLabel, randomNumber } from 'support/util/random'; import type { Linode, Region } from '@linode/api-v4'; import { chooseRegions } from 'support/util/regions'; +import { createTestLinode } from 'support/util/linodes'; const mockRegions: Region[] = [ regionFactory.build({ @@ -66,7 +66,7 @@ const migrationNoticeSubstrings = [ authenticate(); describe('Migrate Linode With Firewall', () => { before(() => { - cleanUp('firewalls'); + cleanUp(['firewalls', 'linodes']); }); /* @@ -138,13 +138,14 @@ describe('Migrate Linode With Firewall', () => { const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), region: migrationRegionStart.id, + booted: false, }); interceptCreateFirewall().as('createFirewall'); interceptGetFirewalls().as('getFirewalls'); // Create a Linode, then navigate to the Firewalls landing page. - cy.defer(createLinode(linodePayload)).then((linode: Linode) => { + cy.defer(createTestLinode(linodePayload)).then((linode: Linode) => { interceptMigrateLinode(linode.id).as('migrateLinode'); cy.visitWithLogin('/firewalls'); cy.wait('@getFirewalls'); @@ -194,7 +195,7 @@ describe('Migrate Linode With Firewall', () => { // Make sure Linode is running before attempting to migrate. cy.get('[data-qa-linode-status]').within(() => { - cy.findByText('RUNNING'); + cy.findByText('OFFLINE'); }); ui.actionMenu diff --git a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts index 01694c1fa4a..4fd976daa48 100644 --- a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable sonarjs/no-duplicate-string */ import type { Linode } from '@linode/api-v4'; -import { createLinode } from '@linode/api-v4'; import { linodeFactory, linodeBackupsFactory, @@ -27,6 +26,7 @@ import { randomLabel } from 'support/util/random'; import { dcPricingMockLinodeTypesForBackups } from 'support/constants/dc-specific-pricing'; import { chooseRegion } from 'support/util/regions'; import { expectManagedDisabled } from 'support/api/managed'; +import { createTestLinode } from 'support/util/linodes'; authenticate(); describe('linode backups', () => { @@ -53,7 +53,7 @@ describe('linode backups', () => { booted: false, }); - cy.defer(createLinode(createLinodeRequest), 'creating Linode').then( + cy.defer(createTestLinode(createLinodeRequest), 'creating Linode').then( (linode: Linode) => { interceptGetLinode(linode.id).as('getLinode'); interceptEnableLinodeBackups(linode.id).as('enableBackups'); @@ -116,7 +116,7 @@ describe('linode backups', () => { const snapshotName = randomLabel(); - cy.defer(createLinode(createLinodeRequest), 'creating Linode').then( + cy.defer(createTestLinode(createLinodeRequest), 'creating Linode').then( (linode: Linode) => { interceptGetLinode(linode.id).as('getLinode'); interceptCreateLinodeSnapshot(linode.id).as('createSnapshot'); diff --git a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts index 3e4901afb86..afc1c505ac4 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -1,4 +1,3 @@ -import { Linode, createLinode } from '@linode/api-v4'; import { linodeFactory, createLinodeRequestFactory } from '@src/factories'; import { interceptCloneLinode, @@ -18,6 +17,8 @@ import { chooseRegion, getRegionById } from 'support/util/regions'; import { randomLabel } from 'support/util/random'; import { authenticate } from 'support/api/authentication'; import { cleanUp } from 'support/util/cleanup'; +import { createTestLinode } from 'support/util/linodes'; +import type { Linode } from '@linode/api-v4'; /** * Returns the Cloud Manager URL to clone a given Linode. @@ -49,12 +50,13 @@ describe('clone linode', () => { region: linodeRegion.id, // Specifying no image allows the Linode to provision and clone faster. image: undefined, + booted: false, type: 'g6-nanode-1', }); const newLinodeLabel = `${linodePayload.label}-clone`; - cy.defer(createLinode(linodePayload)).then((linode: Linode) => { + cy.defer(createTestLinode(linodePayload)).then((linode: Linode) => { const linodeRegion = getRegionById(linodePayload.region!); interceptCloneLinode(linode.id).as('cloneLinode'); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index 51b32e4ca54..ae8f2c04460 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -234,7 +234,7 @@ describe('Linode Config management', () => { */ it('Boots a config', () => { cy.defer( - createLinodeAndGetConfig(null, { waitForBoot: true }), + createLinodeAndGetConfig({ booted: true }, { waitForBoot: true }), 'Creating and booting test Linode' ).then(([linode, config]: [Linode, Config]) => { const kernel = findKernelById(kernels, config.kernel); @@ -281,8 +281,8 @@ describe('Linode Config management', () => { // Create clone source and destination Linodes. const createCloneTestLinodes = async () => { return Promise.all([ - createTestLinode(null, { waitForBoot: true }), - createTestLinode(), + createTestLinode({ booted: true }, { waitForBoot: true }), + createTestLinode({ booted: true }), ]); }; diff --git a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts index 41c59858748..de3d7603a40 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { Linode } from '@linode/api-v4'; import { authenticate } from 'support/api/authentication'; -import { createLinode } from 'support/api/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { containsVisible, fbtClick, fbtVisible } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; @@ -103,8 +103,8 @@ describe('linode storage tab', () => { }); it('try to delete in use disk', () => { - const diskName = 'Debian 10 Disk'; - createLinode().then((linode) => { + const diskName = 'Debian 11 Disk'; + cy.defer(createTestLinode({ booted: true })).then((linode) => { cy.intercept( 'DELETE', apiMatcher(`linode/instances/${linode.id}/disks/*`) @@ -127,7 +127,7 @@ describe('linode storage tab', () => { it('delete disk', () => { const diskName = 'cy-test-disk'; - createLinode({ image: null }).then((linode: Linode) => { + cy.defer(createTestLinode({ image: null })).then((linode) => { cy.intercept( 'DELETE', apiMatcher(`linode/instances/${linode.id}/disks/*`) @@ -157,7 +157,7 @@ describe('linode storage tab', () => { it('add a disk', () => { const diskName = 'cy-test-disk'; - createLinode({ image: null }).then((linode: Linode) => { + cy.defer(createTestLinode({ image: null })).then((linode: Linode) => { cy.intercept( 'POST', apiMatcher(`/linode/instances/${linode.id}/disks`) @@ -171,7 +171,7 @@ describe('linode storage tab', () => { it('resize disk', () => { const diskName = 'Debian 10 Disk'; - createLinode({ image: null }).then((linode: Linode) => { + cy.defer(createTestLinode({ image: null })).then((linode: Linode) => { cy.intercept( 'POST', apiMatcher(`linode/instances/${linode.id}/disks`) diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index 55b5e951243..0e6f00e2ffa 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -1,4 +1,4 @@ -import { createLinode, CreateLinodeRequest, Linode } from '@linode/api-v4'; +import { CreateLinodeRequest, Linode } from '@linode/api-v4'; import { ui } from 'support/ui'; import { randomString, randomLabel } from 'support/util/random'; import { authenticate } from 'support/api/authentication'; @@ -12,6 +12,7 @@ import { mockGetLinodeDetails, mockRebuildLinodeError, } from 'support/intercepts/linodes'; +import { createTestLinode } from 'support/util/linodes'; /** * Creates a Linode and StackScript. @@ -27,7 +28,7 @@ const createStackScriptAndLinode = async ( ) => { return Promise.all([ createStackScript(stackScriptRequestPayload), - createLinode(linodeRequestPayload), + createTestLinode(linodeRequestPayload), ]); }; @@ -117,7 +118,7 @@ describe('rebuild linode', () => { region: chooseRegion().id, }); - cy.defer(createLinode(linodeCreatePayload), 'creating Linode').then( + cy.defer(createTestLinode(linodeCreatePayload), 'creating Linode').then( (linode: Linode) => { interceptRebuildLinode(linode.id).as('linodeRebuild'); @@ -171,7 +172,7 @@ describe('rebuild linode', () => { region: chooseRegion().id, }); - cy.defer(createLinode(linodeCreatePayload), 'creating Linode').then( + cy.defer(createTestLinode(linodeCreatePayload), 'creating Linode').then( (linode: Linode) => { interceptRebuildLinode(linode.id).as('linodeRebuild'); interceptGetStackScripts().as('getStackScripts'); diff --git a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts index 2d62584aa8e..658de76da84 100644 --- a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts @@ -1,5 +1,4 @@ import type { Linode } from '@linode/api-v4'; -import { createLinode } from '@linode/api-v4'; import { createLinodeRequestFactory, linodeFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { @@ -12,6 +11,7 @@ import { } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; @@ -43,7 +43,7 @@ describe('Rescue Linodes', () => { region: chooseRegion().id, }); - cy.defer(createLinode(linodePayload), 'creating Linode').then( + cy.defer(createTestLinode(linodePayload), 'creating Linode').then( (linode: Linode) => { interceptGetLinodeDetails(linode.id).as('getLinode'); interceptRebootLinodeIntoRescueMode(linode.id).as( diff --git a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts index d55affe595c..69e0f3980b0 100644 --- a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts @@ -1,4 +1,4 @@ -import { createLinode } from 'support/api/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { containsVisible, fbtVisible, getClick } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; @@ -15,7 +15,7 @@ describe('resize linode', () => { it('resizes a linode by increasing size: warm migration', () => { mockGetFeatureFlagClientstream().as('getClientStream'); - createLinode().then((linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode) => { interceptLinodeResize(linode.id).as('linodeResize'); cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); cy.findByText('Shared CPU').click({ scrollBehavior: false }); @@ -35,7 +35,7 @@ describe('resize linode', () => { it('resizes a linode by increasing size: cold migration', () => { mockGetFeatureFlagClientstream().as('getClientStream'); - createLinode().then((linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode) => { interceptLinodeResize(linode.id).as('linodeResize'); cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); cy.findByText('Shared CPU').click({ scrollBehavior: false }); @@ -56,7 +56,7 @@ describe('resize linode', () => { it('resizes a linode by increasing size when offline: cold migration', () => { mockGetFeatureFlagClientstream().as('getClientStream'); - createLinode().then((linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); // Turn off the linode to resize the disk @@ -97,83 +97,87 @@ describe('resize linode', () => { }); }); - it('resizes a linode by decreasing size', () => { - createLinode().then((linode) => { - const diskName = 'Debian 10 Disk'; - const size = '50000'; // 50 GB - - // Error flow when attempting to resize a linode to a smaller size without - // resizing the disk to the requested size first. - interceptLinodeResize(linode.id).as('linodeResize'); - cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); - cy.findByText('Shared CPU').click({ scrollBehavior: false }); - containsVisible('Linode 2 GB'); - getClick('[id="g6-standard-1"]'); - cy.get('[data-testid="textfield-input"]').type(linode.label); - cy.get('[data-qa-resize="true"]').should('be.enabled').click(); - cy.wait('@linodeResize'); - // Failed to reduce the size of the linode - cy.contains( - 'The current disk size of your Linode is too large for the new service plan. Please resize your disk to accommodate the new plan. You can read our Resize Your Linode guide for more detailed instructions.' - ) - .scrollIntoView() - .should('be.visible'); - - // Normal flow when resizing a linode to a smaller size after first resizing - // its disk. - cy.visitWithLogin(`/linodes/${linode.id}`); - - // Turn off the linode to resize the disk - ui.button.findByTitle('Power Off').should('be.visible').click(); - - ui.dialog - .findByTitle(`Power Off Linode ${linode.label}?`) - .should('be.visible') - .then(() => { - ui.button - .findByTitle(`Power Off Linode`) - .should('be.visible') - .click(); + it.only('resizes a linode by decreasing size', () => { + cy.defer(createTestLinode({ booted: true, type: 'g6-standard-2' })).then( + (linode) => { + const diskName = 'Debian 11 Disk'; + const size = '50000'; // 50 GB + + // Error flow when attempting to resize a linode to a smaller size without + // resizing the disk to the requested size first. + interceptLinodeResize(linode.id).as('linodeResize'); + cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 2 GB'); + getClick('[id="g6-standard-1"]'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); + cy.wait('@linodeResize'); + // Failed to reduce the size of the linode + cy.contains( + 'The current disk size of your Linode is too large for the new service plan. Please resize your disk to accommodate the new plan. You can read our Resize Your Linode guide for more detailed instructions.' + ) + .scrollIntoView() + .should('be.visible'); + + // Normal flow when resizing a linode to a smaller size after first resizing + // its disk. + cy.visitWithLogin(`/linodes/${linode.id}`); + + // Turn off the linode to resize the disk + ui.button.findByTitle('Power Off').should('be.visible').click(); + + ui.dialog + .findByTitle(`Power Off Linode ${linode.label}?`) + .should('be.visible') + .then(() => { + ui.button + .findByTitle(`Power Off Linode`) + .should('be.visible') + .click(); + }); + + containsVisible('OFFLINE'); + + cy.visitWithLogin(`linodes/${linode.id}/storage`); + fbtVisible(diskName); + + cy.get(`[data-qa-disk="${diskName}"]`).within(() => { + cy.contains('Resize').should('be.enabled').click(); }); - containsVisible('OFFLINE'); - - cy.visitWithLogin(`linodes/${linode.id}/storage`); - fbtVisible(diskName); - - cy.get(`[data-qa-disk="${diskName}"]`).within(() => { - cy.contains('Resize').should('be.enabled').click(); - }); - - ui.drawer.findByTitle(`Resize Debian 10 Disk`); - - ui.drawer - .findByTitle(`Resize ${diskName}`) - .should('be.visible') - .within(() => { - cy.get('[id="size"]').should('be.visible').click().clear().type(size); - - ui.buttonGroup - .findButtonByTitle('Resize') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Wait until the disk resize is done. - ui.toast.assertMessage(`Disk ${diskName} successfully resized.`); - - interceptLinodeResize(linode.id).as('linodeResize'); - cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); - cy.findByText('Shared CPU').click({ scrollBehavior: false }); - containsVisible('Linode 2 GB'); - getClick('[id="g6-standard-1"]'); - cy.get('[data-testid="textfield-input"]').type(linode.label); - cy.get('[data-qa-resize="true"]').should('be.enabled').click(); - cy.wait('@linodeResize'); - cy.contains( - 'Your Linode will soon be automatically powered off, migrated, and restored to its previous state (booted or powered off).' - ).should('be.visible'); - }); + ui.drawer + .findByTitle(`Resize ${diskName}`) + .should('be.visible') + .within(() => { + cy.get('[id="size"]') + .should('be.visible') + .click() + .clear() + .type(size); + + ui.buttonGroup + .findButtonByTitle('Resize') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Wait until the disk resize is done. + ui.toast.assertMessage(`Disk ${diskName} successfully resized.`); + + interceptLinodeResize(linode.id).as('linodeResize'); + cy.visitWithLogin(`/linodes/${linode.id}?resize=true`); + cy.findByText('Shared CPU').click({ scrollBehavior: false }); + containsVisible('Linode 2 GB'); + getClick('[id="g6-standard-1"]'); + cy.get('[data-testid="textfield-input"]').type(linode.label); + cy.get('[data-qa-resize="true"]').should('be.enabled').click(); + cy.wait('@linodeResize'); + cy.contains( + 'Your Linode will soon be automatically powered off, migrated, and restored to its previous state (booted or powered off).' + ).should('be.visible'); + } + ); }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts index 403ff604712..cac96713e76 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts @@ -1,5 +1,5 @@ import { authenticate } from 'support/api/authentication'; -import { createLinode } from '@linode/api-v4/lib/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { createLinodeRequestFactory } from '@src/factories/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; @@ -73,7 +73,7 @@ describe('delete linode', () => { const linodeCreatePayload = createLinodeRequestFactory.build({ label: randomLabel(), }); - cy.defer(createLinode(linodeCreatePayload)).then((linode) => { + cy.defer(createTestLinode(linodeCreatePayload)).then((linode) => { // catch delete request interceptDeleteLinode(linode.id).as('deleteLinode'); cy.visitWithLogin(`/linodes/${linode.id}`); @@ -120,7 +120,7 @@ describe('delete linode', () => { const linodeCreatePayload = createLinodeRequestFactory.build({ label: randomLabel(), }); - cy.defer(createLinode(linodeCreatePayload)).then((linode) => { + cy.defer(createTestLinode(linodeCreatePayload)).then((linode) => { // catch delete request interceptDeleteLinode(linode.id).as('deleteLinode'); cy.visitWithLogin(`/linodes/${linode.id}`); @@ -171,7 +171,7 @@ describe('delete linode', () => { const linodeCreatePayload = createLinodeRequestFactory.build({ label: randomLabel(), }); - cy.defer(createLinode(linodeCreatePayload)).then((linode) => { + cy.defer(createTestLinode(linodeCreatePayload)).then((linode) => { // catch delete request interceptDeleteLinode(linode.id).as('deleteLinode'); cy.visitWithLogin(`/linodes`); @@ -219,10 +219,10 @@ describe('delete linode', () => { const createTwoLinodes = async (): Promise<[Linode, Linode]> => { return Promise.all([ - createLinode( + createTestLinode( createLinodeRequestFactory.build({ label: randomLabel() }) ), - createLinode( + createTestLinode( createLinodeRequestFactory.build({ label: randomLabel() }) ), ]); diff --git a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts index 848aeb2fc87..43291d33f15 100644 --- a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts @@ -17,7 +17,7 @@ describe('switch linode state', () => { * - Does not wait for Linode to finish being shut down before succeeding. */ it('powers off a linode from landing page', () => { - cy.defer(createTestLinode()).then((linode: Linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') @@ -58,7 +58,7 @@ describe('switch linode state', () => { * - Waits for Linode to fully shut down before succeeding. */ it('powers off a linode from details page', () => { - cy.defer(createTestLinode()).then((linode: Linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); cy.contains('RUNNING').should('be.visible'); cy.findByText(linode.label).should('be.visible'); @@ -156,7 +156,7 @@ describe('switch linode state', () => { * - Does not wait for Linode to finish rebooting before succeeding. */ it('reboots a linode from landing page', () => { - cy.defer(createTestLinode()).then((linode: Linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') @@ -197,7 +197,7 @@ describe('switch linode state', () => { * - Waits for Linode to finish rebooting before succeeding. */ it('reboots a linode from details page', () => { - cy.defer(createTestLinode()).then((linode: Linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); cy.contains('RUNNING').should('be.visible'); cy.findByText(linode.label).should('be.visible'); diff --git a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts index ee944990f44..31e0ed477b6 100644 --- a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts @@ -1,5 +1,4 @@ -import { createLinode } from 'support/api/linodes'; -import { containsVisible } from 'support/helpers'; +import { createTestLinode } from 'support/util/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { authenticate } from 'support/api/authentication'; @@ -12,10 +11,10 @@ describe('update linode label', () => { }); it('updates a linode label from details page', () => { - createLinode().then((linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode) => { const newLinodeLabel = randomLabel(); cy.visitWithLogin(`/linodes/${linode.id}`); - containsVisible('RUNNING'); + cy.contains('RUNNING').should('be.visible'); cy.get(`[aria-label="Edit ${linode.label}"]`).click(); cy.get(`[id="edit-${linode.label}-label"]`) @@ -29,10 +28,10 @@ describe('update linode label', () => { }); it('updates a linode label from the "Settings" tab', () => { - createLinode().then((linode) => { + cy.defer(createTestLinode({ booted: true })).then((linode) => { const newLinodeLabel = randomLabel(); cy.visitWithLogin(`/linodes/${linode.id}`); - containsVisible('RUNNING'); + cy.contains('RUNNING').should('be.visible'); cy.visitWithLogin(`/linodes/${linode.id}/settings`); cy.get('[id="label"]').click().clear().type(`${newLinodeLabel}{enter}`); diff --git a/packages/manager/cypress/e2e/core/longview/longview.spec.ts b/packages/manager/cypress/e2e/core/longview/longview.spec.ts index d45259e8a6a..15e4295a12a 100644 --- a/packages/manager/cypress/e2e/core/longview/longview.spec.ts +++ b/packages/manager/cypress/e2e/core/longview/longview.spec.ts @@ -18,7 +18,7 @@ import { } from 'support/intercepts/longview'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createAndBootLinode } from 'support/util/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomString } from 'support/util/random'; // Timeout if Linode creation and boot takes longer than 1 and a half minutes. @@ -122,9 +122,10 @@ describe('longview', () => { const createLinodeAndClient = async () => { return Promise.all([ - createAndBootLinode({ + createTestLinode({ root_pass: linodePassword, type: 'g6-standard-1', + booted: true, }), createLongviewClient(randomLabel()), ]); diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index 0672c1accd1..3cd493d1c1b 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts @@ -1,12 +1,5 @@ import { entityTag } from 'support/constants/cypress'; -import { createLinode } from 'support/api/linodes'; -import { - containsClick, - fbtClick, - fbtVisible, - getClick, - getVisible, -} from 'support/helpers'; +import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; import { chooseRegion, getRegionById } from 'support/util/regions'; @@ -34,8 +27,12 @@ const createNodeBalancerWithUI = ( const regionName = getRegionById(nodeBal.region).label; cy.visitWithLogin('/nodebalancers/create'); - getVisible('[id="nodebalancer-label"]').click().clear().type(nodeBal.label); - containsClick('create a tag').type(entityTag); + cy.get('[id="nodebalancer-label"]') + .should('be.visible') + .click() + .clear() + .type(nodeBal.label); + cy.contains('create a tag').click().type(entityTag); if (isDcPricingTest) { const newRegion = getRegionById('br-gru'); @@ -66,7 +63,7 @@ const createNodeBalancerWithUI = ( ui.regionSelect.find().click().clear().type(`${regionName}{enter}`); // node backend config - fbtClick('Label').type(randomLabel()); + cy.findByText('Label').click().type(randomLabel()); cy.findByLabelText('IP Address') .should('be.visible') @@ -85,9 +82,14 @@ describe('create NodeBalancer', () => { }); it('creates a NodeBalancer in a region with base pricing', () => { - // create a linode in NW where the NB will be created const region = chooseRegion(); - createLinode({ region: region.id }).then((linode) => { + const linodePayload = { + region: region.id, + // NodeBalancers require Linodes with private IPs. + private_ip: true, + }; + + cy.defer(createTestLinode(linodePayload)).then((linode) => { const nodeBal = nodeBalancerFactory.build({ label: randomLabel(), region: region.id, @@ -109,7 +111,12 @@ describe('create NodeBalancer', () => { */ it('displays API errors for NodeBalancer Create form fields', () => { const region = chooseRegion(); - createLinode({ region: region.id }).then((linode) => { + const linodePayload = { + region: region.id, + // NodeBalancers require Linodes with private IPs. + private_ip: true, + }; + cy.defer(createTestLinode(linodePayload)).then((linode) => { const nodeBal = nodeBalancerFactory.build({ label: `${randomLabel()}-^`, ipv4: linode.ipv4[1], @@ -120,15 +127,21 @@ describe('create NodeBalancer', () => { interceptCreateNodeBalancer().as('createNodeBalancer'); createNodeBalancerWithUI(nodeBal); - fbtVisible(`Label can't contain special characters or spaces.`); - getVisible('[id="nodebalancer-label"]') + cy.findByText(`Label can't contain special characters or spaces.`).should( + 'be.visible' + ); + cy.get('[id="nodebalancer-label"]') + .should('be.visible') .click() .clear() .type(randomLabel()); - getClick('[data-qa-protocol-select="true"]').type('TCP{enter}'); - getClick('[data-qa-session-stickiness-select]').type( - 'HTTP Cookie{enter}' - ); + + cy.get('[data-qa-protocol-select="true"]').click().type('TCP{enter}'); + + cy.get('[data-qa-session-stickiness-select]') + .click() + .type('HTTP Cookie{enter}'); + deployNodeBalancer(); const errMessage = `Stickiness http_cookie requires protocol 'http' or 'https'`; cy.wait('@createNodeBalancer') @@ -136,7 +149,8 @@ describe('create NodeBalancer', () => { .should('deep.equal', { errors: [{ field: 'configs[0].stickiness', reason: errMessage }], }); - fbtVisible(errMessage); + + cy.findByText(errMessage).should('be.visible'); }); }); @@ -146,7 +160,12 @@ describe('create NodeBalancer', () => { */ it('shows DC-specific pricing information when creating a NodeBalancer', () => { const initialRegion = getRegionById('us-west'); - createLinode({ region: initialRegion.id }).then((linode) => { + const linodePayload = { + region: initialRegion.id, + // NodeBalancers require Linodes with private IPs. + private_ip: true, + }; + cy.defer(createTestLinode(linodePayload)).then((linode) => { const nodeBal = nodeBalancerFactory.build({ label: randomLabel(), region: initialRegion.id, diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index 926843d8496..4a23eec11c1 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -12,12 +12,11 @@ import { import { interceptCreateLinode } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { createLinodeRequestFactory } from 'src/factories'; -import { createLinode, getLinodeDisks } from '@linode/api-v4/lib/linodes'; -import { createImage } from '@linode/api-v4/lib/images'; +import { createImage, getLinodeDisks, resizeLinodeDisk } from '@linode/api-v4'; import { chooseRegion } from 'support/util/regions'; import { SimpleBackoffMethod } from 'support/util/backoff'; import { cleanUp } from 'support/util/cleanup'; -import { resizeLinodeDisk } from '@linode/api-v4/lib'; +import { createTestLinode } from 'support/util/linodes'; // StackScript fixture paths. const stackscriptBasicPath = 'stackscripts/stackscript-basic.sh'; @@ -113,7 +112,7 @@ const createLinodeAndImage = async () => { // 1.5GB // Shout out to Debian for fitting on a 1.5GB disk. const resizedDiskSize = 1536; - const linode = await createLinode( + const linode = await createTestLinode( createLinodeRequestFactory.build({ label: randomLabel(), region: chooseRegion().id, diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts index 2ebd072b40e..7d827e662ad 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts @@ -11,7 +11,7 @@ import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { cleanUp } from 'support/util/cleanup'; import { interceptCreateLinode } from 'support/intercepts/linodes'; -import { getProfile } from '@linode/api-v4/lib'; +import { getProfile } from '@linode/api-v4'; import { Profile, StackScript } from '@linode/api-v4'; import { formatDate } from '@src/utilities/formatDate'; diff --git a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts index 42adfdfd0ec..647a53a42cd 100644 --- a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts @@ -1,4 +1,3 @@ -import { createLinode } from '@linode/api-v4/lib/linodes'; import { createVolume } from '@linode/api-v4/lib/volumes'; import { Linode, Volume } from '@linode/api-v4'; import { createLinodeRequestFactory } from 'src/factories/linodes'; @@ -13,6 +12,7 @@ import { ui } from 'support/ui'; import { chooseRegion } from 'support/util/regions'; import { interceptGetLinodeConfigs } from 'support/intercepts/configs'; import { cleanUp } from 'support/util/cleanup'; +import { createTestLinode } from 'support/util/linodes'; // Local storage override to force volume table to list up to 100 items. // This is a workaround while we wait to get stuck volumes removed. @@ -48,7 +48,7 @@ const pageSizeOverride = { authenticate(); describe('volume attach and detach flows', () => { before(() => { - cleanUp('volumes'); + cleanUp(['volumes', 'linodes']); }); /* @@ -66,11 +66,12 @@ describe('volume attach and detach flows', () => { label: randomLabel(), region: commonRegion.id, root_pass: randomString(32), + booted: false, }); const entityPromise = Promise.all([ createVolume(volumeRequest), - createLinode(linodeRequest), + createTestLinode(linodeRequest), ]); cy.defer(entityPromise, 'creating Volume and Linode').then( diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index 5304208c626..6131cd87cde 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,5 +1,5 @@ import type { Linode } from '@linode/api-v4'; -import { createLinode } from '@linode/api-v4/lib/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { createLinodeRequestFactory } from 'src/factories/linodes'; import { authenticate } from 'support/api/authentication'; import { cleanUp } from 'support/util/cleanup'; @@ -76,6 +76,7 @@ describe('volume create flow', () => { label: randomLabel(), region: region.id, root_pass: randomString(16), + booted: false, }); const volume = { @@ -85,54 +86,56 @@ describe('volume create flow', () => { regionLabel: region.label, }; - cy.defer(createLinode(linodeRequest), 'creating Linode').then((linode) => { - interceptCreateVolume().as('createVolume'); + cy.defer(createTestLinode(linodeRequest), 'creating Linode').then( + (linode) => { + interceptCreateVolume().as('createVolume'); - cy.visitWithLogin('/volumes/create', { - localStorageOverrides: pageSizeOverride, - }); - - // Fill out and submit volume create form. - containsClick('Label').type(volume.label); - containsClick('Size').type(`{selectall}{backspace}${volume.size}`); - ui.regionSelect.find().click().type(`${volume.region}{enter}`); - - cy.findByLabelText('Linode') - .should('be.visible') - .click() - .type(linode.label); - - ui.autocompletePopper - .findByTitle(linode.label) - .should('be.visible') - .click(); - - fbtClick('Create Volume'); - cy.wait('@createVolume'); - - // Confirm volume configuration drawer opens, then close it. - fbtVisible('Volume scheduled for creation.'); - getClick('[data-qa-close-drawer="true"]'); - - // Confirm that volume is listed on landing page with expected configuration. - cy.findByText(volume.label) - .closest('tr') - .within(() => { - cy.findByText(volume.label).should('be.visible'); - cy.findByText(`${volume.size} GB`).should('be.visible'); - cy.findByText(volume.regionLabel).should('be.visible'); - cy.findByText(linode.label).should('be.visible'); + cy.visitWithLogin('/volumes/create', { + localStorageOverrides: pageSizeOverride, }); - // Confirm that volume is listed on Linode 'Storage' details page. - cy.visitWithLogin(`/linodes/${linode.id}/storage`); - cy.findByText(volume.label) - .closest('tr') - .within(() => { - fbtVisible(volume.label); - fbtVisible(`${volume.size} GB`); - }); - }); + // Fill out and submit volume create form. + containsClick('Label').type(volume.label); + containsClick('Size').type(`{selectall}{backspace}${volume.size}`); + ui.regionSelect.find().click().type(`${volume.region}{enter}`); + + cy.findByLabelText('Linode') + .should('be.visible') + .click() + .type(linode.label); + + ui.autocompletePopper + .findByTitle(linode.label) + .should('be.visible') + .click(); + + fbtClick('Create Volume'); + cy.wait('@createVolume'); + + // Confirm volume configuration drawer opens, then close it. + fbtVisible('Volume scheduled for creation.'); + getClick('[data-qa-close-drawer="true"]'); + + // Confirm that volume is listed on landing page with expected configuration. + cy.findByText(volume.label) + .closest('tr') + .within(() => { + cy.findByText(volume.label).should('be.visible'); + cy.findByText(`${volume.size} GB`).should('be.visible'); + cy.findByText(volume.regionLabel).should('be.visible'); + cy.findByText(linode.label).should('be.visible'); + }); + + // Confirm that volume is listed on Linode 'Storage' details page. + cy.visitWithLogin(`/linodes/${linode.id}/storage`); + cy.findByText(volume.label) + .closest('tr') + .within(() => { + fbtVisible(volume.label); + fbtVisible(`${volume.size} GB`); + }); + } + ); }); /* @@ -145,9 +148,10 @@ describe('volume create flow', () => { label: randomLabel(), root_pass: randomString(16), region: chooseRegion().id, + booted: false, }); - cy.defer(createLinode(linodeRequest), 'creating Linode').then( + cy.defer(createTestLinode(linodeRequest), 'creating Linode').then( (linode: Linode) => { const volume = { label: randomLabel(), diff --git a/packages/manager/cypress/e2e/region/images/create-machine-image-from-linode.spec.ts b/packages/manager/cypress/e2e/region/images/create-machine-image-from-linode.spec.ts index e324a416e03..dacc00ad54c 100644 --- a/packages/manager/cypress/e2e/region/images/create-machine-image-from-linode.spec.ts +++ b/packages/manager/cypress/e2e/region/images/create-machine-image-from-linode.spec.ts @@ -1,43 +1,17 @@ -import type { CreateLinodeRequest, Disk, Linode } from '@linode/api-v4'; -import { createLinode, getLinodeDisks } from '@linode/api-v4'; +import type { Disk, Linode } from '@linode/api-v4'; +import { createTestLinode } from 'support/util/linodes'; import { createLinodeRequestFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { imageCaptureProcessingTimeout } from 'support/constants/images'; import { ui } from 'support/ui'; -import { SimpleBackoffMethod } from 'support/util/backoff'; import { cleanUp } from 'support/util/cleanup'; -import { depaginate } from 'support/util/paginate'; -import { pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomPhrase, randomString } from 'support/util/random'; import { testRegions } from 'support/util/regions'; -/** - * Creates a Linode, waits for it to boot, and returns the Linode and its disk. - * - * @param linodePayload - Linode create API request payload. - * - * @returns Promise that resolves to a tuple containing the created Linode and its disk. - */ -const createAndBootLinode = async ( - linodePayload: CreateLinodeRequest -): Promise<[Linode, Disk]> => { - const linode = await createLinode(linodePayload); - // Wait 25 seconds to begin polling, then poll every 5 seconds until Linode boots. - await pollLinodeStatus( - linode.id, - 'running', - new SimpleBackoffMethod(5000, { - initialDelay: 25000, - }) - ); - const disks = await depaginate((page) => getLinodeDisks(linode.id, { page })); - return [linode, disks[0]]; -}; - authenticate(); describe('Capture Machine Images', () => { before(() => { - cleanUp('images'); + cleanUp(['images', 'linodes']); }); /* @@ -54,10 +28,11 @@ describe('Capture Machine Images', () => { label: randomLabel(), root_pass: randomString(32), region: region.id, + booted: true, }); cy.defer( - createAndBootLinode(linodePayload), + createTestLinode(linodePayload, { waitForBoot: true }), 'creating and booting Linode' ).then(([linode, disk]: [Linode, Disk]) => { cy.visitWithLogin('/images/create/disk'); diff --git a/packages/manager/cypress/e2e/region/linodes/delete-linode.spec.ts b/packages/manager/cypress/e2e/region/linodes/delete-linode.spec.ts index 0c92f09ce01..dec88cbe6cc 100644 --- a/packages/manager/cypress/e2e/region/linodes/delete-linode.spec.ts +++ b/packages/manager/cypress/e2e/region/linodes/delete-linode.spec.ts @@ -2,7 +2,6 @@ import { createLinodeRequestFactory } from '@src/factories'; import { describeRegions } from 'support/util/regions'; import { randomLabel, randomString } from 'support/util/random'; import { Region } from '@linode/api-v4'; -import { createLinode } from '@linode/api-v4'; import type { Linode } from '@linode/api-v4'; import { ui } from 'support/ui'; import { authenticate } from 'support/api/authentication'; @@ -11,6 +10,7 @@ import { interceptGetLinodes, } from 'support/intercepts/linodes'; import { cleanUp } from 'support/util/cleanup'; +import { createTestLinode } from 'support/util/linodes'; authenticate(); describeRegions('Delete Linodes', (region: Region) => { @@ -28,11 +28,12 @@ describeRegions('Delete Linodes', (region: Region) => { label: randomLabel(), region: region.id, root_pass: randomString(32), + booted: false, }); // Create a Linode before navigating to its details page to delete it. cy.defer( - createLinode(linodeCreatePayload), + createTestLinode(linodeCreatePayload), `creating Linode in ${region.label}` ).then((linode: Linode) => { interceptGetLinodeDetails(linode.id).as('getLinode'); diff --git a/packages/manager/cypress/e2e/region/linodes/update-linode.spec.ts b/packages/manager/cypress/e2e/region/linodes/update-linode.spec.ts index a62e9e7bdd5..6850c680e42 100644 --- a/packages/manager/cypress/e2e/region/linodes/update-linode.spec.ts +++ b/packages/manager/cypress/e2e/region/linodes/update-linode.spec.ts @@ -1,5 +1,5 @@ import type { Disk, Linode } from '@linode/api-v4'; -import { createLinode, getLinodeDisks } from '@linode/api-v4'; +import { getLinodeDisks } from '@linode/api-v4'; import { createLinodeRequestFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { interceptGetLinodeDetails } from 'support/intercepts/linodes'; @@ -8,6 +8,7 @@ import { cleanUp } from 'support/util/cleanup'; import { depaginate } from 'support/util/paginate'; import { randomLabel, randomString } from 'support/util/random'; import { describeRegions } from 'support/util/regions'; +import { createTestLinode } from 'support/util/linodes'; /* * Returns a Linode create payload for the given region. @@ -34,7 +35,7 @@ describeRegions('Can update Linodes', (region) => { */ it('can update a Linode label', () => { cy.defer( - createLinode(makeLinodePayload(region.id, true)), + createTestLinode(makeLinodePayload(region.id, true)), 'creating Linode' ).then((linode: Linode) => { const newLabel = randomLabel(); @@ -89,7 +90,9 @@ describeRegions('Can update Linodes', (region) => { const newPassword = randomString(32); const createLinodeAndGetDisk = async (): Promise<[Linode, Disk]> => { - const linode = await createLinode(makeLinodePayload(region.id, false)); + const linode = await createTestLinode( + makeLinodePayload(region.id, false) + ); const disks = await depaginate((page) => getLinodeDisks(linode.id, { page }) ); diff --git a/packages/manager/cypress/support/api/linodes.ts b/packages/manager/cypress/support/api/linodes.ts index 6865519b8ac..9fbe30f3302 100644 --- a/packages/manager/cypress/support/api/linodes.ts +++ b/packages/manager/cypress/support/api/linodes.ts @@ -1,14 +1,10 @@ import { Linode, deleteLinode, getLinodes } from '@linode/api-v4'; -import { CreateLinodeRequest } from '@linode/api-v4'; import { linodeFactory } from '@src/factories'; import { makeResourcePage } from '@src/mocks/serverHandlers'; -import { oauthToken, pageSize } from 'support/constants/api'; -import { entityTag } from 'support/constants/cypress'; +import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; -import { randomLabel, randomString } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; -import { apiCheckErrors, deleteById, isTestLabel } from './common'; +import { deleteById, isTestLabel } from './common'; export const createMockLinodeList = (data?: {}, listNumber: number = 1) => { return makeResourcePage( @@ -18,47 +14,6 @@ export const createMockLinodeList = (data?: {}, listNumber: number = 1) => { ); }; -const defaultLinodeRequestBody = { - authorized_users: [], - backups_enabled: false, - booted: true, - image: 'linode/debian10', - private_ip: true, - region: chooseRegion().id, - root_pass: randomString(32), - tags: [entityTag], - type: 'g6-standard-2', -}; - -const linodeRequest = (linodeData: CreateLinodeRequest) => { - return cy.request({ - auth: { - bearer: oauthToken, - }, - body: linodeData, - method: 'POST', - url: Cypress.env('REACT_APP_API_ROOT') + '/linode/instances', - }); -}; - -export const requestBody = (data: Partial) => { - const label = randomLabel(); - return linodeRequest({ label, ...defaultLinodeRequestBody, ...data }); -}; - -/** - * Deprecated. Use `createTestLinode()` with `cy.defer()` instead. - * - * @deprecated - */ -export const createLinode = (data = {}) => { - return requestBody(data).then((resp) => { - apiCheckErrors(resp); - console.log(`Created Linode ${resp.body.label} successfully`, resp); - return resp.body; - }); -}; - export const deleteLinodeById = (linodeId: number) => deleteById('linode/instances', linodeId); From b3bb923375a47466e373d9b5e48eb72ef9be1068 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 21 May 2024 15:22:04 -0400 Subject: [PATCH 03/12] Add constants for test dependency entities --- .../manager/cypress/support/constants/cypress.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/support/constants/cypress.ts b/packages/manager/cypress/support/constants/cypress.ts index 3df9ce61a59..e2a6fc2a6ee 100644 --- a/packages/manager/cypress/support/constants/cypress.ts +++ b/packages/manager/cypress/support/constants/cypress.ts @@ -3,10 +3,15 @@ */ /** - * Tag to use to identify test entities, resources, etc. + * Tag to identify test entities, resources, etc. */ export const entityTag = 'cy-test'; +/** + * Tag to identify resources that tests depend on. + */ +export const dependencyTag = 'cy-dep'; + /** * Prefix for entity names and labels that will be created by Cypress tests. * @@ -16,3 +21,11 @@ export const entityTag = 'cy-test'; * clean-up purposes. */ export const entityPrefix = `${entityTag}-`; + +/** + * Prefix for entity names and labels that will be created by Cypress tests. + * + * Dependency entities may be relied upon by multiple tests and have different + * clean up behavior. + */ +export const dependencyPrefix = `${dependencyTag}-`; From e730c1f516f0199a863cad2928fd6d1b830ab044 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 30 May 2024 08:55:32 -0400 Subject: [PATCH 04/12] Improve security of test Linode creation --- .../manager/cypress/support/util/linodes.ts | 89 ++++++++++++++----- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index b5436c386b6..76186a2ccc0 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -2,13 +2,29 @@ import { createLinode, getLinodeConfigs } from '@linode/api-v4'; import { createLinodeRequestFactory } from '@src/factories'; import { SimpleBackoffMethod } from 'support/util/backoff'; import { pollLinodeDiskStatuses, pollLinodeStatus } from 'support/util/polling'; -import { randomLabel } from 'support/util/random'; +import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { depaginate } from './paginate'; import { pageSize } from 'support/constants/api'; -import type { Config, Linode } from '@linode/api-v4'; -import type { CreateLinodeRequest } from '@linode/api-v4'; +import type { Config, CreateLinodeRequest, Linode } from '@linode/api-v4'; +import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; + +/** + * Methods used to secure test Linodes. + * + * - `firewall`: A firewall is used to secure the created Linode. If a suitable + * firewall does not exist, one is created first. + * + * - `vlan_no_internet`: The created Linode's `eth0` network interface is set to + * a VLAN, and no public internet interface is configured. + * + * - `powered_off`: The created Linode is not booted upon creation. + */ +export type CreateTestLinodeSecurityMethod = + | 'firewall' + | 'vlan_no_internet' + | 'powered_off'; /** * Options to control the behavior of test Linode creation. @@ -19,6 +35,9 @@ export interface CreateTestLinodeOptions { /** Whether to wait for created Linode to boot before resolving. */ waitForBoot: boolean; + + /** Method to use to secure the test Linode. */ + securityMethod: CreateTestLinodeSecurityMethod; } /** @@ -27,6 +46,7 @@ export interface CreateTestLinodeOptions { export const defaultCreateTestLinodeOptions = { waitForDisks: false, waitForBoot: false, + securityMethod: 'firewall', }; /** @@ -46,6 +66,34 @@ export const createTestLinode = async ( ...(options || {}), }; + const securityMethodPayload: Partial = await (async () => { + switch (resolvedOptions.securityMethod) { + case 'firewall': + default: + const firewall = await findOrCreateDependencyFirewall(); + return { + firewall_id: firewall.id, + }; + + case 'vlan_no_internet': + return { + interfaces: [ + { + purpose: 'vlan', + primary: false, + label: randomLabel(), + ipam_address: null, + }, + ], + }; + + case 'powered_off': + return { + booted: false, + }; + } + })(); + const resolvedCreatePayload = { ...createLinodeRequestFactory.build({ label: randomLabel(), @@ -54,6 +102,16 @@ export const createTestLinode = async ( booted: false, }), ...(createRequestPayload || {}), + ...securityMethodPayload, + + // Override given root password; mitigate against using default factory password, inadvertent logging, etc. + root_pass: randomString(64, { + spaces: true, + symbols: true, + numbers: true, + lowercase: true, + uppercase: true, + }), }; // Display warnings for certain combinations of options/request payloads... @@ -106,7 +164,10 @@ export const createTestLinode = async ( consoleProps: () => { return { options: resolvedOptions, - payload: resolvedCreatePayload, + payload: { + ...resolvedCreatePayload, + root_pass: '(redacted)', + }, linode, }; }, @@ -115,26 +176,6 @@ export const createTestLinode = async ( return linode; }; -/** - * Creates a Linode and waits for it to be in "running" state. - * - * Deprecated. Use `createTestLinode` with `waitForBoot` set to `true`. - * - * @param createPayload - Optional Linode create payload options. - * - * @deprecated - * - * @returns Promis that resolves when Linode is created and booted. - */ -export const createAndBootLinode = async ( - createPayload?: Partial -): Promise => { - console.warn( - '`createAndBootLinode()` is deprecated. Use `createTestLinode()` instead.' - ); - return createTestLinode(createPayload, { waitForBoot: true }); -}; - /** * Retrieves all Config objects belonging to a Linode. * From 476eb21a21a64d3d1a390ea04af73004e1617830 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 30 May 2024 08:57:25 -0400 Subject: [PATCH 05/12] Add utils to find or create Firewall for test Linodes --- .../manager/cypress/support/api/common.ts | 21 ++++- .../manager/cypress/support/api/firewalls.ts | 83 ++++++++++++++++++- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/packages/manager/cypress/support/api/common.ts b/packages/manager/cypress/support/api/common.ts index e9b88d819df..9c04cb630cd 100644 --- a/packages/manager/cypress/support/api/common.ts +++ b/packages/manager/cypress/support/api/common.ts @@ -1,5 +1,9 @@ import { oauthToken } from 'support/constants/api'; -import { entityPrefix, entityTag } from 'support/constants/cypress'; +import { + dependencyPrefix, + entityPrefix, + entityTag, +} from 'support/constants/cypress'; const apiroot = Cypress.env('REACT_APP_API_ROOT') + '/'; const apirootBeta = Cypress.env('REACT_APP_API_ROOT') + 'beta/'; @@ -123,3 +127,18 @@ export const isTestEntity = (entity: { export const isTestLabel = (label: string) => { return label.startsWith(entityPrefix); }; + +/** + * Determines whether or not a label is a dependency label. + * + * @param label - Label to check. + * + * @example + * isDependencyLabel('my-label'); // `false`. + * isDependencyLabel('cy-dep-my-firewall'); // `true`. + * + * @returns True if label is a dependency label, false otherwise. + */ +export const isDependencyLabel = (label: string) => { + return label.startsWith(dependencyPrefix); +}; diff --git a/packages/manager/cypress/support/api/firewalls.ts b/packages/manager/cypress/support/api/firewalls.ts index 4c48bf3c16d..a808b6153ef 100644 --- a/packages/manager/cypress/support/api/firewalls.ts +++ b/packages/manager/cypress/support/api/firewalls.ts @@ -1,8 +1,70 @@ -import { Firewall, deleteFirewall, getFirewalls } from '@linode/api-v4'; +import { + Firewall, + deleteFirewall, + getFirewalls, + createFirewall, +} from '@linode/api-v4'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; +import { randomString } from 'support/util/random'; -import { isTestLabel } from './common'; +import { isTestLabel, isDependencyLabel } from './common'; + +/** + * Determines if a Firewall is sufficiently locked down to use for a test resource. + * + * Returns `true` if the Firewall has a default inbound policy to drop connections + * and does not have any additional inbound rules. + * + * @param firewall - Firewall for which to check inbound rules and policies. + * + * @returns `true` if Firewall is locked down, `false` otherwise. + */ +export const isFirewallLockedDown = (firewall: Firewall) => { + const hasInboundRules = + !!firewall.rules.inbound && firewall.rules.inbound.length > 0; + const hasInboundDropPolicy = firewall.rules.inbound_policy === 'DROP'; + + return hasInboundDropPolicy && !hasInboundRules; +}; + +/** + * Returns a firewall to use for a test resource, creating it if one does not already exist. + * + * @returns Promise that resolves to existing or new Firewall. + */ +export const findOrCreateDependencyFirewall = async () => { + const firewalls = await depaginate((page: number) => + getFirewalls({ page, page_size: pageSize }) + ); + + const suitableFirewalls = firewalls.filter( + (firewall: Firewall) => + isDependencyLabel(firewall.label) && isFirewallLockedDown(firewall) + ); + + if (suitableFirewalls.length > 0) { + return suitableFirewalls[0]; + } + + // No suitable firewalls exist, so we'll create one and return it. + const firewallLabel = `cy-dep-${randomString(10, { + lowercase: true, + spaces: false, + symbols: false, + uppercase: false, + numbers: false, + })}`; + return createFirewall({ + label: firewallLabel, + rules: { + inbound: [], + outbound: [], + inbound_policy: 'DROP', + outbound_policy: 'DROP', + }, + }); +}; /** * Deletes all Firewalls whose labels are prefixed "cy-test-". @@ -20,3 +82,20 @@ export const deleteAllTestFirewalls = async (): Promise => { await Promise.all(deletionPromises); }; + +/** + * Deletes all Firewalls whose labels are prefixed with "cy-dep-". + * + * @returns Promise that resolves when Firewalls have been deleted. + */ +export const deleteAllDependencyFirewalls = async (): Promise => { + const firewalls = await depaginate((page: number) => + getFirewalls({ page, page_size: pageSize }) + ); + + const deletionPromises = firewalls + .filter((firewall: Firewall) => isDependencyLabel(firewall.label)) + .map((firewall: Firewall) => deleteFirewall(firewall.id)); + + await Promise.all(deletionPromises); +}; From 7bc697260691f5a25469ab80853d5af299135471 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 30 May 2024 08:59:47 -0400 Subject: [PATCH 06/12] Fix failing tests by specifying a security method that is compatible with the test flow --- .../e2e/core/account/service-transfer.spec.ts | 9 ++- .../core/firewalls/create-firewall.spec.ts | 76 +++++++++---------- .../migrate-linode-with-firewall.spec.ts | 5 +- .../core/linodes/switch-linode-state.spec.ts | 32 +++++++- 4 files changed, 75 insertions(+), 47 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 60f73cd436e..5a4cdd0ec5d 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -252,8 +252,11 @@ describe('Account service transfers', () => { booted: true, }); - const linode: Linode = await createTestLinode(payload); - await pollLinodeStatus(linode.id, 'running', { + const linode: Linode = await createTestLinode(payload, { + securityMethod: 'powered_off', + }); + + await pollLinodeStatus(linode.id, 'offline', { initialDelay: 15000, }); @@ -321,7 +324,7 @@ describe('Account service transfers', () => { cy.get('[data-qa-close-drawer]').should('be.visible').click(); }); - // Attempt to receive the an invalid token. + // Attempt to receive an invalid token. redeemToken(randomUuid()); assertReceiptError('Not found'); diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index fdf73697611..298417a1a72 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -68,55 +68,55 @@ describe('create firewall', () => { label: randomLabel(), region: region.id, root_pass: randomString(16), - booted: false, }); const firewall = { label: randomLabel(), }; - cy.defer(createTestLinode(linodeRequest), 'creating Linode').then( - (linode) => { - interceptCreateFirewall().as('createFirewall'); - cy.visitWithLogin('/firewalls/create'); + cy.defer( + createTestLinode(linodeRequest, { securityMethod: 'powered_off' }), + 'creating Linode' + ).then((linode) => { + interceptCreateFirewall().as('createFirewall'); + cy.visitWithLogin('/firewalls/create'); - ui.drawer - .findByTitle('Create Firewall') - .should('be.visible') - .within(() => { - // Fill out and submit firewall create form. - cy.contains('Label').click().type(firewall.label); - cy.findByLabelText('Linodes') - .should('be.visible') - .click() - .type(linode.label); + ui.drawer + .findByTitle('Create Firewall') + .should('be.visible') + .within(() => { + // Fill out and submit firewall create form. + cy.contains('Label').click().type(firewall.label); + cy.findByLabelText('Linodes') + .should('be.visible') + .click() + .type(linode.label); - ui.autocompletePopper - .findByTitle(linode.label) - .should('be.visible') - .click(); + ui.autocompletePopper + .findByTitle(linode.label) + .should('be.visible') + .click(); - cy.findByLabelText('Linodes').should('be.visible').click(); + cy.findByLabelText('Linodes').should('be.visible').click(); - ui.buttonGroup - .findButtonByTitle('Create Firewall') - .should('be.visible') - .should('be.enabled') - .click(); - }); + ui.buttonGroup + .findButtonByTitle('Create Firewall') + .should('be.visible') + .should('be.enabled') + .click(); + }); - cy.wait('@createFirewall'); + cy.wait('@createFirewall'); - // Confirm that firewall is listed on landing page with expected configuration. - cy.findByText(firewall.label) - .closest('tr') - .within(() => { - cy.findByText(firewall.label).should('be.visible'); - cy.findByText('Enabled').should('be.visible'); - cy.findByText('No rules').should('be.visible'); - cy.findByText(linode.label).should('be.visible'); - }); - } - ); + // Confirm that firewall is listed on landing page with expected configuration. + cy.findByText(firewall.label) + .closest('tr') + .within(() => { + cy.findByText(firewall.label).should('be.visible'); + cy.findByText('Enabled').should('be.visible'); + cy.findByText('No rules').should('be.visible'); + cy.findByText(linode.label).should('be.visible'); + }); + }); }); }); 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 711649a88d0..2ff4bd31f67 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 @@ -138,14 +138,15 @@ describe('Migrate Linode With Firewall', () => { const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), region: migrationRegionStart.id, - booted: false, }); interceptCreateFirewall().as('createFirewall'); interceptGetFirewalls().as('getFirewalls'); // Create a Linode, then navigate to the Firewalls landing page. - cy.defer(createTestLinode(linodePayload)).then((linode: Linode) => { + cy.defer( + createTestLinode(linodePayload, { securityMethod: 'powered_off' }) + ).then((linode: Linode) => { interceptMigrateLinode(linode.id).as('migrateLinode'); cy.visitWithLogin('/firewalls'); cy.wait('@getFirewalls'); diff --git a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts index 43291d33f15..ec8cbeab797 100644 --- a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts @@ -17,7 +17,13 @@ describe('switch linode state', () => { * - Does not wait for Linode to finish being shut down before succeeding. */ it('powers off a linode from landing page', () => { - cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { + // Use `vlan_no_internet` security method. + // This works around an issue where the Linode API responds with a 400 + // when attempting to reboot shortly after booting up when the Linode is + // attached to a Cloud Firewall. + cy.defer( + createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) + ).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') @@ -58,7 +64,13 @@ describe('switch linode state', () => { * - Waits for Linode to fully shut down before succeeding. */ it('powers off a linode from details page', () => { - cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { + // Use `vlan_no_internet` security method. + // This works around an issue where the Linode API responds with a 400 + // when attempting to reboot shortly after booting up when the Linode is + // attached to a Cloud Firewall. + cy.defer( + createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) + ).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); cy.contains('RUNNING').should('be.visible'); cy.findByText(linode.label).should('be.visible'); @@ -156,7 +168,13 @@ describe('switch linode state', () => { * - Does not wait for Linode to finish rebooting before succeeding. */ it('reboots a linode from landing page', () => { - cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { + // Use `vlan_no_internet` security method. + // This works around an issue where the Linode API responds with a 400 + // when attempting to reboot shortly after booting up when the Linode is + // attached to a Cloud Firewall. + cy.defer( + createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) + ).then((linode: Linode) => { cy.visitWithLogin('/linodes'); cy.get(`[data-qa-linode="${linode.label}"]`) .should('be.visible') @@ -197,7 +215,13 @@ describe('switch linode state', () => { * - Waits for Linode to finish rebooting before succeeding. */ it('reboots a linode from details page', () => { - cy.defer(createTestLinode({ booted: true })).then((linode: Linode) => { + // Use `vlan_no_internet` security method. + // This works around an issue where the Linode API responds with a 400 + // when attempting to reboot shortly after booting up when the Linode is + // attached to a Cloud Firewall. + cy.defer( + createTestLinode({ booted: true }, { securityMethod: 'vlan_no_internet' }) + ).then((linode: Linode) => { cy.visitWithLogin(`/linodes/${linode.id}`); cy.contains('RUNNING').should('be.visible'); cy.findByText(linode.label).should('be.visible'); From b19fdf6f40d8ee5c040b30d6eef45113f8458e30 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 30 May 2024 09:03:46 -0400 Subject: [PATCH 07/12] Temporarily skip Longview test --- packages/manager/cypress/e2e/core/longview/longview.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/longview/longview.spec.ts b/packages/manager/cypress/e2e/core/longview/longview.spec.ts index c94936a662a..ebccb894f55 100644 --- a/packages/manager/cypress/e2e/core/longview/longview.spec.ts +++ b/packages/manager/cypress/e2e/core/longview/longview.spec.ts @@ -111,7 +111,8 @@ describe('longview', () => { * - Creates a Linode, connects to it via SSH, and installs Longview using the given cURL command. * - Confirms that Cloud Manager UI updates to reflect Longview installation and data. */ - it('can install Longview client on a Linode', () => { + // TODO Unskip for M3-8107. + it.skip('can install Longview client on a Linode', () => { const linodePassword = randomString(32, { symbols: false, lowercase: true, From aa80da5be25b803ab3cef796850d3e951970f059 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Fri, 31 May 2024 16:13:44 -0400 Subject: [PATCH 08/12] Use regular test tag for firewall labels --- .../manager/cypress/support/api/common.ts | 21 +------- .../manager/cypress/support/api/firewalls.ts | 49 +++++++------------ .../cypress/support/constants/cypress.ts | 13 ----- 3 files changed, 18 insertions(+), 65 deletions(-) diff --git a/packages/manager/cypress/support/api/common.ts b/packages/manager/cypress/support/api/common.ts index 9c04cb630cd..e9b88d819df 100644 --- a/packages/manager/cypress/support/api/common.ts +++ b/packages/manager/cypress/support/api/common.ts @@ -1,9 +1,5 @@ import { oauthToken } from 'support/constants/api'; -import { - dependencyPrefix, - entityPrefix, - entityTag, -} from 'support/constants/cypress'; +import { entityPrefix, entityTag } from 'support/constants/cypress'; const apiroot = Cypress.env('REACT_APP_API_ROOT') + '/'; const apirootBeta = Cypress.env('REACT_APP_API_ROOT') + 'beta/'; @@ -127,18 +123,3 @@ export const isTestEntity = (entity: { export const isTestLabel = (label: string) => { return label.startsWith(entityPrefix); }; - -/** - * Determines whether or not a label is a dependency label. - * - * @param label - Label to check. - * - * @example - * isDependencyLabel('my-label'); // `false`. - * isDependencyLabel('cy-dep-my-firewall'); // `true`. - * - * @returns True if label is a dependency label, false otherwise. - */ -export const isDependencyLabel = (label: string) => { - return label.startsWith(dependencyPrefix); -}; diff --git a/packages/manager/cypress/support/api/firewalls.ts b/packages/manager/cypress/support/api/firewalls.ts index a808b6153ef..72054dba464 100644 --- a/packages/manager/cypress/support/api/firewalls.ts +++ b/packages/manager/cypress/support/api/firewalls.ts @@ -6,26 +6,35 @@ import { } from '@linode/api-v4'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; -import { randomString } from 'support/util/random'; +import { randomLabel } from 'support/util/random'; -import { isTestLabel, isDependencyLabel } from './common'; +import { isTestLabel } from './common'; /** * Determines if a Firewall is sufficiently locked down to use for a test resource. * - * Returns `true` if the Firewall has a default inbound policy to drop connections - * and does not have any additional inbound rules. + * Returns `true` if the Firewall has default inbound and outbound policies to + * drop connections and does not have any additional rules. * - * @param firewall - Firewall for which to check inbound rules and policies. + * @param firewall - Firewall for which to check rules and policies. * * @returns `true` if Firewall is locked down, `false` otherwise. */ export const isFirewallLockedDown = (firewall: Firewall) => { + const hasOutboundRules = + !!firewall.rules.outbound && firewall.rules.outbound.length > 0; const hasInboundRules = !!firewall.rules.inbound && firewall.rules.inbound.length > 0; + + const hasOutboundDropPolicy = firewall.rules.outbound_policy === 'DROP'; const hasInboundDropPolicy = firewall.rules.inbound_policy === 'DROP'; - return hasInboundDropPolicy && !hasInboundRules; + return ( + hasInboundDropPolicy && + hasOutboundDropPolicy && + !hasInboundRules && + !hasOutboundRules + ); }; /** @@ -40,7 +49,7 @@ export const findOrCreateDependencyFirewall = async () => { const suitableFirewalls = firewalls.filter( (firewall: Firewall) => - isDependencyLabel(firewall.label) && isFirewallLockedDown(firewall) + isTestLabel(firewall.label) && isFirewallLockedDown(firewall) ); if (suitableFirewalls.length > 0) { @@ -48,15 +57,8 @@ export const findOrCreateDependencyFirewall = async () => { } // No suitable firewalls exist, so we'll create one and return it. - const firewallLabel = `cy-dep-${randomString(10, { - lowercase: true, - spaces: false, - symbols: false, - uppercase: false, - numbers: false, - })}`; return createFirewall({ - label: firewallLabel, + label: randomLabel(), rules: { inbound: [], outbound: [], @@ -82,20 +84,3 @@ export const deleteAllTestFirewalls = async (): Promise => { await Promise.all(deletionPromises); }; - -/** - * Deletes all Firewalls whose labels are prefixed with "cy-dep-". - * - * @returns Promise that resolves when Firewalls have been deleted. - */ -export const deleteAllDependencyFirewalls = async (): Promise => { - const firewalls = await depaginate((page: number) => - getFirewalls({ page, page_size: pageSize }) - ); - - const deletionPromises = firewalls - .filter((firewall: Firewall) => isDependencyLabel(firewall.label)) - .map((firewall: Firewall) => deleteFirewall(firewall.id)); - - await Promise.all(deletionPromises); -}; diff --git a/packages/manager/cypress/support/constants/cypress.ts b/packages/manager/cypress/support/constants/cypress.ts index e2a6fc2a6ee..c5ab5c563da 100644 --- a/packages/manager/cypress/support/constants/cypress.ts +++ b/packages/manager/cypress/support/constants/cypress.ts @@ -7,11 +7,6 @@ */ export const entityTag = 'cy-test'; -/** - * Tag to identify resources that tests depend on. - */ -export const dependencyTag = 'cy-dep'; - /** * Prefix for entity names and labels that will be created by Cypress tests. * @@ -21,11 +16,3 @@ export const dependencyTag = 'cy-dep'; * clean-up purposes. */ export const entityPrefix = `${entityTag}-`; - -/** - * Prefix for entity names and labels that will be created by Cypress tests. - * - * Dependency entities may be relied upon by multiple tests and have different - * clean up behavior. - */ -export const dependencyPrefix = `${dependencyTag}-`; From c89ec2fdfe524d4aa710678b2551ae0bbbdbec01 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Fri, 31 May 2024 16:26:39 -0400 Subject: [PATCH 09/12] Add changeset --- packages/manager/.changeset/pr-10538-tests-1717187190983.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10538-tests-1717187190983.md diff --git a/packages/manager/.changeset/pr-10538-tests-1717187190983.md b/packages/manager/.changeset/pr-10538-tests-1717187190983.md new file mode 100644 index 00000000000..13ed2f1b676 --- /dev/null +++ b/packages/manager/.changeset/pr-10538-tests-1717187190983.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve test Linode security ([#10538](https://github.com/linode/manager/pull/10538)) From 9d57d15b139abaaa7171696a4b37b5018fbc12ea Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 4 Jun 2024 09:39:02 -0400 Subject: [PATCH 10/12] Improve readability by destructuring Firewalls --- .../manager/cypress/support/api/firewalls.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/manager/cypress/support/api/firewalls.ts b/packages/manager/cypress/support/api/firewalls.ts index 72054dba464..5a31465b87d 100644 --- a/packages/manager/cypress/support/api/firewalls.ts +++ b/packages/manager/cypress/support/api/firewalls.ts @@ -3,6 +3,7 @@ import { deleteFirewall, getFirewalls, createFirewall, + FirewallRules, } from '@linode/api-v4'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; @@ -11,27 +12,24 @@ import { randomLabel } from 'support/util/random'; import { isTestLabel } from './common'; /** - * Determines if a Firewall is sufficiently locked down to use for a test resource. + * Determines if Firewall rules are sufficiently locked down to use for a test resource. * - * Returns `true` if the Firewall has default inbound and outbound policies to - * drop connections and does not have any additional rules. + * Returns `true` if the rules have default inbound and outbound policies to + * drop connections and do not have any additional rules. * - * @param firewall - Firewall for which to check rules and policies. + * @param rules - Firewall rules to assess. * - * @returns `true` if Firewall is locked down, `false` otherwise. + * @returns `true` if Firewall rules are locked down, `false` otherwise. */ -export const isFirewallLockedDown = (firewall: Firewall) => { - const hasOutboundRules = - !!firewall.rules.outbound && firewall.rules.outbound.length > 0; - const hasInboundRules = - !!firewall.rules.inbound && firewall.rules.inbound.length > 0; +export const areFirewallRulesLockedDown = (rules: FirewallRules) => { + const { outbound, outbound_policy, inbound, inbound_policy } = rules; - const hasOutboundDropPolicy = firewall.rules.outbound_policy === 'DROP'; - const hasInboundDropPolicy = firewall.rules.inbound_policy === 'DROP'; + const hasOutboundRules = !!outbound && outbound.length > 0; + const hasInboundRules = !!inbound && inbound.length > 0; return ( - hasInboundDropPolicy && - hasOutboundDropPolicy && + outbound_policy === 'DROP' && + inbound_policy === 'DROP' && !hasInboundRules && !hasOutboundRules ); @@ -48,8 +46,8 @@ export const findOrCreateDependencyFirewall = async () => { ); const suitableFirewalls = firewalls.filter( - (firewall: Firewall) => - isTestLabel(firewall.label) && isFirewallLockedDown(firewall) + ({ label, rules }: Firewall) => + isTestLabel(label) && areFirewallRulesLockedDown(rules) ); if (suitableFirewalls.length > 0) { From 28c654b60db7a1442d84f60146bf5d9f825dc75e Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 4 Jun 2024 09:43:57 -0400 Subject: [PATCH 11/12] Avoid 400 response by using VLAN/no internet security method --- .../manager/cypress/e2e/core/linodes/linode-config.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index ae8f2c04460..81ec20da2d5 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -234,7 +234,10 @@ describe('Linode Config management', () => { */ it('Boots a config', () => { cy.defer( - createLinodeAndGetConfig({ booted: true }, { waitForBoot: true }), + createLinodeAndGetConfig( + { booted: true }, + { waitForBoot: true, securityMethod: 'vlan_no_internet' } + ), 'Creating and booting test Linode' ).then(([linode, config]: [Linode, Config]) => { const kernel = findKernelById(kernels, config.kernel); From 5bb21562827c0b9fe949f87fba5d28adbb5fb476 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 4 Jun 2024 15:56:37 -0400 Subject: [PATCH 12/12] Remove contradictory `booted` property from create payload --- .../manager/cypress/e2e/core/account/service-transfer.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 5a4cdd0ec5d..9268d097c88 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -244,12 +244,11 @@ describe('Account service transfers', () => { * - Confirms that users can cancel a service transfer */ it('can initiate and cancel a service transfer', () => { - // Create a Linode to transfer and wait for it to boot. + // Create a Linode to transfer. const setupLinode = async (): Promise => { const payload = createLinodeRequestFactory.build({ label: randomLabel(), region: chooseRegion().id, - booted: true, }); const linode: Linode = await createTestLinode(payload, {