Refactored TestDatabases to no longer depend on backend-common
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Refactored `TestDatabases` to no longer depend on `backend-common`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Deprecated `dropDatabase`
|
||||
@@ -312,7 +312,7 @@ export class DockerContainerRunner implements ContainerRunner {
|
||||
runContainer(options: RunContainerOptions): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export function dropDatabase(
|
||||
dbConfig: Config,
|
||||
...databaseNames: string[]
|
||||
|
||||
@@ -38,7 +38,7 @@ export const databaseServiceFactory: () => ServiceFactory<
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export function dropDatabase(
|
||||
dbConfig: Config,
|
||||
...databaseNames: string[]
|
||||
|
||||
@@ -234,9 +234,10 @@ export class DatabaseManager implements LegacyRootDatabaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for deleting databases, only exists for backend-test-utils for now.
|
||||
* Helper for deleting databases.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Will be removed in a future release.
|
||||
*/
|
||||
export async function dropDatabase(
|
||||
dbConfig: Config,
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-app-api": "workspace:^",
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
@@ -65,9 +64,11 @@
|
||||
"msw": "^1.0.0",
|
||||
"mysql2": "^3.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pg-connection-string": "^2.3.0",
|
||||
"testcontainers": "^10.0.0",
|
||||
"textextensions": "^5.16.0",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.0",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
@@ -14,28 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import knexFactory from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
import { startMysqlContainer } from './startMysqlContainer';
|
||||
import { startPostgresContainer } from './startPostgresContainer';
|
||||
import { TestDatabases } from './TestDatabases';
|
||||
|
||||
const itIfDocker = isDockerDisabledForTests() ? it.skip : it;
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('TestDatabases', () => {
|
||||
const OLD_ENV = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...OLD_ENV };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = OLD_ENV;
|
||||
});
|
||||
|
||||
describe('each create', () => {
|
||||
const dbs = TestDatabases.create();
|
||||
|
||||
@@ -55,254 +38,4 @@ describe('TestDatabases', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('each connect', () => {
|
||||
const dbs = TestDatabases.create();
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 16',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:16');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_16');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 15',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:15');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES15_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_15');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 14',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:14');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES14_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_14');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 13',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:13');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_13');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 12',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:12');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_12');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 11',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:11');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_11');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker(
|
||||
'obeys a provided connection string for postgres 9',
|
||||
async () => {
|
||||
const { host, port, user, password, stop } =
|
||||
await startPostgresContainer('postgres:9');
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
|
||||
const input = await dbs.init('POSTGRES_9');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker('obeys a provided connection string for mysql 8', async () => {
|
||||
const { host, port, user, password, stop } = await startMysqlContainer(
|
||||
'mysql:8',
|
||||
);
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING = `mysql://${user}:${password}@${host}:${port}/ignored`;
|
||||
const input = await dbs.init('MYSQL_8');
|
||||
await input.schema.createTable('a', table =>
|
||||
table.string('x').primary(),
|
||||
);
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = input.client.config.connection.database;
|
||||
const output = knexFactory({
|
||||
client: 'mysql2',
|
||||
connection: { host, port, user, password, database },
|
||||
});
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([
|
||||
{ x: 'y' },
|
||||
]);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,27 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DatabaseManager, dropDatabase } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { Knex } from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
import { startMysqlContainer } from './startMysqlContainer';
|
||||
import { startPostgresContainer } from './startPostgresContainer';
|
||||
import { MysqlEngine } from './mysql';
|
||||
import { PostgresEngine } from './postgres';
|
||||
import { SqliteEngine } from './sqlite';
|
||||
import {
|
||||
allDatabases,
|
||||
Instance,
|
||||
Engine,
|
||||
TestDatabaseId,
|
||||
TestDatabaseProperties,
|
||||
allDatabases,
|
||||
} from './types';
|
||||
|
||||
const LARGER_POOL_CONFIG = {
|
||||
pool: {
|
||||
min: 0,
|
||||
max: 50,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Encapsulates the creation of ephemeral test database instances for use
|
||||
* inside unit or integration tests.
|
||||
@@ -42,7 +33,17 @@ const LARGER_POOL_CONFIG = {
|
||||
* @public
|
||||
*/
|
||||
export class TestDatabases {
|
||||
private readonly instanceById: Map<string, Instance>;
|
||||
private readonly engineFactoryByDriver: Record<
|
||||
string,
|
||||
(properties: TestDatabaseProperties) => Promise<Engine>
|
||||
> = {
|
||||
pg: PostgresEngine.create,
|
||||
mysql: MysqlEngine.create,
|
||||
mysql2: MysqlEngine.create,
|
||||
'better-sqlite3': SqliteEngine.create,
|
||||
sqlite3: SqliteEngine.create,
|
||||
};
|
||||
private readonly engineByTestDatabaseId: Map<string, Engine>;
|
||||
private readonly supportedIds: TestDatabaseId[];
|
||||
private static defaultIds?: TestDatabaseId[];
|
||||
|
||||
@@ -114,7 +115,7 @@ export class TestDatabases {
|
||||
}
|
||||
|
||||
private constructor(supportedIds: TestDatabaseId[]) {
|
||||
this.instanceById = new Map();
|
||||
this.engineByTestDatabaseId = new Map();
|
||||
this.supportedIds = supportedIds;
|
||||
}
|
||||
|
||||
@@ -148,185 +149,29 @@ export class TestDatabases {
|
||||
);
|
||||
}
|
||||
|
||||
let instance: Instance | undefined = this.instanceById.get(id);
|
||||
|
||||
// Ensure that a testcontainers instance is up for this ID
|
||||
if (!instance) {
|
||||
instance = await this.initAny(properties);
|
||||
this.instanceById.set(id, instance);
|
||||
}
|
||||
|
||||
// Ensure that a unique logical database is created in the instance
|
||||
const databaseName = `db${randomBytes(16).toString('hex')}`;
|
||||
const connection = await instance.databaseManager
|
||||
.forPlugin(databaseName)
|
||||
.getClient();
|
||||
|
||||
instance.connections.push(connection);
|
||||
instance.databaseNames.push(databaseName);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async initAny(properties: TestDatabaseProperties): Promise<Instance> {
|
||||
// Use the connection string if provided
|
||||
if (properties.driver === 'pg' || properties.driver === 'mysql2') {
|
||||
const envVarName = properties.connectionStringEnvironmentVariableName;
|
||||
if (envVarName) {
|
||||
const connectionString = process.env[envVarName];
|
||||
if (connectionString) {
|
||||
const config = new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
knexConfig: properties.driver.includes('sqlite')
|
||||
? {}
|
||||
: LARGER_POOL_CONFIG,
|
||||
client: properties.driver,
|
||||
connection: connectionString,
|
||||
},
|
||||
},
|
||||
});
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
const databaseNames: Array<string> = [];
|
||||
return {
|
||||
dropDatabases: async () => {
|
||||
await dropDatabase(
|
||||
config.getConfig('backend.database'),
|
||||
...databaseNames.map(
|
||||
databaseName => `backstage_plugin_${databaseName}`,
|
||||
),
|
||||
);
|
||||
},
|
||||
databaseManager,
|
||||
databaseNames,
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise start a container for the purpose
|
||||
switch (properties.driver) {
|
||||
case 'pg':
|
||||
return this.initPostgres(properties);
|
||||
case 'mysql2':
|
||||
return this.initMysql(properties);
|
||||
case 'better-sqlite3':
|
||||
case 'sqlite3':
|
||||
return this.initSqlite(properties);
|
||||
default:
|
||||
let engine = this.engineByTestDatabaseId.get(id);
|
||||
if (!engine) {
|
||||
const factory = this.engineFactoryByDriver[properties.driver];
|
||||
if (!factory) {
|
||||
throw new Error(`Unknown database driver ${properties.driver}`);
|
||||
}
|
||||
engine = await factory(properties);
|
||||
this.engineByTestDatabaseId.set(id, engine);
|
||||
}
|
||||
}
|
||||
|
||||
private async initPostgres(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const { host, port, user, password, stop } = await startPostgresContainer(
|
||||
properties.dockerImageName!,
|
||||
);
|
||||
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
knexConfig: LARGER_POOL_CONFIG,
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
return {
|
||||
stopContainer: stop,
|
||||
databaseManager,
|
||||
databaseNames: [],
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async initMysql(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const { host, port, user, password, stop } = await startMysqlContainer(
|
||||
properties.dockerImageName!,
|
||||
);
|
||||
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
knexConfig: LARGER_POOL_CONFIG,
|
||||
client: 'mysql2',
|
||||
connection: { host, port, user, password },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
stopContainer: stop,
|
||||
databaseManager,
|
||||
databaseNames: [],
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async initSqlite(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: properties.driver,
|
||||
connection: ':memory:',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
return {
|
||||
databaseManager,
|
||||
databaseNames: [],
|
||||
connections: [],
|
||||
};
|
||||
return await engine.createDatabaseInstance();
|
||||
}
|
||||
|
||||
private async shutdown() {
|
||||
const instances = [...this.instanceById.values()];
|
||||
this.instanceById.clear();
|
||||
const engines = [...this.engineByTestDatabaseId.values()];
|
||||
this.engineByTestDatabaseId.clear();
|
||||
|
||||
for (const {
|
||||
stopContainer,
|
||||
dropDatabases,
|
||||
connections,
|
||||
databaseManager,
|
||||
} of instances) {
|
||||
for (const connection of connections) {
|
||||
try {
|
||||
await connection.destroy();
|
||||
} catch (error) {
|
||||
console.warn(`TestDatabases: Failed to destroy connection`, {
|
||||
connection,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If the database is not running in docker then drop the databases
|
||||
for (const engine of engines) {
|
||||
try {
|
||||
await dropDatabases?.();
|
||||
await engine.shutdown();
|
||||
} catch (error) {
|
||||
console.warn(`TestDatabases: Failed to drop databases`, {
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await stopContainer?.();
|
||||
} catch (error) {
|
||||
console.warn(`TestDatabases: Failed to stop container`, {
|
||||
databaseManager,
|
||||
console.warn(`TestDatabases: Failed to shutdown engine`, {
|
||||
engine,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
import { MysqlEngine, startMysqlContainer } from './mysql';
|
||||
import { Engine, TestDatabaseId, allDatabases } from './types';
|
||||
|
||||
const itIfDocker = isDockerDisabledForTests() ? it.skip : it;
|
||||
const ourDatabaseIds = Object.entries(allDatabases)
|
||||
.filter(([, properties]) => properties.driver.includes('mysql'))
|
||||
.map(([id]) => id as TestDatabaseId);
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('startMysqlContainer', () => {
|
||||
itIfDocker(
|
||||
'successfully launches the container and can stop it without problems',
|
||||
async () => {
|
||||
const { connection, stopContainer } = await startMysqlContainer(
|
||||
'mysql:8',
|
||||
);
|
||||
const db = knexFactory({ client: 'mysql2', connection });
|
||||
try {
|
||||
const result = await db.select(db.raw('version() AS version'));
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(result[0]?.version).toContain('8.');
|
||||
} finally {
|
||||
await db.destroy();
|
||||
await stopContainer();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('MysqlEngine', () => {
|
||||
const OLD_ENV = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...OLD_ENV };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = OLD_ENV;
|
||||
});
|
||||
|
||||
itIfDocker.each(ourDatabaseIds)(
|
||||
'uses given connection string, %p',
|
||||
async testDatabaseId => {
|
||||
const properties = allDatabases[testDatabaseId];
|
||||
const { connection } = await startMysqlContainer(
|
||||
properties.dockerImageName!,
|
||||
);
|
||||
|
||||
const outerKnex = knexFactory({ client: properties.driver, connection });
|
||||
const databases = await outerKnex
|
||||
.raw('SHOW DATABASES')
|
||||
.then(rows => rows[0].length); // account for meta databases, if any
|
||||
|
||||
let knex: Knex | undefined;
|
||||
let engine: Engine | undefined;
|
||||
try {
|
||||
process.env[
|
||||
properties.connectionStringEnvironmentVariableName!
|
||||
] = `mysql://${connection.user}:${connection.password}@${connection.host}:${connection.port}/ignored`;
|
||||
engine = await MysqlEngine.create(properties);
|
||||
knex = await engine.createDatabaseInstance();
|
||||
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(
|
||||
outerKnex.raw('SHOW DATABASES').then(rows => rows[0].length),
|
||||
).resolves.toBe(databases + 1);
|
||||
} finally {
|
||||
await outerKnex.destroy();
|
||||
await knex?.destroy();
|
||||
await engine?.shutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker.each(ourDatabaseIds)(
|
||||
'creates docker containers, %p',
|
||||
async testDatabaseId => {
|
||||
const properties = allDatabases[testDatabaseId];
|
||||
delete process.env[properties.connectionStringEnvironmentVariableName!];
|
||||
const engine = await MysqlEngine.create(properties);
|
||||
|
||||
try {
|
||||
const knex = await engine.createDatabaseInstance();
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(
|
||||
knex.select(knex.raw('version() as version')),
|
||||
).resolves.toEqual([{ version: expect.any(String) }]);
|
||||
} finally {
|
||||
await engine.shutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
import { randomBytes } from 'crypto';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import yn from 'yn';
|
||||
import { Engine, LARGER_POOL_CONFIG, TestDatabaseProperties } from './types';
|
||||
|
||||
async function waitForMysqlReady(
|
||||
connection: Knex.MySqlConnectionConfig,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let attempts = 0;
|
||||
for (;;) {
|
||||
attempts += 1;
|
||||
|
||||
let knex: Knex | undefined;
|
||||
try {
|
||||
knex = knexFactory({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
// make a copy because the driver mutates this
|
||||
...connection,
|
||||
},
|
||||
});
|
||||
const result = await knex.select(knex.raw('version() AS version'));
|
||||
if (Array.isArray(result) && result[0]?.version) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
} finally {
|
||||
await knex?.destroy();
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > 30_000) {
|
||||
throw new Error(
|
||||
`Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${
|
||||
lastError
|
||||
? `last error was ${stringifyError(lastError)}`
|
||||
: '(no errors thrown)'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
export async function startMysqlContainer(image: string): Promise<{
|
||||
connection: Knex.MySqlConnectionConfig;
|
||||
stopContainer: () => Promise<void>;
|
||||
}> {
|
||||
const user = 'root';
|
||||
const password = uuid();
|
||||
|
||||
// Lazy-load to avoid side-effect of importing testcontainers
|
||||
const { GenericContainer } = await import('testcontainers');
|
||||
|
||||
const container = await new GenericContainer(image)
|
||||
.withExposedPorts(3306)
|
||||
.withEnvironment({ MYSQL_ROOT_PASSWORD: password })
|
||||
.withTmpFs({ '/var/lib/mysql': 'rw' })
|
||||
.start();
|
||||
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(3306);
|
||||
const connection = { host, port, user, password };
|
||||
const stopContainer = async () => {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
};
|
||||
|
||||
await waitForMysqlReady(connection);
|
||||
|
||||
return { connection, stopContainer };
|
||||
}
|
||||
|
||||
export function parseMysqlConnectionString(
|
||||
connectionString: string,
|
||||
): Knex.MySqlConnectionConfig {
|
||||
try {
|
||||
const {
|
||||
protocol,
|
||||
username,
|
||||
password,
|
||||
port,
|
||||
hostname,
|
||||
pathname,
|
||||
searchParams,
|
||||
} = new URL(connectionString);
|
||||
|
||||
if (protocol !== 'mysql:') {
|
||||
throw new Error(`Unknown protocol ${protocol}`);
|
||||
} else if (!username || !password) {
|
||||
throw new Error(`Missing username/password`);
|
||||
} else if (!pathname.match(/^\/[^/]+$/)) {
|
||||
throw new Error(`Expected single path segment`);
|
||||
}
|
||||
|
||||
const result: Knex.MySqlConnectionConfig = {
|
||||
user: username,
|
||||
password,
|
||||
host: hostname,
|
||||
port: Number(port || 3306),
|
||||
database: decodeURIComponent(pathname.substring(1)),
|
||||
};
|
||||
|
||||
const ssl = searchParams.get('ssl');
|
||||
if (ssl) {
|
||||
result.ssl = ssl;
|
||||
}
|
||||
|
||||
const debug = searchParams.get('debug');
|
||||
if (debug) {
|
||||
result.debug = yn(debug);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new Error(`Error while parsing MySQL connection string, ${e}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
export class MysqlEngine implements Engine {
|
||||
static async create(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<MysqlEngine> {
|
||||
const { connectionStringEnvironmentVariableName, dockerImageName } =
|
||||
properties;
|
||||
|
||||
if (connectionStringEnvironmentVariableName) {
|
||||
const connectionString =
|
||||
process.env[connectionStringEnvironmentVariableName];
|
||||
if (connectionString) {
|
||||
const connection = parseMysqlConnectionString(connectionString);
|
||||
return new MysqlEngine(
|
||||
properties,
|
||||
connection as Knex.MySqlConnectionConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (dockerImageName) {
|
||||
const { connection, stopContainer } = await startMysqlContainer(
|
||||
dockerImageName,
|
||||
);
|
||||
return new MysqlEngine(properties, connection, stopContainer);
|
||||
}
|
||||
|
||||
throw new Error(`Test databasee for ${properties.name} not configured`);
|
||||
}
|
||||
|
||||
readonly #properties: TestDatabaseProperties;
|
||||
readonly #connection: Knex.MySqlConnectionConfig;
|
||||
readonly #knexInstances: Knex[];
|
||||
readonly #databaseNames: string[];
|
||||
readonly #stopContainer?: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
properties: TestDatabaseProperties,
|
||||
connection: Knex.MySqlConnectionConfig,
|
||||
stopContainer?: () => Promise<void>,
|
||||
) {
|
||||
this.#properties = properties;
|
||||
this.#connection = connection;
|
||||
this.#knexInstances = [];
|
||||
this.#databaseNames = [];
|
||||
this.#stopContainer = stopContainer;
|
||||
}
|
||||
|
||||
async createDatabaseInstance(): Promise<Knex> {
|
||||
const adminConnection = this.#connectAdmin();
|
||||
try {
|
||||
const databaseName = `db${randomBytes(16).toString('hex')}`;
|
||||
|
||||
await adminConnection.raw('CREATE DATABASE ??', [databaseName]);
|
||||
this.#databaseNames.push(databaseName);
|
||||
|
||||
const knexInstance = knexFactory({
|
||||
client: this.#properties.driver,
|
||||
connection: {
|
||||
...this.#connection,
|
||||
database: databaseName,
|
||||
},
|
||||
...LARGER_POOL_CONFIG,
|
||||
});
|
||||
this.#knexInstances.push(knexInstance);
|
||||
|
||||
return knexInstance;
|
||||
} finally {
|
||||
await adminConnection.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
for (const instance of this.#knexInstances) {
|
||||
await instance.destroy();
|
||||
}
|
||||
|
||||
const adminConnection = this.#connectAdmin();
|
||||
try {
|
||||
for (const databaseName of this.#databaseNames) {
|
||||
await adminConnection.raw('DROP DATABASE ??', [databaseName]);
|
||||
}
|
||||
} finally {
|
||||
await adminConnection.destroy();
|
||||
}
|
||||
|
||||
await this.#stopContainer?.();
|
||||
}
|
||||
|
||||
#connectAdmin(): Knex {
|
||||
const connection = {
|
||||
...this.#connection,
|
||||
database: null as unknown as string,
|
||||
};
|
||||
return knexFactory({
|
||||
client: this.#properties.driver,
|
||||
connection,
|
||||
pool: {
|
||||
acquireTimeoutMillis: 10000,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
import { startPostgresContainer, PostgresEngine } from './postgres';
|
||||
import { Engine, TestDatabaseId, allDatabases } from './types';
|
||||
|
||||
const itIfDocker = isDockerDisabledForTests() ? it.skip : it;
|
||||
const ourDatabaseIds = Object.entries(allDatabases)
|
||||
.filter(([, properties]) => properties.driver === 'pg')
|
||||
.map(([id]) => id as TestDatabaseId);
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('startPostgresContainer', () => {
|
||||
itIfDocker(
|
||||
'successfully launches the container and can stop it without problems',
|
||||
async () => {
|
||||
const { connection, stopContainer } = await startPostgresContainer(
|
||||
'postgres:13',
|
||||
);
|
||||
const db = knexFactory({ client: 'pg', connection });
|
||||
try {
|
||||
const result = await db.select(db.raw('version()'));
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(result[0]?.version).toContain('PostgreSQL');
|
||||
} finally {
|
||||
await db.destroy();
|
||||
await stopContainer();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('PostgresEngine', () => {
|
||||
const OLD_ENV = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...OLD_ENV };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = OLD_ENV;
|
||||
});
|
||||
|
||||
itIfDocker.each(ourDatabaseIds)(
|
||||
'uses given connection string, %p',
|
||||
async testDatabaseId => {
|
||||
const properties = allDatabases[testDatabaseId];
|
||||
const { connection } = await startPostgresContainer(
|
||||
properties.dockerImageName!,
|
||||
);
|
||||
|
||||
const outerKnex = knexFactory({ client: properties.driver, connection });
|
||||
const databases = await outerKnex
|
||||
.from('pg_database')
|
||||
.then(rows => rows.length); // account for postgres, template0 etc
|
||||
|
||||
let knex: Knex | undefined;
|
||||
let engine: Engine | undefined;
|
||||
try {
|
||||
process.env[
|
||||
properties.connectionStringEnvironmentVariableName!
|
||||
] = `postgres://${connection.user}:${connection.password}@${connection.host}:${connection.port}`;
|
||||
engine = await PostgresEngine.create(properties);
|
||||
knex = await engine.createDatabaseInstance();
|
||||
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(outerKnex.from('pg_database')).resolves.toHaveLength(
|
||||
databases + 1,
|
||||
);
|
||||
} finally {
|
||||
await outerKnex.destroy();
|
||||
await knex?.destroy();
|
||||
await engine?.shutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
itIfDocker.each(ourDatabaseIds)(
|
||||
'creates docker containers, %p',
|
||||
async testDatabaseId => {
|
||||
const properties = allDatabases[testDatabaseId];
|
||||
delete process.env[properties.connectionStringEnvironmentVariableName!];
|
||||
const engine = await PostgresEngine.create(properties);
|
||||
|
||||
try {
|
||||
const knex = await engine.createDatabaseInstance();
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
await expect(knex.select(knex.raw('version()'))).resolves.toEqual([
|
||||
{ version: expect.stringContaining('PostgreSQL') },
|
||||
]);
|
||||
} finally {
|
||||
await engine.shutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
import { randomBytes } from 'crypto';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { parse as parsePgConnectionString } from 'pg-connection-string';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Engine, LARGER_POOL_CONFIG, TestDatabaseProperties } from './types';
|
||||
|
||||
async function waitForPostgresReady(
|
||||
connection: Knex.PgConnectionConfig,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let attempts = 0;
|
||||
for (;;) {
|
||||
attempts += 1;
|
||||
|
||||
let knex: Knex | undefined;
|
||||
try {
|
||||
knex = knexFactory({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
// make a copy because the driver mutates this
|
||||
...connection,
|
||||
},
|
||||
});
|
||||
const result = await knex.select(knex.raw('version()'));
|
||||
if (Array.isArray(result) && result[0]?.version) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
} finally {
|
||||
await knex?.destroy();
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > 30_000) {
|
||||
throw new Error(
|
||||
`Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${
|
||||
lastError
|
||||
? `last error was ${stringifyError(lastError)}`
|
||||
: '(no errors thrown)'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
export async function startPostgresContainer(image: string): Promise<{
|
||||
connection: Knex.PgConnectionConfig;
|
||||
stopContainer: () => Promise<void>;
|
||||
}> {
|
||||
const user = 'postgres';
|
||||
const password = uuid();
|
||||
|
||||
// Lazy-load to avoid side-effect of importing testcontainers
|
||||
const { GenericContainer } = await import('testcontainers');
|
||||
|
||||
const container = await new GenericContainer(image)
|
||||
.withExposedPorts(5432)
|
||||
.withEnvironment({ POSTGRES_PASSWORD: password })
|
||||
.withTmpFs({ '/var/lib/postgresql/data': 'rw' })
|
||||
.start();
|
||||
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(5432);
|
||||
const connection = { host, port, user, password };
|
||||
const stopContainer = async () => {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
};
|
||||
|
||||
await waitForPostgresReady(connection);
|
||||
|
||||
return { connection, stopContainer };
|
||||
}
|
||||
|
||||
export class PostgresEngine implements Engine {
|
||||
static async create(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<PostgresEngine> {
|
||||
const { connectionStringEnvironmentVariableName, dockerImageName } =
|
||||
properties;
|
||||
|
||||
if (connectionStringEnvironmentVariableName) {
|
||||
const connectionString =
|
||||
process.env[connectionStringEnvironmentVariableName];
|
||||
if (connectionString) {
|
||||
const connection = parsePgConnectionString(connectionString);
|
||||
return new PostgresEngine(
|
||||
properties,
|
||||
connection as Knex.PgConnectionConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (dockerImageName) {
|
||||
const { connection, stopContainer } = await startPostgresContainer(
|
||||
dockerImageName,
|
||||
);
|
||||
return new PostgresEngine(properties, connection, stopContainer);
|
||||
}
|
||||
|
||||
throw new Error(`Test databasee for ${properties.name} not configured`);
|
||||
}
|
||||
|
||||
readonly #properties: TestDatabaseProperties;
|
||||
readonly #connection: Knex.PgConnectionConfig;
|
||||
readonly #knexInstances: Knex[];
|
||||
readonly #databaseNames: string[];
|
||||
readonly #stopContainer?: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
properties: TestDatabaseProperties,
|
||||
connection: Knex.PgConnectionConfig,
|
||||
stopContainer?: () => Promise<void>,
|
||||
) {
|
||||
this.#properties = properties;
|
||||
this.#connection = connection;
|
||||
this.#knexInstances = [];
|
||||
this.#databaseNames = [];
|
||||
this.#stopContainer = stopContainer;
|
||||
}
|
||||
|
||||
async createDatabaseInstance(): Promise<Knex> {
|
||||
const adminConnection = this.#connectAdmin();
|
||||
try {
|
||||
const databaseName = `db${randomBytes(16).toString('hex')}`;
|
||||
|
||||
await adminConnection.raw('CREATE DATABASE ??', [databaseName]);
|
||||
this.#databaseNames.push(databaseName);
|
||||
|
||||
const knexInstance = knexFactory({
|
||||
client: this.#properties.driver,
|
||||
connection: {
|
||||
...this.#connection,
|
||||
database: databaseName,
|
||||
},
|
||||
...LARGER_POOL_CONFIG,
|
||||
});
|
||||
this.#knexInstances.push(knexInstance);
|
||||
|
||||
return knexInstance;
|
||||
} finally {
|
||||
await adminConnection.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
for (const instance of this.#knexInstances) {
|
||||
await instance.destroy();
|
||||
}
|
||||
|
||||
const adminConnection = this.#connectAdmin();
|
||||
try {
|
||||
for (const databaseName of this.#databaseNames) {
|
||||
await adminConnection.raw('DROP DATABASE ??', [databaseName]);
|
||||
}
|
||||
} finally {
|
||||
await adminConnection.destroy();
|
||||
}
|
||||
|
||||
await this.#stopContainer?.();
|
||||
}
|
||||
|
||||
#connectAdmin(): Knex {
|
||||
return knexFactory({
|
||||
client: this.#properties.driver,
|
||||
connection: {
|
||||
...this.#connection,
|
||||
database: 'postgres',
|
||||
},
|
||||
pool: {
|
||||
acquireTimeoutMillis: 10000,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SqliteEngine } from './sqlite';
|
||||
import { TestDatabaseId, allDatabases } from './types';
|
||||
|
||||
const ourDatabaseIds = Object.entries(allDatabases)
|
||||
.filter(([, properties]) => properties.driver.includes('sqlite'))
|
||||
.map(([id]) => id as TestDatabaseId);
|
||||
|
||||
describe('SqliteEngine', () => {
|
||||
it.each(ourDatabaseIds)(
|
||||
'should create a database instance, %p',
|
||||
async testDatabaseId => {
|
||||
for (let i = 0; i < 100; ++i) {
|
||||
const properties = allDatabases[testDatabaseId];
|
||||
const engine = await SqliteEngine.create(properties);
|
||||
|
||||
const instance1 = await engine.createDatabaseInstance();
|
||||
const instance2 = await engine.createDatabaseInstance();
|
||||
expect(instance1).toBeDefined();
|
||||
expect(instance2).toBeDefined();
|
||||
expect(instance1).not.toEqual(instance2);
|
||||
|
||||
await instance1.schema.createTable('t', table => {
|
||||
table.string('value');
|
||||
});
|
||||
await instance2.schema.createTable('t', table => {
|
||||
table.string('value');
|
||||
});
|
||||
|
||||
await instance1('t').insert({ value: 'value1' });
|
||||
await instance2('t').insert({ value: 'value2' });
|
||||
|
||||
await expect(instance1('t')).resolves.toEqual([{ value: 'value1' }]);
|
||||
await expect(instance2('t')).resolves.toEqual([{ value: 'value2' }]);
|
||||
|
||||
await engine.shutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { Engine, TestDatabaseProperties } from './types';
|
||||
|
||||
export class SqliteEngine implements Engine {
|
||||
static async create(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<SqliteEngine> {
|
||||
return new SqliteEngine(properties);
|
||||
}
|
||||
|
||||
readonly #properties: TestDatabaseProperties;
|
||||
readonly #instances: Knex[];
|
||||
|
||||
constructor(properties: TestDatabaseProperties) {
|
||||
this.#properties = properties;
|
||||
this.#instances = [];
|
||||
}
|
||||
|
||||
async createDatabaseInstance(): Promise<Knex> {
|
||||
const instance = knexFactory({
|
||||
client: this.#properties.driver,
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
|
||||
instance.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
|
||||
this.#instances.push(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
for (const instance of this.#instances) {
|
||||
await instance.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import createConnection from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
import { startMysqlContainer } from './startMysqlContainer';
|
||||
|
||||
const itIfDocker = isDockerDisabledForTests() ? it.skip : it;
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('startMysqlContainer', () => {
|
||||
itIfDocker('successfully launches the container', async () => {
|
||||
const { stop, ...connection } = await startMysqlContainer('mysql:8');
|
||||
const db = createConnection({ client: 'mysql2', connection });
|
||||
try {
|
||||
const result = await db.select(db.raw('version() AS version'));
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(result[0]?.version).toContain('8.');
|
||||
} finally {
|
||||
await db.destroy();
|
||||
await stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import createConnection, { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
async function waitForMysqlReady(
|
||||
connection: Knex.MySqlConnectionConfig,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const db = createConnection({ client: 'mysql2', connection });
|
||||
|
||||
try {
|
||||
for (;;) {
|
||||
try {
|
||||
const result = await db.select(db.raw('version() AS version'));
|
||||
if (result[0]?.version) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if (Date.now() - startTime > 30_000) {
|
||||
throw new Error(
|
||||
`Timed out waiting for the database to be ready for connections, ${e}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
} finally {
|
||||
db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function startMysqlContainer(image: string) {
|
||||
const user = 'root';
|
||||
const password = uuid();
|
||||
|
||||
// Lazy-load to avoid side-effect of importing testcontainers
|
||||
const { GenericContainer } = await import('testcontainers');
|
||||
|
||||
const container = await new GenericContainer(image)
|
||||
.withExposedPorts(3306)
|
||||
.withEnvironment({ MYSQL_ROOT_PASSWORD: password })
|
||||
.withTmpFs({ '/var/lib/mysql': 'rw' })
|
||||
.start();
|
||||
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(3306);
|
||||
const stop = async () => {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
};
|
||||
|
||||
await waitForMysqlReady({ host, port, user, password });
|
||||
|
||||
return { host, port, user, password, stop };
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import createConnection from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
import { startPostgresContainer } from './startPostgresContainer';
|
||||
|
||||
const itIfDocker = isDockerDisabledForTests() ? it.skip : it;
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('startPostgresContainer', () => {
|
||||
itIfDocker('successfully launches the container', async () => {
|
||||
const { stop, ...connection } = await startPostgresContainer('postgres:13');
|
||||
const db = createConnection({ client: 'pg', connection });
|
||||
try {
|
||||
const result = await db.select(db.raw('version()'));
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(result[0]?.version).toContain('PostgreSQL');
|
||||
} finally {
|
||||
await db.destroy();
|
||||
await stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import createConnection, { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
async function waitForPostgresReady(
|
||||
connection: Knex.PgConnectionConfig,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const db = createConnection({ client: 'pg', connection });
|
||||
|
||||
try {
|
||||
for (;;) {
|
||||
try {
|
||||
const result = await db.select(db.raw('version()'));
|
||||
if (Array.isArray(result) && result[0]?.version) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if (Date.now() - startTime > 30_000) {
|
||||
throw new Error(
|
||||
`Timed out waiting for the database to be ready for connections, ${e}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
} finally {
|
||||
db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function startPostgresContainer(image: string) {
|
||||
const user = 'postgres';
|
||||
const password = uuid();
|
||||
|
||||
// Lazy-load to avoid side-effect of importing testcontainers
|
||||
const { GenericContainer } = await import('testcontainers');
|
||||
|
||||
const container = await new GenericContainer(image)
|
||||
.withExposedPorts(5432)
|
||||
.withEnvironment({ POSTGRES_PASSWORD: password })
|
||||
.withTmpFs({ '/var/lib/postgresql/data': 'rw' })
|
||||
.start();
|
||||
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(5432);
|
||||
const stop = async () => {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
};
|
||||
|
||||
await waitForPostgresReady({ host, port, user, password });
|
||||
|
||||
return { host, port, user, password, stop };
|
||||
}
|
||||
@@ -14,10 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { Knex } from 'knex';
|
||||
import { getDockerImageForName } from '../util/getDockerImageForName';
|
||||
|
||||
export interface Engine {
|
||||
createDatabaseInstance(): Promise<Knex>;
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible databases to test against.
|
||||
*
|
||||
@@ -41,14 +45,6 @@ export type TestDatabaseProperties = {
|
||||
connectionStringEnvironmentVariableName?: string;
|
||||
};
|
||||
|
||||
export type Instance = {
|
||||
stopContainer?: () => Promise<void>;
|
||||
dropDatabases?: () => Promise<void>;
|
||||
databaseManager: DatabaseManager;
|
||||
connections: Array<Knex>;
|
||||
databaseNames: Array<string>;
|
||||
};
|
||||
|
||||
export const allDatabases: Record<TestDatabaseId, TestDatabaseProperties> =
|
||||
Object.freeze({
|
||||
POSTGRES_16: {
|
||||
@@ -112,3 +108,10 @@ export const allDatabases: Record<TestDatabaseId, TestDatabaseProperties> =
|
||||
driver: 'better-sqlite3',
|
||||
},
|
||||
});
|
||||
|
||||
export const LARGER_POOL_CONFIG = {
|
||||
pool: {
|
||||
min: 0,
|
||||
max: 50,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3574,7 +3574,6 @@ __metadata:
|
||||
resolution: "@backstage/backend-test-utils@workspace:packages/backend-test-utils"
|
||||
dependencies:
|
||||
"@backstage/backend-app-api": "workspace:^"
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
@@ -3595,10 +3594,12 @@ __metadata:
|
||||
msw: ^1.0.0
|
||||
mysql2: ^3.0.0
|
||||
pg: ^8.11.3
|
||||
pg-connection-string: ^2.3.0
|
||||
supertest: ^6.1.3
|
||||
testcontainers: ^10.0.0
|
||||
textextensions: ^5.16.0
|
||||
uuid: ^9.0.0
|
||||
yn: ^4.0.0
|
||||
peerDependencies:
|
||||
"@types/jest": "*"
|
||||
languageName: unknown
|
||||
|
||||
Reference in New Issue
Block a user