diff --git a/e2e-tests/.env b/e2e-tests/.env new file mode 100644 index 0000000000..2d53cf6043 --- /dev/null +++ b/e2e-tests/.env @@ -0,0 +1,3 @@ +DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable +WM_IMAGE=ghcr.io/windmill-labs/windmill-ee:main + diff --git a/e2e-tests/docker-compose.yml b/e2e-tests/docker-compose.yml index 645016976e..f34c71da7e 100644 --- a/e2e-tests/docker-compose.yml +++ b/e2e-tests/docker-compose.yml @@ -34,11 +34,14 @@ services: deploy: replicas: 1 restart: unless-stopped + ports: + - 8000:8000 expose: - 8000 - 2525 environment: - DATABASE_URL=${DATABASE_URL} + - LICENSE_KEY=${LICENSE_KEY} - MODE=server depends_on: db: @@ -65,6 +68,7 @@ services: # privileged: true environment: - DATABASE_URL=${DATABASE_URL} + - LICENSE_KEY=${LICENSE_KEY} - MODE=worker - WORKER_GROUP=default # Uncomment to enable PID namespace isolation (requires privileged: true above) @@ -100,6 +104,7 @@ services: # privileged: true environment: - DATABASE_URL=${DATABASE_URL} + - LICENSE_KEY=${LICENSE_KEY} - MODE=worker - WORKER_GROUP=native - NUM_WORKERS=8 @@ -113,6 +118,25 @@ services: - worker_logs:/tmp/windmill/logs logging: *default-logging + caddy: + image: ghcr.io/windmill-labs/caddy-l4:latest + restart: unless-stopped + # Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed + volumes: + - ../Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + # - ../certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caddyfile + ports: + # To change the exposed port, simply change 80:80 to :80. No other changes needed + - 80:80 + - 25:25 + # - 443:443 # Uncomment to enable HTTPS handling by Caddy + environment: + - BASE_URL=":80" + # - BASE_URL=":443" # uncomment and comment line above to enable HTTPS via custom certificate and key files + # - BASE_URL=mydomain.com # Uncomment and comment line above to enable HTTPS handling by Caddy + logging: *default-logging + # E2E Testing Services mysql_e2e: image: mysql:8.0 @@ -227,6 +251,7 @@ volumes: db_data: null worker_dependency_cache: null worker_logs: null + caddy_data: null mysql_data: null mssql_data: null oracle_data: null diff --git a/frontend/e2e/DbManagerPage.ts b/frontend/e2e/DbManagerPage.ts index 036f7b39bb..d183e6a3da 100644 --- a/frontend/e2e/DbManagerPage.ts +++ b/frontend/e2e/DbManagerPage.ts @@ -4,8 +4,9 @@ import { expect, Locator, Page } from '@playwright/test' import { getDbFeatures } from '../src/lib/components/apps/components/display/dbtable/dbFeatures' import { ConfirmationModal, Toast } from './utils' import { DbInput, DbType } from '../src/lib/components/dbTypes' +import { DB_TYPES } from '../src/lib/consts' -export async function runDbManagerSimpleCRUDTest(page: Page, db: _DbType) { +export async function runDbManagerSimpleCRUDTest(page: Page, dbType: _DbType) { let dbManager = new DbManagerPage(page) await dbManager.expectToBeVisible() @@ -14,7 +15,8 @@ export async function runDbManagerSimpleCRUDTest(page: Page, db: _DbType) { // Create table const tableEditor = await dbManager.openCreateTableDrawer() await tableEditor.setTableName(friendTableName) - await tableEditor.addColumn('name', 'TEXT') + await tableEditor.addColumn('name', getDbDatatype(dbType, 'TEXT')) + await tableEditor.getColumn('id').delete() // remove default id column await tableEditor.createTable() await Toast.expectSuccess(page, `${friendTableName} created`) @@ -60,14 +62,14 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { let tableEditor = await dbManager.openCreateTableDrawer() await tableEditor.setTableName(friendTableName) let friendIdCol = await tableEditor.getColumn('id') // deafult id column - await friendIdCol.setType('INT') + await friendIdCol.setType(getDbDatatype(dbType, 'INT')) if (dbFeatures.primaryKeys) { friendIdCol.setPrimaryKey(true) } else { await expect(await friendIdCol.primaryKeyCheckbox()).toBeHidden() } - await tableEditor.addColumn('name', 'TEXT') - await tableEditor.addColumn('created_at', dbType === 'ms_sql_server' ? 'DATETIME2' : 'TIMESTAMP') + await tableEditor.addColumn('name', getDbDatatype(dbType, 'TEXT')) + await tableEditor.addColumn('created_at', getDbDatatype(dbType, 'TIMESTAMP')) await tableEditor.createTable() await Toast.expectSuccess(page, `${friendTableName} created`) await page.waitForTimeout(100) @@ -77,12 +79,12 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { tableEditor = await dbManager.openCreateTableDrawer() await tableEditor.setTableName(messageTableName) let messageIdCol = await tableEditor.getColumn('id') // deafult id column - await messageIdCol.setType('INT') + await messageIdCol.setType(getDbDatatype(dbType, 'INT')) if (dbFeatures.primaryKeys) messageIdCol.setPrimaryKey(true) - await tableEditor.addColumn('friend_id', 'INT') - let contentColumn = await tableEditor.addColumn('content', 'TEXT') + await tableEditor.addColumn('friend_id', getDbDatatype(dbType, 'INT')) + let contentColumn = await tableEditor.addColumn('content', getDbDatatype(dbType, 'TEXT')) await contentColumn.setSettings({ nullable: true }) - await tableEditor.addColumn('created_at', dbType === 'ms_sql_server' ? 'DATETIME2' : 'TIMESTAMP') + await tableEditor.addColumn('created_at', getDbDatatype(dbType, 'TIMESTAMP')) if (dbFeatures.foreignKeys) { await tableEditor.addForeignKey(friendTableName, 'friend_id', 'id', { onDelete: 'Cascade', @@ -119,7 +121,7 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { await tableEditor.setTableName(postsTableName) await idCol.delete() await friendCol.setName('person_id') - await friendCol.setType('BIGINT') + await friendCol.setType(getDbDatatype(dbType, 'BIGINT')) await friendCol.setSettings({ defaultValue: dbFeatures.defaultValues ? '123' : undefined, nullable: false @@ -141,11 +143,11 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { tableEditor = new TableEditorDrawer(page) await idCol.checkNotExists() await friendCol.checkNameIs('person_id') - await friendCol.checkTypeIs('BIGINT') + await friendCol.checkTypeIs(getDbDatatype(dbType, 'BIGINT')) if (dbFeatures.defaultValues) { await friendCol.checkSettingsIs({ defaultValue: /123/ }) } - await createdAtCol.checkTypeIs('TIMESTAMP') + await createdAtCol.checkTypeIs(getDbDatatype(dbType, 'TIMESTAMP')) await createdAtCol.checkNameIs('created_at') if (dbFeatures.primaryKeys) { await friendCol.checkPrimaryKeyIs(true) @@ -359,6 +361,7 @@ class Column { async delete() { const deleteBtn = (await this.row()).locator('button.delete-column-btn') await deleteBtn.click() + await this.page.waitForTimeout(50) } async setPrimaryKey(isPrimaryKey: boolean) { @@ -490,3 +493,10 @@ function getDbInput(dbType: _DbType): DbInput { return { type: 'database', resourceType: dbType, resourcePath: '' } } } + +// Ensure exact casing of datatype as per DB_TYPES +function getDbDatatype(dbType: _DbType, datatype: string): string { + if (dbType === 'ms_sql_server' && datatype.toLowerCase() === 'timestamp') datatype = 'datetime2' + const allDataTypes = DB_TYPES[dbType == 'ducklake' ? 'duckdb' : dbType] || [] + return allDataTypes.find((dt) => dt.toLowerCase() === datatype.toLowerCase()) || datatype +} diff --git a/frontend/e2e/global-setup.ts b/frontend/e2e/global-setup.ts index d835eb73d0..a173077457 100644 --- a/frontend/e2e/global-setup.ts +++ b/frontend/e2e/global-setup.ts @@ -47,6 +47,16 @@ async function globalSetup(config: FullConfig) { await page.waitForURL('**/') await page.locator('text=Home').first().waitFor({ state: 'visible' }) + const hubSyncBtn = page.locator('text=u/admin/hub_sync') + await hubSyncBtn.waitFor({ state: 'visible' }) + await hubSyncBtn.click() + + const runBtn = page.locator('#run-form-run-button') + await runBtn.waitFor({ state: 'visible' }) + await runBtn.click() + await page.waitForURL(/\/run\/.+/) + await page.locator('text=Success').waitFor({ timeout: 30000, state: 'visible' }) + // Save the authenticated state await context.storageState({ path: './e2e/auth.json' }) await browser.close() diff --git a/frontend/e2e/resource-dbmanager.spec.ts b/frontend/e2e/resource-dbmanager.spec.ts index 1ff3376fc8..ead320acbe 100644 --- a/frontend/e2e/resource-dbmanager.spec.ts +++ b/frontend/e2e/resource-dbmanager.spec.ts @@ -14,15 +14,15 @@ test.describe('DB Manager', () => { const resourceByDbType = { postgresql: { - host: 'localhost', + host: 'postgres_e2e', port: 5432, - dbname: 'testing', - user: 'postgres', - password: 'changeme', + dbname: 'test_db', + user: 'test_user', + password: 'postgres_password', sslmode: 'disable' }, mysql: { - host: 'localhost', + host: 'mysql_e2e', port: 3306, user: 'test_user', database: 'test_db', @@ -32,10 +32,10 @@ const resourceByDbType = { oracle: { user: 'test_user', password: 'test_password', - database: 'localhost:1521/test_db' + database: 'oracle_e2e:1521/test_db' }, ms_sql_server: { - host: 'localhost', + host: 'mssql_e2e', user: 'sa', password: 'MsSql_Pass123!', port: 1433, @@ -79,8 +79,6 @@ async function setupNewResource( const jsonEditor = page.locator('.simple-editor .view-lines') await expect(jsonEditor).toBeVisible() await jsonEditor.click({ clickCount: 4 }) // Select all existing text - // We do copy paste to avoid issues with monaco helping with autocomplete and messing up the test - await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) await page.evaluate((c) => navigator.clipboard.writeText(c), JSON.stringify(resourceObj)) await page.keyboard.press('ControlOrMeta+V') @@ -101,3 +99,4 @@ async function setupNewResourceAndOpenDbManager(page: Page, dbType: DbType) { const manageButton = resourceRow.locator('button:has-text("Manage")') await manageButton.click() } +declare const process: any // avoid TS errors diff --git a/frontend/src/lib/components/RunForm.svelte b/frontend/src/lib/components/RunForm.svelte index 66eaa74552..346a7c90a0 100644 --- a/frontend/src/lib/components/RunForm.svelte +++ b/frontend/src/lib/components/RunForm.svelte @@ -283,6 +283,7 @@