diff --git a/.changeset/wise-pens-count.md b/.changeset/wise-pens-count.md deleted file mode 100644 index 26eda866bc..0000000000 --- a/.changeset/wise-pens-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Use `TestDatabases` from `@backstage/backend-testing` diff --git a/.github/workflows/master-win.yml b/.github/workflows/master-win.yml index c59941df29..3850146388 100644 --- a/.github/workflows/master-win.yml +++ b/.github/workflows/master-win.yml @@ -2,7 +2,6 @@ name: Master Build Windows on: workflow_dispatch: - pull_request: push: branches: [master] @@ -14,41 +13,6 @@ jobs: matrix: node-version: [12.x, 14.x] - services: - postgres13: - image: postgres:13 - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432/tcp - postgres9: - image: postgres:9 - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432/tcp - mysql8: - image: mysql:8 - env: - MYSQL_ROOT_PASSWORD: root - options: >- - --health-cmd "mysqladmin ping -h localhost" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 3306/tcp - env: CI: true NODE_OPTIONS: --max-old-space-size=4096 @@ -92,9 +56,7 @@ jobs: - name: test run: yarn lerna -- run test env: - BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} - BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} - BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored + BACKSTAGE_TEST_DISABLE_DOCKER: 1 # credit: https://github.com/appleboy/discord-action/issues/3#issuecomment-731426861 - name: Discord notification diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 60db9ac16b..75e26d52d3 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,6 +16,41 @@ jobs: matrix: node-version: [12.x, 14.x] + services: + postgres13: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432/tcp + postgres9: + image: postgres:9 + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432/tcp + mysql8: + image: mysql:8 + env: + MYSQL_ROOT_PASSWORD: root + options: >- + --health-cmd "mysqladmin ping -h localhost" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 3306/tcp + env: CI: true NODE_OPTIONS: --max-old-space-size=4096 @@ -86,6 +121,10 @@ jobs: # Upload code coverage for some specific flags. Also see .codecov.yml bash <(curl -s https://codecov.io/bash) -f packages/core/coverage/* -F core bash <(curl -s https://codecov.io/bash) -f packages/core-api/coverage/* -F core-api + env: + BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} + BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} + BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored - name: Discord notification if: ${{ failure() }} diff --git a/packages/backend-testing/.eslintrc.js b/packages/backend-test-utils/.eslintrc.js similarity index 100% rename from packages/backend-testing/.eslintrc.js rename to packages/backend-test-utils/.eslintrc.js diff --git a/packages/backend-testing/README.md b/packages/backend-test-utils/README.md similarity index 85% rename from packages/backend-testing/README.md rename to packages/backend-test-utils/README.md index f12e94ac25..38756fa5d7 100644 --- a/packages/backend-testing/README.md +++ b/packages/backend-test-utils/README.md @@ -1,4 +1,4 @@ -# @backstage/backend-testing +# @backstage/backend-test-utils Test helpers library for Backstage backends. @@ -9,7 +9,7 @@ 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 +yarn add --dev @backstage/backend-test-utils ``` ## Documentation diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md new file mode 100644 index 0000000000..28d4a00463 --- /dev/null +++ b/packages/backend-test-utils/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/backend-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Knex } from 'knex'; + +// @public (undocumented) +export function isDockerDisabledForTests(): boolean; + +// @public +export type TestDatabaseId = 'POSTGRES_13' | 'POSTGRES_9' | 'MYSQL_8' | 'SQLITE_3'; + +// @public +export class TestDatabases { + static create(options?: { + ids?: TestDatabaseId[]; + disableDocker?: boolean; + }): TestDatabases; + // (undocumented) + eachSupportedId(): [TestDatabaseId][]; + init(id: TestDatabaseId): Promise; + // (undocumented) + supports(id: TestDatabaseId): boolean; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/backend-testing/package.json b/packages/backend-test-utils/package.json similarity index 92% rename from packages/backend-testing/package.json rename to packages/backend-test-utils/package.json index 47719b5b59..95276651ca 100644 --- a/packages/backend-testing/package.json +++ b/packages/backend-test-utils/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/backend-testing", + "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", "version": "0.1.1", "main": "src/index.ts", @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "packages/backend-testing" + "directory": "packages/backend-test-utils" }, "keywords": [ "backstage", diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts new file mode 100644 index 0000000000..7c1e6e1bdd --- /dev/null +++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts @@ -0,0 +1,157 @@ +/* + * 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 { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { startMysqlContainer } from './startMysqlContainer'; +import { startPostgresContainer } from './startPostgresContainer'; +import { TestDatabases } from './TestDatabases'; + +const itIfDocker = isDockerDisabledForTests() ? it.skip : it; + +describe('TestDatabases', () => { + const OLD_ENV = process.env; + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + afterAll(() => { + process.env = OLD_ENV; + }); + + describe('each', () => { + const dbs = TestDatabases.create(); + + it.each(dbs.eachSupportedId())( + 'creates distinct %p databases', + async databaseId => { + if (!dbs.supports(databaseId)) { + return; + } + 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, + ); + }); + + itIfDocker( + 'obeys a provided connection string for postgres 13', + async () => { + const dbs = TestDatabases.create(); + 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 = 'backstage_plugin_0'; + 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(); + } + }, + 60_000, + ); + + itIfDocker( + 'obeys a provided connection string for postgres 9', + async () => { + const dbs = TestDatabases.create(); + 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 = 'backstage_plugin_0'; + 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(); + } + }, + 60_000, + ); + + itIfDocker( + 'obeys a provided connection string for mysql 8', + async () => { + const dbs = TestDatabases.create(); + 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 = 'backstage_plugin_0'; + 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(); + } + }, + 60_000, + ); +}); diff --git a/packages/backend-testing/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts similarity index 60% rename from packages/backend-testing/src/database/TestDatabases.ts rename to packages/backend-test-utils/src/database/TestDatabases.ts index 927ff2632c..a7b08cd593 100644 --- a/packages/backend-testing/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -17,66 +17,24 @@ 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; -}; - -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'; +import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { startMysqlContainer } from './startMysqlContainer'; +import { startPostgresContainer } from './startPostgresContainer'; +import { + allDatabases, + Instance, + TestDatabaseId, + TestDatabaseProperties, +} from './types'; /** * Encapsulates the creation of ephemeral test database instances for use * inside unit or integration tests. */ export class TestDatabases { - private instanceById: Map = new Map(); - private lastDatabaseId = 0; + private readonly instanceById: Map; + private readonly supportedIds: TestDatabaseId[]; + private lastDatabaseIndex: number; /** * Creates an empty `TestDatabases` instance, and sets up Jest to clean up @@ -89,8 +47,43 @@ export class TestDatabases { * But initializing a new logical database inside that instance using `init` * is very fast. */ - static create() { - const databases = new TestDatabases(); + static create(options?: { + ids?: TestDatabaseId[]; + disableDocker?: boolean; + }): TestDatabases { + const defaultOptions = { + ids: Object.keys(allDatabases) as TestDatabaseId[], + disableDocker: isDockerDisabledForTests(), + }; + + const { ids, disableDocker } = Object.assign(defaultOptions, options ?? {}); + + const supportedIds = ids.filter(id => { + const properties = allDatabases[id]; + if (!properties) { + return false; + } + // If the caller has set up the env with an explicit connection string, + // we'll assume that this database will work + if ( + properties.connectionStringEnvironmentVariableName && + process.env[properties.connectionStringEnvironmentVariableName] + ) { + return true; + } + // If the database doesn't require docker at all, there's nothing to worry + // about + if (!properties.dockerImageName) { + return true; + } + // If the database requires docker, but docker is disabled, we will fail. + if (disableDocker) { + return false; + } + return true; + }); + + const databases = new TestDatabases(supportedIds); afterAll(async () => { await databases.shutdown(); @@ -99,7 +92,19 @@ export class TestDatabases { return databases; } - private constructor() {} + private constructor(supportedIds: TestDatabaseId[]) { + this.instanceById = new Map(); + this.supportedIds = supportedIds; + this.lastDatabaseIndex = 0; + } + + supports(id: TestDatabaseId): boolean { + return this.supportedIds.includes(id); + } + + eachSupportedId(): [TestDatabaseId][] { + return this.supportedIds.map(id => [id]); + } /** * Returns a fresh, unique, empty logical database on an instance of the @@ -109,11 +114,17 @@ export class TestDatabases { * @returns A `Knex` connection object */ async init(id: TestDatabaseId): Promise { - const properties = supportedDatabases[id]; + const properties = allDatabases[id]; if (!properties) { - const candidates = Object.keys(supportedDatabases).join(', '); + const candidates = Object.keys(allDatabases).join(', '); throw new Error( - `Unsupported test database ${id}, possible values are ${candidates}`, + `Unknown test database ${id}, possible values are ${candidates}`, + ); + } + if (!this.supportedIds.includes(id)) { + const candidates = this.supportedIds.join(', '); + throw new Error( + `Unsupported test database ${id} for this environment, possible values are ${candidates}`, ); } @@ -127,7 +138,7 @@ export class TestDatabases { // Ensure that a unique logical database is created in the instance const connection = await instance.databaseManager - .forPlugin(String(this.lastDatabaseId++)) + .forPlugin(String(this.lastDatabaseIndex++)) .getClient(); instance.connections.push(connection); @@ -176,32 +187,23 @@ export class TestDatabases { private async initPostgres( properties: TestDatabaseProperties, ): Promise { - const password = uuid(); - - const container = await new GenericContainer(properties.dockerImageName!) - .withExposedPorts(5432) - .withEnv('POSTGRES_PASSWORD', password) - .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) - .start(); + const { host, port, user, password, stop } = await startPostgresContainer( + properties.dockerImageName!, + ); const databaseManager = SingleConnectionDatabaseManager.fromConfig( new ConfigReader({ backend: { database: { client: 'pg', - connection: { - host: container.getHost(), - port: container.getMappedPort(5432), - user: 'postgres', - password, - }, + connection: { host, port, user, password }, }, }, }), ); return { - container, + stopContainer: stop, databaseManager, connections: [], }; @@ -210,32 +212,23 @@ export class TestDatabases { private async initMysql( properties: TestDatabaseProperties, ): Promise { - const password = uuid(); - - const container = await new GenericContainer(properties.dockerImageName!) - .withExposedPorts(3306) - .withEnv('MYSQL_ROOT_PASSWORD', password) - .withTmpFs({ '/var/lib/mysql': 'rw' }) - .start(); + const { host, port, user, password, stop } = await startMysqlContainer( + properties.dockerImageName!, + ); const databaseManager = SingleConnectionDatabaseManager.fromConfig( new ConfigReader({ backend: { database: { client: 'mysql2', - connection: { - host: container.getHost(), - port: container.getMappedPort(3306), - user: 'root', - password, - }, + connection: { host, port, user, password }, }, }, }), ); return { - container, + stopContainer: stop, databaseManager, connections: [], }; @@ -262,17 +255,20 @@ export class TestDatabases { } 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 - } - } + const instances = [...this.instanceById.values()]; + await Promise.all( + instances.map(async ({ stopContainer, connections }) => { + try { + await Promise.all(connections.map(c => c.destroy())); + } catch { + // ignore + } + try { + await stopContainer?.(); + } catch { + // ignore + } + }), + ); } } diff --git a/packages/backend-test-utils/src/database/index.ts b/packages/backend-test-utils/src/database/index.ts new file mode 100644 index 0000000000..6988f0b80b --- /dev/null +++ b/packages/backend-test-utils/src/database/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +export { TestDatabases } from './TestDatabases'; +export type { TestDatabaseId } from './types'; diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.test.ts b/packages/backend-test-utils/src/database/startMysqlContainer.test.ts new file mode 100644 index 0000000000..e210ccb413 --- /dev/null +++ b/packages/backend-test-utils/src/database/startMysqlContainer.test.ts @@ -0,0 +1,40 @@ +/* + * 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 createConnection from 'knex'; +import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { startMysqlContainer } from './startMysqlContainer'; + +const itIfDocker = isDockerDisabledForTests() ? it.skip : it; + +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(); + } + }, + 60_000, + ); +}); diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.ts b/packages/backend-test-utils/src/database/startMysqlContainer.ts new file mode 100644 index 0000000000..739298dfe5 --- /dev/null +++ b/packages/backend-test-utils/src/database/startMysqlContainer.ts @@ -0,0 +1,68 @@ +/* + * 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 createConnection, { Knex } from 'knex'; +import { GenericContainer } from 'testcontainers'; +import { v4 as uuid } from 'uuid'; + +async function waitForMysqlReady( + connection: Knex.MySqlConnectionConfig, +): Promise { + 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(); + + const container = await new GenericContainer(image) + .withExposedPorts(3306) + .withEnv('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 }; +} diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.test.ts b/packages/backend-test-utils/src/database/startPostgresContainer.test.ts new file mode 100644 index 0000000000..4e8acc8410 --- /dev/null +++ b/packages/backend-test-utils/src/database/startPostgresContainer.test.ts @@ -0,0 +1,42 @@ +/* + * 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 createConnection from 'knex'; +import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { startPostgresContainer } from './startPostgresContainer'; + +const itIfDocker = isDockerDisabledForTests() ? it.skip : it; + +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(); + } + }, + 60_000, + ); +}); diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.ts b/packages/backend-test-utils/src/database/startPostgresContainer.ts new file mode 100644 index 0000000000..1c2ecdcb60 --- /dev/null +++ b/packages/backend-test-utils/src/database/startPostgresContainer.ts @@ -0,0 +1,68 @@ +/* + * 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 createConnection, { Knex } from 'knex'; +import { GenericContainer } from 'testcontainers'; +import { v4 as uuid } from 'uuid'; + +async function waitForPostgresReady( + connection: Knex.PgConnectionConfig, +): Promise { + 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(); + + const container = await new GenericContainer(image) + .withExposedPorts(5432) + .withEnv('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 }; +} diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts new file mode 100644 index 0000000000..791cf09b7b --- /dev/null +++ b/packages/backend-test-utils/src/database/types.ts @@ -0,0 +1,71 @@ +/* + * 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 { Knex } from 'knex'; + +/** + * The possible databases to test against. + */ +export type TestDatabaseId = + | 'POSTGRES_13' + | 'POSTGRES_9' + | 'MYSQL_8' + | 'SQLITE_3'; + +export type TestDatabaseProperties = { + name: string; + driver: string; + dockerImageName?: string; + connectionStringEnvironmentVariableName?: string; +}; + +export type Instance = { + stopContainer?: () => Promise; + databaseManager: SingleConnectionDatabaseManager; + connections: Array; +}; + +export const allDatabases: Record< + TestDatabaseId, + TestDatabaseProperties +> = 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', + }, +}); diff --git a/packages/backend-testing/src/index.ts b/packages/backend-test-utils/src/index.ts similarity index 96% rename from packages/backend-testing/src/index.ts rename to packages/backend-test-utils/src/index.ts index 3febc35a77..ad3e422f44 100644 --- a/packages/backend-testing/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -15,3 +15,4 @@ */ export * from './database'; +export * from './util'; diff --git a/packages/backend-testing/src/setupTests.ts b/packages/backend-test-utils/src/setupTests.ts similarity index 100% rename from packages/backend-testing/src/setupTests.ts rename to packages/backend-test-utils/src/setupTests.ts diff --git a/packages/backend-testing/src/database/index.ts b/packages/backend-test-utils/src/util/index.ts similarity index 85% rename from packages/backend-testing/src/database/index.ts rename to packages/backend-test-utils/src/util/index.ts index b88ba38010..e4e0e97ac1 100644 --- a/packages/backend-testing/src/database/index.ts +++ b/packages/backend-test-utils/src/util/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { TestDatabases } from './TestDatabases'; -export type { TestDatabaseId } from './TestDatabases'; +export { isDockerDisabledForTests } from './isDockerDisabledForTests'; diff --git a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts new file mode 100644 index 0000000000..60d27bc154 --- /dev/null +++ b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts @@ -0,0 +1,19 @@ +/* + * 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 function isDockerDisabledForTests() { + return Boolean(process.env.BACKSTAGE_TEST_DISABLE_DOCKER); +} diff --git a/packages/backend-testing/api-report.md b/packages/backend-testing/api-report.md deleted file mode 100644 index c4c78408ec..0000000000 --- a/packages/backend-testing/api-report.md +++ /dev/null @@ -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; - } - - -// (No @packageDocumentation comment for this package) - -``` diff --git a/packages/backend-testing/src/database/TestDatabases.test.ts b/packages/backend-testing/src/database/TestDatabases.test.ts deleted file mode 100644 index 61220e3090..0000000000 --- a/packages/backend-testing/src/database/TestDatabases.test.ts +++ /dev/null @@ -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); -}); diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 0db12c6d96..2f0164ace5 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -64,7 +64,6 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/backend-testing": "^0.1.0", "@backstage/cli": "^0.6.12", "@backstage/test-utils": "^0.1.12", "@types/core-js": "^2.5.4", diff --git a/plugins/catalog-backend/src/next/Stitcher.test.ts b/plugins/catalog-backend/src/next/Stitcher.test.ts index e4ff41039d..f38af384fa 100644 --- a/plugins/catalog-backend/src/next/Stitcher.test.ts +++ b/plugins/catalog-backend/src/next/Stitcher.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabases } from '@backstage/backend-testing'; +import { Knex } from 'knex'; import { DatabaseManager } from './database/DatabaseManager'; import { DbRefreshStateReferencesRow, @@ -26,200 +26,181 @@ import { DbSearchRow } from './search'; import { DbFinalEntitiesRow, Stitcher } from './Stitcher'; describe('Stitcher', () => { - const databases = TestDatabases.create(); + let db: Knex; const logger = getVoidLogger(); - it.each([ - ['POSTGRES_13'], - ['POSTGRES_9'], - ['SQLITE_3'], - // TODO(freben): mysql:8 is not ready to use yet - ] as const)( - 'runs the happy path for %p', - async databaseId => { - const db = await databases.init(databaseId); - await DatabaseManager.createDatabase(db); + beforeEach(async () => { + db = await DatabaseManager.createTestDatabaseConnection(); + await DatabaseManager.createDatabase(db); + }); - const stitcher = new Stitcher(db, logger); + it('runs the happy path', async () => { + const stitcher = new Stitcher(db, logger); - await db.transaction(async tx => { - await tx('refresh_state').insert([ + await db.transaction(async tx => { + await tx('refresh_state').insert([ + { + entity_id: 'my-id', + entity_ref: 'k:ns/n', + unprocessed_entity: JSON.stringify({}), + processed_entity: JSON.stringify({ + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + }, + spec: { + k: 'v', + }, + }), + errors: '[]', + next_update_at: tx.fn.now(), + last_discovery_at: tx.fn.now(), + }, + ]); + await tx('refresh_state_references').insert([ + { source_key: 'a', target_entity_ref: 'k:ns/n' }, + ]); + await tx('relations').insert([ + { + originating_entity_id: 'my-id', + source_entity_ref: 'k:ns/n', + type: 'looksAt', + target_entity_ref: 'k:ns/other', + }, + ]); + }); + + await stitcher.stitch(new Set(['k:ns/n'])); + + let firstHash: string; + await db.transaction(async tx => { + const entities = await tx('final_entities'); + + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entity).toEqual({ + relations: [ { - entity_id: 'my-id', - entity_ref: 'k:ns/n', - unprocessed_entity: JSON.stringify({}), - processed_entity: JSON.stringify({ - apiVersion: 'a', + type: 'looksAt', + target: { kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - }, - spec: { - k: 'v', - }, - }), - errors: '[]', - next_update_at: tx.fn.now(), - last_discovery_at: tx.fn.now(), + namespace: 'ns', + name: 'other', + }, }, - ]); - await tx( - 'refresh_state_references', - ).insert([{ source_key: 'a', target_entity_ref: 'k:ns/n' }]); - await tx('relations').insert([ + ], + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + etag: expect.any(String), + generation: 1, + uid: 'my-id', + }, + spec: { + k: 'v', + }, + }); + + expect(entity.metadata.etag).toEqual(entities[0].hash); + firstHash = entities[0].hash; + + const search = await tx('search'); + expect(search).toEqual( + expect.arrayContaining([ + { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, + { entity_id: 'my-id', key: 'apiversion', value: 'a' }, + { entity_id: 'my-id', key: 'kind', value: 'k' }, + { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, + { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, + { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, + { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + ]), + ); + }); + + // Re-stitch without any changes + await stitcher.stitch(new Set(['k:ns/n'])); + + await db.transaction(async tx => { + const entities = await tx('final_entities'); + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entities[0].hash).toEqual(firstHash); + expect(entity.metadata.etag).toEqual(firstHash); + }); + + // Now add one more relation and re-stitch + await db.transaction(async tx => { + await tx('relations').insert([ + { + originating_entity_id: 'my-id', + source_entity_ref: 'k:ns/n', + type: 'looksAt', + target_entity_ref: 'k:ns/third', + }, + ]); + }); + + await stitcher.stitch(new Set(['k:ns/n'])); + + await db.transaction(async tx => { + const entities = await tx('final_entities'); + + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entity).toEqual({ + relations: expect.arrayContaining([ { - originating_entity_id: 'my-id', - source_entity_ref: 'k:ns/n', type: 'looksAt', - target_entity_ref: 'k:ns/other', - }, - ]); - }); - - await stitcher.stitch(new Set(['k:ns/n'])); - - let firstHash: string; - await db.transaction(async tx => { - const entities = await tx('final_entities'); - - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entity).toEqual({ - relations: [ - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'other', - }, + target: { + kind: 'k', + namespace: 'ns', + name: 'other', }, - ], - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - etag: expect.any(String), - generation: 1, - uid: 'my-id', }, - spec: { - k: 'v', - }, - }); - - expect(entity.metadata.etag).toEqual(entities[0].hash); - firstHash = entities[0].hash; - - const search = await tx('search'); - expect(search).toEqual( - expect.arrayContaining([ - { - entity_id: 'my-id', - key: 'relations.looksat', - value: 'k:ns/other', - }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, - ]), - ); - }); - - // Re-stitch without any changes - await stitcher.stitch(new Set(['k:ns/n'])); - - await db.transaction(async tx => { - const entities = await tx('final_entities'); - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entities[0].hash).toEqual(firstHash); - expect(entity.metadata.etag).toEqual(firstHash); - }); - - // Now add one more relation and re-stitch - await db.transaction(async tx => { - await tx('relations').insert([ { - originating_entity_id: 'my-id', - source_entity_ref: 'k:ns/n', type: 'looksAt', - target_entity_ref: 'k:ns/third', + target: { + kind: 'k', + namespace: 'ns', + name: 'third', + }, }, - ]); + ]), + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + etag: expect.any(String), + generation: 1, + uid: 'my-id', + }, + spec: { + k: 'v', + }, }); - await stitcher.stitch(new Set(['k:ns/n'])); + expect(entities[0].hash).not.toEqual(firstHash); + expect(entities[0].hash).toEqual(entity.metadata.etag); - await db.transaction(async tx => { - const entities = await tx('final_entities'); - - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entity).toEqual({ - relations: expect.arrayContaining([ - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'other', - }, - }, - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'third', - }, - }, - ]), - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - etag: expect.any(String), - generation: 1, - uid: 'my-id', - }, - spec: { - k: 'v', - }, - }); - - expect(entities[0].hash).not.toEqual(firstHash); - expect(entities[0].hash).toEqual(entity.metadata.etag); - - const search = await tx('search'); - expect(search).toEqual( - expect.arrayContaining([ - { - entity_id: 'my-id', - key: 'relations.looksat', - value: 'k:ns/other', - }, - { - entity_id: 'my-id', - key: 'relations.looksat', - value: 'k:ns/third', - }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, - ]), - ); - }); - }, - 30000, - ); + const search = await tx('search'); + expect(search).toEqual( + expect.arrayContaining([ + { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, + { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/third' }, + { entity_id: 'my-id', key: 'apiversion', value: 'a' }, + { entity_id: 'my-id', key: 'kind', value: 'k' }, + { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, + { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, + { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, + { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + ]), + ); + }); + }); }); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 4fe76cb38d..a0d794b745 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -62,7 +62,7 @@ PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = function tryGetPackag const DOCUMENTED_PACKAGES = [ 'packages/backend-common', - 'packages/backend-testing', + 'packages/backend-test-utils', 'packages/catalog-client', 'packages/catalog-model', 'packages/cli-common',