Revert "Introduce @backstage/backend-testing, with TestDatabases to start off"
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
# @backstage/backend-testing
|
||||
|
||||
Test helpers library for Backstage backends.
|
||||
|
||||
## Usage
|
||||
|
||||
Add the library as a `devDependency` to your backend package:
|
||||
|
||||
```sh
|
||||
# From the Backstage root directory, go to your backend package, or to a backend plugin
|
||||
cd plugins/my-plugin-backend
|
||||
yarn add --dev @backstage/backend-testing
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md)
|
||||
@@ -1,21 +0,0 @@
|
||||
## API Report File for "@backstage/backend-testing"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
|
||||
import { Knex } from 'knex';
|
||||
|
||||
// @public
|
||||
export type TestDatabaseId = 'POSTGRES_13' | 'POSTGRES_9' | 'MYSQL_8' | 'SQLITE_3';
|
||||
|
||||
// @public
|
||||
export class TestDatabases {
|
||||
static create(): TestDatabases;
|
||||
init(id: TestDatabaseId): Promise<Knex>;
|
||||
}
|
||||
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
```
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "@backstage/backend-testing",
|
||||
"description": "Test helpers library for Backstage backends",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/backend-testing"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"test"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "backstage-cli build --outputs cjs,types",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"knex": "^0.95.1",
|
||||
"mysql2": "^2.2.5",
|
||||
"pg": "^8.3.0",
|
||||
"sqlite3": "^5.0.0",
|
||||
"testcontainers": "^7.10.0",
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"jest": "^26.0.1"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 from 'knex';
|
||||
import { GenericContainer } from 'testcontainers';
|
||||
import { TestDatabases } from './TestDatabases';
|
||||
|
||||
describe('TestDatabases', () => {
|
||||
const OLD_ENV = process.env;
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...OLD_ENV };
|
||||
});
|
||||
afterAll(() => {
|
||||
process.env = OLD_ENV;
|
||||
});
|
||||
|
||||
it.each([
|
||||
['POSTGRES_13'],
|
||||
['POSTGRES_9'],
|
||||
['MYSQL_8'],
|
||||
['SQLITE_3'],
|
||||
] as const)(
|
||||
'creates distinct %p databases',
|
||||
async databaseId => {
|
||||
const dbs = TestDatabases.create();
|
||||
const db1 = await dbs.init(databaseId);
|
||||
const db2 = await dbs.init(databaseId);
|
||||
await db1.schema.createTable('a', table => table.string('x').primary());
|
||||
await db2.schema.createTable('a', table => table.string('y').primary());
|
||||
await expect(db1.select({ a: db1.raw('1') })).resolves.toEqual([
|
||||
{ a: 1 },
|
||||
]);
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
|
||||
it('obeys a provided connection string for postgres 13', async () => {
|
||||
const dbs = TestDatabases.create();
|
||||
const container = await new GenericContainer('postgres:13')
|
||||
.withExposedPorts(5432)
|
||||
.withEnv('POSTGRES_USER', 'user')
|
||||
.withEnv('POSTGRES_PASSWORD', 'pass')
|
||||
.withTmpFs({ '/var/lib/postgresql/data': 'rw' })
|
||||
.start();
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(5432);
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING = `postgresql://user:pass@${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 output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
database: 'backstage_plugin_0',
|
||||
},
|
||||
});
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]);
|
||||
} finally {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it('obeys a provided connection string for postgres 9', async () => {
|
||||
const dbs = TestDatabases.create();
|
||||
const container = await new GenericContainer('postgres:9')
|
||||
.withExposedPorts(5432)
|
||||
.withEnv('POSTGRES_USER', 'user')
|
||||
.withEnv('POSTGRES_PASSWORD', 'pass')
|
||||
.withTmpFs({ '/var/lib/postgresql/data': 'rw' })
|
||||
.start();
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(5432);
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING = `postgresql://user:pass@${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 output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
database: 'backstage_plugin_0',
|
||||
},
|
||||
});
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]);
|
||||
} finally {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it('obeys a provided connection string for mysql 8', async () => {
|
||||
const dbs = TestDatabases.create();
|
||||
const container = await new GenericContainer('mysql:8')
|
||||
.withExposedPorts(3306)
|
||||
.withEnv('MYSQL_ROOT_PASSWORD', 'pass')
|
||||
.withTmpFs({ '/var/lib/mysql': 'rw' })
|
||||
.start();
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(3306);
|
||||
|
||||
try {
|
||||
// Leave a mark
|
||||
process.env.BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING = `mysql://root:pass@${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 output = knexFactory({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
user: 'root',
|
||||
password: 'pass',
|
||||
database: 'backstage_plugin_0',
|
||||
},
|
||||
});
|
||||
await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]);
|
||||
} finally {
|
||||
await container.stop({ timeout: 10_000 });
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -1,278 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { SingleConnectionDatabaseManager } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
type TestDatabaseProperties = {
|
||||
name: string;
|
||||
driver: string;
|
||||
dockerImageName?: string;
|
||||
connectionStringEnvironmentVariableName?: string;
|
||||
};
|
||||
|
||||
type Instance = {
|
||||
container?: StartedTestContainer;
|
||||
databaseManager: SingleConnectionDatabaseManager;
|
||||
connections: Array<Knex>;
|
||||
};
|
||||
|
||||
const supportedDatabases = Object.freeze({
|
||||
POSTGRES_13: {
|
||||
name: 'Postgres 13.x',
|
||||
driver: 'pg',
|
||||
dockerImageName: 'postgres:13',
|
||||
connectionStringEnvironmentVariableName:
|
||||
'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING',
|
||||
},
|
||||
POSTGRES_9: {
|
||||
name: 'Postgres 9.x',
|
||||
driver: 'pg',
|
||||
dockerImageName: 'postgres:9',
|
||||
connectionStringEnvironmentVariableName:
|
||||
'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING',
|
||||
},
|
||||
MYSQL_8: {
|
||||
name: 'MySQL 8.x',
|
||||
driver: 'mysql2',
|
||||
dockerImageName: 'mysql:8',
|
||||
connectionStringEnvironmentVariableName:
|
||||
'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING',
|
||||
},
|
||||
SQLITE_3: {
|
||||
name: 'SQLite 3.x',
|
||||
driver: 'sqlite3',
|
||||
},
|
||||
} as const);
|
||||
|
||||
/**
|
||||
* The possible databases to test against.
|
||||
*/
|
||||
export type TestDatabaseId =
|
||||
| 'POSTGRES_13'
|
||||
| 'POSTGRES_9'
|
||||
| 'MYSQL_8'
|
||||
| 'SQLITE_3';
|
||||
|
||||
/**
|
||||
* Encapsulates the creation of ephemeral test database instances for use
|
||||
* inside unit or integration tests.
|
||||
*/
|
||||
export class TestDatabases {
|
||||
private instanceById: Map<string, Instance> = new Map();
|
||||
private lastDatabaseId = 0;
|
||||
|
||||
/**
|
||||
* Creates an empty `TestDatabases` instance, and sets up Jest to clean up
|
||||
* all of its acquired resources after all tests finish.
|
||||
*
|
||||
* You typically want to create just a single instance like this at the top
|
||||
* of your test file or `describe` block, and then call `init` many times on
|
||||
* that instance inside the individual tests. Spinning up a "physical"
|
||||
* database instance takes a considerable amount of time, slowing down tests.
|
||||
* But initializing a new logical database inside that instance using `init`
|
||||
* is very fast.
|
||||
*/
|
||||
static create() {
|
||||
const databases = new TestDatabases();
|
||||
|
||||
afterAll(async () => {
|
||||
await databases.shutdown();
|
||||
});
|
||||
|
||||
return databases;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Returns a fresh, unique, empty logical database on an instance of the
|
||||
* given database ID platform.
|
||||
*
|
||||
* @param id The ID of the database platform to use, e.g. 'POSTGRES_13'
|
||||
* @returns A `Knex` connection object
|
||||
*/
|
||||
async init(id: TestDatabaseId): Promise<Knex> {
|
||||
const properties = supportedDatabases[id];
|
||||
if (!properties) {
|
||||
const candidates = Object.keys(supportedDatabases).join(', ');
|
||||
throw new Error(
|
||||
`Unsupported test database ${id}, possible values are ${candidates}`,
|
||||
);
|
||||
}
|
||||
|
||||
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 connection = await instance.databaseManager
|
||||
.forPlugin(String(this.lastDatabaseId++))
|
||||
.getClient();
|
||||
|
||||
instance.connections.push(connection);
|
||||
|
||||
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 databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: properties.driver,
|
||||
connection: connectionString,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
return {
|
||||
databaseManager,
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise start a container for the purpose
|
||||
switch (properties.driver) {
|
||||
case 'pg':
|
||||
return this.initPostgres(properties);
|
||||
case 'mysql2':
|
||||
return this.initMysql(properties);
|
||||
case 'sqlite3':
|
||||
return this.initSqlite(properties);
|
||||
default:
|
||||
throw new Error(`Unknown database driver ${properties.driver}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async initPostgres(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const password = uuid();
|
||||
|
||||
const container = await new GenericContainer(properties.dockerImageName!)
|
||||
.withExposedPorts(5432)
|
||||
.withEnv('POSTGRES_PASSWORD', password)
|
||||
.withTmpFs({ '/var/lib/postgresql/data': 'rw' })
|
||||
.start();
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host: container.getHost(),
|
||||
port: container.getMappedPort(5432),
|
||||
user: 'postgres',
|
||||
password,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
container,
|
||||
databaseManager,
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async initMysql(
|
||||
properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const password = uuid();
|
||||
|
||||
const container = await new GenericContainer(properties.dockerImageName!)
|
||||
.withExposedPorts(3306)
|
||||
.withEnv('MYSQL_ROOT_PASSWORD', password)
|
||||
.withTmpFs({ '/var/lib/mysql': 'rw' })
|
||||
.start();
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host: container.getHost(),
|
||||
port: container.getMappedPort(3306),
|
||||
user: 'root',
|
||||
password,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
container,
|
||||
databaseManager,
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async initSqlite(
|
||||
_properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
databaseManager,
|
||||
connections: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async shutdown() {
|
||||
for (const { container, connections } of this.instanceById.values()) {
|
||||
try {
|
||||
await Promise.all(connections.map(c => c.destroy()));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
await container?.stop({ timeout: 10_000 });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { TestDatabases } from './TestDatabases';
|
||||
export type { TestDatabaseId } from './TestDatabases';
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './database';
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user