From c0369b739e2b7af1aaa6fa9476c072d2200108d3 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Mon, 27 Nov 2023 13:55:44 +0100 Subject: [PATCH] refactor: isolate tests (#5433) This PR fixes a race condition between e2e tests where bulk archiving all toggles in the default project would delete toggles used for the features e2e tests. It does by isolating the features.spec and overview.spec to their respective projects, so that they always operate on isolated data. ### Future enhancements: I'm not particularly fond of passing the projectName through to all the helper methods. It complicates the tests more than it should. I would like to be able to set the project once per test and have all the helper methods be aware of the context. Something like this should work: ``` before(() => { cy.wrap('projectId').as('project'); }) ``` And in the helpers: ``` export const createFeature_API = ( featureName: string, options?: Partial, ): Chainable => { return cy.get('@project').then((project) => { projectName = project || 'default'; return cy.request({ url: `${baseUrl}/api/admin/projects/${projectName}/features`, method: 'POST', body: { name: `${featureName}`, description: 'hello-world', type: 'release', impressionData: false, }, ...options, }); }); }; ``` --- frontend/cypress/global.d.ts | 7 +++- .../integration/feature/feature.spec.ts | 38 ++++++++++++++----- .../integration/projects/overview.spec.ts | 22 ++++++----- frontend/cypress/support/API.ts | 9 ++++- frontend/cypress/support/UI.ts | 4 +- 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/frontend/cypress/global.d.ts b/frontend/cypress/global.d.ts index 92009e4931..01731c5134 100644 --- a/frontend/cypress/global.d.ts +++ b/frontend/cypress/global.d.ts @@ -59,7 +59,10 @@ declare namespace Cypress { addFlexibleRolloutStrategyToFeature_UI( options: AddFlexibleRolloutStrategyOptions, ): Chainable; - updateFlexibleRolloutStrategy_UI(featureToggleName: string); + updateFlexibleRolloutStrategy_UI( + featureToggleName: string, + projectName?: string, + ); deleteFeatureStrategy_UI( featureName: string, shouldWait?: boolean, @@ -84,7 +87,7 @@ declare namespace Cypress { projectName?: string, options?: Partial, ): Chainable; - deleteFeature_API(name: string): Chainable; + deleteFeature_API(name: string, projectName?: string): Chainable; createEnvironment_API( environment: IEnvironment, options?: Partial, diff --git a/frontend/cypress/integration/feature/feature.spec.ts b/frontend/cypress/integration/feature/feature.spec.ts index bfd6cec5e9..ca5cf4b7b0 100644 --- a/frontend/cypress/integration/feature/feature.spec.ts +++ b/frontend/cypress/integration/feature/feature.spec.ts @@ -3,16 +3,20 @@ describe('feature', () => { const randomId = String(Math.random()).split('.')[1]; const featureToggleName = `unleash-e2e-${randomId}`; + const projectName = `unleash-e2e-project-${randomId}`; const variant1 = 'variant1'; const variant2 = 'variant2'; before(() => { cy.runBefore(); + cy.login_UI(); + cy.createProject_API(projectName); }); after(() => { - cy.deleteFeature_API(featureToggleName); + cy.deleteFeature_API(featureToggleName, projectName); + cy.deleteProject_API(projectName); }); beforeEach(() => { @@ -21,19 +25,19 @@ describe('feature', () => { }); it('can create a feature toggle', () => { - cy.createFeature_UI(featureToggleName, true); + cy.createFeature_UI(featureToggleName, true, projectName); cy.url().should('include', featureToggleName); }); it('gives an error if a toggle exists with the same name', () => { - cy.createFeature_UI(featureToggleName, false); + cy.createFeature_UI(featureToggleName, false, projectName); cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( 'A toggle with that name already exists', ); }); it('gives an error if a toggle name is url unsafe', () => { - cy.createFeature_UI('featureToggleUnsafe####$#//', false); + cy.createFeature_UI('featureToggleUnsafe####$#//', false, projectName); cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( `"name" must be URL friendly`, ); @@ -42,19 +46,33 @@ describe('feature', () => { it('can add, update and delete a gradual rollout strategy to the development environment', () => { cy.addFlexibleRolloutStrategyToFeature_UI({ featureToggleName, + project: projectName, }).then(() => { - cy.updateFlexibleRolloutStrategy_UI(featureToggleName).then(() => - cy.deleteFeatureStrategy_UI(featureToggleName), + cy.updateFlexibleRolloutStrategy_UI( + featureToggleName, + projectName, + ).then(() => + cy.deleteFeatureStrategy_UI( + featureToggleName, + false, + projectName, + ), ); }); }); it('can add variants to the development environment', () => { - cy.addVariantsToFeature_UI(featureToggleName, [variant1, variant2]); + cy.addVariantsToFeature_UI( + featureToggleName, + [variant1, variant2], + projectName, + ); }); it('can update variants', () => { - cy.visit(`/projects/default/features/${featureToggleName}/variants`); + cy.visit( + `/projects/${projectName}/features/${featureToggleName}/variants`, + ); cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); cy.get('[data-testid=VARIANT_NAME_INPUT]') @@ -70,7 +88,7 @@ describe('feature', () => { cy.intercept( 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`, + `/api/admin/projects/${projectName}/features/${featureToggleName}/environments/development/variants`, (req) => { expect(req.body[0].op).to.equal('replace'); expect(req.body[0].path).to.equal('/1/weightType'); @@ -92,6 +110,6 @@ describe('feature', () => { }); it('can delete variants', () => { - cy.deleteVariant_UI(featureToggleName, variant2); + cy.deleteVariant_UI(featureToggleName, variant2, projectName); }); }); diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts index 72e712ac4c..20dc503128 100644 --- a/frontend/cypress/integration/projects/overview.spec.ts +++ b/frontend/cypress/integration/projects/overview.spec.ts @@ -12,23 +12,26 @@ describe('project overview', () => { const randomId = String(Math.random()).split('.')[1]; const featureTogglePrefix = 'unleash-e2e-project-overview'; const featureToggleName = `${featureTogglePrefix}-${randomId}`; + const projectName = `unleash-e2e-project-overview-${randomId}`; const baseUrl = Cypress.config().baseUrl; const selectAll = '[title="Toggle All Rows Selected"] input[type="checkbox"]'; before(() => { cy.runBefore(); + cy.login_UI(); + cy.createProject_API(projectName); }); after(() => { cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/projects/default/features/${featureToggleName}-A`, + url: `${baseUrl}/api/admin/projects/${projectName}/features/${featureToggleName}-A`, failOnStatusCode: false, }); cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/projects/default/features/${featureToggleName}-B`, + url: `${baseUrl}/api/admin/projects/${projectName}/features/${featureToggleName}-B`, failOnStatusCode: false, }); cy.request({ @@ -39,13 +42,14 @@ describe('project overview', () => { method: 'DELETE', url: `${baseUrl}/api/admin/archive/${featureToggleName}-B`, }); + cy.deleteProject_API(projectName); }); it('loads the table', () => { cy.login_UI(); - cy.createFeature_API(`${featureToggleName}-A`); - cy.createFeature_API(`${featureToggleName}-B`); - cy.visit('/projects/default'); + cy.createFeature_API(`${featureToggleName}-A`, projectName); + cy.createFeature_API(`${featureToggleName}-B`, projectName); + cy.visit(`/projects/${projectName}`); // Use search to filter feature toggles and check that the feature toggle is listed in the table. cy.get(`[data-testid="${SEARCH_INPUT}"]`).as('search').click(); @@ -58,7 +62,7 @@ describe('project overview', () => { it('can select and deselect feature toggles', () => { cy.login_UI(); - cy.visit('/projects/default'); + cy.visit(`/projects/${projectName}`); cy.viewport(1920, 1080); cy.get(`[data-testid="${SEARCH_INPUT}"]`).as('search').click(); cy.get('@search').type(featureToggleName); @@ -120,7 +124,7 @@ describe('project overview', () => { it('can mark selected togggles as stale', () => { cy.login_UI(); - cy.visit('/projects/default'); + cy.visit(`/projects/${projectName}`); cy.viewport(1920, 1080); cy.get(`[data-testid="${SEARCH_INPUT}"]`).as('search').click(); cy.get('@search').type(featureToggleName); @@ -134,13 +138,13 @@ describe('project overview', () => { cy.get('[role="menuitem"]').contains('Mark as stale').click(); - cy.visit(`/projects/default/features/${featureToggleName}-A`); + cy.visit(`/projects/${projectName}/features/${featureToggleName}-A`); cy.get('[title="Feature toggle is deprecated."]').should('exist'); }); it('can archive selected togggles', () => { cy.login_UI(); - cy.visit('/projects/default'); + cy.visit(`/projects/${projectName}`); cy.viewport(1920, 1080); cy.get(`[data-testid="${SEARCH_INPUT}"]`).as('search').click(); cy.get('@search').type(featureToggleName); diff --git a/frontend/cypress/support/API.ts b/frontend/cypress/support/API.ts index d4784a7462..005f73c1ce 100644 --- a/frontend/cypress/support/API.ts +++ b/frontend/cypress/support/API.ts @@ -4,6 +4,7 @@ import Chainable = Cypress.Chainable; const baseUrl = Cypress.config().baseUrl; const password = `${Cypress.env(`AUTH_PASSWORD`)}_A`; const PROJECT_MEMBER = 5; + export const createFeature_API = ( featureName: string, projectName?: string, @@ -23,10 +24,14 @@ export const createFeature_API = ( }); }; -export const deleteFeature_API = (name: string): Chainable => { +export const deleteFeature_API = ( + name: string, + projectName?: string, +): Chainable => { + const project = projectName || 'default'; cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/projects/default/features/${name}`, + url: `${baseUrl}/api/admin/projects/${projectName}/features/${name}`, }); return cy.request({ method: 'DELETE', diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index dc1fc18504..2a3d764a3f 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -54,7 +54,7 @@ export const createFeature_UI = ( project?: string, ): Chainable => { const projectName = project || 'default'; - + cy.visit(`/projects/${project}`); cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( @@ -118,7 +118,7 @@ export const addFlexibleRolloutStrategyToFeature_UI = ( const env = environment || 'development'; const defaultStickiness = stickiness || 'default'; - cy.visit(`/projects/default/features/${featureToggleName}`); + cy.visit(`/projects/${projectName}/features/${featureToggleName}`); cy.intercept( 'POST',