backend-test-utils: drop database in shutdown

Signed-off-by: zcmander <zcmander@gmail.com>
This commit is contained in:
zcmander
2023-12-22 10:02:39 +02:00
parent 04d94dc3d0
commit e85aa989de
10 changed files with 211 additions and 19 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-test-utils': minor
'@backstage/backend-common': minor
---
drop databases after unit tests if the database instance is not running in docker
+6
View File
@@ -276,6 +276,12 @@ export class DockerContainerRunner implements ContainerRunner {
runContainer(options: RunContainerOptions): Promise<void>;
}
// @public
export function dropDatabase(
dbConfig: Config,
...databases: Array<string>
): Promise<void>;
// @public
export function ensureDatabaseExists(
dbConfig: Config,
@@ -19,10 +19,11 @@ import {
createDatabaseClient,
createNameOverride,
createSchemaOverride,
dropDatabase,
ensureSchemaExists,
parseConnectionString,
} from './connection';
import { pgConnector } from './connectors';
import { mysqlConnector, pgConnector } from './connectors';
const mocked = (f: Function) => f as jest.Mock;
@@ -30,8 +31,13 @@ jest.mock('./connectors', () => {
const connectors = jest.requireActual('./connectors');
return {
...connectors,
mysqlConnector: {
...connectors.mysqlConnector,
dropDatabase: jest.fn(),
},
pgConnector: {
...connectors.pgConnector,
dropDatabase: jest.fn(),
ensureSchemaExists: jest.fn(),
},
};
@@ -229,4 +235,74 @@ describe('database connection', () => {
).resolves.toBeUndefined();
});
});
describe('dropDatabase', () => {
it('returns successfully with pg client', async () => {
await dropDatabase(
new ConfigReader({
client: 'pg',
schema: 'catalog',
connection: 'postgresql://testuser:testpass@acme:5432/userdbname',
}),
'backstage_plugin_foobar',
);
const mockCalls = mocked(
pgConnector.dropDatabase as Function,
).mock.calls.splice(-1);
const [baseConfig, databaseName] = mockCalls[0];
expect(baseConfig.get()).toMatchObject({
client: 'pg',
connection: 'postgresql://testuser:testpass@acme:5432/userdbname',
});
expect(databaseName).toEqual('backstage_plugin_foobar');
});
it('returns successfully with mysql client', async () => {
await dropDatabase(
new ConfigReader({
client: 'mysql2',
connection: {
host: '127.0.0.1',
user: 'foo',
password: 'bar',
database: 'dbname',
},
}),
'backstage_plugin_foobar',
);
const mockCalls = mocked(
mysqlConnector.dropDatabase as Function,
).mock.calls.splice(-1);
const [baseConfig, databaseName] = mockCalls[0];
expect(baseConfig.get()).toMatchObject({
client: 'mysql2',
connection: {
host: '127.0.0.1',
user: 'foo',
password: 'bar',
database: 'dbname',
},
});
expect(databaseName).toEqual('backstage_plugin_foobar');
});
it('does nothing in other database drivers', () => {
return expect(
dropDatabase(
new ConfigReader({
client: 'better-sqlite3',
schema: 'catalog',
connection: ':memory:',
}),
'catalog',
),
).resolves.toBeUndefined();
});
});
});
@@ -95,6 +95,22 @@ export async function ensureDatabaseExists(
);
}
/**
* Drops the given databases.
*
* @public
*/
export async function dropDatabase(
dbConfig: Config,
...databases: Array<string>
): Promise<void> {
const client: DatabaseClient = dbConfig.getString('client');
return await ddlLimiter(() =>
ConnectorMapping[client]?.dropDatabase?.(dbConfig, ...databases),
);
}
/**
* Ensures that the given schemas all exist, creating them if they do not.
*
@@ -183,6 +183,40 @@ export async function ensureMysqlDatabaseExists(
}
}
/**
* Drops the given mysql databases.
*
* @param dbConfig - The database config
* @param databases - The names of the databases to create
*/
export async function dropMysqlDatabase(
dbConfig: Config,
...databases: Array<string>
) {
const admin = createMysqlDatabaseClient(dbConfig, {
connection: {
database: null as unknown as string,
},
pool: {
min: 0,
acquireTimeoutMillis: 10000,
},
});
try {
const dropDatabase = async (database: string) => {
await admin.raw(`DROP DATABASE ??`, [database]);
};
await Promise.all(
databases.map(async database => {
return await dropDatabase(database);
}),
);
} finally {
await admin.destroy();
}
}
/**
* MySQL database connector.
*
@@ -193,4 +227,5 @@ export const mysqlConnector: DatabaseConnector = Object.freeze({
ensureDatabaseExists: ensureMysqlDatabaseExists,
createNameOverride: defaultNameOverride,
parseConnectionString: parseMysqlConnectionString,
dropDatabase: dropMysqlDatabase,
});
@@ -199,6 +199,24 @@ export async function ensurePgSchemaExists(
}
}
/**
* Drops the Postgres databases.
*
* @param dbConfig - The database config
* @param databases - The name of the databases to drop
*/
export async function dropPgDatabase(
dbConfig: Config,
...databases: Array<string>
) {
const admin = createPgDatabaseClient(dbConfig);
await Promise.all(
databases.map(async database => {
await admin.raw(`DROP DATABASE ??`, [database]);
}),
);
}
/**
* PostgreSQL database connector.
*
@@ -211,4 +229,5 @@ export const pgConnector: DatabaseConnector = Object.freeze({
createNameOverride: defaultNameOverride,
createSchemaOverride: defaultSchemaOverride,
parseConnectionString: parsePgConnectionString,
dropDatabase: dropPgDatabase,
});
@@ -20,7 +20,11 @@ export * from './DatabaseManager';
* Undocumented API surface from connection is being reduced for future deprecation.
* Avoid exporting additional symbols.
*/
export { createDatabaseClient, ensureDatabaseExists } from './connection';
export {
createDatabaseClient,
ensureDatabaseExists,
dropDatabase,
} from './connection';
export type { PluginDatabaseManager } from './types';
export { isDatabaseConflictError } from './util';
@@ -79,4 +79,6 @@ export interface DatabaseConnector {
dbConfig: Config,
...schemas: Array<string>
): Promise<void>;
dropDatabase?(dbConfig: Config, ...databases: Array<string>): Promise<void>;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import { DatabaseManager, dropDatabase } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { randomBytes } from 'crypto';
import { Knex } from 'knex';
@@ -157,11 +157,13 @@ export class TestDatabases {
}
// Ensure that a unique logical database is created in the instance
const databaseName = `db${randomBytes(16).toString('hex')}`;
const connection = await instance.databaseManager
.forPlugin(`db${randomBytes(16).toString('hex')}`)
.forPlugin(databaseName)
.getClient();
instance.connections.push(connection);
instance.databaseNames.push(databaseName);
return connection;
}
@@ -173,21 +175,30 @@ export class TestDatabases {
if (envVarName) {
const connectionString = process.env[envVarName];
if (connectionString) {
const databaseManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
knexConfig: properties.driver.includes('sqlite')
? {}
: LARGER_POOL_CONFIG,
client: properties.driver,
connection: 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: [],
};
}
@@ -226,10 +237,10 @@ export class TestDatabases {
},
}),
);
return {
stopContainer: stop,
databaseManager,
databaseNames: [],
connections: [],
};
}
@@ -256,6 +267,7 @@ export class TestDatabases {
return {
stopContainer: stop,
databaseManager,
databaseNames: [],
connections: [],
};
}
@@ -273,9 +285,9 @@ export class TestDatabases {
},
}),
);
return {
databaseManager,
databaseNames: [],
connections: [],
};
}
@@ -284,7 +296,12 @@ export class TestDatabases {
const instances = [...this.instanceById.values()];
this.instanceById.clear();
for (const { stopContainer, connections, databaseManager } of instances) {
for (const {
stopContainer,
dropDatabases,
connections,
databaseManager,
} of instances) {
for (const connection of connections) {
try {
await connection.destroy();
@@ -296,6 +313,15 @@ export class TestDatabases {
}
}
// If the database is not running in docker then drop the databases
try {
await dropDatabases?.();
} catch (error) {
console.warn(`TestDatabases: Failed to drop databases`, {
error,
});
}
try {
await stopContainer?.();
} catch (error) {
@@ -43,8 +43,10 @@ export type TestDatabaseProperties = {
export type Instance = {
stopContainer?: () => Promise<void>;
dropDatabases?: () => Promise<void>;
databaseManager: DatabaseManager;
connections: Array<Knex>;
databaseNames: Array<string>;
};
export const allDatabases: Record<TestDatabaseId, TestDatabaseProperties> =