diff --git a/.github/workflows/validate-migrations.yaml b/.github/workflows/validate-migrations.yaml
new file mode 100644
index 0000000000..45bb55019d
--- /dev/null
+++ b/.github/workflows/validate-migrations.yaml
@@ -0,0 +1,44 @@
+name: Test db migrations
+
+on:
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'src/migrations/**'
+ - '.github/workflows/validate-migrations.yaml'
+ - 'test-migrations/**'
+ - 'frontend/cypress'
+ workflow_dispatch:
+jobs:
+ test-migrations:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - 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 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 db-migrate up
+ # 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
+ spec: cypress/oss/**/*.spec.ts
diff --git a/frontend/cypress/global.d.ts b/frontend/cypress/global.d.ts
index 01731c5134..fa6ef08671 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
new file mode 100644
index 0000000000..660339d328
--- /dev/null
+++ b/frontend/cypress/oss/feature/feature.spec.ts
@@ -0,0 +1,24 @@
+///
+
+describe('feature', () => {
+ const randomId = String(Math.random()).split('.')[1];
+ const featureToggleName = `unleash-e2e-${randomId}`;
+
+ 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, 'default', true);
+ cy.url().should('include', featureToggleName);
+ });
+});
diff --git a/frontend/cypress/support/API.ts b/frontend/cypress/support/API.ts
index 005f73c1ce..603dbbad0c 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 2a3d764a3f..363eb9eeff 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) {
@@ -52,10 +52,12 @@ export const createFeature_UI = (
name: string,
shouldWait?: boolean,
project?: string,
+ forceInteractions?: boolean,
): Chainable => {
const projectName = project || 'default';
- cy.visit(`/projects/${project}`);
- cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click();
+ const uiOpts = forceInteractions ? { force: true } : undefined;
+ cy.visit(`/projects/${projectName}`);
+ cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(uiOpts);
cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as(
'createFeature',
@@ -63,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');
};
@@ -283,7 +288,8 @@ export const addVariantsToFeature_UI = (
) => {
const project = projectName || 'default';
cy.visit(`/projects/${project}/features/${featureToggleName}/variants`);
- cy.wait(1000);
+ cy.wait(200);
+
cy.intercept(
'PATCH',
`/api/admin/projects/${project}/features/${featureToggleName}/environments/development/variants`,
diff --git a/frontend/package.json b/frontend/package.json
index 0b7de1de11..5a6ac36794 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
new file mode 100644
index 0000000000..ee88f279da
--- /dev/null
+++ b/test-migrations/docker-compose.yml
@@ -0,0 +1,47 @@
+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
+ pull_policy: "always"
+ 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
+ healthcheck:
+ test: wget --no-verbose --tries=1 --spider http://localhost:4242/health || exit 1
+ interval: 1s
+ timeout: 1m
+ retries: 5
+ start_period: 15s
+
+ db:
+ ports:
+ - "5432:5432"
+ 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