From 158964c4d2b4a7f0f6139932d2f7aade56e52f54 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 26 Mar 2024 11:22:40 +0000 Subject: [PATCH 01/30] Got container reuse strategy in place, need to convert tests now. --- .../src/integration-test/postgres.spec.ts | 55 ++++++++++--------- .../src/integrations/tests/utils/index.ts | 53 ++++++++++++++---- .../src/integrations/tests/utils/mariadb.ts | 22 ++------ .../src/integrations/tests/utils/mongodb.ts | 23 ++------ .../src/integrations/tests/utils/mssql.ts | 22 ++------ .../src/integrations/tests/utils/mysql.ts | 23 ++------ .../src/integrations/tests/utils/postgres.ts | 23 ++------ 7 files changed, 98 insertions(+), 123 deletions(-) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 107c4ade1e..57da02b507 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -16,7 +16,7 @@ import { import _ from "lodash" import { generator } from "@budibase/backend-core/tests" import { utils } from "@budibase/backend-core" -import { databaseTestProviders } from "../integrations/tests/utils" +import { DatabaseName, getDatasource } from "../integrations/tests/utils" import { Client } from "pg" // @ts-ignore fetch.mockSearch() @@ -41,18 +41,17 @@ describe("postgres integrations", () => { makeRequest = generateMakeRequest(apiKey, true) postgresDatasource = await config.api.datasource.create( - await databaseTestProviders.postgres.datasource() + await getDatasource(DatabaseName.POSTGRES) ) }) - afterAll(async () => { - await databaseTestProviders.postgres.stop() - }) - beforeEach(async () => { async function createAuxTable(prefix: string) { return await config.createTable({ - name: `${prefix}_${generator.word({ length: 6 })}`, + name: `${prefix}_${generator + .guid() + .replaceAll("-", "") + .substring(0, 6)}`, type: "table", primary: ["id"], primaryDisplay: "title", @@ -89,7 +88,7 @@ describe("postgres integrations", () => { } primaryPostgresTable = await config.createTable({ - name: `p_${generator.word({ length: 6 })}`, + name: `p_${generator.guid().replaceAll("-", "").substring(0, 6)}`, type: "table", primary: ["id"], schema: { @@ -251,7 +250,7 @@ describe("postgres integrations", () => { async function createDefaultPgTable() { return await config.createTable({ - name: generator.word({ length: 10 }), + name: generator.guid().replaceAll("-", "").substring(0, 10), type: "table", primary: ["id"], schema: { @@ -1043,7 +1042,7 @@ describe("postgres integrations", () => { it("should be able to verify the connection", async () => { await config.api.datasource.verify( { - datasource: await databaseTestProviders.postgres.datasource(), + datasource: await getDatasource(DatabaseName.POSTGRES), }, { body: { @@ -1054,7 +1053,7 @@ describe("postgres integrations", () => { }) it("should state an invalid datasource cannot connect", async () => { - const dbConfig = await databaseTestProviders.postgres.datasource() + const dbConfig = await getDatasource(DatabaseName.POSTGRES) await config.api.datasource.verify( { datasource: { @@ -1089,21 +1088,21 @@ describe("postgres integrations", () => { describe("POST /api/datasources/:datasourceId/schema", () => { let client: Client + let tableName: string beforeEach(async () => { - client = new Client( - (await databaseTestProviders.postgres.datasource()).config! - ) + tableName = generator.guid().replaceAll("-", "").substring(0, 10) + client = new Client((await getDatasource(DatabaseName.POSTGRES)).config!) await client.connect() }) afterEach(async () => { - await client.query(`DROP TABLE IF EXISTS "table"`) + await client.query(`DROP TABLE IF EXISTS "${tableName}"`) await client.end() }) it("recognises when a table has no primary key", async () => { - await client.query(`CREATE TABLE "table" (id SERIAL)`) + await client.query(`CREATE TABLE "${tableName}" (id SERIAL)`) const response = await makeRequest( "post", @@ -1111,12 +1110,14 @@ describe("postgres integrations", () => { ) expect(response.body.errors).toEqual({ - table: "Table must have a primary key.", + [tableName]: "Table must have a primary key.", }) }) it("recognises when a table is using a reserved column name", async () => { - await client.query(`CREATE TABLE "table" (_id SERIAL PRIMARY KEY) `) + await client.query( + `CREATE TABLE "${tableName}" (_id SERIAL PRIMARY KEY) ` + ) const response = await makeRequest( "post", @@ -1124,18 +1125,22 @@ describe("postgres integrations", () => { ) expect(response.body.errors).toEqual({ - table: "Table contains invalid columns.", + [tableName]: "Table contains invalid columns.", }) }) }) describe("Integration compatibility with postgres search_path", () => { - let client: Client, pathDatasource: Datasource - const schema1 = "test1", - schema2 = "test-2" + let client: Client, + pathDatasource: Datasource, + schema1: string, + schema2: string - beforeAll(async () => { - const dsConfig = await databaseTestProviders.postgres.datasource() + beforeEach(async () => { + schema1 = generator.guid().replaceAll("-", "") + schema2 = generator.guid().replaceAll("-", "") + + const dsConfig = await getDatasource(DatabaseName.POSTGRES) const dbConfig = dsConfig.config! client = new Client(dbConfig) @@ -1153,7 +1158,7 @@ describe("postgres integrations", () => { pathDatasource = await config.api.datasource.create(pathConfig) }) - afterAll(async () => { + afterEach(async () => { await client.query(`DROP SCHEMA "${schema1}" CASCADE;`) await client.query(`DROP SCHEMA "${schema2}" CASCADE;`) await client.end() diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index b2be3df4e0..650a1b414d 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -1,19 +1,50 @@ jest.unmock("pg") import { Datasource } from "@budibase/types" -import * as postgres from "./postgres" -import * as mongodb from "./mongodb" -import * as mysql from "./mysql" -import * as mssql from "./mssql" -import * as mariadb from "./mariadb" -import { StartedTestContainer } from "testcontainers" +import { postgres } from "./postgres" +import { mongodb } from "./mongodb" +import { mysql } from "./mysql" +import { mssql } from "./mssql" +import { mariadb } from "./mariadb" -jest.setTimeout(30000) +export type DatasourceProvider = () => Promise -export interface DatabaseProvider { - start(): Promise - stop(): Promise - datasource(): Promise +export enum DatabaseName { + POSTGRES = "postgres", + MONGODB = "mongodb", + MYSQL = "mysql", + SQL_SERVER = "mssql", + MARIADB = "mariadb", +} + +const providers: Record = { + [DatabaseName.POSTGRES]: postgres, + [DatabaseName.MONGODB]: mongodb, + [DatabaseName.MYSQL]: mysql, + [DatabaseName.SQL_SERVER]: mssql, + [DatabaseName.MARIADB]: mariadb, +} + +export function getDatasourceProviders( + ...sourceNames: DatabaseName[] +): Promise[] { + return sourceNames.map(sourceName => providers[sourceName]()) +} + +export function getDatasourceProvider( + sourceName: DatabaseName +): DatasourceProvider { + return providers[sourceName] +} + +export function getDatasource(sourceName: DatabaseName): Promise { + return providers[sourceName]() +} + +export async function getDatasources( + ...sourceNames: DatabaseName[] +): Promise { + return Promise.all(sourceNames.map(sourceName => providers[sourceName]())) } export const databaseTestProviders = { diff --git a/packages/server/src/integrations/tests/utils/mariadb.ts b/packages/server/src/integrations/tests/utils/mariadb.ts index a097e0aaa1..a10c36f9ff 100644 --- a/packages/server/src/integrations/tests/utils/mariadb.ts +++ b/packages/server/src/integrations/tests/utils/mariadb.ts @@ -1,9 +1,7 @@ import { Datasource, SourceName } from "@budibase/types" -import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { GenericContainer, Wait } from "testcontainers" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" -let container: StartedTestContainer | undefined - class MariaDBWaitStrategy extends AbstractWaitStrategy { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { // Because MariaDB first starts itself up, runs an init script, then restarts, @@ -21,18 +19,15 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy { } } -export async function start(): Promise { - return await new GenericContainer("mariadb:lts") +export async function mariadb(): Promise { + const container = await new GenericContainer("mariadb:lts") + .withName("budibase-test-mariadb") + .withReuse() .withExposedPorts(3306) .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" }) .withWaitStrategy(new MariaDBWaitStrategy()) .start() -} -export async function datasource(): Promise { - if (!container) { - container = await start() - } const host = container.getHost() const port = container.getMappedPort(3306) @@ -49,10 +44,3 @@ export async function datasource(): Promise { }, } } - -export async function stop() { - if (container) { - await container.stop() - container = undefined - } -} diff --git a/packages/server/src/integrations/tests/utils/mongodb.ts b/packages/server/src/integrations/tests/utils/mongodb.ts index 0baafc6276..ff24bbc62e 100644 --- a/packages/server/src/integrations/tests/utils/mongodb.ts +++ b/packages/server/src/integrations/tests/utils/mongodb.ts @@ -1,10 +1,10 @@ import { Datasource, SourceName } from "@budibase/types" -import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { GenericContainer, Wait } from "testcontainers" -let container: StartedTestContainer | undefined - -export async function start(): Promise { - return await new GenericContainer("mongo:7.0-jammy") +export async function mongodb(): Promise { + const container = await new GenericContainer("mongo:7.0-jammy") + .withName("budibase-test-mongodb") + .withReuse() .withExposedPorts(27017) .withEnvironment({ MONGO_INITDB_ROOT_USERNAME: "mongo", @@ -16,14 +16,10 @@ export async function start(): Promise { ).withStartupTimeout(10000) ) .start() -} -export async function datasource(): Promise { - if (!container) { - container = await start() - } const host = container.getHost() const port = container.getMappedPort(27017) + return { type: "datasource", source: SourceName.MONGODB, @@ -34,10 +30,3 @@ export async function datasource(): Promise { }, } } - -export async function stop() { - if (container) { - await container.stop() - container = undefined - } -} diff --git a/packages/server/src/integrations/tests/utils/mssql.ts b/packages/server/src/integrations/tests/utils/mssql.ts index 6bd4290a90..0f4e290526 100644 --- a/packages/server/src/integrations/tests/utils/mssql.ts +++ b/packages/server/src/integrations/tests/utils/mssql.ts @@ -1,12 +1,12 @@ import { Datasource, SourceName } from "@budibase/types" -import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { GenericContainer, Wait } from "testcontainers" -let container: StartedTestContainer | undefined - -export async function start(): Promise { - return await new GenericContainer( +export async function mssql(): Promise { + const container = await new GenericContainer( "mcr.microsoft.com/mssql/server:2022-latest" ) + .withName("budibase-test-mssql") + .withReuse() .withExposedPorts(1433) .withEnvironment({ ACCEPT_EULA: "Y", @@ -23,12 +23,7 @@ export async function start(): Promise { ) ) .start() -} -export async function datasource(): Promise { - if (!container) { - container = await start() - } const host = container.getHost() const port = container.getMappedPort(1433) @@ -47,10 +42,3 @@ export async function datasource(): Promise { }, } } - -export async function stop() { - if (container) { - await container.stop() - container = undefined - } -} diff --git a/packages/server/src/integrations/tests/utils/mysql.ts b/packages/server/src/integrations/tests/utils/mysql.ts index 5e51478998..665d6f0ecf 100644 --- a/packages/server/src/integrations/tests/utils/mysql.ts +++ b/packages/server/src/integrations/tests/utils/mysql.ts @@ -1,9 +1,7 @@ import { Datasource, SourceName } from "@budibase/types" -import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { GenericContainer, Wait } from "testcontainers" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" -let container: StartedTestContainer | undefined - class MySQLWaitStrategy extends AbstractWaitStrategy { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { // Because MySQL first starts itself up, runs an init script, then restarts, @@ -24,18 +22,14 @@ class MySQLWaitStrategy extends AbstractWaitStrategy { } } -export async function start(): Promise { - return await new GenericContainer("mysql:8.3") +export async function mysql(): Promise { + const container = await new GenericContainer("mysql:8.3") + .withName("budibase-test-mysql") + .withReuse() .withExposedPorts(3306) .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" }) .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000)) .start() -} - -export async function datasource(): Promise { - if (!container) { - container = await start() - } const host = container.getHost() const port = container.getMappedPort(3306) @@ -52,10 +46,3 @@ export async function datasource(): Promise { }, } } - -export async function stop() { - if (container) { - await container.stop() - container = undefined - } -} diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts index 82a62e3916..896c7ea3e0 100644 --- a/packages/server/src/integrations/tests/utils/postgres.ts +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -1,10 +1,10 @@ import { Datasource, SourceName } from "@budibase/types" -import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" +import { GenericContainer, Wait } from "testcontainers" -let container: StartedTestContainer | undefined - -export async function start(): Promise { - return await new GenericContainer("postgres:16.1-bullseye") +export async function postgres(): Promise { + const container = await new GenericContainer("postgres:16.1-bullseye") + .withName("budibase-test-postgres") + .withReuse() .withExposedPorts(5432) .withEnvironment({ POSTGRES_PASSWORD: "password" }) .withWaitStrategy( @@ -13,12 +13,6 @@ export async function start(): Promise { ).withStartupTimeout(10000) ) .start() -} - -export async function datasource(): Promise { - if (!container) { - container = await start() - } const host = container.getHost() const port = container.getMappedPort(5432) @@ -39,10 +33,3 @@ export async function datasource(): Promise { }, } } - -export async function stop() { - if (container) { - await container.stop() - container = undefined - } -} From 5530d7f4b63b717c7879a6bfaf70387d35844aa8 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 26 Mar 2024 14:05:58 +0000 Subject: [PATCH 02/30] Migrate mongodb.spec.ts to new datasource providers. --- .../routes/tests/queries/generic-sql.spec.ts | 28 ++++++++++--------- .../api/routes/tests/queries/mongodb.spec.ts | 17 ++++++----- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts index f9a3ac6e03..d393430060 100644 --- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts @@ -1,6 +1,9 @@ import { Datasource, Query, SourceName } from "@budibase/types" import * as setup from "../utilities" -import { databaseTestProviders } from "../../../../integrations/tests/utils" +import { + DatabaseName, + getDatasource, +} from "../../../../integrations/tests/utils" import pg from "pg" import mysql from "mysql2/promise" import mssql from "mssql" @@ -34,12 +37,14 @@ const createTableSQL: Record = { const insertSQL = `INSERT INTO test_table (name) VALUES ('one'), ('two'), ('three'), ('four'), ('five')` const dropTableSQL = `DROP TABLE test_table;` -describe.each([ - ["postgres", databaseTestProviders.postgres], - ["mysql", databaseTestProviders.mysql], - ["mssql", databaseTestProviders.mssql], - ["mariadb", databaseTestProviders.mariadb], -])("queries (%s)", (dbName, dsProvider) => { +describe.each( + [ + DatabaseName.POSTGRES, + DatabaseName.MYSQL, + DatabaseName.SQL_SERVER, + DatabaseName.MARIADB, + ].map(name => [name, getDatasource(name)]) +)("queries (%s)", (dbName, dsProvider) => { const config = setup.getConfig() let datasource: Datasource @@ -61,7 +66,7 @@ describe.each([ // We re-fetch the datasource here because the one returned by // config.api.datasource.create has the password field blanked out, and we // need the password to connect to the database. - const ds = await dsProvider.datasource() + const ds = await dsProvider switch (ds.source) { case SourceName.POSTGRES: { const client = new pg.Client(ds.config!) @@ -97,9 +102,7 @@ describe.each([ beforeAll(async () => { await config.init() - datasource = await config.api.datasource.create( - await dsProvider.datasource() - ) + datasource = await config.api.datasource.create(await dsProvider) }) beforeEach(async () => { @@ -112,7 +115,6 @@ describe.each([ }) afterAll(async () => { - await dsProvider.stop() setup.afterAll() }) @@ -443,7 +445,7 @@ describe.each([ } catch (err: any) { error = err.message } - if (dbName === "mssql") { + if (dbName === DatabaseName.SQL_SERVER) { expect(error).toBeUndefined() } else { expect(error).toBeDefined() diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index 492f24abf9..148f2c15ec 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -1,14 +1,17 @@ import { Datasource, Query } from "@budibase/types" import * as setup from "../utilities" -import { databaseTestProviders } from "../../../../integrations/tests/utils" +import { + DatabaseName, + getDatasource, +} from "../../../../integrations/tests/utils" import { MongoClient, type Collection, BSON } from "mongodb" - -const collection = "test_collection" +import { generator } from "@budibase/backend-core/tests" const expectValidId = expect.stringMatching(/^\w{24}$/) const expectValidBsonObjectId = expect.any(BSON.ObjectId) describe("/queries", () => { + let collection: string let config = setup.getConfig() let datasource: Datasource @@ -37,7 +40,7 @@ describe("/queries", () => { async function withClient( callback: (client: MongoClient) => Promise ): Promise { - const ds = await databaseTestProviders.mongodb.datasource() + const ds = await getDatasource(DatabaseName.MONGODB) const client = new MongoClient(ds.config!.connectionString) await client.connect() try { @@ -52,25 +55,25 @@ describe("/queries", () => { ): Promise { return await withClient(async client => { const db = client.db( - (await databaseTestProviders.mongodb.datasource()).config!.db + (await getDatasource(DatabaseName.MONGODB)).config!.db ) return await callback(db.collection(collection)) }) } afterAll(async () => { - await databaseTestProviders.mongodb.stop() setup.afterAll() }) beforeAll(async () => { await config.init() datasource = await config.api.datasource.create( - await databaseTestProviders.mongodb.datasource() + await getDatasource(DatabaseName.MONGODB) ) }) beforeEach(async () => { + collection = generator.guid() await withCollection(async collection => { await collection.insertMany([ { name: "one" }, From 2304aeaa719937130fbdd07cee6580414b2f72af Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 26 Mar 2024 14:36:18 +0000 Subject: [PATCH 03/30] Migrate mysql.spec.ts to new datasource providers. --- .../server/src/api/routes/tests/row.spec.ts | 39 +++++++++---------- .../server/src/integration-test/mysql.spec.ts | 30 +++++++------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index f638f2c4bf..8910522565 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1,4 +1,4 @@ -import { databaseTestProviders } from "../../../integrations/tests/utils" +import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import tk from "timekeeper" import { outputProcessing } from "../../../utilities/rowProcessor" @@ -34,10 +34,10 @@ jest.unmock("pg") describe.each([ ["internal", undefined], - ["postgres", databaseTestProviders.postgres], - ["mysql", databaseTestProviders.mysql], - ["mssql", databaseTestProviders.mssql], - ["mariadb", databaseTestProviders.mariadb], + [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], ])("/rows (%s)", (__, dsProvider) => { const isInternal = dsProvider === undefined const config = setup.getConfig() @@ -49,23 +49,23 @@ describe.each([ await config.init() if (dsProvider) { datasource = await config.createDatasource({ - datasource: await dsProvider.datasource(), + datasource: await dsProvider, }) } }) afterAll(async () => { - if (dsProvider) { - await dsProvider.stop() - } setup.afterAll() }) function saveTableRequest( - ...overrides: Partial[] + // We omit the name field here because it's generated in the function with a + // high likelihood to be unique. Tests should not have any reason to control + // the table name they're writing to. + ...overrides: Partial>[] ): SaveTableRequest { const req: SaveTableRequest = { - name: uuid.v4().substring(0, 16), + name: uuid.v4().substring(0, 10), type: "table", sourceType: datasource ? TableSourceType.EXTERNAL @@ -87,7 +87,10 @@ describe.each([ } function defaultTable( - ...overrides: Partial[] + // We omit the name field here because it's generated in the function with a + // high likelihood to be unique. Tests should not have any reason to control + // the table name they're writing to. + ...overrides: Partial>[] ): SaveTableRequest { return saveTableRequest( { @@ -194,7 +197,6 @@ describe.each([ const newTable = await config.api.table.save( saveTableRequest({ - name: "TestTableAuto", schema: { "Row ID": { name: "Row ID", @@ -383,11 +385,9 @@ describe.each([ isInternal && it("doesn't allow creating in user table", async () => { - const userTableId = InternalTable.USER_METADATA const response = await config.api.row.save( - userTableId, + InternalTable.USER_METADATA, { - tableId: userTableId, firstName: "Joe", lastName: "Joe", email: "joe@joe.com", @@ -462,7 +462,6 @@ describe.each([ table = await config.api.table.save(defaultTable()) otherTable = await config.api.table.save( defaultTable({ - name: "a", schema: { relationship: { name: "relationship", @@ -898,8 +897,8 @@ describe.each([ let o2mTable: Table let m2mTable: Table beforeAll(async () => { - o2mTable = await config.api.table.save(defaultTable({ name: "o2m" })) - m2mTable = await config.api.table.save(defaultTable({ name: "m2m" })) + o2mTable = await config.api.table.save(defaultTable()) + m2mTable = await config.api.table.save(defaultTable()) }) describe.each([ @@ -1256,7 +1255,6 @@ describe.each([ otherTable = await config.api.table.save(defaultTable()) table = await config.api.table.save( saveTableRequest({ - name: "b", schema: { links: { name: "links", @@ -1354,7 +1352,6 @@ describe.each([ const table = await config.api.table.save( saveTableRequest({ - name: "table", schema: { text: { name: "text", diff --git a/packages/server/src/integration-test/mysql.spec.ts b/packages/server/src/integration-test/mysql.spec.ts index 92420fb336..65fbe2949d 100644 --- a/packages/server/src/integration-test/mysql.spec.ts +++ b/packages/server/src/integration-test/mysql.spec.ts @@ -3,7 +3,6 @@ import { generateMakeRequest, MakeRequestResponse, } from "../api/routes/public/tests/utils" -import { v4 as uuidv4 } from "uuid" import * as setup from "../api/routes/tests/utilities" import { Datasource, @@ -12,9 +11,10 @@ import { TableRequest, TableSourceType, } from "@budibase/types" -import { databaseTestProviders } from "../integrations/tests/utils" +import { DatabaseName, getDatasource } from "../integrations/tests/utils" import mysql from "mysql2/promise" import { builderSocket } from "../websockets" +import { generator } from "@budibase/backend-core/tests" // @ts-ignore fetch.mockSearch() @@ -47,17 +47,13 @@ describe("mysql integrations", () => { makeRequest = generateMakeRequest(apiKey, true) mysqlDatasource = await config.api.datasource.create( - await databaseTestProviders.mysql.datasource() + await getDatasource(DatabaseName.MYSQL) ) }) - afterAll(async () => { - await databaseTestProviders.mysql.stop() - }) - beforeEach(async () => { primaryMySqlTable = await config.createTable({ - name: uuidv4(), + name: generator.guid().replaceAll("-", "_").substring(0, 10), type: "table", primary: ["id"], schema: { @@ -117,7 +113,7 @@ describe("mysql integrations", () => { it("should be able to verify the connection", async () => { await config.api.datasource.verify( { - datasource: await databaseTestProviders.mysql.datasource(), + datasource: await getDatasource(DatabaseName.MYSQL), }, { body: { @@ -128,7 +124,7 @@ describe("mysql integrations", () => { }) it("should state an invalid datasource cannot connect", async () => { - const dbConfig = await databaseTestProviders.mysql.datasource() + const dbConfig = await getDatasource(DatabaseName.MYSQL) await config.api.datasource.verify( { datasource: { @@ -168,7 +164,7 @@ describe("mysql integrations", () => { const database2 = "test-2" beforeAll(async () => { - const dsConfig = await databaseTestProviders.mysql.datasource() + const dsConfig = await getDatasource(DatabaseName.MYSQL) const dbConfig = dsConfig.config! client = await mysql.createConnection(dbConfig) @@ -237,11 +233,11 @@ describe("mysql integrations", () => { beforeEach(async () => { client = await mysql.createConnection( ( - await databaseTestProviders.mysql.datasource() + await getDatasource(DatabaseName.MYSQL) ).config! ) mysqlDatasource = await config.api.datasource.create( - await databaseTestProviders.mysql.datasource() + await getDatasource(DatabaseName.MYSQL) ) }) @@ -253,7 +249,7 @@ describe("mysql integrations", () => { const addColumnToTable: TableRequest = { type: "table", sourceType: TableSourceType.EXTERNAL, - name: "table", + name: generator.guid().replaceAll("-", "_").substring(0, 10), sourceId: mysqlDatasource._id!, primary: ["id"], schema: { @@ -301,14 +297,16 @@ describe("mysql integrations", () => { }, }, created: true, - _id: `${mysqlDatasource._id}__table`, + _id: `${mysqlDatasource._id}__${addColumnToTable.name}`, } delete expectedTable._add expect(emitDatasourceUpdateMock).toHaveBeenCalledTimes(1) const emittedDatasource: Datasource = emitDatasourceUpdateMock.mock.calls[0][1] - expect(emittedDatasource.entities!["table"]).toEqual(expectedTable) + expect(emittedDatasource.entities![expectedTable.name]).toEqual( + expectedTable + ) }) it("will rename a column", async () => { From b84bbd6003bf1317a5fe4e6430c56c30ad4dfead Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 26 Mar 2024 14:43:17 +0000 Subject: [PATCH 04/30] Migrate viewV2.spec.ts to new datasource providers. --- .../src/api/routes/tests/viewV2.spec.ts | 24 +++++++------------ .../src/integrations/tests/utils/index.ts | 8 ------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index f9d213a26b..ce959ac429 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -19,8 +19,7 @@ import { ViewV2, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" -import * as uuid from "uuid" -import { databaseTestProviders } from "../../../integrations/tests/utils" +import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" import { roles } from "@budibase/backend-core" @@ -30,10 +29,10 @@ jest.unmock("pg") describe.each([ ["internal", undefined], - ["postgres", databaseTestProviders.postgres], - ["mysql", databaseTestProviders.mysql], - ["mssql", databaseTestProviders.mssql], - ["mariadb", databaseTestProviders.mariadb], + [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], ])("/v2/views (%s)", (_, dsProvider) => { const config = setup.getConfig() const isInternal = !dsProvider @@ -42,10 +41,10 @@ describe.each([ let datasource: Datasource function saveTableRequest( - ...overrides: Partial[] + ...overrides: Partial>[] ): SaveTableRequest { const req: SaveTableRequest = { - name: uuid.v4().substring(0, 16), + name: generator.guid().replaceAll("-", "").substring(0, 16), type: "table", sourceType: datasource ? TableSourceType.EXTERNAL @@ -90,16 +89,13 @@ describe.each([ if (dsProvider) { datasource = await config.createDatasource({ - datasource: await dsProvider.datasource(), + datasource: await dsProvider, }) } table = await config.api.table.save(priceTable()) }) afterAll(async () => { - if (dsProvider) { - await dsProvider.stop() - } setup.afterAll() }) @@ -507,7 +503,6 @@ describe.each([ it("views have extra data trimmed", async () => { const table = await config.api.table.save( saveTableRequest({ - name: "orders", schema: { Country: { type: FieldType.STRING, @@ -523,7 +518,7 @@ describe.each([ const view = await config.api.viewV2.create({ tableId: table._id!, - name: uuid.v4(), + name: generator.guid(), schema: { Country: { visible: true, @@ -853,7 +848,6 @@ describe.each([ beforeAll(async () => { table = await config.api.table.save( saveTableRequest({ - name: `users_${uuid.v4()}`, type: "table", schema: { name: { diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index 650a1b414d..57aae02865 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -46,11 +46,3 @@ export async function getDatasources( ): Promise { return Promise.all(sourceNames.map(sourceName => providers[sourceName]())) } - -export const databaseTestProviders = { - postgres, - mongodb, - mysql, - mssql, - mariadb, -} From 1eae212f8392ef102690447c025b682bc64c803a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 26 Mar 2024 15:41:51 +0000 Subject: [PATCH 05/30] Stop relying on config.request and create a supertest instance per request. --- globalSetup.ts | 2 ++ .../src/api/routes/tests/viewV2.spec.ts | 7 +++--- .../server/src/tests/utilities/api/base.ts | 22 +++++++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/globalSetup.ts b/globalSetup.ts index 4cb542a3c3..66d3f5fd8c 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -2,6 +2,8 @@ import { GenericContainer, Wait } from "testcontainers" export default async function setup() { await new GenericContainer("budibase/couchdb") + .withName("budibase-test-couchdb") + .withReuse() .withExposedPorts(5984) .withEnvironment({ COUCHDB_PASSWORD: "budibase", diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index ce959ac429..d3e38b0f23 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -227,7 +227,7 @@ describe.each([ view = await config.api.viewV2.create({ tableId: table._id!, - name: "View A", + name: generator.guid(), }) }) @@ -303,12 +303,13 @@ describe.each([ it("can update an existing view name", async () => { const tableId = table._id! - await config.api.viewV2.update({ ...view, name: "View B" }) + const newName = generator.guid() + await config.api.viewV2.update({ ...view, name: newName }) expect(await config.api.table.get(tableId)).toEqual( expect.objectContaining({ views: { - "View B": { ...view, name: "View B", schema: expect.anything() }, + [newName]: { ...view, name: newName, schema: expect.anything() }, }, }) ) diff --git a/packages/server/src/tests/utilities/api/base.ts b/packages/server/src/tests/utilities/api/base.ts index 4df58ff425..e4094b8126 100644 --- a/packages/server/src/tests/utilities/api/base.ts +++ b/packages/server/src/tests/utilities/api/base.ts @@ -1,6 +1,7 @@ import TestConfiguration from "../TestConfiguration" -import { SuperTest, Test, Response } from "supertest" +import request, { SuperTest, Test, Response } from "supertest" import { ReadStream } from "fs" +import { getServer } from "../../../app" type Headers = Record type Method = "get" | "post" | "put" | "patch" | "delete" @@ -107,26 +108,29 @@ export abstract class TestAPI { const headersFn = publicUser ? this.config.publicHeaders.bind(this.config) : this.config.defaultHeaders.bind(this.config) - let request = this.request[method](url).set( + + const app = getServer() + let req = request(app)[method](url) + req = req.set( headersFn({ "x-budibase-include-stacktrace": "true", }) ) if (headers) { - request = request.set(headers) + req = req.set(headers) } if (body) { - request = request.send(body) + req = req.send(body) } for (const [key, value] of Object.entries(fields)) { - request = request.field(key, value) + req = req.field(key, value) } for (const [key, value] of Object.entries(files)) { if (isAttachedFile(value)) { - request = request.attach(key, value.file, value.name) + req = req.attach(key, value.file, value.name) } else { - request = request.attach(key, value as any) + req = req.attach(key, value as any) } } if (expectations?.headers) { @@ -136,11 +140,11 @@ export abstract class TestAPI { `Got an undefined expected value for header "${key}", if you want to check for the absence of a header, use headersNotPresent` ) } - request = request.expect(key, value as any) + req = req.expect(key, value as any) } } - return await request + return await req } protected _checkResponse = ( From f1609e6763c0ff603dfe164b0b7e5e45a0549a4f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 26 Mar 2024 17:25:24 +0000 Subject: [PATCH 06/30] Retry socket hangups. --- .../server/src/tests/utilities/api/base.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/server/src/tests/utilities/api/base.ts b/packages/server/src/tests/utilities/api/base.ts index e4094b8126..3a5f6529f8 100644 --- a/packages/server/src/tests/utilities/api/base.ts +++ b/packages/server/src/tests/utilities/api/base.ts @@ -77,7 +77,8 @@ export abstract class TestAPI { protected _requestRaw = async ( method: "get" | "post" | "put" | "patch" | "delete", url: string, - opts?: RequestOpts + opts?: RequestOpts, + attempt = 0 ): Promise => { const { headers = {}, @@ -144,7 +145,21 @@ export abstract class TestAPI { } } - return await req + try { + return await req + } catch (e: any) { + // We've found that occasionally the connection between supertest and the + // server supertest starts gets reset. Not sure why, but retrying it + // appears to work. I don't particularly like this, but it's better than + // flakiness. + if (e.code === "ECONNRESET") { + if (attempt > 2) { + throw e + } + return await this._requestRaw(method, url, opts, attempt + 1) + } + throw e + } } protected _checkResponse = ( @@ -174,7 +189,18 @@ export abstract class TestAPI { } } - throw new Error(message) + if (response.error) { + // Sometimes the error can be between supertest and the app, and when + // that happens response.error is sometimes populated with `text` that + // gives more detail about the error. The `message` is almost always + // useless from what I've seen. + if (response.error.text) { + response.error.message = response.error.text + } + throw new Error(message, { cause: response.error }) + } else { + throw new Error(message) + } } if (expectations?.headersNotPresent) { From 831c1743625f6c416808109518c1611033c10fe0 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 15:25:37 +0000 Subject: [PATCH 07/30] Give SQL integrations their own database when fetching a new datasource. --- .../core/utilities/structures/generator.ts | 1 + packages/server/scripts/test.sh | 2 + .../routes/tests/queries/generic-sql.spec.ts | 66 ++++++------------- .../server/src/integration-test/mysql.spec.ts | 11 +++- .../src/integrations/tests/utils/index.ts | 39 +++++++---- .../src/integrations/tests/utils/mariadb.ts | 27 +++++--- .../src/integrations/tests/utils/mongodb.ts | 2 +- .../src/integrations/tests/utils/mssql.ts | 30 ++++++++- .../src/integrations/tests/utils/mysql.ts | 28 +++++++- .../src/integrations/tests/utils/postgres.ts | 30 ++++++++- 10 files changed, 162 insertions(+), 74 deletions(-) diff --git a/packages/backend-core/tests/core/utilities/structures/generator.ts b/packages/backend-core/tests/core/utilities/structures/generator.ts index 64eb5ecc97..2a7eba6bbe 100644 --- a/packages/backend-core/tests/core/utilities/structures/generator.ts +++ b/packages/backend-core/tests/core/utilities/structures/generator.ts @@ -1,3 +1,4 @@ import Chance from "./Chance" export const generator = new Chance() + diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index 3ecf8bb794..c9f063c409 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +export DEBUG=testcontainers* + if [[ -n $CI ]] then export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts index d393430060..ff77d3dc52 100644 --- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts @@ -3,6 +3,7 @@ import * as setup from "../utilities" import { DatabaseName, getDatasource, + rawQuery, } from "../../../../integrations/tests/utils" import pg from "pg" import mysql from "mysql2/promise" @@ -46,6 +47,7 @@ describe.each( ].map(name => [name, getDatasource(name)]) )("queries (%s)", (dbName, dsProvider) => { const config = setup.getConfig() + let rawDatasource: Datasource let datasource: Datasource async function createQuery(query: Partial): Promise { @@ -62,56 +64,19 @@ describe.each( return await config.api.query.save({ ...defaultQuery, ...query }) } - async function rawQuery(sql: string): Promise { - // We re-fetch the datasource here because the one returned by - // config.api.datasource.create has the password field blanked out, and we - // need the password to connect to the database. - const ds = await dsProvider - switch (ds.source) { - case SourceName.POSTGRES: { - const client = new pg.Client(ds.config!) - await client.connect() - try { - const { rows } = await client.query(sql) - return rows - } finally { - await client.end() - } - } - case SourceName.MYSQL: { - const con = await mysql.createConnection(ds.config!) - try { - const [rows] = await con.query(sql) - return rows - } finally { - con.end() - } - } - case SourceName.SQL_SERVER: { - const pool = new mssql.ConnectionPool(ds.config! as mssql.config) - const client = await pool.connect() - try { - const { recordset } = await client.query(sql) - return recordset - } finally { - await pool.close() - } - } - } - } - beforeAll(async () => { await config.init() - datasource = await config.api.datasource.create(await dsProvider) + rawDatasource = await dsProvider + datasource = await config.api.datasource.create(rawDatasource) }) beforeEach(async () => { - await rawQuery(createTableSQL[datasource.source]) - await rawQuery(insertSQL) + await rawQuery(rawDatasource, createTableSQL[datasource.source]) + await rawQuery(rawDatasource, insertSQL) }) afterEach(async () => { - await rawQuery(dropTableSQL) + await rawQuery(rawDatasource, dropTableSQL) }) afterAll(async () => { @@ -145,7 +110,10 @@ describe.each( }, ]) - const rows = await rawQuery("SELECT * FROM test_table WHERE name = 'baz'") + const rows = await rawQuery( + rawDatasource, + "SELECT * FROM test_table WHERE name = 'baz'" + ) expect(rows).toHaveLength(1) }) @@ -173,6 +141,7 @@ describe.each( expect(result.data).toEqual([{ created: true }]) const rows = await rawQuery( + rawDatasource, `SELECT * FROM test_table WHERE birthday = '${date.toISOString()}'` ) expect(rows).toHaveLength(1) @@ -204,6 +173,7 @@ describe.each( expect(result.data).toEqual([{ created: true }]) const rows = await rawQuery( + rawDatasource, `SELECT * FROM test_table WHERE name = '${notDateStr}'` ) expect(rows).toHaveLength(1) @@ -340,7 +310,10 @@ describe.each( }, ]) - const rows = await rawQuery("SELECT * FROM test_table WHERE id = 1") + const rows = await rawQuery( + rawDatasource, + "SELECT * FROM test_table WHERE id = 1" + ) expect(rows).toEqual([ { id: 1, name: "foo", birthday: null, number: null }, ]) @@ -408,7 +381,10 @@ describe.each( }, ]) - const rows = await rawQuery("SELECT * FROM test_table WHERE id = 1") + const rows = await rawQuery( + rawDatasource, + "SELECT * FROM test_table WHERE id = 1" + ) expect(rows).toHaveLength(0) }) }) diff --git a/packages/server/src/integration-test/mysql.spec.ts b/packages/server/src/integration-test/mysql.spec.ts index 65fbe2949d..fb2d3c5285 100644 --- a/packages/server/src/integration-test/mysql.spec.ts +++ b/packages/server/src/integration-test/mysql.spec.ts @@ -18,6 +18,13 @@ import { generator } from "@budibase/backend-core/tests" // @ts-ignore fetch.mockSearch() +function uniqueTableName(length?: number): string { + return generator + .guid() + .replaceAll("-", "_") + .substring(0, length || 10) +} + const config = setup.getConfig()! jest.mock("../websockets", () => ({ @@ -53,7 +60,7 @@ describe("mysql integrations", () => { beforeEach(async () => { primaryMySqlTable = await config.createTable({ - name: generator.guid().replaceAll("-", "_").substring(0, 10), + name: uniqueTableName(), type: "table", primary: ["id"], schema: { @@ -249,7 +256,7 @@ describe("mysql integrations", () => { const addColumnToTable: TableRequest = { type: "table", sourceType: TableSourceType.EXTERNAL, - name: generator.guid().replaceAll("-", "_").substring(0, 10), + name: uniqueTableName(), sourceId: mysqlDatasource._id!, primary: ["id"], schema: { diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index 57aae02865..5760273d51 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -1,11 +1,11 @@ jest.unmock("pg") -import { Datasource } from "@budibase/types" -import { postgres } from "./postgres" -import { mongodb } from "./mongodb" -import { mysql } from "./mysql" -import { mssql } from "./mssql" -import { mariadb } from "./mariadb" +import { Datasource, SourceName } from "@budibase/types" +import * as postgres from "./postgres" +import * as mongodb from "./mongodb" +import * as mysql from "./mysql" +import * as mssql from "./mssql" +import * as mariadb from "./mariadb" export type DatasourceProvider = () => Promise @@ -18,11 +18,11 @@ export enum DatabaseName { } const providers: Record = { - [DatabaseName.POSTGRES]: postgres, - [DatabaseName.MONGODB]: mongodb, - [DatabaseName.MYSQL]: mysql, - [DatabaseName.SQL_SERVER]: mssql, - [DatabaseName.MARIADB]: mariadb, + [DatabaseName.POSTGRES]: postgres.getDatasource, + [DatabaseName.MONGODB]: mongodb.getDatasource, + [DatabaseName.MYSQL]: mysql.getDatasource, + [DatabaseName.SQL_SERVER]: mssql.getDatasource, + [DatabaseName.MARIADB]: mariadb.getDatasource, } export function getDatasourceProviders( @@ -46,3 +46,20 @@ export async function getDatasources( ): Promise { return Promise.all(sourceNames.map(sourceName => providers[sourceName]())) } + +export async function rawQuery(ds: Datasource, sql: string): Promise { + switch (ds.source) { + case SourceName.POSTGRES: { + return postgres.rawQuery(ds, sql) + } + case SourceName.MYSQL: { + return mysql.rawQuery(ds, sql) + } + case SourceName.SQL_SERVER: { + return mssql.rawQuery(ds, sql) + } + default: { + throw new Error(`Unsupported source: ${ds.source}`) + } + } +} diff --git a/packages/server/src/integrations/tests/utils/mariadb.ts b/packages/server/src/integrations/tests/utils/mariadb.ts index a10c36f9ff..c8890af1fb 100644 --- a/packages/server/src/integrations/tests/utils/mariadb.ts +++ b/packages/server/src/integrations/tests/utils/mariadb.ts @@ -1,6 +1,8 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" +import { rawQuery } from "./mysql" +import { generator } from "@budibase/backend-core/tests" class MariaDBWaitStrategy extends AbstractWaitStrategy { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { @@ -19,7 +21,7 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy { } } -export async function mariadb(): Promise { +export async function getDatasource(): Promise { const container = await new GenericContainer("mariadb:lts") .withName("budibase-test-mariadb") .withReuse() @@ -31,16 +33,23 @@ export async function mariadb(): Promise { const host = container.getHost() const port = container.getMappedPort(3306) - return { + const config = { + host, + port, + user: "root", + password: "password", + database: "mysql", + } + + const datasource = { type: "datasource_plus", source: SourceName.MYSQL, plus: true, - config: { - host, - port, - user: "root", - password: "password", - database: "mysql", - }, + config, } + + const database = generator.guid().replaceAll("-", "") + await rawQuery(datasource, `CREATE DATABASE \`${database}\``) + datasource.config.database = database + return datasource } diff --git a/packages/server/src/integrations/tests/utils/mongodb.ts b/packages/server/src/integrations/tests/utils/mongodb.ts index ff24bbc62e..6ab5b11191 100644 --- a/packages/server/src/integrations/tests/utils/mongodb.ts +++ b/packages/server/src/integrations/tests/utils/mongodb.ts @@ -1,7 +1,7 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" -export async function mongodb(): Promise { +export async function getDatasource(): Promise { const container = await new GenericContainer("mongo:7.0-jammy") .withName("budibase-test-mongodb") .withReuse() diff --git a/packages/server/src/integrations/tests/utils/mssql.ts b/packages/server/src/integrations/tests/utils/mssql.ts index 0f4e290526..c0875b84db 100644 --- a/packages/server/src/integrations/tests/utils/mssql.ts +++ b/packages/server/src/integrations/tests/utils/mssql.ts @@ -1,7 +1,9 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" +import mssql from "mssql" +import { generator } from "@budibase/backend-core/tests" -export async function mssql(): Promise { +export async function getDatasource(): Promise { const container = await new GenericContainer( "mcr.microsoft.com/mssql/server:2022-latest" ) @@ -27,7 +29,7 @@ export async function mssql(): Promise { const host = container.getHost() const port = container.getMappedPort(1433) - return { + const datasource: Datasource = { type: "datasource_plus", source: SourceName.SQL_SERVER, plus: true, @@ -41,4 +43,28 @@ export async function mssql(): Promise { }, }, } + + const database = generator.guid().replaceAll("-", "") + await rawQuery(datasource, `CREATE DATABASE "${database}"`) + datasource.config!.database = database + + return datasource +} + +export async function rawQuery(ds: Datasource, sql: string) { + if (!ds.config) { + throw new Error("Datasource config is missing") + } + if (ds.source !== SourceName.SQL_SERVER) { + throw new Error("Datasource source is not SQL Server") + } + + const pool = new mssql.ConnectionPool(ds.config! as mssql.config) + const client = await pool.connect() + try { + const { recordset } = await client.query(sql) + return recordset + } finally { + await pool.close() + } } diff --git a/packages/server/src/integrations/tests/utils/mysql.ts b/packages/server/src/integrations/tests/utils/mysql.ts index 665d6f0ecf..9fa8b0bd86 100644 --- a/packages/server/src/integrations/tests/utils/mysql.ts +++ b/packages/server/src/integrations/tests/utils/mysql.ts @@ -1,6 +1,8 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" +import mysql from "mysql2/promise" +import { generator } from "@budibase/backend-core/tests" class MySQLWaitStrategy extends AbstractWaitStrategy { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { @@ -22,7 +24,7 @@ class MySQLWaitStrategy extends AbstractWaitStrategy { } } -export async function mysql(): Promise { +export async function getDatasource(): Promise { const container = await new GenericContainer("mysql:8.3") .withName("budibase-test-mysql") .withReuse() @@ -33,7 +35,7 @@ export async function mysql(): Promise { const host = container.getHost() const port = container.getMappedPort(3306) - return { + const datasource: Datasource = { type: "datasource_plus", source: SourceName.MYSQL, plus: true, @@ -45,4 +47,26 @@ export async function mysql(): Promise { database: "mysql", }, } + + const database = generator.guid().replaceAll("-", "") + await rawQuery(datasource, `CREATE DATABASE \`${database}\``) + datasource.config!.database = database + return datasource +} + +export async function rawQuery(ds: Datasource, sql: string) { + if (!ds.config) { + throw new Error("Datasource config is missing") + } + if (ds.source !== SourceName.MYSQL) { + throw new Error("Datasource source is not MySQL") + } + + const connection = await mysql.createConnection(ds.config) + try { + const [rows] = await connection.query(sql) + return rows + } finally { + connection.end() + } } diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts index 896c7ea3e0..b10dfe44cf 100644 --- a/packages/server/src/integrations/tests/utils/postgres.ts +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -1,7 +1,9 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" +import pg from "pg" +import { generator } from "@budibase/backend-core/tests" -export async function postgres(): Promise { +export async function getDatasource(): Promise { const container = await new GenericContainer("postgres:16.1-bullseye") .withName("budibase-test-postgres") .withReuse() @@ -16,7 +18,7 @@ export async function postgres(): Promise { const host = container.getHost() const port = container.getMappedPort(5432) - return { + const datasource: Datasource = { type: "datasource_plus", source: SourceName.POSTGRES, plus: true, @@ -32,4 +34,28 @@ export async function postgres(): Promise { ca: false, }, } + + const database = generator.guid().replaceAll("-", "") + await rawQuery(datasource, `CREATE DATABASE "${database}"`) + datasource.config!.database = database + + return datasource +} + +export async function rawQuery(ds: Datasource, sql: string) { + if (!ds.config) { + throw new Error("Datasource config is missing") + } + if (ds.source !== SourceName.POSTGRES) { + throw new Error("Datasource source is not Postgres") + } + + const client = new pg.Client(ds.config) + await client.connect() + try { + const { rows } = await client.query(sql) + return rows + } finally { + await client.end() + } } From 385197a5f7da587cce795f68aebcd7ed27a03276 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 15:55:22 +0000 Subject: [PATCH 08/30] Fix postgres.spec.ts --- .../src/integration-test/postgres.spec.ts | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 57da02b507..5ecc3ca3ef 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -16,8 +16,12 @@ import { import _ from "lodash" import { generator } from "@budibase/backend-core/tests" import { utils } from "@budibase/backend-core" -import { DatabaseName, getDatasource } from "../integrations/tests/utils" -import { Client } from "pg" +import { + DatabaseName, + getDatasource, + rawQuery, +} from "../integrations/tests/utils" + // @ts-ignore fetch.mockSearch() @@ -28,7 +32,8 @@ jest.mock("../websockets") describe("postgres integrations", () => { let makeRequest: MakeRequestResponse, - postgresDatasource: Datasource, + rawDatasource: Datasource, + datasource: Datasource, primaryPostgresTable: Table, oneToManyRelationshipInfo: ForeignTableInfo, manyToOneRelationshipInfo: ForeignTableInfo, @@ -40,9 +45,8 @@ describe("postgres integrations", () => { makeRequest = generateMakeRequest(apiKey, true) - postgresDatasource = await config.api.datasource.create( - await getDatasource(DatabaseName.POSTGRES) - ) + rawDatasource = await getDatasource(DatabaseName.POSTGRES) + datasource = await config.api.datasource.create(rawDatasource) }) beforeEach(async () => { @@ -66,7 +70,7 @@ describe("postgres integrations", () => { type: FieldType.STRING, }, }, - sourceId: postgresDatasource._id, + sourceId: datasource._id, sourceType: TableSourceType.EXTERNAL, }) } @@ -143,7 +147,7 @@ describe("postgres integrations", () => { main: true, }, }, - sourceId: postgresDatasource._id, + sourceId: datasource._id, sourceType: TableSourceType.EXTERNAL, }) }) @@ -260,7 +264,7 @@ describe("postgres integrations", () => { autocolumn: true, }, }, - sourceId: postgresDatasource._id, + sourceId: datasource._id, sourceType: TableSourceType.EXTERNAL, }) } @@ -298,19 +302,16 @@ describe("postgres integrations", () => { } it("validate table schema", async () => { - const res = await makeRequest( - "get", - `/api/datasources/${postgresDatasource._id}` - ) + const res = await makeRequest("get", `/api/datasources/${datasource._id}`) expect(res.status).toBe(200) expect(res.body).toEqual({ config: { ca: false, - database: "postgres", - host: postgresDatasource.config!.host, + database: expect.any(String), + host: datasource.config!.host, password: "--secret-value--", - port: postgresDatasource.config!.port, + port: datasource.config!.port, rejectUnauthorized: false, schema: "public", ssl: false, @@ -1078,7 +1079,7 @@ describe("postgres integrations", () => { it("should fetch information about postgres datasource", async () => { const primaryName = primaryPostgresTable.name const response = await makeRequest("post", "/api/datasources/info", { - datasource: postgresDatasource, + datasource: datasource, }) expect(response.status).toBe(200) expect(response.body.tableNames).toBeDefined() @@ -1087,26 +1088,22 @@ describe("postgres integrations", () => { }) describe("POST /api/datasources/:datasourceId/schema", () => { - let client: Client let tableName: string beforeEach(async () => { tableName = generator.guid().replaceAll("-", "").substring(0, 10) - client = new Client((await getDatasource(DatabaseName.POSTGRES)).config!) - await client.connect() }) afterEach(async () => { - await client.query(`DROP TABLE IF EXISTS "${tableName}"`) - await client.end() + await rawQuery(rawDatasource, `DROP TABLE IF EXISTS "${tableName}"`) }) it("recognises when a table has no primary key", async () => { - await client.query(`CREATE TABLE "${tableName}" (id SERIAL)`) + await rawQuery(rawDatasource, `CREATE TABLE "${tableName}" (id SERIAL)`) const response = await makeRequest( "post", - `/api/datasources/${postgresDatasource._id}/schema` + `/api/datasources/${datasource._id}/schema` ) expect(response.body.errors).toEqual({ @@ -1115,13 +1112,14 @@ describe("postgres integrations", () => { }) it("recognises when a table is using a reserved column name", async () => { - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE "${tableName}" (_id SERIAL PRIMARY KEY) ` ) const response = await makeRequest( "post", - `/api/datasources/${postgresDatasource._id}/schema` + `/api/datasources/${datasource._id}/schema` ) expect(response.body.errors).toEqual({ @@ -1131,8 +1129,8 @@ describe("postgres integrations", () => { }) describe("Integration compatibility with postgres search_path", () => { - let client: Client, - pathDatasource: Datasource, + let rawDatasource: Datasource, + datasource: Datasource, schema1: string, schema2: string @@ -1140,39 +1138,38 @@ describe("postgres integrations", () => { schema1 = generator.guid().replaceAll("-", "") schema2 = generator.guid().replaceAll("-", "") - const dsConfig = await getDatasource(DatabaseName.POSTGRES) - const dbConfig = dsConfig.config! + rawDatasource = await getDatasource(DatabaseName.POSTGRES) + const dbConfig = rawDatasource.config! - client = new Client(dbConfig) - await client.connect() - await client.query(`CREATE SCHEMA "${schema1}";`) - await client.query(`CREATE SCHEMA "${schema2}";`) + await rawQuery(rawDatasource, `CREATE SCHEMA "${schema1}";`) + await rawQuery(rawDatasource, `CREATE SCHEMA "${schema2}";`) const pathConfig: any = { - ...dsConfig, + ...rawDatasource, config: { ...dbConfig, schema: `${schema1}, ${schema2}`, }, } - pathDatasource = await config.api.datasource.create(pathConfig) + datasource = await config.api.datasource.create(pathConfig) }) afterEach(async () => { - await client.query(`DROP SCHEMA "${schema1}" CASCADE;`) - await client.query(`DROP SCHEMA "${schema2}" CASCADE;`) - await client.end() + await rawQuery(rawDatasource, `DROP SCHEMA "${schema1}" CASCADE;`) + await rawQuery(rawDatasource, `DROP SCHEMA "${schema2}" CASCADE;`) }) it("discovers tables from any schema in search path", async () => { - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE "${schema1}".table1 (id1 SERIAL PRIMARY KEY);` ) - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE "${schema2}".table2 (id2 SERIAL PRIMARY KEY);` ) const response = await makeRequest("post", "/api/datasources/info", { - datasource: pathDatasource, + datasource: datasource, }) expect(response.status).toBe(200) expect(response.body.tableNames).toBeDefined() @@ -1183,15 +1180,17 @@ describe("postgres integrations", () => { it("does not mix columns from different tables", async () => { const repeated_table_name = "table_same_name" - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE "${schema1}".${repeated_table_name} (id SERIAL PRIMARY KEY, val1 TEXT);` ) - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE "${schema2}".${repeated_table_name} (id2 SERIAL PRIMARY KEY, val2 TEXT);` ) const response = await makeRequest( "post", - `/api/datasources/${pathDatasource._id}/schema`, + `/api/datasources/${datasource._id}/schema`, { tablesFilter: [repeated_table_name], } From 1205cfcbcc3329a498ee8a8bcf297ea40df0abe9 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 16:31:31 +0000 Subject: [PATCH 09/30] Fix mysql.spec.ts --- .../routes/tests/queries/generic-sql.spec.ts | 3 - .../server/src/integration-test/mysql.spec.ts | 112 ++++++++---------- 2 files changed, 48 insertions(+), 67 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts index ff77d3dc52..585288bc43 100644 --- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts @@ -5,9 +5,6 @@ import { getDatasource, rawQuery, } from "../../../../integrations/tests/utils" -import pg from "pg" -import mysql from "mysql2/promise" -import mssql from "mssql" jest.unmock("pg") diff --git a/packages/server/src/integration-test/mysql.spec.ts b/packages/server/src/integration-test/mysql.spec.ts index fb2d3c5285..7e54b53b15 100644 --- a/packages/server/src/integration-test/mysql.spec.ts +++ b/packages/server/src/integration-test/mysql.spec.ts @@ -11,8 +11,11 @@ import { TableRequest, TableSourceType, } from "@budibase/types" -import { DatabaseName, getDatasource } from "../integrations/tests/utils" -import mysql from "mysql2/promise" +import { + DatabaseName, + getDatasource, + rawQuery, +} from "../integrations/tests/utils" import { builderSocket } from "../websockets" import { generator } from "@budibase/backend-core/tests" // @ts-ignore @@ -44,7 +47,8 @@ jest.mock("../websockets", () => ({ describe("mysql integrations", () => { let makeRequest: MakeRequestResponse, - mysqlDatasource: Datasource, + rawDatasource: Datasource, + datasource: Datasource, primaryMySqlTable: Table beforeAll(async () => { @@ -53,9 +57,8 @@ describe("mysql integrations", () => { makeRequest = generateMakeRequest(apiKey, true) - mysqlDatasource = await config.api.datasource.create( - await getDatasource(DatabaseName.MYSQL) - ) + rawDatasource = await getDatasource(DatabaseName.MYSQL) + datasource = await config.api.datasource.create(rawDatasource) }) beforeEach(async () => { @@ -82,7 +85,7 @@ describe("mysql integrations", () => { type: FieldType.NUMBER, }, }, - sourceId: mysqlDatasource._id, + sourceId: datasource._id, sourceType: TableSourceType.EXTERNAL, }) }) @@ -90,18 +93,15 @@ describe("mysql integrations", () => { afterAll(config.end) it("validate table schema", async () => { - const res = await makeRequest( - "get", - `/api/datasources/${mysqlDatasource._id}` - ) + const res = await makeRequest("get", `/api/datasources/${datasource._id}`) expect(res.status).toBe(200) expect(res.body).toEqual({ config: { - database: "mysql", - host: mysqlDatasource.config!.host, + database: expect.any(String), + host: datasource.config!.host, password: "--secret-value--", - port: mysqlDatasource.config!.port, + port: datasource.config!.port, user: "root", }, plus: true, @@ -120,7 +120,7 @@ describe("mysql integrations", () => { it("should be able to verify the connection", async () => { await config.api.datasource.verify( { - datasource: await getDatasource(DatabaseName.MYSQL), + datasource: rawDatasource, }, { body: { @@ -131,13 +131,12 @@ describe("mysql integrations", () => { }) it("should state an invalid datasource cannot connect", async () => { - const dbConfig = await getDatasource(DatabaseName.MYSQL) await config.api.datasource.verify( { datasource: { - ...dbConfig, + ...rawDatasource, config: { - ...dbConfig.config, + ...rawDatasource.config, password: "wrongpassword", }, }, @@ -157,7 +156,7 @@ describe("mysql integrations", () => { it("should fetch information about mysql datasource", async () => { const primaryName = primaryMySqlTable.name const response = await makeRequest("post", "/api/datasources/info", { - datasource: mysqlDatasource, + datasource: datasource, }) expect(response.status).toBe(200) expect(response.body.tableNames).toBeDefined() @@ -166,40 +165,38 @@ describe("mysql integrations", () => { }) describe("Integration compatibility with mysql search_path", () => { - let client: mysql.Connection, pathDatasource: Datasource - const database = "test1" - const database2 = "test-2" + let datasource: Datasource, rawDatasource: Datasource + const database = generator.guid() + const database2 = generator.guid() beforeAll(async () => { - const dsConfig = await getDatasource(DatabaseName.MYSQL) - const dbConfig = dsConfig.config! + rawDatasource = await getDatasource(DatabaseName.MYSQL) - client = await mysql.createConnection(dbConfig) - await client.query(`CREATE DATABASE \`${database}\`;`) - await client.query(`CREATE DATABASE \`${database2}\`;`) + await rawQuery(rawDatasource, `CREATE DATABASE \`${database}\`;`) + await rawQuery(rawDatasource, `CREATE DATABASE \`${database2}\`;`) const pathConfig: any = { - ...dsConfig, + ...rawDatasource, config: { - ...dbConfig, + ...rawDatasource.config!, database, }, } - pathDatasource = await config.api.datasource.create(pathConfig) + datasource = await config.api.datasource.create(pathConfig) }) afterAll(async () => { - await client.query(`DROP DATABASE \`${database}\`;`) - await client.query(`DROP DATABASE \`${database2}\`;`) - await client.end() + await rawQuery(rawDatasource, `DROP DATABASE \`${database}\`;`) + await rawQuery(rawDatasource, `DROP DATABASE \`${database2}\`;`) }) it("discovers tables from any schema in search path", async () => { - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE \`${database}\`.table1 (id1 SERIAL PRIMARY KEY);` ) const response = await makeRequest("post", "/api/datasources/info", { - datasource: pathDatasource, + datasource: datasource, }) expect(response.status).toBe(200) expect(response.body.tableNames).toBeDefined() @@ -210,15 +207,17 @@ describe("mysql integrations", () => { it("does not mix columns from different tables", async () => { const repeated_table_name = "table_same_name" - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE \`${database}\`.${repeated_table_name} (id SERIAL PRIMARY KEY, val1 TEXT);` ) - await client.query( + await rawQuery( + rawDatasource, `CREATE TABLE \`${database2}\`.${repeated_table_name} (id2 SERIAL PRIMARY KEY, val2 TEXT);` ) const response = await makeRequest( "post", - `/api/datasources/${pathDatasource._id}/schema`, + `/api/datasources/${datasource._id}/schema`, { tablesFilter: [repeated_table_name], } @@ -234,30 +233,14 @@ describe("mysql integrations", () => { }) describe("POST /api/tables/", () => { - let client: mysql.Connection const emitDatasourceUpdateMock = jest.fn() - beforeEach(async () => { - client = await mysql.createConnection( - ( - await getDatasource(DatabaseName.MYSQL) - ).config! - ) - mysqlDatasource = await config.api.datasource.create( - await getDatasource(DatabaseName.MYSQL) - ) - }) - - afterEach(async () => { - await client.end() - }) - it("will emit the datasource entity schema with externalType to the front-end when adding a new column", async () => { const addColumnToTable: TableRequest = { type: "table", sourceType: TableSourceType.EXTERNAL, name: uniqueTableName(), - sourceId: mysqlDatasource._id!, + sourceId: datasource._id!, primary: ["id"], schema: { id: { @@ -304,7 +287,7 @@ describe("mysql integrations", () => { }, }, created: true, - _id: `${mysqlDatasource._id}__${addColumnToTable.name}`, + _id: `${datasource._id}__${addColumnToTable.name}`, } delete expectedTable._add @@ -351,17 +334,18 @@ describe("mysql integrations", () => { "/api/tables/", renameColumnOnTable ) - mysqlDatasource = ( - await makeRequest( - "post", - `/api/datasources/${mysqlDatasource._id}/schema` - ) + + const ds = ( + await makeRequest("post", `/api/datasources/${datasource._id}/schema`) ).body.datasource expect(response.status).toEqual(200) - expect( - Object.keys(mysqlDatasource.entities![primaryMySqlTable.name].schema) - ).toEqual(["id", "name", "description", "age"]) + expect(Object.keys(ds.entities![primaryMySqlTable.name].schema)).toEqual([ + "id", + "name", + "description", + "age", + ]) }) }) }) From eb33dac9b1cd0afcee95f3990b957f5b75042f62 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 16:40:41 +0000 Subject: [PATCH 10/30] Make container reuse optional, disabled by default. --- globalSetup.ts | 11 +++++++---- .../src/integrations/tests/utils/mariadb.ts | 15 +++++++++------ .../src/integrations/tests/utils/mongodb.ts | 15 +++++++++------ .../server/src/integrations/tests/utils/mssql.ts | 15 +++++++++------ .../server/src/integrations/tests/utils/mysql.ts | 16 ++++++++++------ .../src/integrations/tests/utils/postgres.ts | 16 ++++++++++------ 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/globalSetup.ts b/globalSetup.ts index 66d3f5fd8c..00d5e3f2dc 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -1,9 +1,7 @@ import { GenericContainer, Wait } from "testcontainers" export default async function setup() { - await new GenericContainer("budibase/couchdb") - .withName("budibase-test-couchdb") - .withReuse() + let couchdb = new GenericContainer("budibase/couchdb") .withExposedPorts(5984) .withEnvironment({ COUCHDB_PASSWORD: "budibase", @@ -23,5 +21,10 @@ export default async function setup() { "curl http://budibase:budibase@localhost:5984/_up" ).withStartupTimeout(20000) ) - .start() + + if (process.env.REUSE_CONTAINERS) { + couchdb = couchdb.withReuse() + } + + await couchdb.start() } diff --git a/packages/server/src/integrations/tests/utils/mariadb.ts b/packages/server/src/integrations/tests/utils/mariadb.ts index c8890af1fb..2634c9f913 100644 --- a/packages/server/src/integrations/tests/utils/mariadb.ts +++ b/packages/server/src/integrations/tests/utils/mariadb.ts @@ -22,16 +22,19 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy { } export async function getDatasource(): Promise { - const container = await new GenericContainer("mariadb:lts") - .withName("budibase-test-mariadb") - .withReuse() + let container = new GenericContainer("mariadb:lts") .withExposedPorts(3306) .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" }) .withWaitStrategy(new MariaDBWaitStrategy()) - .start() - const host = container.getHost() - const port = container.getMappedPort(3306) + if (process.env.REUSE_CONTAINERS) { + container = container.withReuse() + } + + const startedContainer = await container.start() + + const host = startedContainer.getHost() + const port = startedContainer.getMappedPort(3306) const config = { host, diff --git a/packages/server/src/integrations/tests/utils/mongodb.ts b/packages/server/src/integrations/tests/utils/mongodb.ts index 6ab5b11191..26fbff966e 100644 --- a/packages/server/src/integrations/tests/utils/mongodb.ts +++ b/packages/server/src/integrations/tests/utils/mongodb.ts @@ -2,9 +2,7 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" export async function getDatasource(): Promise { - const container = await new GenericContainer("mongo:7.0-jammy") - .withName("budibase-test-mongodb") - .withReuse() + let container = new GenericContainer("mongo:7.0-jammy") .withExposedPorts(27017) .withEnvironment({ MONGO_INITDB_ROOT_USERNAME: "mongo", @@ -15,10 +13,15 @@ export async function getDatasource(): Promise { `mongosh --eval "db.version()"` ).withStartupTimeout(10000) ) - .start() - const host = container.getHost() - const port = container.getMappedPort(27017) + if (process.env.REUSE_CONTAINERS) { + container = container.withReuse() + } + + const startedContainer = await container.start() + + const host = startedContainer.getHost() + const port = startedContainer.getMappedPort(27017) return { type: "datasource", diff --git a/packages/server/src/integrations/tests/utils/mssql.ts b/packages/server/src/integrations/tests/utils/mssql.ts index c0875b84db..290bc78246 100644 --- a/packages/server/src/integrations/tests/utils/mssql.ts +++ b/packages/server/src/integrations/tests/utils/mssql.ts @@ -4,11 +4,9 @@ import mssql from "mssql" import { generator } from "@budibase/backend-core/tests" export async function getDatasource(): Promise { - const container = await new GenericContainer( + let container = new GenericContainer( "mcr.microsoft.com/mssql/server:2022-latest" ) - .withName("budibase-test-mssql") - .withReuse() .withExposedPorts(1433) .withEnvironment({ ACCEPT_EULA: "Y", @@ -24,10 +22,15 @@ export async function getDatasource(): Promise { "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'" ) ) - .start() - const host = container.getHost() - const port = container.getMappedPort(1433) + if (process.env.REUSE_CONTAINERS) { + container = container.withReuse() + } + + const startedContainer = await container.start() + + const host = startedContainer.getHost() + const port = startedContainer.getMappedPort(1433) const datasource: Datasource = { type: "datasource_plus", diff --git a/packages/server/src/integrations/tests/utils/mysql.ts b/packages/server/src/integrations/tests/utils/mysql.ts index 9fa8b0bd86..0f83128f26 100644 --- a/packages/server/src/integrations/tests/utils/mysql.ts +++ b/packages/server/src/integrations/tests/utils/mysql.ts @@ -25,15 +25,19 @@ class MySQLWaitStrategy extends AbstractWaitStrategy { } export async function getDatasource(): Promise { - const container = await new GenericContainer("mysql:8.3") - .withName("budibase-test-mysql") - .withReuse() + let container = new GenericContainer("mysql:8.3") .withExposedPorts(3306) .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" }) .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000)) - .start() - const host = container.getHost() - const port = container.getMappedPort(3306) + + if (process.env.REUSE_CONTAINERS) { + container = container.withReuse() + } + + const startedContainer = await container.start() + + const host = startedContainer.getHost() + const port = startedContainer.getMappedPort(3306) const datasource: Datasource = { type: "datasource_plus", diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts index b10dfe44cf..237bc19a17 100644 --- a/packages/server/src/integrations/tests/utils/postgres.ts +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -4,9 +4,7 @@ import pg from "pg" import { generator } from "@budibase/backend-core/tests" export async function getDatasource(): Promise { - const container = await new GenericContainer("postgres:16.1-bullseye") - .withName("budibase-test-postgres") - .withReuse() + let container = new GenericContainer("postgres:16.1-bullseye") .withExposedPorts(5432) .withEnvironment({ POSTGRES_PASSWORD: "password" }) .withWaitStrategy( @@ -14,9 +12,15 @@ export async function getDatasource(): Promise { "pg_isready -h localhost -p 5432" ).withStartupTimeout(10000) ) - .start() - const host = container.getHost() - const port = container.getMappedPort(5432) + + if (process.env.REUSE_CONTAINERS) { + container = container.withReuse() + } + + const startedContainer = await container.start() + + const host = startedContainer.getHost() + const port = startedContainer.getMappedPort(5432) const datasource: Datasource = { type: "datasource_plus", From 204b16876a74f549aff70bd5ee77a5e2bbe95aae Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 16:46:11 +0000 Subject: [PATCH 11/30] Run yarn lint:fix --- .../backend-core/tests/core/utilities/structures/generator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-core/tests/core/utilities/structures/generator.ts b/packages/backend-core/tests/core/utilities/structures/generator.ts index 2a7eba6bbe..64eb5ecc97 100644 --- a/packages/backend-core/tests/core/utilities/structures/generator.ts +++ b/packages/backend-core/tests/core/utilities/structures/generator.ts @@ -1,4 +1,3 @@ import Chance from "./Chance" export const generator = new Chance() - From 4183a0aeb785c4081f9b1924e2e2ab716593300b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 16:57:31 +0000 Subject: [PATCH 12/30] Set default packages/server test timeout to 30 seconds, and unmock console.log. --- .../server/src/api/routes/public/tests/metrics.spec.js | 2 -- packages/server/src/api/routes/tests/appImport.spec.ts | 1 - .../server/src/api/routes/tests/automation.spec.ts | 2 -- packages/server/src/api/routes/tests/user.spec.ts | 2 -- packages/server/src/migrations/tests/index.spec.ts | 2 -- .../src/sdk/app/rows/search/tests/external.spec.ts | 2 -- packages/server/src/tests/jestSetup.ts | 10 ++-------- 7 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/server/src/api/routes/public/tests/metrics.spec.js b/packages/server/src/api/routes/public/tests/metrics.spec.js index 8231596d59..2fb5e91000 100644 --- a/packages/server/src/api/routes/public/tests/metrics.spec.js +++ b/packages/server/src/api/routes/public/tests/metrics.spec.js @@ -1,7 +1,5 @@ const setup = require("../../tests/utilities") -jest.setTimeout(30000) - describe("/metrics", () => { let request = setup.getRequest() let config = setup.getConfig() diff --git a/packages/server/src/api/routes/tests/appImport.spec.ts b/packages/server/src/api/routes/tests/appImport.spec.ts index 75e9f91d63..bc211024d4 100644 --- a/packages/server/src/api/routes/tests/appImport.spec.ts +++ b/packages/server/src/api/routes/tests/appImport.spec.ts @@ -1,7 +1,6 @@ import * as setup from "./utilities" import path from "path" -jest.setTimeout(15000) const PASSWORD = "testtest" describe("/applications/:appId/import", () => { diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 322694df75..7885e97fbf 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -23,8 +23,6 @@ let { collectAutomation, } = setup.structures -jest.setTimeout(30000) - describe("/automations", () => { let request = setup.getRequest() let config = setup.getConfig() diff --git a/packages/server/src/api/routes/tests/user.spec.ts b/packages/server/src/api/routes/tests/user.spec.ts index ff8c0d54b3..a46de8f3b3 100644 --- a/packages/server/src/api/routes/tests/user.spec.ts +++ b/packages/server/src/api/routes/tests/user.spec.ts @@ -3,8 +3,6 @@ import { checkPermissionsEndpoint } from "./utilities/TestFunctions" import * as setup from "./utilities" import { UserMetadata } from "@budibase/types" -jest.setTimeout(30000) - jest.mock("../../../utilities/workerRequests", () => ({ getGlobalUsers: jest.fn(() => { return {} diff --git a/packages/server/src/migrations/tests/index.spec.ts b/packages/server/src/migrations/tests/index.spec.ts index 8eb59b8a0e..d06cd37b69 100644 --- a/packages/server/src/migrations/tests/index.spec.ts +++ b/packages/server/src/migrations/tests/index.spec.ts @@ -25,8 +25,6 @@ const clearMigrations = async () => { } } -jest.setTimeout(10000) - describe("migrations", () => { const config = new TestConfig() diff --git a/packages/server/src/sdk/app/rows/search/tests/external.spec.ts b/packages/server/src/sdk/app/rows/search/tests/external.spec.ts index bae58d6a2c..596e41cece 100644 --- a/packages/server/src/sdk/app/rows/search/tests/external.spec.ts +++ b/packages/server/src/sdk/app/rows/search/tests/external.spec.ts @@ -17,8 +17,6 @@ import { generator, } from "@budibase/backend-core/tests" -jest.setTimeout(30000) - describe("external search", () => { const config = new TestConfiguration() diff --git a/packages/server/src/tests/jestSetup.ts b/packages/server/src/tests/jestSetup.ts index e233e7152e..c01f415f9e 100644 --- a/packages/server/src/tests/jestSetup.ts +++ b/packages/server/src/tests/jestSetup.ts @@ -2,17 +2,11 @@ import env from "../environment" import { env as coreEnv, timers } from "@budibase/backend-core" import { testContainerUtils } from "@budibase/backend-core/tests" -if (!process.env.DEBUG) { - global.console.log = jest.fn() // console.log are ignored in tests - global.console.warn = jest.fn() // console.warn are ignored in tests -} - if (!process.env.CI) { - // set a longer timeout in dev for debugging - // 100 seconds + // set a longer timeout in dev for debugging 100 seconds jest.setTimeout(100 * 1000) } else { - jest.setTimeout(10 * 1000) + jest.setTimeout(30 * 1000) } testContainerUtils.setupEnv(env, coreEnv) From ecdb5eec36accc0ed3b00aab5773ace31773efef Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 16:59:27 +0000 Subject: [PATCH 13/30] Remove testcontainers debug logging. --- packages/server/scripts/test.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index c9f063c409..3ecf8bb794 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -1,8 +1,6 @@ #!/bin/bash set -e -export DEBUG=testcontainers* - if [[ -n $CI ]] then export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" From d7f112dc9559b37f297c9bd8de23cf4956e2bedf Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 17:47:03 +0000 Subject: [PATCH 14/30] Free up space in the server tests run. --- .github/workflows/budibase_ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index a9cb89d4e6..f1dd54ef18 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -153,6 +153,11 @@ jobs: node-version: 20.x cache: yarn + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + android: true + - name: Pull testcontainers images run: | docker pull mcr.microsoft.com/mssql/server:2022-latest From 29a928eb09b07d1dec9a13353197cde200235978 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 27 Mar 2024 17:51:19 +0000 Subject: [PATCH 15/30] Turn off some expensive options on the disk free up. --- .github/workflows/budibase_ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index f1dd54ef18..be588549a1 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -156,6 +156,9 @@ jobs: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main with: + large-packages: false + swap-storage: false + docker-images: false android: true - name: Pull testcontainers images From f43f03a3b495b5ac3e97782824a961cdf49f93e4 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 28 Mar 2024 10:31:28 +0000 Subject: [PATCH 16/30] Use new larger runners. --- .github/workflows/budibase_ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index be588549a1..6ad5e68cbd 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -136,7 +136,7 @@ jobs: fi test-server: - runs-on: ubuntu-latest + runs-on: budi-tubby-tornado-quad-core-150gb env: DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull steps: @@ -153,14 +153,6 @@ jobs: node-version: 20.x cache: yarn - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - large-packages: false - swap-storage: false - docker-images: false - android: true - - name: Pull testcontainers images run: | docker pull mcr.microsoft.com/mssql/server:2022-latest From 90cfdd661de448ebb484a426dc2822bf9c1feb2f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 28 Mar 2024 17:36:26 +0000 Subject: [PATCH 17/30] Rework how we connect to containers. --- .github/workflows/budibase_ci.yml | 3 +- .../core/utilities/testContainerUtils.ts | 47 ++++++++++++----- .../src/integrations/tests/utils/index.ts | 25 ++++++++++ .../src/integrations/tests/utils/mariadb.ts | 29 ++++++----- .../src/integrations/tests/utils/mongodb.ts | 40 ++++++++------- .../src/integrations/tests/utils/mssql.ts | 50 +++++++++---------- .../src/integrations/tests/utils/mysql.ts | 26 +++++----- .../src/integrations/tests/utils/postgres.ts | 32 ++++++------ 8 files changed, 152 insertions(+), 100 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 6ad5e68cbd..224537d216 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -136,7 +136,8 @@ jobs: fi test-server: - runs-on: budi-tubby-tornado-quad-core-150gb + runs-on: + group: hosted-runners env: DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull steps: diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index 5d4f5a3c11..2f33db65d3 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -1,6 +1,8 @@ import { DatabaseImpl } from "../../../src/db" import { execSync } from "child_process" +const IPV4_PORT_REGEX = new RegExp(`0\\.0\\.0\\.0:(\\d+)->(\\d+)/tcp`, "g") + interface ContainerInfo { Command: string CreatedAt: string @@ -19,7 +21,10 @@ interface ContainerInfo { } function getTestcontainers(): ContainerInfo[] { - return execSync("docker ps --format json") + // We use --format json to make sure the output is nice and machine-readable, + // and we use --no-trunc so that the command returns full container IDs so we + // can filter on them correctly. + return execSync("docker ps --format json --no-trunc") .toString() .split("\n") .filter(x => x.length > 0) @@ -27,32 +32,51 @@ function getTestcontainers(): ContainerInfo[] { .filter(x => x.Labels.includes("org.testcontainers=true")) } -function getContainerByImage(image: string) { - return getTestcontainers().find(x => x.Image.startsWith(image)) +export function getContainerByImage(image: string) { + const containers = getTestcontainers().filter(x => x.Image.startsWith(image)) + if (containers.length > 1) { + throw new Error(`Multiple containers found with image: ${image}`) + } + return containers[0] } -function getExposedPort(container: ContainerInfo, port: number) { - const match = container.Ports.match(new RegExp(`0.0.0.0:(\\d+)->${port}/tcp`)) - if (!match) { - return undefined +export function getContainerById(id: string) { + return getTestcontainers().find(x => x.ID === id) +} + +export interface Port { + host: number + container: number +} + +export function getExposedV4Ports(container: ContainerInfo): Port[] { + let ports: Port[] = [] + for (const match of container.Ports.matchAll(IPV4_PORT_REGEX)) { + ports.push({ host: parseInt(match[1]), container: parseInt(match[2]) }) } - return parseInt(match[1]) + return ports +} + +export function getExposedV4Port(container: ContainerInfo, port: number) { + return getExposedV4Ports(container).find(x => x.container === port)?.host } export function setupEnv(...envs: any[]) { + // We start couchdb in globalSetup.ts, in the root of the monorepo, so it + // should be relatively safe to look for it by its image name. const couch = getContainerByImage("budibase/couchdb") if (!couch) { throw new Error("CouchDB container not found") } - const couchPort = getExposedPort(couch, 5984) + const couchPort = getExposedV4Port(couch, 5984) if (!couchPort) { throw new Error("CouchDB port not found") } const configs = [ { key: "COUCH_DB_PORT", value: `${couchPort}` }, - { key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` }, + { key: "COUCH_DB_URL", value: `http://127.0.0.1:${couchPort}` }, ] for (const config of configs.filter(x => !!x.value)) { @@ -60,7 +84,4 @@ export function setupEnv(...envs: any[]) { env._set(config.key, config.value) } } - - // @ts-expect-error - DatabaseImpl.nano = undefined } diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index 5760273d51..bbdb41b38a 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -6,6 +6,8 @@ import * as mongodb from "./mongodb" import * as mysql from "./mysql" import * as mssql from "./mssql" import * as mariadb from "./mariadb" +import { GenericContainer } from "testcontainers" +import { testContainerUtils } from "@budibase/backend-core/tests" export type DatasourceProvider = () => Promise @@ -63,3 +65,26 @@ export async function rawQuery(ds: Datasource, sql: string): Promise { } } } + +export async function startContainer(container: GenericContainer) { + if (process.env.REUSE_CONTAINERS) { + container = container.withReuse() + } + + const startedContainer = await container.start() + + const info = testContainerUtils.getContainerById(startedContainer.getId()) + if (!info) { + throw new Error("Container not found") + } + + // Some Docker runtimes, when you expose a port, will bind it to both + // 127.0.0.1 and ::1, so ipv4 and ipv6. The port spaces of ipv4 and ipv6 + // addresses are not shared, and testcontainers will sometimes give you back + // the ipv6 port. There's no way to know that this has happened, and if you + // try to then connect to `localhost:port` you may attempt to bind to the v4 + // address which could be unbound or even an entirely different container. For + // that reason, we don't use testcontainers' `getExposedPort` function, + // preferring instead our own method that guaranteed v4 ports. + return testContainerUtils.getExposedV4Ports(info) +} diff --git a/packages/server/src/integrations/tests/utils/mariadb.ts b/packages/server/src/integrations/tests/utils/mariadb.ts index 2634c9f913..fcd79b8e56 100644 --- a/packages/server/src/integrations/tests/utils/mariadb.ts +++ b/packages/server/src/integrations/tests/utils/mariadb.ts @@ -2,7 +2,10 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" import { rawQuery } from "./mysql" -import { generator } from "@budibase/backend-core/tests" +import { generator, testContainerUtils } from "@budibase/backend-core/tests" +import { startContainer } from "." + +let ports: Promise class MariaDBWaitStrategy extends AbstractWaitStrategy { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { @@ -22,22 +25,22 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy { } export async function getDatasource(): Promise { - let container = new GenericContainer("mariadb:lts") - .withExposedPorts(3306) - .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" }) - .withWaitStrategy(new MariaDBWaitStrategy()) - - if (process.env.REUSE_CONTAINERS) { - container = container.withReuse() + if (!ports) { + ports = startContainer( + new GenericContainer("mariadb:lts") + .withExposedPorts(3306) + .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" }) + .withWaitStrategy(new MariaDBWaitStrategy()) + ) } - const startedContainer = await container.start() - - const host = startedContainer.getHost() - const port = startedContainer.getMappedPort(3306) + const port = (await ports).find(x => x.container === 3306)?.host + if (!port) { + throw new Error("MariaDB port not found") + } const config = { - host, + host: "127.0.0.1", port, user: "root", password: "password", diff --git a/packages/server/src/integrations/tests/utils/mongodb.ts b/packages/server/src/integrations/tests/utils/mongodb.ts index 26fbff966e..c5c0340dc9 100644 --- a/packages/server/src/integrations/tests/utils/mongodb.ts +++ b/packages/server/src/integrations/tests/utils/mongodb.ts @@ -1,34 +1,38 @@ +import { testContainerUtils } from "@budibase/backend-core/tests" import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" +import { startContainer } from "." + +let ports: Promise export async function getDatasource(): Promise { - let container = new GenericContainer("mongo:7.0-jammy") - .withExposedPorts(27017) - .withEnvironment({ - MONGO_INITDB_ROOT_USERNAME: "mongo", - MONGO_INITDB_ROOT_PASSWORD: "password", - }) - .withWaitStrategy( - Wait.forSuccessfulCommand( - `mongosh --eval "db.version()"` - ).withStartupTimeout(10000) + if (!ports) { + ports = startContainer( + new GenericContainer("mongo:7.0-jammy") + .withExposedPorts(27017) + .withEnvironment({ + MONGO_INITDB_ROOT_USERNAME: "mongo", + MONGO_INITDB_ROOT_PASSWORD: "password", + }) + .withWaitStrategy( + Wait.forSuccessfulCommand( + `mongosh --eval "db.version()"` + ).withStartupTimeout(10000) + ) ) - - if (process.env.REUSE_CONTAINERS) { - container = container.withReuse() } - const startedContainer = await container.start() - - const host = startedContainer.getHost() - const port = startedContainer.getMappedPort(27017) + const port = (await ports).find(x => x.container === 27017) + if (!port) { + throw new Error("MongoDB port not found") + } return { type: "datasource", source: SourceName.MONGODB, plus: false, config: { - connectionString: `mongodb://mongo:password@${host}:${port}`, + connectionString: `mongodb://mongo:password@127.0.0.1:${port.host}`, db: "mongo", }, } diff --git a/packages/server/src/integrations/tests/utils/mssql.ts b/packages/server/src/integrations/tests/utils/mssql.ts index 290bc78246..647f461272 100644 --- a/packages/server/src/integrations/tests/utils/mssql.ts +++ b/packages/server/src/integrations/tests/utils/mssql.ts @@ -1,43 +1,41 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import mssql from "mssql" -import { generator } from "@budibase/backend-core/tests" +import { generator, testContainerUtils } from "@budibase/backend-core/tests" +import { startContainer } from "." + +let ports: Promise export async function getDatasource(): Promise { - let container = new GenericContainer( - "mcr.microsoft.com/mssql/server:2022-latest" - ) - .withExposedPorts(1433) - .withEnvironment({ - ACCEPT_EULA: "Y", - MSSQL_SA_PASSWORD: "Password_123", - // This is important, as Microsoft allow us to use the "Developer" edition - // of SQL Server for development and testing purposes. We can't use other - // versions without a valid license, and we cannot use the Developer - // version in production. - MSSQL_PID: "Developer", - }) - .withWaitStrategy( - Wait.forSuccessfulCommand( - "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'" - ) + if (!ports) { + ports = startContainer( + new GenericContainer("mcr.microsoft.com/mssql/server:2022-latest") + .withExposedPorts(1433) + .withEnvironment({ + ACCEPT_EULA: "Y", + MSSQL_SA_PASSWORD: "Password_123", + // This is important, as Microsoft allow us to use the "Developer" edition + // of SQL Server for development and testing purposes. We can't use other + // versions without a valid license, and we cannot use the Developer + // version in production. + MSSQL_PID: "Developer", + }) + .withWaitStrategy( + Wait.forSuccessfulCommand( + "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'" + ) + ) ) - - if (process.env.REUSE_CONTAINERS) { - container = container.withReuse() } - const startedContainer = await container.start() - - const host = startedContainer.getHost() - const port = startedContainer.getMappedPort(1433) + const port = (await ports).find(x => x.container === 1433)?.host const datasource: Datasource = { type: "datasource_plus", source: SourceName.SQL_SERVER, plus: true, config: { - server: host, + server: "127.0.0.1", port, user: "sa", password: "Password_123", diff --git a/packages/server/src/integrations/tests/utils/mysql.ts b/packages/server/src/integrations/tests/utils/mysql.ts index 0f83128f26..a78833e1de 100644 --- a/packages/server/src/integrations/tests/utils/mysql.ts +++ b/packages/server/src/integrations/tests/utils/mysql.ts @@ -2,7 +2,10 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy" import mysql from "mysql2/promise" -import { generator } from "@budibase/backend-core/tests" +import { generator, testContainerUtils } from "@budibase/backend-core/tests" +import { startContainer } from "." + +let ports: Promise class MySQLWaitStrategy extends AbstractWaitStrategy { async waitUntilReady(container: any, boundPorts: any, startTime?: Date) { @@ -25,26 +28,23 @@ class MySQLWaitStrategy extends AbstractWaitStrategy { } export async function getDatasource(): Promise { - let container = new GenericContainer("mysql:8.3") - .withExposedPorts(3306) - .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" }) - .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000)) - - if (process.env.REUSE_CONTAINERS) { - container = container.withReuse() + if (!ports) { + ports = startContainer( + new GenericContainer("mysql:8.3") + .withExposedPorts(3306) + .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" }) + .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000)) + ) } - const startedContainer = await container.start() - - const host = startedContainer.getHost() - const port = startedContainer.getMappedPort(3306) + const port = (await ports).find(x => x.container === 3306)?.host const datasource: Datasource = { type: "datasource_plus", source: SourceName.MYSQL, plus: true, config: { - host, + host: "127.0.0.1", port, user: "root", password: "password", diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts index 237bc19a17..4191b107e9 100644 --- a/packages/server/src/integrations/tests/utils/postgres.ts +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -1,33 +1,33 @@ import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import pg from "pg" -import { generator } from "@budibase/backend-core/tests" +import { generator, testContainerUtils } from "@budibase/backend-core/tests" +import { startContainer } from "." + +let ports: Promise export async function getDatasource(): Promise { - let container = new GenericContainer("postgres:16.1-bullseye") - .withExposedPorts(5432) - .withEnvironment({ POSTGRES_PASSWORD: "password" }) - .withWaitStrategy( - Wait.forSuccessfulCommand( - "pg_isready -h localhost -p 5432" - ).withStartupTimeout(10000) + if (!ports) { + ports = startContainer( + new GenericContainer("postgres:16.1-bullseye") + .withExposedPorts(5432) + .withEnvironment({ POSTGRES_PASSWORD: "password" }) + .withWaitStrategy( + Wait.forSuccessfulCommand( + "pg_isready -h localhost -p 5432" + ).withStartupTimeout(10000) + ) ) - - if (process.env.REUSE_CONTAINERS) { - container = container.withReuse() } - const startedContainer = await container.start() - - const host = startedContainer.getHost() - const port = startedContainer.getMappedPort(5432) + const port = (await ports).find(x => x.container === 5432)?.host const datasource: Datasource = { type: "datasource_plus", source: SourceName.POSTGRES, plus: true, config: { - host, + host: "127.0.0.1", port, database: "postgres", user: "postgres", From dbccfd115b9b92d40beb12450eb5ef2192df7319 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 11:16:20 +0100 Subject: [PATCH 18/30] Update CI workflow to use new larger runners. --- .github/workflows/budibase_ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index f7efb82711..919ad7dfb2 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -138,8 +138,7 @@ jobs: fi test-server: - runs-on: - group: hosted-runners + runs-on: budi-tubby-tornado-quad-core-150gb env: DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull steps: From 3dff4bf334ad0021ccbdafda0bc54a1ad3239d0e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 11:20:43 +0100 Subject: [PATCH 19/30] Fix lint. --- packages/backend-core/tests/core/utilities/testContainerUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index 2f33db65d3..dbb6fc9861 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -1,4 +1,3 @@ -import { DatabaseImpl } from "../../../src/db" import { execSync } from "child_process" const IPV4_PORT_REGEX = new RegExp(`0\\.0\\.0\\.0:(\\d+)->(\\d+)/tcp`, "g") From 8cffdeda568396f176068051b97135d38c27bd30 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 11:33:39 +0100 Subject: [PATCH 20/30] Re-use containers and create namespaces for each test. --- .github/workflows/budibase_ci.yml | 5 +++++ globalSetup.ts | 6 ++++++ .../tests/core/utilities/testContainerUtils.ts | 12 +++++++++++- .../server/src/integrations/tests/utils/index.ts | 6 ++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 919ad7dfb2..3d166dc262 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -91,6 +91,9 @@ jobs: test-libraries: runs-on: ubuntu-latest + env: + REUSE_CONTAINERS: true + CONTAINER_NAMESPACE: test-server steps: - name: Checkout repo uses: actions/checkout@v4 @@ -141,6 +144,8 @@ jobs: runs-on: budi-tubby-tornado-quad-core-150gb env: DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull + REUSE_CONTAINERS: true + CONTAINER_NAMESPACE: test-server steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/globalSetup.ts b/globalSetup.ts index 00d5e3f2dc..a1f66d4fc8 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -26,5 +26,11 @@ export default async function setup() { couchdb = couchdb.withReuse() } + if (process.env.CONTAINER_NAMESPACE) { + couchdb = couchdb.withLabels({ + "org.testcontainers.namespace": process.env.CONTAINER_NAMESPACE, + }) + } + await couchdb.start() } diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index dbb6fc9861..b0fbc2228a 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -23,12 +23,22 @@ function getTestcontainers(): ContainerInfo[] { // We use --format json to make sure the output is nice and machine-readable, // and we use --no-trunc so that the command returns full container IDs so we // can filter on them correctly. - return execSync("docker ps --format json --no-trunc") + let containers = execSync("docker ps --format json --no-trunc") .toString() .split("\n") .filter(x => x.length > 0) .map(x => JSON.parse(x) as ContainerInfo) .filter(x => x.Labels.includes("org.testcontainers=true")) + + if (process.env.CONTAINER_NAMESPACE) { + containers = containers.filter(x => + x.Labels.includes( + `org.testcontainers.namespace=${process.env.CONTAINER_NAMESPACE}` + ) + ) + } + + return containers } export function getContainerByImage(image: string) { diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index bbdb41b38a..3286f9a3b8 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -67,6 +67,12 @@ export async function rawQuery(ds: Datasource, sql: string): Promise { } export async function startContainer(container: GenericContainer) { + if (process.env.CONTAINER_NAMESPACE) { + container = container.withLabels({ + "org.testcontainers.namespace": process.env.CONTAINER_NAMESPACE, + }) + } + if (process.env.REUSE_CONTAINERS) { container = container.withReuse() } From 258226ddef46cecd6768f0d4f6cdc4f7ade09950 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 11:40:14 +0100 Subject: [PATCH 21/30] Better error message when multiple images are found. --- .../backend-core/tests/core/utilities/testContainerUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index b0fbc2228a..eeb9d7e6d3 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -44,7 +44,11 @@ function getTestcontainers(): ContainerInfo[] { export function getContainerByImage(image: string) { const containers = getTestcontainers().filter(x => x.Image.startsWith(image)) if (containers.length > 1) { - throw new Error(`Multiple containers found with image: ${image}`) + let errorMessage = `Multiple containers found starting with image: "${image}"\n\n` + for (const container of containers) { + errorMessage += JSON.stringify(container, null, 2) + } + throw new Error(errorMessage) } return containers[0] } From c31614a582e746d4c51c6cb916d938556c4986fe Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 11:45:41 +0100 Subject: [PATCH 22/30] Give test-libraries its own container namespace, give packages/server/scripts/test.sh some new params in the larger runners. --- .github/workflows/budibase_ci.yml | 2 +- packages/server/scripts/test.sh | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 3d166dc262..b72d6793af 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -93,7 +93,7 @@ jobs: runs-on: ubuntu-latest env: REUSE_CONTAINERS: true - CONTAINER_NAMESPACE: test-server + CONTAINER_NAMESPACE: test-libraries steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index 3ecf8bb794..3100d1911f 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -1,14 +1,13 @@ #!/bin/bash set -e +export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" + if [[ -n $CI ]] then - export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" - echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" - jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ + echo "jest --coverage --maxWorkers=4 --forceExit --bail $@" + jest --coverage --maxWorkers=2 --forceExit --bail $@ else - # --maxWorkers performs better in development - export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" echo "jest --coverage --maxWorkers=2 --forceExit $@" jest --coverage --maxWorkers=2 --forceExit $@ fi \ No newline at end of file From c764b0f22e7c7fe73f469f163565d577b99a3c4a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 11:55:38 +0100 Subject: [PATCH 23/30] Testcontainer debug logging in test-libraries. --- .github/workflows/budibase_ci.yml | 1 + packages/server/scripts/test.sh | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index b72d6793af..2876d60f51 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -92,6 +92,7 @@ jobs: test-libraries: runs-on: ubuntu-latest env: + DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull REUSE_CONTAINERS: true CONTAINER_NAMESPACE: test-libraries steps: diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index 3100d1911f..48766026aa 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -1,13 +1,14 @@ #!/bin/bash set -e -export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" - if [[ -n $CI ]] then - echo "jest --coverage --maxWorkers=4 --forceExit --bail $@" - jest --coverage --maxWorkers=2 --forceExit --bail $@ + export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" + echo "jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" + jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ else + # --maxWorkers performs better in development + export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" echo "jest --coverage --maxWorkers=2 --forceExit $@" jest --coverage --maxWorkers=2 --forceExit $@ fi \ No newline at end of file From dda0d1fb5be3f437443fafe45eeec4c3e78a5840 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:06:49 +0100 Subject: [PATCH 24/30] Lock globalSetup to prevent multiple containers starting. --- .github/workflows/budibase_ci.yml | 20 +++++---- globalSetup.ts | 69 +++++++++++++++++++------------ package.json | 5 ++- yarn.lock | 12 ++++++ 4 files changed, 71 insertions(+), 35 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 2876d60f51..6dccaa9f46 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -108,6 +108,10 @@ jobs: with: node-version: 20.x cache: yarn + - name: Pull testcontainers images + run: | + docker pull budibase/couchdb + - run: yarn --frozen-lockfile - name: Test run: | @@ -163,13 +167,15 @@ jobs: - name: Pull testcontainers images run: | - docker pull mcr.microsoft.com/mssql/server:2022-latest - docker pull mysql:8.3 - docker pull postgres:16.1-bullseye - docker pull mongo:7.0-jammy - docker pull mariadb:lts - docker pull testcontainers/ryuk:0.5.1 - docker pull budibase/couchdb + docker pull mcr.microsoft.com/mssql/server:2022-latest & + docker pull mysql:8.3 & + docker pull postgres:16.1-bullseye & + docker pull mongo:7.0-jammy & + docker pull mariadb:lts & + docker pull testcontainers/ryuk:0.5.1 & + docker pull budibase/couchdb & + + wait $(jobs -p) - run: yarn --frozen-lockfile diff --git a/globalSetup.ts b/globalSetup.ts index a1f66d4fc8..cc3ec6bb24 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -1,36 +1,51 @@ import { GenericContainer, Wait } from "testcontainers" +import lockfile from "proper-lockfile" export default async function setup() { - let couchdb = new GenericContainer("budibase/couchdb") - .withExposedPorts(5984) - .withEnvironment({ - COUCHDB_PASSWORD: "budibase", - COUCHDB_USER: "budibase", - }) - .withCopyContentToContainer([ - { - content: ` + if (process.env.REUSE_CONTAINERS) { + // If you run multiple tests at the same time, it's possible for the CouchDB + // shared container to get started multiple times despite having an + // identical reuse hash. To avoid that, we do a filesystem-based lock so + // that only one globalSetup.ts is running at a time. + lockfile.lockSync("globalSetup.lock") + } + + try { + let couchdb = new GenericContainer("budibase/couchdb") + .withExposedPorts(5984) + .withEnvironment({ + COUCHDB_PASSWORD: "budibase", + COUCHDB_USER: "budibase", + }) + .withCopyContentToContainer([ + { + content: ` [log] level = warn `, - target: "/opt/couchdb/etc/local.d/test-couchdb.ini", - }, - ]) - .withWaitStrategy( - Wait.forSuccessfulCommand( - "curl http://budibase:budibase@localhost:5984/_up" - ).withStartupTimeout(20000) - ) + target: "/opt/couchdb/etc/local.d/test-couchdb.ini", + }, + ]) + .withWaitStrategy( + Wait.forSuccessfulCommand( + "curl http://budibase:budibase@localhost:5984/_up" + ).withStartupTimeout(20000) + ) - if (process.env.REUSE_CONTAINERS) { - couchdb = couchdb.withReuse() + if (process.env.REUSE_CONTAINERS) { + couchdb = couchdb.withReuse() + } + + if (process.env.CONTAINER_NAMESPACE) { + couchdb = couchdb.withLabels({ + "org.testcontainers.namespace": process.env.CONTAINER_NAMESPACE, + }) + } + + await couchdb.start() + } finally { + if (process.env.REUSE_CONTAINERS) { + lockfile.unlockSync("globalSetup.lock") + } } - - if (process.env.CONTAINER_NAMESPACE) { - couchdb = couchdb.withLabels({ - "org.testcontainers.namespace": process.env.CONTAINER_NAMESPACE, - }) - } - - await couchdb.start() } diff --git a/package.json b/package.json index c927002c88..90675ce8c8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@babel/preset-env": "^7.22.5", "@esbuild-plugins/tsconfig-paths": "^0.1.2", "@types/node": "20.10.0", + "@types/proper-lockfile": "^4.1.4", "@typescript-eslint/parser": "6.9.0", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.8.0", @@ -116,5 +117,7 @@ "engines": { "node": ">=20.0.0 <21.0.0" }, - "dependencies": {} + "dependencies": { + "proper-lockfile": "^4.1.2" + } } diff --git a/yarn.lock b/yarn.lock index 6acdcce3b6..516ec66c30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5875,6 +5875,13 @@ "@types/pouchdb-node" "*" "@types/pouchdb-replication" "*" +"@types/proper-lockfile@^4.1.4": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz#cd9fab92bdb04730c1ada542c356f03620f84008" + integrity sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ== + dependencies: + "@types/retry" "*" + "@types/qs@*": version "6.9.7" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -5937,6 +5944,11 @@ dependencies: "@types/node" "*" +"@types/retry@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e" + integrity sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw== + "@types/rimraf@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8" From b25912bc5edfa7ef0a414656aa255ebdc25eab0b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:11:28 +0100 Subject: [PATCH 25/30] Correct the lock path. --- globalSetup.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/globalSetup.ts b/globalSetup.ts index cc3ec6bb24..0d0ec25bf8 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -1,13 +1,15 @@ import { GenericContainer, Wait } from "testcontainers" +import path from "path" import lockfile from "proper-lockfile" export default async function setup() { + const lockPath = path.resolve(__dirname, "globalSetup.ts") if (process.env.REUSE_CONTAINERS) { // If you run multiple tests at the same time, it's possible for the CouchDB // shared container to get started multiple times despite having an // identical reuse hash. To avoid that, we do a filesystem-based lock so // that only one globalSetup.ts is running at a time. - lockfile.lockSync("globalSetup.lock") + lockfile.lockSync(lockPath) } try { @@ -45,7 +47,7 @@ export default async function setup() { await couchdb.start() } finally { if (process.env.REUSE_CONTAINERS) { - lockfile.unlockSync("globalSetup.lock") + lockfile.unlockSync(lockPath) } } } From 04aa53c3068e806f29728c0e338c4eec34e3a77e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:22:11 +0100 Subject: [PATCH 26/30] Trying with 8 workers. --- .github/workflows/budibase_ci.yml | 7 ++++++- packages/server/scripts/test.sh | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 6dccaa9f46..38874b48fd 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -110,7 +110,11 @@ jobs: cache: yarn - name: Pull testcontainers images run: | - docker pull budibase/couchdb + docker pull testcontainers/ryuk:0.5.1 & + docker pull budibase/couchdb & + docker pull redis & + + wait $(jobs -p) - run: yarn --frozen-lockfile - name: Test @@ -174,6 +178,7 @@ jobs: docker pull mariadb:lts & docker pull testcontainers/ryuk:0.5.1 & docker pull budibase/couchdb & + docker pull redis & wait $(jobs -p) diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index 48766026aa..a147843bb3 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -4,8 +4,8 @@ set -e if [[ -n $CI ]] then export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" - echo "jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" - jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ + echo "jest --coverage --maxWorkers=8 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" + jest --coverage --maxWorkers=8 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ else # --maxWorkers performs better in development export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" From b98e80f6873fd3f0330c834d0aacc8188755a128 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:28:00 +0100 Subject: [PATCH 27/30] Back down to 4. --- packages/server/scripts/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index a147843bb3..48766026aa 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -4,8 +4,8 @@ set -e if [[ -n $CI ]] then export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" - echo "jest --coverage --maxWorkers=8 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" - jest --coverage --maxWorkers=8 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ + echo "jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" + jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ else # --maxWorkers performs better in development export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" From 79184e70aff2f42f42294ef8cccdbe3aa33eafb6 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:32:29 +0100 Subject: [PATCH 28/30] Remove CONTAINER_NAMESPACE, it wasn't the solution. --- .github/workflows/budibase_ci.yml | 2 -- globalSetup.ts | 6 ------ .../tests/core/utilities/testContainerUtils.ts | 12 +----------- .../server/src/integrations/tests/utils/index.ts | 6 ------ 4 files changed, 1 insertion(+), 25 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 38874b48fd..42d73ba8bb 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -94,7 +94,6 @@ jobs: env: DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull REUSE_CONTAINERS: true - CONTAINER_NAMESPACE: test-libraries steps: - name: Checkout repo uses: actions/checkout@v4 @@ -154,7 +153,6 @@ jobs: env: DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull REUSE_CONTAINERS: true - CONTAINER_NAMESPACE: test-server steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/globalSetup.ts b/globalSetup.ts index 0d0ec25bf8..7bf5e2152c 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -38,12 +38,6 @@ export default async function setup() { couchdb = couchdb.withReuse() } - if (process.env.CONTAINER_NAMESPACE) { - couchdb = couchdb.withLabels({ - "org.testcontainers.namespace": process.env.CONTAINER_NAMESPACE, - }) - } - await couchdb.start() } finally { if (process.env.REUSE_CONTAINERS) { diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index eeb9d7e6d3..951a6f0517 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -23,22 +23,12 @@ function getTestcontainers(): ContainerInfo[] { // We use --format json to make sure the output is nice and machine-readable, // and we use --no-trunc so that the command returns full container IDs so we // can filter on them correctly. - let containers = execSync("docker ps --format json --no-trunc") + return execSync("docker ps --format json --no-trunc") .toString() .split("\n") .filter(x => x.length > 0) .map(x => JSON.parse(x) as ContainerInfo) .filter(x => x.Labels.includes("org.testcontainers=true")) - - if (process.env.CONTAINER_NAMESPACE) { - containers = containers.filter(x => - x.Labels.includes( - `org.testcontainers.namespace=${process.env.CONTAINER_NAMESPACE}` - ) - ) - } - - return containers } export function getContainerByImage(image: string) { diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index 3286f9a3b8..bbdb41b38a 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -67,12 +67,6 @@ export async function rawQuery(ds: Datasource, sql: string): Promise { } export async function startContainer(container: GenericContainer) { - if (process.env.CONTAINER_NAMESPACE) { - container = container.withLabels({ - "org.testcontainers.namespace": process.env.CONTAINER_NAMESPACE, - }) - } - if (process.env.REUSE_CONTAINERS) { container = container.withReuse() } From 0881f3450840645a4bb3a81d0e4740694a93977a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:46:05 +0100 Subject: [PATCH 29/30] Move proper-lockfile to devDependencies. --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 90675ce8c8..4b6716f7e7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "nx-cloud": "16.0.5", "prettier": "2.8.8", "prettier-plugin-svelte": "^2.3.0", + "proper-lockfile": "^4.1.2", "svelte": "^4.2.10", "svelte-eslint-parser": "^0.33.1", "typescript": "5.2.2", @@ -117,7 +118,5 @@ "engines": { "node": ">=20.0.0 <21.0.0" }, - "dependencies": { - "proper-lockfile": "^4.1.2" - } + "dependencies": {} } From db877b7802ded722419b53ecb2d4bca006aadf5f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 3 Apr 2024 12:55:44 +0100 Subject: [PATCH 30/30] Create unique MongoDB databases. --- .../src/api/routes/tests/queries/mongodb.spec.ts | 16 +++++++++------- .../src/integrations/tests/utils/mongodb.ts | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index 148f2c15ec..bdcfd85437 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -4,7 +4,7 @@ import { DatabaseName, getDatasource, } from "../../../../integrations/tests/utils" -import { MongoClient, type Collection, BSON } from "mongodb" +import { MongoClient, type Collection, BSON, Db } from "mongodb" import { generator } from "@budibase/backend-core/tests" const expectValidId = expect.stringMatching(/^\w{24}$/) @@ -40,8 +40,7 @@ describe("/queries", () => { async function withClient( callback: (client: MongoClient) => Promise ): Promise { - const ds = await getDatasource(DatabaseName.MONGODB) - const client = new MongoClient(ds.config!.connectionString) + const client = new MongoClient(datasource.config!.connectionString) await client.connect() try { return await callback(client) @@ -50,13 +49,16 @@ describe("/queries", () => { } } + async function withDb(callback: (db: Db) => Promise): Promise { + return await withClient(async client => { + return await callback(client.db(datasource.config!.db)) + }) + } + async function withCollection( callback: (collection: Collection) => Promise ): Promise { - return await withClient(async client => { - const db = client.db( - (await getDatasource(DatabaseName.MONGODB)).config!.db - ) + return await withDb(async db => { return await callback(db.collection(collection)) }) } diff --git a/packages/server/src/integrations/tests/utils/mongodb.ts b/packages/server/src/integrations/tests/utils/mongodb.ts index c5c0340dc9..0bdbb2808c 100644 --- a/packages/server/src/integrations/tests/utils/mongodb.ts +++ b/packages/server/src/integrations/tests/utils/mongodb.ts @@ -1,4 +1,4 @@ -import { testContainerUtils } from "@budibase/backend-core/tests" +import { generator, testContainerUtils } from "@budibase/backend-core/tests" import { Datasource, SourceName } from "@budibase/types" import { GenericContainer, Wait } from "testcontainers" import { startContainer } from "." @@ -33,7 +33,7 @@ export async function getDatasource(): Promise { plus: false, config: { connectionString: `mongodb://mongo:password@127.0.0.1:${port.host}`, - db: "mongo", + db: generator.guid(), }, } }