From 10ebf2e16214b23459d36edb69946652b0aa3691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 11:47:50 +0100 Subject: [PATCH 01/24] chore: test migration backward compatibility --- .github/workflows/validate-migrations.yaml | 27 ++++++++++ test-migrations/docker-compose.yml | 61 ++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 .github/workflows/validate-migrations.yaml create mode 100644 test-migrations/docker-compose.yml diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml new file mode 100644 index 000000000000..713dea15f112 --- /dev/null +++ b/.github/workflows/validate-migrations.yaml @@ -0,0 +1,27 @@ +name: Test db migrations + +on: + pull_request: + branches: + - main + paths: + - 'src/migrations/**' + workflow_dispatch: + +jobs: + test-migrations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Start Unleash test instance + working-directory: test-migrations + run: docker compose up -d --wait -t 90 + - name: Run Cypress + uses: cypress-io/github-action@v5 + with: + working-directory: frontend + env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all + config: baseUrl=http://localhost:4242 + # only OSS specs + spec: cypress/integration/feature/feature.spec.ts,cypress/integration/projects/access.spec.ts,cypress/integration/segments/segments.spec.ts,cypress/integration/import/import.spec.ts + \ No newline at end of file diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml new file mode 100644 index 000000000000..bc80c2f91297 --- /dev/null +++ b/test-migrations/docker-compose.yml @@ -0,0 +1,61 @@ +version: "3.9" +services: + # The Unleash server waits for the migrations to be applied by waiting on the + # migrations container to be healthy. + unleash: + image: unleashorg/unleash-server:latest # this is the latest stable release + ports: + - "4242:4242" + environment: + DATABASE_URL: "postgres://postgres:unleash@db/unleash" + DATABASE_SSL: "false" + LOG_LEVEL: "debug" + INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" + depends_on: + migrations: + condition: service_healthy + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:4242/health || exit 1 + interval: 1s + timeout: 1m + retries: 5 + start_period: 15s + + # migrations container is meant to apply the latest migrations and can be disposed later. + # We can do it differently but this option serves its purpose + migrations: + image: unleashorg/unleash-server:main-edge-18.18.2-alpine # this represents tip of main + environment: + DATABASE_URL: "postgres://postgres:unleash@db/unleash" + DATABASE_SSL: "false" + INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" + depends_on: + db: + condition: service_healthy + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:4242/health || exit 1 + interval: 1s + timeout: 1m + retries: 5 + start_period: 15s + db: + expose: + - "5432" + image: postgres:16 + environment: + POSTGRES_DB: "unleash" + # trust incoming connections blindly (DON'T DO THIS IN PRODUCTION!) + POSTGRES_HOST_AUTH_METHOD: "trust" + healthcheck: + test: + [ + "CMD", + "pg_isready", + "--username=postgres", + "--host=127.0.0.1", + "--port=5432", + ] + interval: 2s + timeout: 1m + retries: 5 + start_period: 10s \ No newline at end of file From e65dd531c35d88b9ccbdb495e63519d0911669c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 11:52:46 +0100 Subject: [PATCH 02/24] Make sure we run the tests for this PR --- .github/workflows/validate-migrations.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index 713dea15f112..a1bc3faf8d2d 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -5,7 +5,9 @@ on: branches: - main paths: - - 'src/migrations/**' + - 'src/migrations/**' + - '.github/workflows/validate-migrations.yaml' + - 'test-migrations/**' workflow_dispatch: jobs: From 2288754e24f16695a27b26f7402d8be09ce3ca72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 16:09:31 +0100 Subject: [PATCH 03/24] Add OSS UI tests --- .github/workflows/validate-migrations.yaml | 28 +++- frontend/cypress/oss/feature/feature.spec.ts | 97 +++++++++++++ frontend/cypress/oss/import/import.spec.ts | 131 ++++++++++++++++++ .../cypress/oss/segments/segments.spec.ts | 38 +++++ frontend/cypress/support/API.ts | 2 +- frontend/cypress/support/UI.ts | 4 +- package.json | 1 + src/lib/server-impl.ts | 59 ++++---- src/migrate-db.ts | 10 ++ test-migrations/docker-compose.yml | 21 +-- 10 files changed, 340 insertions(+), 51 deletions(-) create mode 100644 frontend/cypress/oss/feature/feature.spec.ts create mode 100644 frontend/cypress/oss/import/import.spec.ts create mode 100644 frontend/cypress/oss/segments/segments.spec.ts create mode 100644 src/migrate-db.ts diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index a1bc3faf8d2d..da46facc6e32 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -8,22 +8,38 @@ on: - 'src/migrations/**' - '.github/workflows/validate-migrations.yaml' - 'test-migrations/**' + - 'frontend/cypress' workflow_dispatch: - +env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/unleash_test?sslmode=disable + DATABASE_SSL: "false" + INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" + POSTGRES_DB: "unleash" + POSTGRES_HOST_AUTH_METHOD: "trust" jobs: test-migrations: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Start Unleash test instance + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'yarn' + - name: Start database + working-directory: test-migrations + run: docker compose up db -d --wait -t 90 + - name: Start stable version of Unleash working-directory: test-migrations - run: docker compose up -d --wait -t 90 + run: docker compose up unleash -d --wait -t 90 + # add some data with terraform + - name: Apply migrations + run: yarn migrate + # run ui tests against previous version of Unleash - name: Run Cypress uses: cypress-io/github-action@v5 with: working-directory: frontend env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all config: baseUrl=http://localhost:4242 - # only OSS specs - spec: cypress/integration/feature/feature.spec.ts,cypress/integration/projects/access.spec.ts,cypress/integration/segments/segments.spec.ts,cypress/integration/import/import.spec.ts - \ No newline at end of file + spec: cypress/oss/**/*.spec.ts diff --git a/frontend/cypress/oss/feature/feature.spec.ts b/frontend/cypress/oss/feature/feature.spec.ts new file mode 100644 index 000000000000..fc0287fcee00 --- /dev/null +++ b/frontend/cypress/oss/feature/feature.spec.ts @@ -0,0 +1,97 @@ +/// + +describe('feature', () => { + const randomId = String(Math.random()).split('.')[1]; + const featureToggleName = `unleash-e2e-${randomId}`; + + const variant1 = 'variant1'; + const variant2 = 'variant2'; + + before(() => { + cy.runBefore(); + }); + + after(() => { + cy.deleteFeature_API(featureToggleName); + }); + + beforeEach(() => { + cy.login_UI(); + cy.visit('/features'); + }); + + it('can create a feature toggle', () => { + cy.createFeature_UI(featureToggleName, true); + cy.url().should('include', featureToggleName); + }); + + it('gives an error if a toggle exists with the same name', () => { + cy.createFeature_UI(featureToggleName, false); + 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.get("[data-testid='INPUT_ERROR_TEXT']").contains( + `"name" must be URL friendly`, + ); + }); + + it('can add, update and delete a gradual rollout strategy to the development environment', () => { + cy.addFlexibleRolloutStrategyToFeature_UI({ + featureToggleName, + }).then(() => { + cy.updateFlexibleRolloutStrategy_UI(featureToggleName).then(() => + cy.deleteFeatureStrategy_UI(featureToggleName), + ); + }); + }); + + it('can add variants to the development environment', () => { + cy.addVariantsToFeature_UI(featureToggleName, [variant1, variant2]); + }); + + it('can update variants', () => { + cy.visit(`/projects/default/features/${featureToggleName}/variants`); + + cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); + cy.get('[data-testid=VARIANT_NAME_INPUT]') + .last() + .children() + .find('input') + .should('have.attr', 'disabled'); + cy.get('[data-testid=VARIANT_WEIGHT_CHECK]') + .last() + .find('input') + .check(); + cy.get('[data-testid=VARIANT_WEIGHT_INPUT]').last().clear().type('15'); + + cy.intercept( + 'PATCH', + `/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`, + (req) => { + expect(req.body[0].op).to.equal('replace'); + expect(req.body[0].path).to.equal('/1/weightType'); + expect(req.body[0].value).to.equal('fix'); + expect(req.body[1].op).to.equal('replace'); + expect(req.body[1].path).to.equal('/1/weight'); + expect(req.body[1].value).to.equal(150); + expect(req.body[2].op).to.equal('replace'); + expect(req.body[2].path).to.equal('/0/weight'); + expect(req.body[2].value).to.equal(850); + }, + ).as('variantUpdate'); + + cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); + cy.get(`[data-testid=VARIANT_WEIGHT_${variant2}]`).should( + 'have.text', + '15 %', + ); + }); + + it('can delete variants', () => { + cy.deleteVariant_UI(featureToggleName, variant2); + }); +}); diff --git a/frontend/cypress/oss/import/import.spec.ts b/frontend/cypress/oss/import/import.spec.ts new file mode 100644 index 000000000000..6c228b3f7c02 --- /dev/null +++ b/frontend/cypress/oss/import/import.spec.ts @@ -0,0 +1,131 @@ +/// + +describe('imports', () => { + const baseUrl = Cypress.config().baseUrl; + const randomSeed = String(Math.random()).split('.')[1]; + const randomFeatureName = `cypress-features${randomSeed}`; + const userIds: any[] = []; + + before(() => { + cy.runBefore(); + cy.login_UI(); + for (let i = 1; i <= 2; i++) { + cy.request('POST', `${baseUrl}/api/admin/user-admin`, { + name: `unleash-e2e-user${i}-${randomFeatureName}`, + email: `unleash-e2e-user${i}-${randomFeatureName}@test.com`, + sendEmail: false, + rootRole: 3, + }).then((response) => userIds.push(response.body.id)); + } + }); + + after(() => { + userIds.forEach((id) => + cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`), + ); + }); + + beforeEach(() => { + cy.login_UI(); + if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { + cy.get("[data-testid='CLOSE_SPLASH']").click(); + } + }); + + it('can import data', () => { + cy.visit('/projects/default'); + cy.get("[data-testid='IMPORT_BUTTON']").click({ force: true }); + + const exportText = { + features: [ + { + name: randomFeatureName, + description: '', + type: 'release', + project: 'default', + stale: false, + impressionData: false, + archived: false, + }, + ], + featureStrategies: [ + { + name: 'flexibleRollout', + id: '14a0d9dd-2b5d-4a21-98fd-ede72bda0328', + featureName: randomFeatureName, + parameters: { + groupId: randomFeatureName, + rollout: '50', + stickiness: 'default', + }, + constraints: [], + segments: [], + }, + ], + featureEnvironments: [ + { + enabled: true, + featureName: randomFeatureName, + environment: 'test', + variants: [], + name: randomFeatureName, + }, + ], + contextFields: [], + featureTags: [ + { + featureName: randomFeatureName, + tagType: 'simple', + tagValue: 'best-tag', + }, + { + featureName: randomFeatureName, + tagType: 'simple', + tagValue: 'rserw', + }, + { + featureName: randomFeatureName, + tagType: 'simple', + tagValue: 'FARO', + }, + ], + segments: [], + tagTypes: [ + { + name: 'simple', + description: 'Used to simplify filtering of features', + icon: '#', + }, + ], + }; + + cy.get("[data-testid='VALIDATE_BUTTON']").should('be.disabled'); + + // cypress can only work with input@file that is visible + cy.get('input[type=file]') + .invoke('attr', 'style', 'display: block') + .selectFile({ + contents: Cypress.Buffer.from(JSON.stringify(exportText)), + fileName: 'upload.json', + lastModified: Date.now(), + }); + cy.get("[data-testid='VALIDATE_BUTTON']").click(); + cy.get("[data-testid='IMPORT_CONFIGURATION_BUTTON']").click(); + // cy.contains('Import completed'); + + cy.visit(`/projects/default/features/${randomFeatureName}`); + + cy.wait(500); + + cy.get( + "[data-testid='feature-toggle-status'] input[type='checkbox']:checked", + ) + .invoke('attr', 'aria-label') + .should('eq', 'development'); + + cy.get( + '[data-testid="FEATURE_ENVIRONMENT_ACCORDION_development"]', + ).click(); + cy.contains('50%'); + }); +}); diff --git a/frontend/cypress/oss/segments/segments.spec.ts b/frontend/cypress/oss/segments/segments.spec.ts new file mode 100644 index 000000000000..209ec2e5f063 --- /dev/null +++ b/frontend/cypress/oss/segments/segments.spec.ts @@ -0,0 +1,38 @@ +/// + +describe('segments', () => { + const randomId = String(Math.random()).split('.')[1]; + const segmentName = `unleash-e2e-${randomId}`; + let segmentId: string; + + before(() => { + cy.runBefore(); + }); + + beforeEach(() => { + cy.login_UI(); + cy.visit('/segments'); + if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { + cy.get("[data-testid='CLOSE_SPLASH']").click(); + } + }); + + it('can create a segment', () => { + cy.createSegment_UI(segmentName); + cy.contains(segmentName); + }); + + it('gives an error if a segment exists with the same name', () => { + cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click(); + cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName); + cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").should('be.disabled'); + cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( + 'Segment name already exists', + ); + }); + + it('can delete a segment', () => { + cy.deleteSegment_UI(segmentName, segmentId); + cy.contains(segmentName).should('not.exist'); + }); +}); diff --git a/frontend/cypress/support/API.ts b/frontend/cypress/support/API.ts index 005f73c1ce19..603dbbad0c91 100644 --- a/frontend/cypress/support/API.ts +++ b/frontend/cypress/support/API.ts @@ -31,7 +31,7 @@ export const deleteFeature_API = ( const project = projectName || 'default'; cy.request({ method: 'DELETE', - url: `${baseUrl}/api/admin/projects/${projectName}/features/${name}`, + url: `${baseUrl}/api/admin/projects/${project}/features/${name}`, }); return cy.request({ method: 'DELETE', diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index 2a3d764a3f8e..e3031b6ebd7f 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -45,6 +45,8 @@ export const login_UI = ( if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); } + + cy.get("[data-testid='CloseIcon']").click(); }); }; @@ -54,7 +56,7 @@ export const createFeature_UI = ( project?: string, ): Chainable => { const projectName = project || 'default'; - cy.visit(`/projects/${project}`); + cy.visit(`/projects/${projectName}`); cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( diff --git a/package.json b/package.json index 8e6165d607d9..a92d9f53c72a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "main": "./dist/lib/server-impl.js", "scripts": { "start": "TZ=UTC node ./dist/server.js", + "migrate": "TZ=UTC node ./dist/migrate-db.js", "copy-templates": "copyfiles -u 1 src/mailtemplates/**/*.mustache dist/", "build:backend": "tsc --pretty --strictNullChecks false", "build:frontend": "yarn --cwd ./frontend run build", diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 93d99c2750dc..db0047bfc80b 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -134,12 +134,45 @@ async function start(opts: IUnleashOptions = {}): Promise { const config = createConfig(opts); const logger = config.getLogger('server-impl.js'); + await applyDbMigration( + opts.flagResolver?.isEnabled('migrationLock') ?? true, + opts, + config, + logger, + ); + + const unleash = await createApp(config, true); + if (config.server.gracefulShutdownEnable) { + registerGracefulShutdown(unleash, logger); + } + return unleash; +} + +async function create(opts: IUnleashOptions): Promise { + const config = createConfig(opts); + const logger = config.getLogger('server-impl.js'); + + const useLock = false; + await applyDbMigration(useLock, opts, config, logger); + return createApp(config, false); +} + +async function applyDbMigration( + useLock: boolean, + opts: IUnleashOptions = {}, + configParam?: IUnleashConfig, + loggerParam?: Logger, +) { + const config = configParam ? configParam : createConfig(opts); + const logger = loggerParam + ? loggerParam + : config.getLogger('server-impl.js'); try { if (config.db.disableMigration) { logger.info('DB migration: disabled'); } else { logger.debug('DB migration: start'); - if (opts.flagResolver?.isEnabled('migrationLock')) { + if (useLock) { logger.info('Running migration with lock'); const lock = withDbLock(config.db, { lockKey: defaultLockKey, @@ -157,29 +190,6 @@ async function start(opts: IUnleashOptions = {}): Promise { logger.error('Failed to migrate db', err); throw err; } - - const unleash = await createApp(config, true); - if (config.server.gracefulShutdownEnable) { - registerGracefulShutdown(unleash, logger); - } - return unleash; -} - -async function create(opts: IUnleashOptions): Promise { - const config = createConfig(opts); - const logger = config.getLogger('server-impl.js'); - - try { - if (config.db.disableMigration) { - logger.info('DB migrations disabled'); - } else { - await migrateDb(config); - } - } catch (err) { - logger.error('Failed to migrate db', err); - throw err; - } - return createApp(config, false); } export default { @@ -190,6 +200,7 @@ export default { export { start, create, + applyDbMigration, Controller, AuthenticationRequired, User, diff --git a/src/migrate-db.ts b/src/migrate-db.ts new file mode 100644 index 000000000000..c14eb2f26af2 --- /dev/null +++ b/src/migrate-db.ts @@ -0,0 +1,10 @@ +import { applyDbMigration } from './lib/server-impl'; + +try { + const useLock = true; + applyDbMigration(useLock); +} catch (error) { + // eslint-disable-next-line no-console + console.error(error); + process.exit(); +} diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index bc80c2f91297..e0728f6d523b 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -11,24 +11,6 @@ services: DATABASE_SSL: "false" LOG_LEVEL: "debug" INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" - depends_on: - migrations: - condition: service_healthy - healthcheck: - test: wget --no-verbose --tries=1 --spider http://localhost:4242/health || exit 1 - interval: 1s - timeout: 1m - retries: 5 - start_period: 15s - - # migrations container is meant to apply the latest migrations and can be disposed later. - # We can do it differently but this option serves its purpose - migrations: - image: unleashorg/unleash-server:main-edge-18.18.2-alpine # this represents tip of main - environment: - DATABASE_URL: "postgres://postgres:unleash@db/unleash" - DATABASE_SSL: "false" - INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" depends_on: db: condition: service_healthy @@ -38,6 +20,7 @@ services: timeout: 1m retries: 5 start_period: 15s + db: expose: - "5432" @@ -58,4 +41,4 @@ services: interval: 2s timeout: 1m retries: 5 - start_period: 10s \ No newline at end of file + start_period: 10s From 33bef395217f2ad66642f81b32393c4c29c71da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 16:14:10 +0100 Subject: [PATCH 04/24] Run with ts-node --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a92d9f53c72a..3f542e42543c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "main": "./dist/lib/server-impl.js", "scripts": { "start": "TZ=UTC node ./dist/server.js", - "migrate": "TZ=UTC node ./dist/migrate-db.js", + "migrate": "TZ=UTC ts-node ./src/migrate-db.ts", "copy-templates": "copyfiles -u 1 src/mailtemplates/**/*.mustache dist/", "build:backend": "tsc --pretty --strictNullChecks false", "build:frontend": "yarn --cwd ./frontend run build", From 9dee2680388bea6cd970fd4d670c1e58a04965bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 16:19:06 +0100 Subject: [PATCH 05/24] Conditional click --- .github/workflows/validate-migrations.yaml | 5 ++++- frontend/cypress/support/UI.ts | 4 +++- package.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index da46facc6e32..167ae7cccd89 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -34,7 +34,10 @@ jobs: run: docker compose up unleash -d --wait -t 90 # add some data with terraform - name: Apply migrations - run: yarn migrate + run: | + yarn install --frozen-lockfile + yarn build:backend + yarn migrate # run ui tests against previous version of Unleash - name: Run Cypress uses: cypress-io/github-action@v5 diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index e3031b6ebd7f..06fa7cc788d9 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -46,7 +46,9 @@ export const login_UI = ( cy.get("[data-testid='CLOSE_SPLASH']").click(); } - cy.get("[data-testid='CloseIcon']").click(); + if (document.querySelector("[data-testid='CloseIcon']")) { + cy.get("[data-testid='CloseIcon']").click(); + } }); }; diff --git a/package.json b/package.json index 3f542e42543c..a92d9f53c72a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "main": "./dist/lib/server-impl.js", "scripts": { "start": "TZ=UTC node ./dist/server.js", - "migrate": "TZ=UTC ts-node ./src/migrate-db.ts", + "migrate": "TZ=UTC node ./dist/migrate-db.js", "copy-templates": "copyfiles -u 1 src/mailtemplates/**/*.mustache dist/", "build:backend": "tsc --pretty --strictNullChecks false", "build:frontend": "yarn --cwd ./frontend run build", From 19111d4d25aab4f2ea7dc4f00f0406b975d11082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 16:30:13 +0100 Subject: [PATCH 06/24] Ignore build-scripts --- .github/workflows/validate-migrations.yaml | 2 +- test-migrations/docker-compose.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index 167ae7cccd89..270199c3c7a8 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -35,7 +35,7 @@ jobs: # add some data with terraform - name: Apply migrations run: | - yarn install --frozen-lockfile + yarn install --frozen-lockfile --ignore-scripts yarn build:backend yarn migrate # run ui tests against previous version of Unleash diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index e0728f6d523b..273d0d6f097d 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -22,6 +22,8 @@ services: start_period: 15s db: + ports: + - "5432:5432" expose: - "5432" image: postgres:16 From dbb7ca613c8c818f4d9d35700f3703e88401b453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 16:49:39 +0100 Subject: [PATCH 07/24] Use db-migrate --- .github/workflows/validate-migrations.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index 270199c3c7a8..5cee682fac0c 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -37,7 +37,7 @@ jobs: run: | yarn install --frozen-lockfile --ignore-scripts yarn build:backend - yarn migrate + yarn db-migrate # run ui tests against previous version of Unleash - name: Run Cypress uses: cypress-io/github-action@v5 From 86d27c0a415810c7092bcfc51e22806d95541266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 17:28:09 +0100 Subject: [PATCH 08/24] Fixes --- .github/workflows/validate-migrations.yaml | 10 +++++----- src/migrate-db.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index 5cee682fac0c..780fa5d4034e 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -11,11 +11,11 @@ on: - 'frontend/cypress' workflow_dispatch: env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/unleash_test?sslmode=disable - DATABASE_SSL: "false" + DATABASE_URL: postgres://postgres:unleash@localhost:5432/unleash + DATABASE_SSL: false INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" - POSTGRES_DB: "unleash" - POSTGRES_HOST_AUTH_METHOD: "trust" + POSTGRES_DB: unleash + POSTGRES_HOST_AUTH_METHOD: trust jobs: test-migrations: runs-on: ubuntu-latest @@ -37,7 +37,7 @@ jobs: run: | yarn install --frozen-lockfile --ignore-scripts yarn build:backend - yarn db-migrate + yarn migrate # run ui tests against previous version of Unleash - name: Run Cypress uses: cypress-io/github-action@v5 diff --git a/src/migrate-db.ts b/src/migrate-db.ts index c14eb2f26af2..178b8e9064c5 100644 --- a/src/migrate-db.ts +++ b/src/migrate-db.ts @@ -2,7 +2,7 @@ import { applyDbMigration } from './lib/server-impl'; try { const useLock = true; - applyDbMigration(useLock); + applyDbMigration(useLock).then(() => process.exit()); } catch (error) { // eslint-disable-next-line no-console console.error(error); From 5e663828200d51c08270c9f4c08cd0b3daee9436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 20:15:31 +0100 Subject: [PATCH 09/24] Yeet the segments splash --- frontend/cypress/oss/feature/feature.spec.ts | 5 +++-- frontend/cypress/oss/import/import.spec.ts | 3 ++- frontend/cypress/oss/segments/segments.spec.ts | 3 ++- frontend/cypress/support/UI.ts | 13 ++++++------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/cypress/oss/feature/feature.spec.ts b/frontend/cypress/oss/feature/feature.spec.ts index fc0287fcee00..0dbf8a26d804 100644 --- a/frontend/cypress/oss/feature/feature.spec.ts +++ b/frontend/cypress/oss/feature/feature.spec.ts @@ -1,4 +1,4 @@ -/// +/// describe('feature', () => { const randomId = String(Math.random()).split('.')[1]; @@ -43,6 +43,7 @@ describe('feature', () => { cy.addFlexibleRolloutStrategyToFeature_UI({ featureToggleName, }).then(() => { + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.updateFlexibleRolloutStrategy_UI(featureToggleName).then(() => cy.deleteFeatureStrategy_UI(featureToggleName), ); @@ -55,7 +56,7 @@ describe('feature', () => { it('can update variants', () => { cy.visit(`/projects/default/features/${featureToggleName}/variants`); - + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); cy.get('[data-testid=VARIANT_NAME_INPUT]') .last() diff --git a/frontend/cypress/oss/import/import.spec.ts b/frontend/cypress/oss/import/import.spec.ts index 6c228b3f7c02..3ae63d3cd1ff 100644 --- a/frontend/cypress/oss/import/import.spec.ts +++ b/frontend/cypress/oss/import/import.spec.ts @@ -1,4 +1,4 @@ -/// +/// describe('imports', () => { const baseUrl = Cypress.config().baseUrl; @@ -34,6 +34,7 @@ describe('imports', () => { it('can import data', () => { cy.visit('/projects/default'); + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.get("[data-testid='IMPORT_BUTTON']").click({ force: true }); const exportText = { diff --git a/frontend/cypress/oss/segments/segments.spec.ts b/frontend/cypress/oss/segments/segments.spec.ts index 209ec2e5f063..c75da6f174a7 100644 --- a/frontend/cypress/oss/segments/segments.spec.ts +++ b/frontend/cypress/oss/segments/segments.spec.ts @@ -1,4 +1,4 @@ -/// +/// describe('segments', () => { const randomId = String(Math.random()).split('.')[1]; @@ -15,6 +15,7 @@ describe('segments', () => { if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); } + cy.get("[data-testid='CloseIcon']").click(); // Close splash }); it('can create a segment', () => { diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index 06fa7cc788d9..a2c0eaeadb2f 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -30,7 +30,7 @@ export const login_UI = ( ): Chainable => { return cy.session(user, () => { cy.visit('/'); - cy.wait(1500); + cy.wait(200); cy.get("[data-testid='LOGIN_EMAIL_ID']").type(user); if (AUTH_PASSWORD) { @@ -45,10 +45,6 @@ export const login_UI = ( if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); } - - if (document.querySelector("[data-testid='CloseIcon']")) { - cy.get("[data-testid='CloseIcon']").click(); - } }); }; @@ -59,6 +55,7 @@ export const createFeature_UI = ( ): Chainable => { const projectName = project || 'default'; cy.visit(`/projects/${projectName}`); + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( @@ -123,7 +120,7 @@ export const addFlexibleRolloutStrategyToFeature_UI = ( const defaultStickiness = stickiness || 'default'; cy.visit(`/projects/${projectName}/features/${featureToggleName}`); - + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.intercept( 'POST', `/api/admin/projects/${projectName}/features/${featureToggleName}/environments/development/strategies`, @@ -287,7 +284,8 @@ export const addVariantsToFeature_UI = ( ) => { const project = projectName || 'default'; cy.visit(`/projects/${project}/features/${featureToggleName}/variants`); - cy.wait(1000); + cy.wait(200); + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.intercept( 'PATCH', `/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`, @@ -322,6 +320,7 @@ export const deleteVariant_UI = ( ): Chainable => { const project = projectName || 'default'; cy.visit(`/projects/${project}/features/${featureToggleName}/variants`); + cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); cy.wait(300); cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variant}]`).first().click(); From 31282b34eb72dcffb3165fc7fa35bc9691ec9f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 20:22:27 +0100 Subject: [PATCH 10/24] Test without env variables in docker compose --- test-migrations/docker-compose.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 273d0d6f097d..bbe83bc92af0 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -6,11 +6,6 @@ services: image: unleashorg/unleash-server:latest # this is the latest stable release ports: - "4242:4242" - environment: - DATABASE_URL: "postgres://postgres:unleash@db/unleash" - DATABASE_SSL: "false" - LOG_LEVEL: "debug" - INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" depends_on: db: condition: service_healthy @@ -27,10 +22,6 @@ services: expose: - "5432" image: postgres:16 - environment: - POSTGRES_DB: "unleash" - # trust incoming connections blindly (DON'T DO THIS IN PRODUCTION!) - POSTGRES_HOST_AUTH_METHOD: "trust" healthcheck: test: [ From 1bc7e9bed02e5d3205af09a2bc4621be7b03b4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 20:26:39 +0100 Subject: [PATCH 11/24] Envs in docker and step --- .github/workflows/validate-migrations.yaml | 9 +++------ test-migrations/docker-compose.yml | 9 +++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index 780fa5d4034e..a1609df209b6 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -10,12 +10,6 @@ on: - 'test-migrations/**' - 'frontend/cypress' workflow_dispatch: -env: - DATABASE_URL: postgres://postgres:unleash@localhost:5432/unleash - DATABASE_SSL: false - INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" - POSTGRES_DB: unleash - POSTGRES_HOST_AUTH_METHOD: trust jobs: test-migrations: runs-on: ubuntu-latest @@ -34,6 +28,9 @@ jobs: run: docker compose up unleash -d --wait -t 90 # add some data with terraform - name: Apply migrations + env: + DATABASE_URL: postgres://postgres:unleash@localhost:5432/unleash + DATABASE_SSL: false run: | yarn install --frozen-lockfile --ignore-scripts yarn build:backend diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index bbe83bc92af0..273d0d6f097d 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -6,6 +6,11 @@ services: image: unleashorg/unleash-server:latest # this is the latest stable release ports: - "4242:4242" + environment: + DATABASE_URL: "postgres://postgres:unleash@db/unleash" + DATABASE_SSL: "false" + LOG_LEVEL: "debug" + INIT_ADMIN_API_TOKENS: "*:*.unleash-insecure-admin-api-token" depends_on: db: condition: service_healthy @@ -22,6 +27,10 @@ services: expose: - "5432" image: postgres:16 + environment: + POSTGRES_DB: "unleash" + # trust incoming connections blindly (DON'T DO THIS IN PRODUCTION!) + POSTGRES_HOST_AUTH_METHOD: "trust" healthcheck: test: [ From 52d62d5804e78b82b9ffdc0d876450b89b318c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 20:32:49 +0100 Subject: [PATCH 12/24] Verify tip of main broke backward compatibility with 5.6.9 --- test-migrations/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 273d0d6f097d..7f453774519c 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -3,7 +3,7 @@ services: # The Unleash server waits for the migrations to be applied by waiting on the # migrations container to be healthy. unleash: - image: unleashorg/unleash-server:latest # this is the latest stable release + image: unleashorg/unleash-server:5.6.9 # this is the latest stable release ports: - "4242:4242" environment: From 6b4105f98c3712c44e76525b77d7f7c41962aa71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 20:42:12 +0100 Subject: [PATCH 13/24] Verify 5.6.10 restores backward compatibility after tip of main migrations --- test-migrations/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 7f453774519c..273d0d6f097d 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -3,7 +3,7 @@ services: # The Unleash server waits for the migrations to be applied by waiting on the # migrations container to be healthy. unleash: - image: unleashorg/unleash-server:5.6.9 # this is the latest stable release + image: unleashorg/unleash-server:latest # this is the latest stable release ports: - "4242:4242" environment: From cb96c257d058c101185c3eb515e4d1a770834cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 20:44:15 +0100 Subject: [PATCH 14/24] Try using db-migrate script with env variables --- .github/workflows/validate-migrations.yaml | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index a1609df209b6..c31b965be1d1 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -33,8 +33,7 @@ jobs: DATABASE_SSL: false run: | yarn install --frozen-lockfile --ignore-scripts - yarn build:backend - yarn migrate + yarn db-migrate # run ui tests against previous version of Unleash - name: Run Cypress uses: cypress-io/github-action@v5 diff --git a/package.json b/package.json index a92d9f53c72a..23a8f987a676 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "main": "./dist/lib/server-impl.js", "scripts": { "start": "TZ=UTC node ./dist/server.js", - "migrate": "TZ=UTC node ./dist/migrate-db.js", "copy-templates": "copyfiles -u 1 src/mailtemplates/**/*.mustache dist/", "build:backend": "tsc --pretty --strictNullChecks false", "build:frontend": "yarn --cwd ./frontend run build", @@ -39,6 +38,7 @@ "prepare:backend": "concurrently \"yarn:copy-templates\" \"yarn:build:backend\"", "prestart:dev": "yarn run clean", "start:dev": "TZ=UTC NODE_ENV=development tsc-watch --strictNullChecks false --onSuccess \"node dist/server-dev.js\"", + "migrate": "TZ=UTC node ./dist/migrate-db.js", "db-migrate": "db-migrate --migrations-dir ./src/migrations", "lint": "biome check .", "lint:fix": "biome check . --apply", From a58156cdf8bbde1f7ef314d6d04684e62c5d50b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 23:13:14 +0100 Subject: [PATCH 15/24] Attempt to fix cypress test --- frontend/cypress/support/UI.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index a2c0eaeadb2f..fb0727cd68ac 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -55,7 +55,9 @@ export const createFeature_UI = ( ): Chainable => { const projectName = project || 'default'; cy.visit(`/projects/${projectName}`); - cy.get("[data-testid='CloseIcon']").click(); // Close splash + if (document.querySelector("[data-testid='CloseIcon']")) { + cy.get("[data-testid='CloseIcon']").click(); // Close splash + } cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( From 9ca252d1e631d1f4b66c3421f85d15027ddba594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 23:15:32 +0100 Subject: [PATCH 16/24] Migrate up --- .github/workflows/validate-migrations.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml index c31b965be1d1..45bb55019d98 100644 --- a/.github/workflows/validate-migrations.yaml +++ b/.github/workflows/validate-migrations.yaml @@ -33,7 +33,7 @@ jobs: DATABASE_SSL: false run: | yarn install --frozen-lockfile --ignore-scripts - yarn db-migrate + yarn db-migrate up # run ui tests against previous version of Unleash - name: Run Cypress uses: cypress-io/github-action@v5 From 70399a739e46468205da782e587942e279b72add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 23:19:35 +0100 Subject: [PATCH 17/24] Remove unneeded changes --- package.json | 1 - src/lib/server-impl.ts | 59 +++++++++++++++++------------------------- src/migrate-db.ts | 10 ------- 3 files changed, 24 insertions(+), 46 deletions(-) delete mode 100644 src/migrate-db.ts diff --git a/package.json b/package.json index 23a8f987a676..8e6165d607d9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "prepare:backend": "concurrently \"yarn:copy-templates\" \"yarn:build:backend\"", "prestart:dev": "yarn run clean", "start:dev": "TZ=UTC NODE_ENV=development tsc-watch --strictNullChecks false --onSuccess \"node dist/server-dev.js\"", - "migrate": "TZ=UTC node ./dist/migrate-db.js", "db-migrate": "db-migrate --migrations-dir ./src/migrations", "lint": "biome check .", "lint:fix": "biome check . --apply", diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index db0047bfc80b..93d99c2750dc 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -134,45 +134,12 @@ async function start(opts: IUnleashOptions = {}): Promise { const config = createConfig(opts); const logger = config.getLogger('server-impl.js'); - await applyDbMigration( - opts.flagResolver?.isEnabled('migrationLock') ?? true, - opts, - config, - logger, - ); - - const unleash = await createApp(config, true); - if (config.server.gracefulShutdownEnable) { - registerGracefulShutdown(unleash, logger); - } - return unleash; -} - -async function create(opts: IUnleashOptions): Promise { - const config = createConfig(opts); - const logger = config.getLogger('server-impl.js'); - - const useLock = false; - await applyDbMigration(useLock, opts, config, logger); - return createApp(config, false); -} - -async function applyDbMigration( - useLock: boolean, - opts: IUnleashOptions = {}, - configParam?: IUnleashConfig, - loggerParam?: Logger, -) { - const config = configParam ? configParam : createConfig(opts); - const logger = loggerParam - ? loggerParam - : config.getLogger('server-impl.js'); try { if (config.db.disableMigration) { logger.info('DB migration: disabled'); } else { logger.debug('DB migration: start'); - if (useLock) { + if (opts.flagResolver?.isEnabled('migrationLock')) { logger.info('Running migration with lock'); const lock = withDbLock(config.db, { lockKey: defaultLockKey, @@ -190,6 +157,29 @@ async function applyDbMigration( logger.error('Failed to migrate db', err); throw err; } + + const unleash = await createApp(config, true); + if (config.server.gracefulShutdownEnable) { + registerGracefulShutdown(unleash, logger); + } + return unleash; +} + +async function create(opts: IUnleashOptions): Promise { + const config = createConfig(opts); + const logger = config.getLogger('server-impl.js'); + + try { + if (config.db.disableMigration) { + logger.info('DB migrations disabled'); + } else { + await migrateDb(config); + } + } catch (err) { + logger.error('Failed to migrate db', err); + throw err; + } + return createApp(config, false); } export default { @@ -200,7 +190,6 @@ export default { export { start, create, - applyDbMigration, Controller, AuthenticationRequired, User, diff --git a/src/migrate-db.ts b/src/migrate-db.ts deleted file mode 100644 index 178b8e9064c5..000000000000 --- a/src/migrate-db.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { applyDbMigration } from './lib/server-impl'; - -try { - const useLock = true; - applyDbMigration(useLock).then(() => process.exit()); -} catch (error) { - // eslint-disable-next-line no-console - console.error(error); - process.exit(); -} From 8f18e07e59486837a67c4be50339fa028c51a83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 29 Nov 2023 23:23:08 +0100 Subject: [PATCH 18/24] Attempt to fix cypress tests --- frontend/cypress/global.d.ts | 1 + frontend/cypress/oss/feature/feature.spec.ts | 11 ++++++++--- frontend/cypress/support/UI.ts | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/cypress/global.d.ts b/frontend/cypress/global.d.ts index 01731c513442..fa6ef0867114 100644 --- a/frontend/cypress/global.d.ts +++ b/frontend/cypress/global.d.ts @@ -33,6 +33,7 @@ declare namespace Cypress { name: string, shouldWait?: boolean, project?: string, + closeSplash?: boolean, // @deprecated to support old tests ): Chainable; // VARIANTS diff --git a/frontend/cypress/oss/feature/feature.spec.ts b/frontend/cypress/oss/feature/feature.spec.ts index 0dbf8a26d804..87f686ab139a 100644 --- a/frontend/cypress/oss/feature/feature.spec.ts +++ b/frontend/cypress/oss/feature/feature.spec.ts @@ -21,19 +21,24 @@ describe('feature', () => { }); it('can create a feature toggle', () => { - cy.createFeature_UI(featureToggleName, true); + cy.createFeature_UI(featureToggleName, true, 'default', true); 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, 'default', true); 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, + 'default', + true, + ); cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( `"name" must be URL friendly`, ); diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index fb0727cd68ac..7397ada85b98 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -52,10 +52,11 @@ export const createFeature_UI = ( name: string, shouldWait?: boolean, project?: string, + closeSplash?: boolean, ): Chainable => { const projectName = project || 'default'; cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CloseIcon']")) { + if (closeSplash ?? false) { cy.get("[data-testid='CloseIcon']").click(); // Close splash } cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); From 3829ca360cd523c5b6cc84bfbf4a7834d1ec42af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 30 Nov 2023 16:39:31 +0100 Subject: [PATCH 19/24] chore: keeping UI test simple and testing it fails in 5.6.9 --- frontend/cypress/oss/feature/feature.spec.ts | 79 ------------------- frontend/cypress/oss/import/import.spec.ts | 3 +- .../cypress/oss/segments/segments.spec.ts | 16 ---- frontend/cypress/support/UI.ts | 21 ++--- frontend/package.json | 1 + test-migrations/docker-compose.yml | 2 +- 6 files changed, 14 insertions(+), 108 deletions(-) diff --git a/frontend/cypress/oss/feature/feature.spec.ts b/frontend/cypress/oss/feature/feature.spec.ts index 87f686ab139a..660339d328e6 100644 --- a/frontend/cypress/oss/feature/feature.spec.ts +++ b/frontend/cypress/oss/feature/feature.spec.ts @@ -4,9 +4,6 @@ describe('feature', () => { const randomId = String(Math.random()).split('.')[1]; const featureToggleName = `unleash-e2e-${randomId}`; - const variant1 = 'variant1'; - const variant2 = 'variant2'; - before(() => { cy.runBefore(); }); @@ -24,80 +21,4 @@ describe('feature', () => { cy.createFeature_UI(featureToggleName, true, 'default', true); cy.url().should('include', featureToggleName); }); - - it('gives an error if a toggle exists with the same name', () => { - cy.createFeature_UI(featureToggleName, false, 'default', true); - 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, - 'default', - true, - ); - cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( - `"name" must be URL friendly`, - ); - }); - - it('can add, update and delete a gradual rollout strategy to the development environment', () => { - cy.addFlexibleRolloutStrategyToFeature_UI({ - featureToggleName, - }).then(() => { - cy.get("[data-testid='CloseIcon']").click(); // Close splash - cy.updateFlexibleRolloutStrategy_UI(featureToggleName).then(() => - cy.deleteFeatureStrategy_UI(featureToggleName), - ); - }); - }); - - it('can add variants to the development environment', () => { - cy.addVariantsToFeature_UI(featureToggleName, [variant1, variant2]); - }); - - it('can update variants', () => { - cy.visit(`/projects/default/features/${featureToggleName}/variants`); - cy.get("[data-testid='CloseIcon']").click(); // Close splash - cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); - cy.get('[data-testid=VARIANT_NAME_INPUT]') - .last() - .children() - .find('input') - .should('have.attr', 'disabled'); - cy.get('[data-testid=VARIANT_WEIGHT_CHECK]') - .last() - .find('input') - .check(); - cy.get('[data-testid=VARIANT_WEIGHT_INPUT]').last().clear().type('15'); - - cy.intercept( - 'PATCH', - `/api/admin/projects/default/features/${featureToggleName}/environments/development/variants`, - (req) => { - expect(req.body[0].op).to.equal('replace'); - expect(req.body[0].path).to.equal('/1/weightType'); - expect(req.body[0].value).to.equal('fix'); - expect(req.body[1].op).to.equal('replace'); - expect(req.body[1].path).to.equal('/1/weight'); - expect(req.body[1].value).to.equal(150); - expect(req.body[2].op).to.equal('replace'); - expect(req.body[2].path).to.equal('/0/weight'); - expect(req.body[2].value).to.equal(850); - }, - ).as('variantUpdate'); - - cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); - cy.get(`[data-testid=VARIANT_WEIGHT_${variant2}]`).should( - 'have.text', - '15 %', - ); - }); - - it('can delete variants', () => { - cy.deleteVariant_UI(featureToggleName, variant2); - }); }); diff --git a/frontend/cypress/oss/import/import.spec.ts b/frontend/cypress/oss/import/import.spec.ts index 3ae63d3cd1ff..e0fd2cb26e07 100644 --- a/frontend/cypress/oss/import/import.spec.ts +++ b/frontend/cypress/oss/import/import.spec.ts @@ -34,8 +34,7 @@ describe('imports', () => { it('can import data', () => { cy.visit('/projects/default'); - cy.get("[data-testid='CloseIcon']").click(); // Close splash - cy.get("[data-testid='IMPORT_BUTTON']").click({ force: true }); + cy.get("[data-testid='IMPORT_BUTTON']").click(); const exportText = { features: [ diff --git a/frontend/cypress/oss/segments/segments.spec.ts b/frontend/cypress/oss/segments/segments.spec.ts index c75da6f174a7..1adca29917ed 100644 --- a/frontend/cypress/oss/segments/segments.spec.ts +++ b/frontend/cypress/oss/segments/segments.spec.ts @@ -3,7 +3,6 @@ describe('segments', () => { const randomId = String(Math.random()).split('.')[1]; const segmentName = `unleash-e2e-${randomId}`; - let segmentId: string; before(() => { cy.runBefore(); @@ -15,25 +14,10 @@ describe('segments', () => { if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { cy.get("[data-testid='CLOSE_SPLASH']").click(); } - cy.get("[data-testid='CloseIcon']").click(); // Close splash }); it('can create a segment', () => { cy.createSegment_UI(segmentName); cy.contains(segmentName); }); - - it('gives an error if a segment exists with the same name', () => { - cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click(); - cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName); - cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").should('be.disabled'); - cy.get("[data-testid='INPUT_ERROR_TEXT']").contains( - 'Segment name already exists', - ); - }); - - it('can delete a segment', () => { - cy.deleteSegment_UI(segmentName, segmentId); - cy.contains(segmentName).should('not.exist'); - }); }); diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index 7397ada85b98..54eedf70ab10 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -52,14 +52,12 @@ export const createFeature_UI = ( name: string, shouldWait?: boolean, project?: string, - closeSplash?: boolean, + forceInteractions?: boolean, ): Chainable => { const projectName = project || 'default'; + const uiOpts = forceInteractions ? { force: true } : undefined; cy.visit(`/projects/${projectName}`); - if (closeSplash ?? false) { - cy.get("[data-testid='CloseIcon']").click(); // Close splash - } - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); + cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(uiOpts); cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( 'createFeature', @@ -67,10 +65,13 @@ export const createFeature_UI = ( cy.wait(300); - cy.get("[data-testid='CF_NAME_ID'").type(name); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - if (!shouldWait) return cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); - else cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); + cy.get("[data-testid='CF_NAME_ID'] input").type(name, uiOpts); + cy.get("[data-testid='CF_DESC_ID'] textarea") + .first() + .type('hello-world', uiOpts); + if (!shouldWait) + return cy.get("[data-testid='CF_CREATE_BTN_ID']").click(uiOpts); + else cy.get("[data-testid='CF_CREATE_BTN_ID']").click(uiOpts); return cy.wait('@createFeature'); }; @@ -288,7 +289,7 @@ export const addVariantsToFeature_UI = ( const project = projectName || 'default'; cy.visit(`/projects/${project}/features/${featureToggleName}/variants`); cy.wait(200); - cy.get("[data-testid='CloseIcon']").click(); // Close splash + cy.intercept( 'PATCH', `/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`, diff --git a/frontend/package.json b/frontend/package.json index 892ee30cc4c2..202b6b8409bf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "fmt:check": "biome check src", "ts:check": "tsc", "e2e": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn run cypress open --config baseUrl='http://localhost:3000' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all", + "e2e:oss": "yarn --cwd frontend run cypress run --spec \"cypress/oss/**/*.spec.ts\" --config baseUrl='http://localhost:4242' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all", "e2e:heroku": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" yarn run cypress open --config baseUrl='https://unleash.herokuapp.com' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all", "gen:api": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" orval --config orval.config.js", "gen:api:demo": "NODE_OPTIONS=\"${NODE_OPTIONS} --no-experimental-fetch\" UNLEASH_OPENAPI_URL=https://app.unleash-hosted.com/demo/docs/openapi.json yarn run gen:api", diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 273d0d6f097d..7f453774519c 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -3,7 +3,7 @@ services: # The Unleash server waits for the migrations to be applied by waiting on the # migrations container to be healthy. unleash: - image: unleashorg/unleash-server:latest # this is the latest stable release + image: unleashorg/unleash-server:5.6.9 # this is the latest stable release ports: - "4242:4242" environment: From a0c3b6ae708b5e6732bd6895aac0ca980e7c87a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 30 Nov 2023 17:22:55 +0100 Subject: [PATCH 20/24] Check that 5.6.10 does not have problems after migration --- test-migrations/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 7f453774519c..97663ec8972e 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -3,7 +3,7 @@ services: # The Unleash server waits for the migrations to be applied by waiting on the # migrations container to be healthy. unleash: - image: unleashorg/unleash-server:5.6.9 # this is the latest stable release + image: unleashorg/unleash-server:5.6.10 # this is the latest stable release ports: - "4242:4242" environment: From 60d03f55fa4210e820ade27e65ce0736078406f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 30 Nov 2023 17:34:52 +0100 Subject: [PATCH 21/24] Keep things simple in the UI tests --- frontend/cypress/oss/import/import.spec.ts | 131 ------------------ .../cypress/oss/segments/segments.spec.ts | 23 --- 2 files changed, 154 deletions(-) delete mode 100644 frontend/cypress/oss/import/import.spec.ts delete mode 100644 frontend/cypress/oss/segments/segments.spec.ts diff --git a/frontend/cypress/oss/import/import.spec.ts b/frontend/cypress/oss/import/import.spec.ts deleted file mode 100644 index e0fd2cb26e07..000000000000 --- a/frontend/cypress/oss/import/import.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -/// - -describe('imports', () => { - const baseUrl = Cypress.config().baseUrl; - const randomSeed = String(Math.random()).split('.')[1]; - const randomFeatureName = `cypress-features${randomSeed}`; - const userIds: any[] = []; - - before(() => { - cy.runBefore(); - cy.login_UI(); - for (let i = 1; i <= 2; i++) { - cy.request('POST', `${baseUrl}/api/admin/user-admin`, { - name: `unleash-e2e-user${i}-${randomFeatureName}`, - email: `unleash-e2e-user${i}-${randomFeatureName}@test.com`, - sendEmail: false, - rootRole: 3, - }).then((response) => userIds.push(response.body.id)); - } - }); - - after(() => { - userIds.forEach((id) => - cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`), - ); - }); - - beforeEach(() => { - cy.login_UI(); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - }); - - it('can import data', () => { - cy.visit('/projects/default'); - cy.get("[data-testid='IMPORT_BUTTON']").click(); - - const exportText = { - features: [ - { - name: randomFeatureName, - description: '', - type: 'release', - project: 'default', - stale: false, - impressionData: false, - archived: false, - }, - ], - featureStrategies: [ - { - name: 'flexibleRollout', - id: '14a0d9dd-2b5d-4a21-98fd-ede72bda0328', - featureName: randomFeatureName, - parameters: { - groupId: randomFeatureName, - rollout: '50', - stickiness: 'default', - }, - constraints: [], - segments: [], - }, - ], - featureEnvironments: [ - { - enabled: true, - featureName: randomFeatureName, - environment: 'test', - variants: [], - name: randomFeatureName, - }, - ], - contextFields: [], - featureTags: [ - { - featureName: randomFeatureName, - tagType: 'simple', - tagValue: 'best-tag', - }, - { - featureName: randomFeatureName, - tagType: 'simple', - tagValue: 'rserw', - }, - { - featureName: randomFeatureName, - tagType: 'simple', - tagValue: 'FARO', - }, - ], - segments: [], - tagTypes: [ - { - name: 'simple', - description: 'Used to simplify filtering of features', - icon: '#', - }, - ], - }; - - cy.get("[data-testid='VALIDATE_BUTTON']").should('be.disabled'); - - // cypress can only work with input@file that is visible - cy.get('input[type=file]') - .invoke('attr', 'style', 'display: block') - .selectFile({ - contents: Cypress.Buffer.from(JSON.stringify(exportText)), - fileName: 'upload.json', - lastModified: Date.now(), - }); - cy.get("[data-testid='VALIDATE_BUTTON']").click(); - cy.get("[data-testid='IMPORT_CONFIGURATION_BUTTON']").click(); - // cy.contains('Import completed'); - - cy.visit(`/projects/default/features/${randomFeatureName}`); - - cy.wait(500); - - cy.get( - "[data-testid='feature-toggle-status'] input[type='checkbox']:checked", - ) - .invoke('attr', 'aria-label') - .should('eq', 'development'); - - cy.get( - '[data-testid="FEATURE_ENVIRONMENT_ACCORDION_development"]', - ).click(); - cy.contains('50%'); - }); -}); diff --git a/frontend/cypress/oss/segments/segments.spec.ts b/frontend/cypress/oss/segments/segments.spec.ts deleted file mode 100644 index 1adca29917ed..000000000000 --- a/frontend/cypress/oss/segments/segments.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// - -describe('segments', () => { - const randomId = String(Math.random()).split('.')[1]; - const segmentName = `unleash-e2e-${randomId}`; - - before(() => { - cy.runBefore(); - }); - - beforeEach(() => { - cy.login_UI(); - cy.visit('/segments'); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - }); - - it('can create a segment', () => { - cy.createSegment_UI(segmentName); - cy.contains(segmentName); - }); -}); From 36e82635877f0906afe3ca954ca6f82b8791ba29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 30 Nov 2023 17:36:01 +0100 Subject: [PATCH 22/24] Verify tests fail in 5.6.9 --- test-migrations/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 97663ec8972e..7f453774519c 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -3,7 +3,7 @@ services: # The Unleash server waits for the migrations to be applied by waiting on the # migrations container to be healthy. unleash: - image: unleashorg/unleash-server:5.6.10 # this is the latest stable release + image: unleashorg/unleash-server:5.6.9 # this is the latest stable release ports: - "4242:4242" environment: From bdaab842dfb07886d20ec8c0bf75d66838500d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 30 Nov 2023 17:43:24 +0100 Subject: [PATCH 23/24] Verify tests succeed with latest: 5.7.0 --- test-migrations/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 7f453774519c..273d0d6f097d 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -3,7 +3,7 @@ services: # The Unleash server waits for the migrations to be applied by waiting on the # migrations container to be healthy. unleash: - image: unleashorg/unleash-server:5.6.9 # this is the latest stable release + image: unleashorg/unleash-server:latest # this is the latest stable release ports: - "4242:4242" environment: From 1e538d4d7cb94b7abea1a61dd61aa667f13bcca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 30 Nov 2023 18:05:19 +0100 Subject: [PATCH 24/24] Remove splash close and add always pull policy --- frontend/cypress/support/UI.ts | 3 +-- test-migrations/docker-compose.yml | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index 54eedf70ab10..363eb9eeffee 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -124,7 +124,7 @@ export const addFlexibleRolloutStrategyToFeature_UI = ( const defaultStickiness = stickiness || 'default'; cy.visit(`/projects/${projectName}/features/${featureToggleName}`); - cy.get("[data-testid='CloseIcon']").click(); // Close splash + cy.intercept( 'POST', `/api/admin/projects/${projectName}/features/${featureToggleName}/environments/development/strategies`, @@ -324,7 +324,6 @@ export const deleteVariant_UI = ( ): Chainable => { const project = projectName || 'default'; cy.visit(`/projects/${project}/features/${featureToggleName}/variants`); - cy.get("[data-testid='CloseIcon']").click(); // Close splash cy.get('[data-testid=EDIT_VARIANTS_BUTTON]').click(); cy.wait(300); cy.get(`[data-testid=VARIANT_DELETE_BUTTON_${variant}]`).first().click(); diff --git a/test-migrations/docker-compose.yml b/test-migrations/docker-compose.yml index 273d0d6f097d..ee88f279da8b 100644 --- a/test-migrations/docker-compose.yml +++ b/test-migrations/docker-compose.yml @@ -4,6 +4,7 @@ services: # migrations container to be healthy. unleash: image: unleashorg/unleash-server:latest # this is the latest stable release + pull_policy: "always" ports: - "4242:4242" environment: