mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 16:02:14 +00:00
a384b4c23d
* clean plate * npm i * log in e2e * global setup login * set license key * Revert "set license key" This reverts commit86d5db2c48. * create datatable test * fix wrong pg_creds * data table + db manager e2e test * DbManagerPage class * small refactor * create resource test + improvements * text db manager in resources * Factor test logic in classes * refactoring * refacto * alter table test * alter table e2e test * set schema in test * nits * fix wrong schema var * Correct setup and parallelization * reducedMotion * tests passing headless ! * bigger timeout * start e2e docker compose * e2e runs on all databases * nit test uid fix * refactp * stash * Better Workspace Storage settings * minio setup * nit * nit * super nit * Permission settings in modal * badge indicator * Fetch alter table metadata much faster * Upgrade duckdb to 1.4.3 * Ducklake tests * Disable transactional DDL for Ducklake (bug on their side) * git ignore env * bigquery tests passes * getJsonEnv * load coldef in parallel * Make Bigquery schema fetching much faster * makeLoadTableMetaDataQuery for entire db in bigquery * refactor getDbSchemas to avoid assignment side effect * fix col def * Better loading state mgmt * snowflake * fix snowflake primary keys * Test CI * fix setTimeout type * remove type node * test e2e ci * Revert "test e2e ci" This reverts commitbf98a755dc. * remove ci * fix snowflake pk query in alternate schemas * nit wait for coldefs * nit snowflake * Snowflake fk fix * UNPROCESSABLE_ENTITY instead of INTERNAL_ERROR * nits * fix alter pk in snowflake * yet other fixes * snowflake tests pass * nits
336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
import { test, expect, Page, Locator } from '@playwright/test'
|
|
import { runDbManagerAlterTableTest, runDbManagerSimpleCRUDTest } from './DbManagerPage'
|
|
import { DbType } from '../src/lib/components/dbTypes'
|
|
import { Dropdown, Toast, prettify } from './utils'
|
|
|
|
const resourceByDbType = {
|
|
postgresql: getJsonEnv('POSTGRESQL_RESOURCE') ?? {
|
|
host: 'postgres_e2e',
|
|
port: 5432,
|
|
dbname: 'test_db',
|
|
user: 'test_user',
|
|
password: 'postgres_password',
|
|
sslmode: 'disable'
|
|
},
|
|
mysql: getJsonEnv('MYSQL_RESOURCE') ?? {
|
|
host: 'mysql_e2e',
|
|
port: 3306,
|
|
user: 'test_user',
|
|
database: 'test_db',
|
|
password: 'test_password',
|
|
ssl: false
|
|
},
|
|
oracledb: getJsonEnv('ORACLEDB_RESOURCE') ?? {
|
|
user: 'test_user',
|
|
password: 'test_password',
|
|
database: 'oracle_e2e:1521/test_db'
|
|
},
|
|
ms_sql_server: getJsonEnv('MS_SQL_SERVER_RESOURCE') ?? {
|
|
host: 'mssql_e2e',
|
|
user: 'sa',
|
|
password: 'MsSql_Pass123!',
|
|
port: 1433,
|
|
dbname: 'master',
|
|
instance_name: '',
|
|
trust_cert: true,
|
|
ca_cert: '',
|
|
encrypt: true
|
|
},
|
|
bigquery: getJsonEnv('BIGQUERY_RESOURCE'),
|
|
snowflake: getJsonEnv('SNOWFLAKE_RESOURCE')
|
|
} as const
|
|
|
|
test.describe('Database resources', () => {
|
|
for (const dbType of ['postgresql', 'mysql', 'bigquery', 'ms_sql_server', 'snowflake'] as const) {
|
|
test.describe(prettify(dbType), () => {
|
|
test.skip(
|
|
resourceByDbType[dbType] === undefined,
|
|
`No resource config for ${dbType}, set ${dbType.toUpperCase()}_RESOURCE`
|
|
)
|
|
test(`simple CRUD with DB Manager`, async ({ page }) => {
|
|
await setupNewResourceAndOpenDbManager(page, dbType)
|
|
await runDbManagerSimpleCRUDTest(page, dbType)
|
|
})
|
|
test(`Alter table with DB Manager`, async ({ page }) => {
|
|
await setupNewResourceAndOpenDbManager(page, dbType)
|
|
await runDbManagerAlterTableTest(page, dbType)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
test.describe('Data tables', () => {
|
|
test('simple CRUD with DB Manager', async ({ page }) => {
|
|
await openDataTableDbManager(page)
|
|
await runDbManagerSimpleCRUDTest(page, 'postgresql')
|
|
})
|
|
test('Alter table with DB Manager', async ({ page }) => {
|
|
await openDataTableDbManager(page)
|
|
await runDbManagerAlterTableTest(page, 'postgresql')
|
|
})
|
|
})
|
|
|
|
test.describe('Ducklake', () => {
|
|
// Avoid race condition when saving settings.
|
|
// Be careful when adding new tests here.
|
|
// TODO : find a way to run these tests in parallel (setup for all storage resources ?)
|
|
test.describe.configure({ mode: 'serial' })
|
|
|
|
test('simple CRUD with DB Manager', async ({ page }) => {
|
|
await openDucklakeDbManager(page, 's3')
|
|
await runDbManagerSimpleCRUDTest(page, 'ducklake')
|
|
})
|
|
test('Alter table with DB Manager', async ({ page }) => {
|
|
await openDucklakeDbManager(page, 's3')
|
|
await runDbManagerAlterTableTest(page, 'ducklake')
|
|
})
|
|
})
|
|
|
|
async function openDucklakeDbManager(page: Page, resource_type: StorageResourceType) {
|
|
let { storage } = await setupWsStorage(page, resource_type)
|
|
await page.goto('/workspace_settings?tab=windmill_lfs')
|
|
const timestamp = Date.now()
|
|
await page.locator('button:has-text("New Ducklake")').click()
|
|
let lastRow = page.locator('.ducklake-settings-table').locator('tr').nth(-2)
|
|
await lastRow.locator('input.ducklake-name').fill(`ducklake_${timestamp}`)
|
|
await lastRow.locator('.ducklake-workspace-storage-select').click()
|
|
await Dropdown.getOption(page, storage, { exact: false }).click()
|
|
await lastRow.locator('input.ducklake-storage-data-path').fill(`ducklake_${timestamp}`)
|
|
await setupCustomInstanceDb(lastRow, page, storage)
|
|
await page.locator('button:has-text("Save ducklake settings")').click()
|
|
if (await page.locator('text=Some databases are not setup').isVisible()) {
|
|
await page.locator('button:has-text("Save anyway")').click()
|
|
}
|
|
await Toast.expectSuccess(page, 'Ducklake settings saved successfully')
|
|
await lastRow.locator('button:has-text("Manage")').click()
|
|
}
|
|
|
|
async function openDataTableDbManager(page: Page) {
|
|
await page.goto('/workspace_settings?tab=windmill_data_tables')
|
|
await setupNewDataTable(page, `datatable_${process.env.TEST_UNIQUE_ID}`)
|
|
const datatableId = `datatable_${process.env.TEST_UNIQUE_ID}`
|
|
let table = page.locator('table')
|
|
await table.waitFor({ state: 'visible' })
|
|
let rows = await table.locator('tr:has(input[id="name"])').all()
|
|
for (const row of rows) {
|
|
const val = await row.locator('input[id="name"]').inputValue()
|
|
if (val === datatableId) {
|
|
await row.locator('button:has-text("Manage")').click()
|
|
return
|
|
}
|
|
}
|
|
throw new Error(`Could not find datatable row ${datatableId}`)
|
|
}
|
|
|
|
declare const process: any // ignore TS errors
|
|
|
|
async function setupNewDataTable(page: Page, datatableId: string) {
|
|
// Check if datatable already exists
|
|
let table = page.locator('table')
|
|
await table.waitFor({ state: 'visible' })
|
|
let rows = await table.locator('tr:has(input[id="name"])').all()
|
|
for (const row of rows) {
|
|
const val = await row.locator('input[id="name"]').inputValue()
|
|
// Don't setup again if it already exists (saving the settings creates race condition)
|
|
if (val === datatableId) return
|
|
}
|
|
|
|
// Click on 'New Data Table' button
|
|
const newDataTableButton = page.locator('button:has-text("New Data Table")')
|
|
await newDataTableButton.click()
|
|
|
|
// Find the second-to-last row in the table (last row contains the button)
|
|
const lastRow = table.locator('tr').nth(-2)
|
|
|
|
// Fill the name input with generated ID
|
|
const nameInput = lastRow.locator('input[id="name"]')
|
|
await nameInput.fill(datatableId)
|
|
|
|
// Verify database type is 'Instance'
|
|
const databaseTypeSelect = lastRow.locator('input[id="database-type-select"]')
|
|
await expect(databaseTypeSelect).toHaveValue('Instance')
|
|
|
|
await setupCustomInstanceDb(lastRow, page, datatableId)
|
|
|
|
const saveBtn = page.locator('button:has-text("Save")')
|
|
await saveBtn.click()
|
|
|
|
if (await page.locator('text=Some databases are not setup').isVisible()) {
|
|
await page.locator('button:has-text("Save anyway")').click()
|
|
}
|
|
|
|
// Verify success toast appears
|
|
await Toast.expectSuccess(page, 'saved successfully')
|
|
|
|
return { datatableId }
|
|
}
|
|
|
|
async function setupCustomInstanceDb(row: Locator, page: Page, name: string) {
|
|
// Click on custom instance DB select and add new database
|
|
const customInstanceDbSelect = row.locator('input[id="custom-instance-db-select"]')
|
|
await customInstanceDbSelect.waitFor({ state: 'visible' })
|
|
await customInstanceDbSelect.click()
|
|
await customInstanceDbSelect.fill(name)
|
|
|
|
// Check if the database already exists in the dropdown
|
|
|
|
if (await Dropdown.getOption(page, name).isVisible()) {
|
|
await Dropdown.getOption(page, name).click()
|
|
return
|
|
}
|
|
|
|
// Click on the "Add new:" button
|
|
const addNewButton = page.locator(`button:has-text("Add new: ")`)
|
|
await addNewButton.waitFor({ state: 'visible' })
|
|
await addNewButton.click()
|
|
|
|
// Click on Setup button
|
|
const setupButton = row.locator('button:has-text("Setup")')
|
|
await setupButton.waitFor({ state: 'visible' })
|
|
await setupButton.click()
|
|
|
|
// Wait for popover to appear and click run setup button
|
|
const runSetupButton = page.locator('button[id="run-custom-instance-db-setup-button"]')
|
|
await runSetupButton.waitFor({ state: 'visible' })
|
|
await runSetupButton.click()
|
|
|
|
const confirmBtn = page.locator('button:has-text("Setup database")')
|
|
await confirmBtn.waitFor({ state: 'visible' })
|
|
await confirmBtn.click()
|
|
|
|
// Verify success toast appears
|
|
await Toast.expectSuccess(page, 'Setup successful')
|
|
|
|
const closeModalBtn = page.locator('button[id="modal-close-button"]')
|
|
await closeModalBtn.click()
|
|
}
|
|
|
|
function getJsonEnv(name: string): object | undefined {
|
|
const env = process.env[name]
|
|
if (env) return JSON.parse(env)
|
|
}
|
|
|
|
const wsStorageResources = {
|
|
s3: {
|
|
bucket: 'test-bucket',
|
|
region: 'us-east-1',
|
|
useSSL: false,
|
|
endPoint: 'minio-e2e:9000',
|
|
accessKey: 'minioadmin',
|
|
pathStyle: true,
|
|
secretKey: 'minioadmin'
|
|
}
|
|
}
|
|
|
|
const storageResourceTypeDropdownLabels = {
|
|
s3: 'S3',
|
|
azure_blob: 'Azure Blob',
|
|
s3_aws_oidc: 'AWS OIDC',
|
|
azure_workload_identity: 'Azure Workload Identity',
|
|
gcloud_storage: 'Google Cloud Storage'
|
|
}
|
|
type StorageResourceType = keyof typeof wsStorageResources
|
|
|
|
async function setupWsStorage(
|
|
page: Page,
|
|
resource_type: StorageResourceType
|
|
): Promise<{ storage: string }> {
|
|
let { resourceName } = await setupNewResource(
|
|
page,
|
|
resource_type,
|
|
wsStorageResources[resource_type]
|
|
)
|
|
let storage = `${resource_type}_storage_e2e`
|
|
|
|
await page.goto('/workspace_settings?tab=windmill_lfs')
|
|
await page.locator('.storage-settings-table').waitFor({ state: 'visible' })
|
|
|
|
let rows = await page
|
|
.locator('.storage-settings-table tr:has(input.secondary-storage-name-input)')
|
|
.all()
|
|
for (const row of rows) {
|
|
const val = await row.locator('input.secondary-storage-name-input').inputValue()
|
|
if (val === storage) return { storage }
|
|
}
|
|
|
|
// Click on the Add Storage button
|
|
await page.locator('button:has-text("Add secondary storage")').click()
|
|
let lastRow = page.locator('.storage-settings-table tr').nth(-2)
|
|
|
|
// Fill the name input with generated ID
|
|
const nameInput = lastRow.locator('input.secondary-storage-name-input')
|
|
await nameInput.fill(storage)
|
|
|
|
// Select resource type
|
|
await lastRow.locator('#storage-resource-type-select').click()
|
|
const dropdownResourceTypeLabel =
|
|
storageResourceTypeDropdownLabels[resource_type] ?? resource_type
|
|
|
|
await Dropdown.getOption(page, dropdownResourceTypeLabel).click()
|
|
|
|
// Select resource
|
|
await lastRow.locator('#resource-picker-select').click()
|
|
await Dropdown.getOption(page, resourceName, { exact: false }).click()
|
|
|
|
await page.locator('button:has-text("Save storage settings")').click()
|
|
await Toast.expectSuccess(page, 'storage settings changed')
|
|
return { storage }
|
|
}
|
|
|
|
async function setupNewResource(
|
|
page: Page,
|
|
resourceType: string,
|
|
resourceObj: object
|
|
): Promise<{ resourceName: string }> {
|
|
const resourceName = `${resourceType}_e2e`
|
|
|
|
await page.goto('/resources')
|
|
|
|
await page.locator('table').waitFor({ state: 'visible' })
|
|
if (await page.locator(`table tr:has-text("${resourceName}")`).isVisible())
|
|
return { resourceName }
|
|
|
|
const newDataTableButton = page.locator('button:text-is("Add resource")')
|
|
await newDataTableButton.click()
|
|
|
|
const addResourceDrawer = page.locator('#add-resource-drawer')
|
|
await expect(addResourceDrawer).toBeVisible()
|
|
|
|
const searchInput = addResourceDrawer.locator('input#search-resource-type')
|
|
await searchInput.fill(resourceType)
|
|
|
|
const resourceTypeBtn = addResourceDrawer.locator(`button:has(:text-is("${resourceType}"))`)
|
|
await resourceTypeBtn.waitFor({ state: 'visible' })
|
|
await resourceTypeBtn.click()
|
|
|
|
const asJsonToggle = addResourceDrawer.locator('label.as-json-toggle')
|
|
await expect(asJsonToggle).toBeVisible()
|
|
await asJsonToggle.check()
|
|
|
|
await addResourceDrawer.locator('input#path').fill(resourceName)
|
|
|
|
const jsonEditor = page.locator('.simple-editor .view-lines')
|
|
await expect(jsonEditor).toBeVisible()
|
|
await jsonEditor.click({ clickCount: 4 }) // Select all existing text
|
|
console.log('Pasting resource config:', resourceObj)
|
|
await page.evaluate((c) => navigator.clipboard.writeText(c), JSON.stringify(resourceObj))
|
|
await page.keyboard.press('ControlOrMeta+V')
|
|
|
|
const saveButton = addResourceDrawer.locator('button:has-text("Save")')
|
|
await saveButton.click()
|
|
|
|
await Toast.expectSuccess(page, 'Saved resource')
|
|
|
|
return { resourceName }
|
|
}
|
|
|
|
async function setupNewResourceAndOpenDbManager(page: Page, dbType: DbType) {
|
|
let { resourceName } = await setupNewResource(page, dbType, resourceByDbType[dbType])
|
|
|
|
const resourceRow = page.locator(`table tr:has-text("${resourceName}")`)
|
|
await expect(resourceRow).toBeVisible()
|
|
|
|
const manageButton = resourceRow.locator('button:has-text("Manage")')
|
|
await manageButton.click()
|
|
}
|