|
|
|
import { Page, selectors } from '@playwright/test';
|
|
|
|
import axios, { AxiosResponse } from 'axios';
|
|
|
|
import { Api, BaseType, ProjectListType, ProjectTypes, UserType, WorkspaceType } from 'nocodb-sdk';
|
|
|
|
import { getDefaultPwd } from '../tests/utils/general';
|
|
|
|
import { Knex, knex } from 'knex';
|
|
|
|
import { promises as fs } from 'fs';
|
|
|
|
import { isEE } from './db';
|
|
|
|
import { resetSakilaPg } from './knexHelper';
|
|
|
|
import path from 'path';
|
|
|
|
|
|
|
|
// MySQL Configuration
|
|
|
|
const mysqlConfig = {
|
|
|
|
client: 'mysql2',
|
|
|
|
connection: {
|
|
|
|
host: 'localhost',
|
|
|
|
port: 3306,
|
|
|
|
user: 'root',
|
|
|
|
password: 'password',
|
|
|
|
database: 'sakila',
|
|
|
|
multipleStatements: true,
|
|
|
|
dateStrings: true,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const extMysqlProject = (title, parallelId) => ({
|
|
|
|
title,
|
|
|
|
sources: [
|
|
|
|
{
|
|
|
|
type: 'mysql2',
|
|
|
|
config: {
|
|
|
|
client: 'mysql2',
|
|
|
|
connection: {
|
|
|
|
host: 'localhost',
|
|
|
|
port: '3306',
|
|
|
|
user: 'root',
|
|
|
|
password: 'password',
|
|
|
|
database: `test_sakila_${parallelId}`,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
inflection_column: 'camelize',
|
|
|
|
inflection_table: 'camelize',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
external: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// PG Configuration
|
|
|
|
//
|
|
|
|
const pgConfig = {
|
|
|
|
client: 'pg',
|
|
|
|
connection: {
|
|
|
|
host: 'localhost',
|
|
|
|
port: 5432,
|
|
|
|
user: 'postgres',
|
|
|
|
password: 'password',
|
|
|
|
database: 'postgres',
|
|
|
|
multipleStatements: true,
|
|
|
|
},
|
|
|
|
searchPath: ['public', 'information_schema'],
|
|
|
|
pool: { min: 0, max: 1 },
|
|
|
|
};
|
|
|
|
|
|
|
|
// Sakila Knex Configuration
|
|
|
|
//
|
|
|
|
const sakilaKnexConfig = (parallelId: string) => ({
|
|
|
|
...pgConfig,
|
|
|
|
connection: {
|
|
|
|
...pgConfig.connection,
|
|
|
|
database: `sakila${parallelId}`,
|
|
|
|
},
|
|
|
|
pool: { min: 0, max: 1 },
|
|
|
|
});
|
|
|
|
|
|
|
|
// External PG Base create payload
|
|
|
|
//
|
|
|
|
const extPgProject = (workspaceId, title, parallelId, baseType) => ({
|
|
|
|
fk_workspace_id: workspaceId,
|
|
|
|
title,
|
|
|
|
type: baseType,
|
|
|
|
sources: [
|
|
|
|
{
|
|
|
|
type: 'pg',
|
|
|
|
config: {
|
|
|
|
client: 'pg',
|
|
|
|
connection: {
|
|
|
|
host: 'localhost',
|
|
|
|
port: '5432',
|
|
|
|
user: 'postgres',
|
|
|
|
password: 'password',
|
|
|
|
database: `sakila${parallelId}`,
|
|
|
|
},
|
|
|
|
searchPath: ['public'],
|
|
|
|
},
|
|
|
|
inflection_column: 'camelize',
|
|
|
|
inflection_table: 'camelize',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
external: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const extPgProjectCE = (title, parallelId) => ({
|
|
|
|
title,
|
|
|
|
sources: [
|
|
|
|
{
|
|
|
|
type: 'pg',
|
|
|
|
config: {
|
|
|
|
client: 'pg',
|
|
|
|
connection: {
|
|
|
|
host: 'localhost',
|
|
|
|
port: '5432',
|
|
|
|
user: 'postgres',
|
|
|
|
password: 'password',
|
|
|
|
database: `sakila${parallelId}`,
|
|
|
|
},
|
|
|
|
searchPath: ['public'],
|
|
|
|
},
|
|
|
|
inflection_column: 'camelize',
|
|
|
|
inflection_table: 'camelize',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
external: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const extSQLiteProjectCE = (title: string, workerId: string) => ({
|
|
|
|
title,
|
|
|
|
sources: [
|
|
|
|
{
|
|
|
|
type: 'sqlite3',
|
|
|
|
config: {
|
|
|
|
client: 'sqlite3',
|
|
|
|
connection: {
|
|
|
|
client: 'sqlite3',
|
|
|
|
connection: {
|
|
|
|
filename: sqliteFilePath(workerId),
|
|
|
|
database: 'test_sakila',
|
|
|
|
multipleStatements: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
inflection_column: 'camelize',
|
|
|
|
inflection_table: 'camelize',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
external: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const workerCount = [0, 0, 0, 0, 0, 0, 0, 0];
|
|
|
|
|
|
|
|
export interface NcContext {
|
|
|
|
base: BaseType;
|
|
|
|
token: string;
|
|
|
|
dbType?: string;
|
|
|
|
workerId?: string;
|
|
|
|
rootUser: UserType & { password: string };
|
|
|
|
workspace: WorkspaceType;
|
|
|
|
defaultProjectTitle: string;
|
|
|
|
defaultTableTitle: string;
|
|
|
|
api: Api<any>;
|
|
|
|
}
|
|
|
|
|
|
|
|
selectors.setTestIdAttribute('data-testid');
|
|
|
|
const sqliteFilePath = (workerId: string) => {
|
|
|
|
const rootDir = process.cwd();
|
|
|
|
return `${rootDir}/../../packages/nocodb/test_sakila_${workerId}.db`;
|
|
|
|
};
|
|
|
|
|
|
|
|
async function localInit({
|
|
|
|
workerId,
|
|
|
|
isEmptyProject = false,
|
|
|
|
baseType = ProjectTypes.DATABASE,
|
|
|
|
isSuperUser = false,
|
|
|
|
dbType,
|
|
|
|
resetSsoClients = false,
|
|
|
|
}: {
|
|
|
|
workerId: string;
|
|
|
|
isEmptyProject?: boolean;
|
|
|
|
baseType?: ProjectTypes;
|
|
|
|
isSuperUser?: boolean;
|
|
|
|
dbType?: string;
|
|
|
|
resetSsoClients?: boolean;
|
|
|
|
}) {
|
|
|
|
const parallelId = process.env.TEST_PARALLEL_INDEX;
|
|
|
|
|
|
|
|
try {
|
|
|
|
let response: AxiosResponse<any, any>;
|
|
|
|
// Login as root user
|
|
|
|
if (isSuperUser && process.env.NC_CLOUD !== 'true') {
|
|
|
|
// required for configuring license key settings
|
|
|
|
response = await axios.post('http://localhost:8080/api/v1/auth/user/signin', {
|
|
|
|
email: `user@nocodb.com`,
|
|
|
|
password: getDefaultPwd(),
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
response = await axios.post('http://localhost:8080/api/v1/auth/user/signin', {
|
|
|
|
email: `user-${parallelId}@nocodb.com`,
|
|
|
|
password: getDefaultPwd(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
const token = response.data.token;
|
|
|
|
|
|
|
|
// Init SDK using token
|
|
|
|
const api = new Api({
|
|
|
|
baseURL: `http://localhost:8080/`,
|
|
|
|
headers: {
|
|
|
|
'xc-auth': token,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
// const workspaceTitle_old = `ws_pgExtREST${+workerId - 1}`;
|
|
|
|
const workspaceTitle = `ws_pgExtREST${workerId}`;
|
|
|
|
const baseTitle = `pgExtREST${workerId}`;
|
|
|
|
|
|
|
|
// console.log(process.env.TEST_WORKER_INDEX, process.env.TEST_PARALLEL_INDEX);
|
|
|
|
|
|
|
|
// delete sso-clients
|
|
|
|
if (resetSsoClients && isEE() && api['ssoClient'] && isSuperUser) {
|
|
|
|
const clients = await api.ssoClient.list();
|
|
|
|
for (const client of clients.list) {
|
|
|
|
try {
|
|
|
|
await api.ssoClient.delete(client.id);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`Error deleting sso-client: ${client.id}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isEE() && api['workspace']) {
|
|
|
|
// Delete associated workspace
|
|
|
|
// Note that: on worker error, entire thread is reset & worker ID numbering is reset too
|
|
|
|
// Hence, workspace delete is based on workerId prefix instead of just workerId
|
|
|
|
const ws = await api['workspace'].list();
|
|
|
|
for (const w of ws.list) {
|
|
|
|
// check if w.title starts with workspaceTitle
|
|
|
|
if (w.title.startsWith(`ws_pgExtREST${process.env.TEST_PARALLEL_INDEX}`)) {
|
|
|
|
try {
|
|
|
|
const bases = await api.workspaceBase.list(w.id);
|
|
|
|
|
|
|
|
for (const base of bases.list) {
|
|
|
|
try {
|
|
|
|
await api.base.delete(base.id);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`Error deleting base: ws delete`, base);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await api['workspace'].delete(w.id);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`Error deleting workspace: ${w.id}`, `user-${parallelId}@nocodb.com`, isSuperUser);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let bases: ProjectListType;
|
|
|
|
try {
|
|
|
|
bases = await api.base.list();
|
|
|
|
} catch (e) {
|
|
|
|
console.log('Error fetching bases', e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bases) {
|
|
|
|
for (const p of bases.list) {
|
|
|
|
// check if p.title starts with baseTitle
|
|
|
|
if (
|
|
|
|
p.title.startsWith(`pgExtREST${process.env.TEST_PARALLEL_INDEX}`) ||
|
|
|
|
p.title.startsWith(`xcdb_p${process.env.TEST_PARALLEL_INDEX}`)
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
await api.base.delete(p.id);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`Error deleting base: ${p.id}`, `user-${parallelId}@nocodb.com`, isSuperUser);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DB reset
|
|
|
|
if (dbType === 'pg' && !isEmptyProject) {
|
|
|
|
await resetSakilaPg(`sakila${workerId}`);
|
|
|
|
} else if (dbType === 'sqlite') {
|
|
|
|
if (await fs.stat(sqliteFilePath(parallelId)).catch(() => null)) {
|
|
|
|
await fs.unlink(sqliteFilePath(parallelId));
|
|
|
|
}
|
|
|
|
if (!isEmptyProject) {
|
|
|
|
const testsDir = path.join(process.cwd(), '../../packages/nocodb/tests');
|
|
|
|
await fs.copyFile(`${testsDir}/sqlite-sakila-db/sakila.db`, sqliteFilePath(parallelId));
|
|
|
|
}
|
|
|
|
} else if (dbType === 'mysql') {
|
|
|
|
const nc_knex = knex(mysqlConfig);
|
|
|
|
|
|
|
|
try {
|
|
|
|
await nc_knex.raw(`USE test_sakila_${parallelId}`);
|
|
|
|
} catch (e) {
|
|
|
|
await nc_knex.raw(`CREATE DATABASE test_sakila_${parallelId}`);
|
|
|
|
await nc_knex.raw(`USE test_sakila_${parallelId}`);
|
|
|
|
}
|
|
|
|
if (!isEmptyProject) {
|
|
|
|
await resetSakilaMysql(nc_knex, parallelId, isEmptyProject);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let workspace;
|
|
|
|
if (isEE() && api['workspace']) {
|
|
|
|
// create a new workspace
|
|
|
|
workspace = await api['workspace'].create({
|
|
|
|
title: workspaceTitle,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let base;
|
|
|
|
if (isEE()) {
|
|
|
|
if (isEmptyProject) {
|
|
|
|
// create a new base under the workspace we just created
|
|
|
|
base = await api.base.create({
|
|
|
|
title: baseTitle,
|
|
|
|
fk_workspace_id: workspace.id,
|
|
|
|
type: baseType,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
if ('id' in workspace) {
|
|
|
|
// @ts-ignore
|
|
|
|
base = await api.base.create(extPgProject(workspace.id, baseTitle, workerId, baseType));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (isEmptyProject) {
|
|
|
|
// create a new base
|
|
|
|
base = await api.base.create({
|
|
|
|
title: baseTitle,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
base = await api.base.create(
|
|
|
|
dbType === 'pg'
|
|
|
|
? extPgProjectCE(baseTitle, workerId)
|
|
|
|
: dbType === 'sqlite'
|
|
|
|
? extSQLiteProjectCE(baseTitle, parallelId)
|
|
|
|
: extMysqlProject(baseTitle, parallelId)
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`Error creating base: ${baseTitle}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// get current user information
|
|
|
|
const user = await api.auth.me();
|
|
|
|
return { data: { base, user, workspace, token, api }, status: 200 };
|
|
|
|
} catch (e) {
|
|
|
|
console.error(`Error resetting base: ${process.env.TEST_PARALLEL_INDEX}`, e);
|
|
|
|
return { data: {}, status: 500 };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const setup = async ({
|
|
|
|
baseType = ProjectTypes.DATABASE,
|
|
|
|
page,
|
|
|
|
isEmptyProject = false,
|
|
|
|
isSuperUser = false,
|
|
|
|
url,
|
|
|
|
resetSsoClients = false,
|
|
|
|
}: {
|
|
|
|
baseType?: ProjectTypes;
|
|
|
|
page: Page;
|
|
|
|
isEmptyProject?: boolean;
|
|
|
|
isSuperUser?: boolean;
|
|
|
|
url?: string;
|
|
|
|
resetSsoClients?: boolean;
|
|
|
|
}): Promise<NcContext> => {
|
|
|
|
console.time('Setup');
|
|
|
|
|
|
|
|
let dbType = process.env.CI ? process.env.E2E_DB_TYPE : process.env.E2E_DEV_DB_TYPE;
|
|
|
|
dbType = dbType || (isEE() ? 'pg' : 'sqlite');
|
|
|
|
|
|
|
|
let response;
|
|
|
|
|
|
|
|
const workerIndex = process.env.TEST_WORKER_INDEX;
|
|
|
|
const parallelIndex = process.env.TEST_PARALLEL_INDEX;
|
|
|
|
|
|
|
|
const workerId = parallelIndex;
|
|
|
|
|
|
|
|
// console.log(process.env.TEST_PARALLEL_INDEX, '#Setup', workerId);
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Localised reset logic
|
|
|
|
response = await localInit({
|
|
|
|
workerId: parallelIndex,
|
|
|
|
isEmptyProject,
|
|
|
|
baseType,
|
|
|
|
isSuperUser,
|
|
|
|
dbType,
|
|
|
|
resetSsoClients,
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error(`Error resetting base: ${process.env.TEST_PARALLEL_INDEX}`, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.status !== 200 || !response.data?.token || !response.data?.base) {
|
|
|
|
console.error('Failed to reset test data', response.data, response.status, dbType);
|
|
|
|
throw new Error('Failed to reset test data');
|
|
|
|
}
|
|
|
|
const token = response.data.token;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const admin = await axios.post('http://localhost:8080/api/v1/auth/user/signin', {
|
|
|
|
email: `user@nocodb.com`,
|
|
|
|
password: getDefaultPwd(),
|
|
|
|
});
|
|
|
|
if (!isEE())
|
|
|
|
await axios.post(
|
|
|
|
`http://localhost:8080/api/v1/license`,
|
|
|
|
{ key: '' },
|
|
|
|
{ headers: { 'xc-auth': admin.data.token } }
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
// ignore error: some roles will not have permission for license reset
|
|
|
|
// console.error(`Error resetting base: ${process.env.TEST_PARALLEL_INDEX}`, e);
|
|
|
|
}
|
|
|
|
await page.addInitScript(
|
|
|
|
async ({ token }) => {
|
|
|
|
if (location.search?.match(/code=|short-token=|skip-init-script=/)) return;
|
|
|
|
try {
|
|
|
|
let initialLocalStorage = {};
|
|
|
|
try {
|
|
|
|
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Failed to parse local storage', e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (initialLocalStorage?.token) return;
|
|
|
|
|
|
|
|
window.localStorage.setItem(
|
|
|
|
'nocodb-gui-v2',
|
|
|
|
JSON.stringify({
|
|
|
|
...initialLocalStorage,
|
|
|
|
token: token,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
window.console.log('initialLocalStorage error');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ token: token }
|
|
|
|
);
|
|
|
|
|
|
|
|
const base = response.data.base;
|
|
|
|
const rootUser = { ...response.data.user, password: getDefaultPwd() };
|
|
|
|
const workspace = response.data.workspace;
|
|
|
|
|
|
|
|
// default landing page for tests
|
|
|
|
let baseUrl;
|
|
|
|
if (isEE()) {
|
|
|
|
switch (base.type) {
|
|
|
|
case ProjectTypes.DOCUMENTATION:
|
|
|
|
baseUrl = url ? url : `/#/${base.fk_workspace_id}/${base.id}/doc`;
|
|
|
|
break;
|
|
|
|
case ProjectTypes.DATABASE:
|
|
|
|
baseUrl = url ? url : `/#/${base.fk_workspace_id}/${base.id}`;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error(`Unknown base type: ${base.type}`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// sample: http://localhost:3000/#/ws/default/base/pdknlfoc5e7bx4w
|
|
|
|
baseUrl = url ? url : `/#/nc/${base.id}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
await page.addInitScript(() => (window.isPlaywright = true));
|
|
|
|
|
|
|
|
await page.goto(baseUrl, {
|
|
|
|
waitUntil: 'networkidle',
|
|
|
|
});
|
|
|
|
|
|
|
|
console.timeEnd('Setup');
|
|
|
|
|
|
|
|
return {
|
|
|
|
base,
|
|
|
|
token,
|
|
|
|
dbType,
|
|
|
|
workerId,
|
|
|
|
rootUser,
|
|
|
|
workspace,
|
|
|
|
defaultProjectTitle: 'Getting Started',
|
|
|
|
defaultTableTitle: 'Features',
|
|
|
|
api: response?.data?.api,
|
|
|
|
} as NcContext;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const unsetup = async (context: NcContext): Promise<void> => {};
|
|
|
|
|
|
|
|
// Reference
|
|
|
|
// packages/nocodb/src/lib/services/test/TestResetService/resetPgSakilaProject.ts
|
|
|
|
|
|
|
|
const resetSakilaMysql = async (knex: Knex, parallelId: string, isEmptyProject: boolean) => {
|
|
|
|
const testsDir = path.join(process.cwd(), '/../../packages/nocodb/tests');
|
|
|
|
|
|
|
|
try {
|
|
|
|
await knex.raw(`DROP DATABASE test_sakila_${parallelId}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log('Error dropping db', e);
|
|
|
|
}
|
|
|
|
await knex.raw(`CREATE DATABASE test_sakila_${parallelId}`);
|
|
|
|
|
|
|
|
if (isEmptyProject) return;
|
|
|
|
|
|
|
|
const trx = await knex.transaction();
|
|
|
|
|
|
|
|
try {
|
|
|
|
const schemaFile = await fs.readFile(`${testsDir}/mysql-sakila-db/03-test-sakila-schema.sql`);
|
|
|
|
const dataFile = await fs.readFile(`${testsDir}/mysql-sakila-db/04-test-sakila-data.sql`);
|
|
|
|
|
|
|
|
await trx.raw(schemaFile.toString().replace(/test_sakila/g, `test_sakila_${parallelId}`));
|
|
|
|
await trx.raw(dataFile.toString().replace(/test_sakila/g, `test_sakila_${parallelId}`));
|
|
|
|
|
|
|
|
await trx.commit();
|
|
|
|
} catch (e) {
|
|
|
|
console.log('Error resetting mysql db', e);
|
|
|
|
await trx.rollback(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// General purpose API based routines
|
|
|
|
//
|
|
|
|
|
|
|
|
export default setup;
|