budibase/packages/server/src/integrations/tests/utils/index.ts

113 lines
3.6 KiB
TypeScript

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"
import { GenericContainer, StartedTestContainer } from "testcontainers"
import { testContainerUtils } from "@budibase/backend-core/tests"
import lockfile from "proper-lockfile"
import path from "path"
import fs from "fs"
export type DatasourceProvider = () => Promise<Datasource>
export enum DatabaseName {
POSTGRES = "postgres",
MONGODB = "mongodb",
MYSQL = "mysql",
SQL_SERVER = "mssql",
MARIADB = "mariadb",
}
const providers: Record<DatabaseName, DatasourceProvider> = {
[DatabaseName.POSTGRES]: postgres.getDatasource,
[DatabaseName.MONGODB]: mongodb.getDatasource,
[DatabaseName.MYSQL]: mysql.getDatasource,
[DatabaseName.SQL_SERVER]: mssql.getDatasource,
[DatabaseName.MARIADB]: mariadb.getDatasource,
}
export function getDatasourceProviders(
...sourceNames: DatabaseName[]
): Promise<Datasource>[] {
return sourceNames.map(sourceName => providers[sourceName]())
}
export function getDatasourceProvider(
sourceName: DatabaseName
): DatasourceProvider {
return providers[sourceName]
}
export function getDatasource(sourceName: DatabaseName): Promise<Datasource> {
return providers[sourceName]()
}
export async function getDatasources(
...sourceNames: DatabaseName[]
): Promise<Datasource[]> {
return Promise.all(sourceNames.map(sourceName => providers[sourceName]()))
}
export async function rawQuery(ds: Datasource, sql: string): Promise<any> {
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}`)
}
}
}
export async function startContainer(container: GenericContainer) {
container = container.withReuse().withLabels({ "com.budibase": "true" })
// If two tests try to spin up the same container at the same time, there's a
// possibility that two containers of the same type will be started. To avoid
// this, we use a filesystem lock to ensure that only one container of a given
// type is started at a time.
const imageName = (container as any).imageName.string as string
const lockPath = path.resolve(
__dirname,
`${imageName.replaceAll("/", "-")}.lock`
)
// The `proper-lockfile` library needs the file we're locking on to exist
// before it can lock it.
if (!fs.existsSync(lockPath)) {
fs.writeFileSync(lockPath, "")
}
const unlock = lockfile.lockSync(lockPath)
let startedContainer: StartedTestContainer
try {
startedContainer = await container.start()
} finally {
unlock()
}
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)
}