@backstage/backend-test-utils 0.3.9-next.0 → 0.4.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -1,24 +1,32 @@
1
1
  'use strict';
2
2
 
3
- var backendCommon = require('@backstage/backend-common');
4
- var config = require('@backstage/config');
5
- var crypto = require('crypto');
6
- var createConnection = require('knex');
3
+ var Keyv = require('keyv');
4
+ var KeyvMemcache = require('@keyv/memcache');
7
5
  var uuid = require('uuid');
6
+ var KeyvRedis = require('@keyv/redis');
7
+ var errors = require('@backstage/errors');
8
+ var crypto = require('crypto');
9
+ var knexFactory = require('knex');
10
+ var yn = require('yn');
11
+ var pgConnectionString = require('pg-connection-string');
8
12
  var os = require('os');
9
13
  var backendPluginApi = require('@backstage/backend-plugin-api');
10
14
  var fs = require('fs-extra');
11
15
  var textextensions = require('textextensions');
12
16
  var path = require('path');
13
17
  var backendAppApi = require('@backstage/backend-app-api');
14
- var errors = require('@backstage/errors');
18
+ var config = require('@backstage/config');
15
19
  var cookie = require('cookie');
16
20
  var pluginEventsNode = require('@backstage/plugin-events-node');
17
21
  var express = require('express');
18
22
 
19
23
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
20
24
 
21
- var createConnection__default = /*#__PURE__*/_interopDefaultCompat(createConnection);
25
+ var Keyv__default = /*#__PURE__*/_interopDefaultCompat(Keyv);
26
+ var KeyvMemcache__default = /*#__PURE__*/_interopDefaultCompat(KeyvMemcache);
27
+ var KeyvRedis__default = /*#__PURE__*/_interopDefaultCompat(KeyvRedis);
28
+ var knexFactory__default = /*#__PURE__*/_interopDefaultCompat(knexFactory);
29
+ var yn__default = /*#__PURE__*/_interopDefaultCompat(yn);
22
30
  var os__default = /*#__PURE__*/_interopDefaultCompat(os);
23
31
  var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
24
32
  var textextensions__default = /*#__PURE__*/_interopDefaultCompat(textextensions);
@@ -28,84 +36,270 @@ function isDockerDisabledForTests() {
28
36
  return Boolean(process.env.BACKSTAGE_TEST_DISABLE_DOCKER) || !Boolean(process.env.CI);
29
37
  }
30
38
 
31
- async function waitForMysqlReady(connection) {
39
+ async function attemptMemcachedConnection(connection) {
32
40
  const startTime = Date.now();
33
- const db = createConnection__default.default({ client: "mysql2", connection });
34
- try {
35
- for (; ; ) {
36
- try {
37
- const result = await db.select(db.raw("version() AS version"));
38
- if (result[0]?.version) {
39
- return;
40
- }
41
- } catch (e) {
42
- if (Date.now() - startTime > 3e4) {
43
- throw new Error(
44
- `Timed out waiting for the database to be ready for connections, ${e}`
45
- );
46
- }
41
+ for (; ; ) {
42
+ try {
43
+ const store = new KeyvMemcache__default.default(connection);
44
+ const keyv = new Keyv__default.default({ store });
45
+ const value = uuid.v4();
46
+ await keyv.set("test", value);
47
+ if (await keyv.get("test") === value) {
48
+ return keyv;
49
+ }
50
+ } catch (e) {
51
+ if (Date.now() - startTime > 3e4) {
52
+ throw new Error(
53
+ `Timed out waiting for memcached to be ready for connections, ${e}`
54
+ );
47
55
  }
48
- await new Promise((resolve) => setTimeout(resolve, 100));
49
56
  }
50
- } finally {
51
- db.destroy();
57
+ await new Promise((resolve) => setTimeout(resolve, 100));
52
58
  }
53
59
  }
54
- async function startMysqlContainer(image) {
55
- const user = "root";
56
- const password = uuid.v4();
60
+ async function connectToExternalMemcache(connection) {
61
+ const keyv = await attemptMemcachedConnection(connection);
62
+ return {
63
+ store: "memcache",
64
+ connection,
65
+ keyv,
66
+ stop: async () => await keyv.disconnect()
67
+ };
68
+ }
69
+ async function startMemcachedContainer(image) {
57
70
  const { GenericContainer } = await import('testcontainers');
58
- const container = await new GenericContainer(image).withExposedPorts(3306).withEnvironment({ MYSQL_ROOT_PASSWORD: password }).withTmpFs({ "/var/lib/mysql": "rw" }).start();
71
+ const container = await new GenericContainer(image).withExposedPorts(11211).start();
59
72
  const host = container.getHost();
60
- const port = container.getMappedPort(3306);
61
- const stop = async () => {
62
- await container.stop({ timeout: 1e4 });
73
+ const port = container.getMappedPort(11211);
74
+ const connection = `${host}:${port}`;
75
+ const keyv = await attemptMemcachedConnection(connection);
76
+ return {
77
+ store: "memcache",
78
+ connection,
79
+ keyv,
80
+ stop: async () => {
81
+ await keyv.disconnect();
82
+ await container.stop({ timeout: 1e4 });
83
+ }
63
84
  };
64
- await waitForMysqlReady({ host, port, user, password });
65
- return { host, port, user, password, stop };
66
85
  }
67
86
 
68
- async function waitForPostgresReady(connection) {
87
+ async function attemptRedisConnection(connection) {
69
88
  const startTime = Date.now();
70
- const db = createConnection__default.default({ client: "pg", connection });
71
- try {
72
- for (; ; ) {
73
- try {
74
- const result = await db.select(db.raw("version()"));
75
- if (Array.isArray(result) && result[0]?.version) {
76
- return;
77
- }
78
- } catch (e) {
79
- if (Date.now() - startTime > 3e4) {
80
- throw new Error(
81
- `Timed out waiting for the database to be ready for connections, ${e}`
82
- );
83
- }
89
+ for (; ; ) {
90
+ try {
91
+ const store = new KeyvRedis__default.default(connection);
92
+ const keyv = new Keyv__default.default({ store });
93
+ const value = uuid.v4();
94
+ await keyv.set("test", value);
95
+ if (await keyv.get("test") === value) {
96
+ return keyv;
97
+ }
98
+ } catch (e) {
99
+ if (Date.now() - startTime > 3e4) {
100
+ throw new Error(
101
+ `Timed out waiting for redis to be ready for connections, ${e}`
102
+ );
84
103
  }
85
- await new Promise((resolve) => setTimeout(resolve, 100));
86
104
  }
87
- } finally {
88
- db.destroy();
105
+ await new Promise((resolve) => setTimeout(resolve, 100));
89
106
  }
90
107
  }
91
- async function startPostgresContainer(image) {
92
- const user = "postgres";
93
- const password = uuid.v4();
108
+ async function connectToExternalRedis(connection) {
109
+ const keyv = await attemptRedisConnection(connection);
110
+ return {
111
+ store: "redis",
112
+ connection,
113
+ keyv,
114
+ stop: async () => await keyv.disconnect()
115
+ };
116
+ }
117
+ async function startRedisContainer(image) {
94
118
  const { GenericContainer } = await import('testcontainers');
95
- const container = await new GenericContainer(image).withExposedPorts(5432).withEnvironment({ POSTGRES_PASSWORD: password }).withTmpFs({ "/var/lib/postgresql/data": "rw" }).start();
119
+ const container = await new GenericContainer(image).withExposedPorts(6379).start();
96
120
  const host = container.getHost();
97
- const port = container.getMappedPort(5432);
98
- const stop = async () => {
99
- await container.stop({ timeout: 1e4 });
121
+ const port = container.getMappedPort(6379);
122
+ const connection = `redis://${host}:${port}`;
123
+ const keyv = await attemptRedisConnection(connection);
124
+ return {
125
+ store: "redis",
126
+ connection,
127
+ keyv,
128
+ stop: async () => {
129
+ await keyv.disconnect();
130
+ await container.stop({ timeout: 1e4 });
131
+ }
100
132
  };
101
- await waitForPostgresReady({ host, port, user, password });
102
- return { host, port, user, password, stop };
103
133
  }
104
134
 
105
135
  const getDockerImageForName = (name) => {
106
136
  return process.env.BACKSTAGE_TEST_DOCKER_REGISTRY ? `${process.env.BACKSTAGE_TEST_DOCKER_REGISTRY}/${name}` : name;
107
137
  };
108
138
 
139
+ const allCaches = Object.freeze({
140
+ REDIS_7: {
141
+ name: "Redis 7.x",
142
+ store: "redis",
143
+ dockerImageName: getDockerImageForName("redis:7"),
144
+ connectionStringEnvironmentVariableName: "BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING"
145
+ },
146
+ MEMCACHED_1: {
147
+ name: "Memcached 1.x",
148
+ store: "memcache",
149
+ dockerImageName: getDockerImageForName("memcached:1"),
150
+ connectionStringEnvironmentVariableName: "BACKSTAGE_TEST_CACHE_MEMCACHED1_CONNECTION_STRING"
151
+ },
152
+ MEMORY: {
153
+ name: "In-memory",
154
+ store: "memory"
155
+ }
156
+ });
157
+
158
+ class TestCaches {
159
+ instanceById;
160
+ supportedIds;
161
+ static defaultIds;
162
+ /**
163
+ * Creates an empty `TestCaches` instance, and sets up Jest to clean up all of
164
+ * its acquired resources after all tests finish.
165
+ *
166
+ * You typically want to create just a single instance like this at the top of
167
+ * your test file or `describe` block, and then call `init` many times on that
168
+ * instance inside the individual tests. Spinning up a "physical" cache
169
+ * instance takes a considerable amount of time, slowing down tests. But
170
+ * wiping the contents of an instance using `init` is very fast.
171
+ */
172
+ static create(options) {
173
+ const ids = options?.ids;
174
+ const disableDocker = options?.disableDocker ?? isDockerDisabledForTests();
175
+ let testCacheIds;
176
+ if (ids) {
177
+ testCacheIds = ids;
178
+ } else if (TestCaches.defaultIds) {
179
+ testCacheIds = TestCaches.defaultIds;
180
+ } else {
181
+ testCacheIds = Object.keys(allCaches);
182
+ }
183
+ const supportedIds = testCacheIds.filter((id) => {
184
+ const properties = allCaches[id];
185
+ if (!properties) {
186
+ return false;
187
+ }
188
+ if (properties.connectionStringEnvironmentVariableName && process.env[properties.connectionStringEnvironmentVariableName]) {
189
+ return true;
190
+ }
191
+ if (!properties.dockerImageName) {
192
+ return true;
193
+ }
194
+ if (disableDocker) {
195
+ return false;
196
+ }
197
+ return true;
198
+ });
199
+ const caches = new TestCaches(supportedIds);
200
+ if (supportedIds.length > 0) {
201
+ afterAll(async () => {
202
+ await caches.shutdown();
203
+ });
204
+ }
205
+ return caches;
206
+ }
207
+ static setDefaults(options) {
208
+ TestCaches.defaultIds = options.ids;
209
+ }
210
+ constructor(supportedIds) {
211
+ this.instanceById = /* @__PURE__ */ new Map();
212
+ this.supportedIds = supportedIds;
213
+ }
214
+ supports(id) {
215
+ return this.supportedIds.includes(id);
216
+ }
217
+ eachSupportedId() {
218
+ return this.supportedIds.map((id) => [id]);
219
+ }
220
+ /**
221
+ * Returns a fresh, empty cache for the given driver.
222
+ *
223
+ * @param id - The ID of the cache to use, e.g. 'REDIS_7'
224
+ * @returns Cache connection properties
225
+ */
226
+ async init(id) {
227
+ const properties = allCaches[id];
228
+ if (!properties) {
229
+ const candidates = Object.keys(allCaches).join(", ");
230
+ throw new Error(
231
+ `Unknown test cache ${id}, possible values are ${candidates}`
232
+ );
233
+ }
234
+ if (!this.supportedIds.includes(id)) {
235
+ const candidates = this.supportedIds.join(", ");
236
+ throw new Error(
237
+ `Unsupported test cache ${id} for this environment, possible values are ${candidates}`
238
+ );
239
+ }
240
+ let instance = this.instanceById.get(id);
241
+ if (!instance) {
242
+ instance = await this.initAny(properties);
243
+ this.instanceById.set(id, instance);
244
+ }
245
+ await instance.keyv.clear();
246
+ return {
247
+ store: instance.store,
248
+ connection: instance.connection,
249
+ keyv: instance.keyv
250
+ };
251
+ }
252
+ async initAny(properties) {
253
+ switch (properties.store) {
254
+ case "memcache":
255
+ return this.initMemcached(properties);
256
+ case "redis":
257
+ return this.initRedis(properties);
258
+ case "memory":
259
+ return {
260
+ store: "memory",
261
+ connection: "memory",
262
+ keyv: new Keyv__default.default(),
263
+ stop: async () => {
264
+ }
265
+ };
266
+ default:
267
+ throw new Error(`Unknown cache store '${properties.store}'`);
268
+ }
269
+ }
270
+ async initMemcached(properties) {
271
+ const envVarName = properties.connectionStringEnvironmentVariableName;
272
+ if (envVarName) {
273
+ const connectionString = process.env[envVarName];
274
+ if (connectionString) {
275
+ return connectToExternalMemcache(connectionString);
276
+ }
277
+ }
278
+ return await startMemcachedContainer(properties.dockerImageName);
279
+ }
280
+ async initRedis(properties) {
281
+ const envVarName = properties.connectionStringEnvironmentVariableName;
282
+ if (envVarName) {
283
+ const connectionString = process.env[envVarName];
284
+ if (connectionString) {
285
+ return connectToExternalRedis(connectionString);
286
+ }
287
+ }
288
+ return await startRedisContainer(properties.dockerImageName);
289
+ }
290
+ async shutdown() {
291
+ const instances = [...this.instanceById.values()];
292
+ this.instanceById.clear();
293
+ await Promise.all(
294
+ instances.map(
295
+ ({ stop }) => stop().catch((error) => {
296
+ console.warn(`TestCaches: Failed to stop container`, { error });
297
+ })
298
+ )
299
+ );
300
+ }
301
+ }
302
+
109
303
  const allDatabases = Object.freeze({
110
304
  POSTGRES_16: {
111
305
  name: "Postgres 16.x",
@@ -160,15 +354,345 @@ const allDatabases = Object.freeze({
160
354
  driver: "better-sqlite3"
161
355
  }
162
356
  });
163
-
164
357
  const LARGER_POOL_CONFIG = {
165
358
  pool: {
166
359
  min: 0,
167
360
  max: 50
168
361
  }
169
362
  };
363
+
364
+ async function waitForMysqlReady(connection) {
365
+ const startTime = Date.now();
366
+ let lastError;
367
+ let attempts = 0;
368
+ for (; ; ) {
369
+ attempts += 1;
370
+ let knex;
371
+ try {
372
+ knex = knexFactory__default.default({
373
+ client: "mysql2",
374
+ connection: {
375
+ // make a copy because the driver mutates this
376
+ ...connection
377
+ }
378
+ });
379
+ const result = await knex.select(knex.raw("version() AS version"));
380
+ if (Array.isArray(result) && result[0]?.version) {
381
+ return;
382
+ }
383
+ } catch (e) {
384
+ lastError = e;
385
+ } finally {
386
+ await knex?.destroy();
387
+ }
388
+ if (Date.now() - startTime > 3e4) {
389
+ throw new Error(
390
+ `Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${lastError ? `last error was ${errors.stringifyError(lastError)}` : "(no errors thrown)"}`
391
+ );
392
+ }
393
+ await new Promise((resolve) => setTimeout(resolve, 100));
394
+ }
395
+ }
396
+ async function startMysqlContainer(image) {
397
+ const user = "root";
398
+ const password = uuid.v4();
399
+ const { GenericContainer } = await import('testcontainers');
400
+ const container = await new GenericContainer(image).withExposedPorts(3306).withEnvironment({ MYSQL_ROOT_PASSWORD: password }).withTmpFs({ "/var/lib/mysql": "rw" }).start();
401
+ const host = container.getHost();
402
+ const port = container.getMappedPort(3306);
403
+ const connection = { host, port, user, password };
404
+ const stopContainer = async () => {
405
+ await container.stop({ timeout: 1e4 });
406
+ };
407
+ await waitForMysqlReady(connection);
408
+ return { connection, stopContainer };
409
+ }
410
+ function parseMysqlConnectionString(connectionString) {
411
+ try {
412
+ const {
413
+ protocol,
414
+ username,
415
+ password,
416
+ port,
417
+ hostname,
418
+ pathname,
419
+ searchParams
420
+ } = new URL(connectionString);
421
+ if (protocol !== "mysql:") {
422
+ throw new Error(`Unknown protocol ${protocol}`);
423
+ } else if (!username || !password) {
424
+ throw new Error(`Missing username/password`);
425
+ } else if (!pathname.match(/^\/[^/]+$/)) {
426
+ throw new Error(`Expected single path segment`);
427
+ }
428
+ const result = {
429
+ user: username,
430
+ password,
431
+ host: hostname,
432
+ port: Number(port || 3306),
433
+ database: decodeURIComponent(pathname.substring(1))
434
+ };
435
+ const ssl = searchParams.get("ssl");
436
+ if (ssl) {
437
+ result.ssl = ssl;
438
+ }
439
+ const debug = searchParams.get("debug");
440
+ if (debug) {
441
+ result.debug = yn__default.default(debug);
442
+ }
443
+ return result;
444
+ } catch (e) {
445
+ throw new Error(`Error while parsing MySQL connection string, ${e}`, e);
446
+ }
447
+ }
448
+ class MysqlEngine {
449
+ static async create(properties) {
450
+ const { connectionStringEnvironmentVariableName, dockerImageName } = properties;
451
+ if (connectionStringEnvironmentVariableName) {
452
+ const connectionString = process.env[connectionStringEnvironmentVariableName];
453
+ if (connectionString) {
454
+ const connection = parseMysqlConnectionString(connectionString);
455
+ return new MysqlEngine(
456
+ properties,
457
+ connection
458
+ );
459
+ }
460
+ }
461
+ if (dockerImageName) {
462
+ const { connection, stopContainer } = await startMysqlContainer(
463
+ dockerImageName
464
+ );
465
+ return new MysqlEngine(properties, connection, stopContainer);
466
+ }
467
+ throw new Error(`Test databasee for ${properties.name} not configured`);
468
+ }
469
+ #properties;
470
+ #connection;
471
+ #knexInstances;
472
+ #databaseNames;
473
+ #stopContainer;
474
+ constructor(properties, connection, stopContainer) {
475
+ this.#properties = properties;
476
+ this.#connection = connection;
477
+ this.#knexInstances = [];
478
+ this.#databaseNames = [];
479
+ this.#stopContainer = stopContainer;
480
+ }
481
+ async createDatabaseInstance() {
482
+ const adminConnection = this.#connectAdmin();
483
+ try {
484
+ const databaseName = `db${crypto.randomBytes(16).toString("hex")}`;
485
+ await adminConnection.raw("CREATE DATABASE ??", [databaseName]);
486
+ this.#databaseNames.push(databaseName);
487
+ const knexInstance = knexFactory__default.default({
488
+ client: this.#properties.driver,
489
+ connection: {
490
+ ...this.#connection,
491
+ database: databaseName
492
+ },
493
+ ...LARGER_POOL_CONFIG
494
+ });
495
+ this.#knexInstances.push(knexInstance);
496
+ return knexInstance;
497
+ } finally {
498
+ await adminConnection.destroy();
499
+ }
500
+ }
501
+ async shutdown() {
502
+ for (const instance of this.#knexInstances) {
503
+ await instance.destroy();
504
+ }
505
+ const adminConnection = this.#connectAdmin();
506
+ try {
507
+ for (const databaseName of this.#databaseNames) {
508
+ await adminConnection.raw("DROP DATABASE ??", [databaseName]);
509
+ }
510
+ } finally {
511
+ await adminConnection.destroy();
512
+ }
513
+ await this.#stopContainer?.();
514
+ }
515
+ #connectAdmin() {
516
+ const connection = {
517
+ ...this.#connection,
518
+ database: null
519
+ };
520
+ return knexFactory__default.default({
521
+ client: this.#properties.driver,
522
+ connection,
523
+ pool: {
524
+ acquireTimeoutMillis: 1e4
525
+ }
526
+ });
527
+ }
528
+ }
529
+
530
+ async function waitForPostgresReady(connection) {
531
+ const startTime = Date.now();
532
+ let lastError;
533
+ let attempts = 0;
534
+ for (; ; ) {
535
+ attempts += 1;
536
+ let knex;
537
+ try {
538
+ knex = knexFactory__default.default({
539
+ client: "pg",
540
+ connection: {
541
+ // make a copy because the driver mutates this
542
+ ...connection
543
+ }
544
+ });
545
+ const result = await knex.select(knex.raw("version()"));
546
+ if (Array.isArray(result) && result[0]?.version) {
547
+ return;
548
+ }
549
+ } catch (e) {
550
+ lastError = e;
551
+ } finally {
552
+ await knex?.destroy();
553
+ }
554
+ if (Date.now() - startTime > 3e4) {
555
+ throw new Error(
556
+ `Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${lastError ? `last error was ${errors.stringifyError(lastError)}` : "(no errors thrown)"}`
557
+ );
558
+ }
559
+ await new Promise((resolve) => setTimeout(resolve, 100));
560
+ }
561
+ }
562
+ async function startPostgresContainer(image) {
563
+ const user = "postgres";
564
+ const password = uuid.v4();
565
+ const { GenericContainer } = await import('testcontainers');
566
+ const container = await new GenericContainer(image).withExposedPorts(5432).withEnvironment({ POSTGRES_PASSWORD: password }).withTmpFs({ "/var/lib/postgresql/data": "rw" }).start();
567
+ const host = container.getHost();
568
+ const port = container.getMappedPort(5432);
569
+ const connection = { host, port, user, password };
570
+ const stopContainer = async () => {
571
+ await container.stop({ timeout: 1e4 });
572
+ };
573
+ await waitForPostgresReady(connection);
574
+ return { connection, stopContainer };
575
+ }
576
+ class PostgresEngine {
577
+ static async create(properties) {
578
+ const { connectionStringEnvironmentVariableName, dockerImageName } = properties;
579
+ if (connectionStringEnvironmentVariableName) {
580
+ const connectionString = process.env[connectionStringEnvironmentVariableName];
581
+ if (connectionString) {
582
+ const connection = pgConnectionString.parse(connectionString);
583
+ return new PostgresEngine(
584
+ properties,
585
+ connection
586
+ );
587
+ }
588
+ }
589
+ if (dockerImageName) {
590
+ const { connection, stopContainer } = await startPostgresContainer(
591
+ dockerImageName
592
+ );
593
+ return new PostgresEngine(properties, connection, stopContainer);
594
+ }
595
+ throw new Error(`Test databasee for ${properties.name} not configured`);
596
+ }
597
+ #properties;
598
+ #connection;
599
+ #knexInstances;
600
+ #databaseNames;
601
+ #stopContainer;
602
+ constructor(properties, connection, stopContainer) {
603
+ this.#properties = properties;
604
+ this.#connection = connection;
605
+ this.#knexInstances = [];
606
+ this.#databaseNames = [];
607
+ this.#stopContainer = stopContainer;
608
+ }
609
+ async createDatabaseInstance() {
610
+ const adminConnection = this.#connectAdmin();
611
+ try {
612
+ const databaseName = `db${crypto.randomBytes(16).toString("hex")}`;
613
+ await adminConnection.raw("CREATE DATABASE ??", [databaseName]);
614
+ this.#databaseNames.push(databaseName);
615
+ const knexInstance = knexFactory__default.default({
616
+ client: this.#properties.driver,
617
+ connection: {
618
+ ...this.#connection,
619
+ database: databaseName
620
+ },
621
+ ...LARGER_POOL_CONFIG
622
+ });
623
+ this.#knexInstances.push(knexInstance);
624
+ return knexInstance;
625
+ } finally {
626
+ await adminConnection.destroy();
627
+ }
628
+ }
629
+ async shutdown() {
630
+ for (const instance of this.#knexInstances) {
631
+ await instance.destroy();
632
+ }
633
+ const adminConnection = this.#connectAdmin();
634
+ try {
635
+ for (const databaseName of this.#databaseNames) {
636
+ await adminConnection.raw("DROP DATABASE ??", [databaseName]);
637
+ }
638
+ } finally {
639
+ await adminConnection.destroy();
640
+ }
641
+ await this.#stopContainer?.();
642
+ }
643
+ #connectAdmin() {
644
+ return knexFactory__default.default({
645
+ client: this.#properties.driver,
646
+ connection: {
647
+ ...this.#connection,
648
+ database: "postgres"
649
+ },
650
+ pool: {
651
+ acquireTimeoutMillis: 1e4
652
+ }
653
+ });
654
+ }
655
+ }
656
+
657
+ class SqliteEngine {
658
+ static async create(properties) {
659
+ return new SqliteEngine(properties);
660
+ }
661
+ #properties;
662
+ #instances;
663
+ constructor(properties) {
664
+ this.#properties = properties;
665
+ this.#instances = [];
666
+ }
667
+ async createDatabaseInstance() {
668
+ const instance = knexFactory__default.default({
669
+ client: this.#properties.driver,
670
+ connection: ":memory:",
671
+ useNullAsDefault: true
672
+ });
673
+ instance.client.pool.on("createSuccess", (_eventId, resource) => {
674
+ resource.run("PRAGMA foreign_keys = ON", () => {
675
+ });
676
+ });
677
+ this.#instances.push(instance);
678
+ return instance;
679
+ }
680
+ async shutdown() {
681
+ for (const instance of this.#instances) {
682
+ await instance.destroy();
683
+ }
684
+ }
685
+ }
686
+
170
687
  class TestDatabases {
171
- instanceById;
688
+ engineFactoryByDriver = {
689
+ pg: PostgresEngine.create,
690
+ mysql: MysqlEngine.create,
691
+ mysql2: MysqlEngine.create,
692
+ "better-sqlite3": SqliteEngine.create,
693
+ sqlite3: SqliteEngine.create
694
+ };
695
+ engineByTestDatabaseId;
172
696
  supportedIds;
173
697
  static defaultIds;
174
698
  /**
@@ -221,7 +745,7 @@ class TestDatabases {
221
745
  TestDatabases.defaultIds = options.ids;
222
746
  }
223
747
  constructor(supportedIds) {
224
- this.instanceById = /* @__PURE__ */ new Map();
748
+ this.engineByTestDatabaseId = /* @__PURE__ */ new Map();
225
749
  this.supportedIds = supportedIds;
226
750
  }
227
751
  supports(id) {
@@ -251,154 +775,26 @@ class TestDatabases {
251
775
  `Unsupported test database ${id} for this environment, possible values are ${candidates}`
252
776
  );
253
777
  }
254
- let instance = this.instanceById.get(id);
255
- if (!instance) {
256
- instance = await this.initAny(properties);
257
- this.instanceById.set(id, instance);
258
- }
259
- const databaseName = `db${crypto.randomBytes(16).toString("hex")}`;
260
- const connection = await instance.databaseManager.forPlugin(databaseName).getClient();
261
- instance.connections.push(connection);
262
- instance.databaseNames.push(databaseName);
263
- return connection;
264
- }
265
- async initAny(properties) {
266
- if (properties.driver === "pg" || properties.driver === "mysql2") {
267
- const envVarName = properties.connectionStringEnvironmentVariableName;
268
- if (envVarName) {
269
- const connectionString = process.env[envVarName];
270
- if (connectionString) {
271
- const config$1 = new config.ConfigReader({
272
- backend: {
273
- database: {
274
- knexConfig: properties.driver.includes("sqlite") ? {} : LARGER_POOL_CONFIG,
275
- client: properties.driver,
276
- connection: connectionString
277
- }
278
- }
279
- });
280
- const databaseManager = backendCommon.DatabaseManager.fromConfig(config$1);
281
- const databaseNames = [];
282
- return {
283
- dropDatabases: async () => {
284
- await backendCommon.dropDatabase(
285
- config$1.getConfig("backend.database"),
286
- ...databaseNames.map(
287
- (databaseName) => `backstage_plugin_${databaseName}`
288
- )
289
- );
290
- },
291
- databaseManager,
292
- databaseNames,
293
- connections: []
294
- };
295
- }
296
- }
297
- }
298
- switch (properties.driver) {
299
- case "pg":
300
- return this.initPostgres(properties);
301
- case "mysql2":
302
- return this.initMysql(properties);
303
- case "better-sqlite3":
304
- case "sqlite3":
305
- return this.initSqlite(properties);
306
- default:
778
+ let engine = this.engineByTestDatabaseId.get(id);
779
+ if (!engine) {
780
+ const factory = this.engineFactoryByDriver[properties.driver];
781
+ if (!factory) {
307
782
  throw new Error(`Unknown database driver ${properties.driver}`);
783
+ }
784
+ engine = await factory(properties);
785
+ this.engineByTestDatabaseId.set(id, engine);
308
786
  }
309
- }
310
- async initPostgres(properties) {
311
- const { host, port, user, password, stop } = await startPostgresContainer(
312
- properties.dockerImageName
313
- );
314
- const databaseManager = backendCommon.DatabaseManager.fromConfig(
315
- new config.ConfigReader({
316
- backend: {
317
- database: {
318
- knexConfig: LARGER_POOL_CONFIG,
319
- client: "pg",
320
- connection: { host, port, user, password }
321
- }
322
- }
323
- })
324
- );
325
- return {
326
- stopContainer: stop,
327
- databaseManager,
328
- databaseNames: [],
329
- connections: []
330
- };
331
- }
332
- async initMysql(properties) {
333
- const { host, port, user, password, stop } = await startMysqlContainer(
334
- properties.dockerImageName
335
- );
336
- const databaseManager = backendCommon.DatabaseManager.fromConfig(
337
- new config.ConfigReader({
338
- backend: {
339
- database: {
340
- knexConfig: LARGER_POOL_CONFIG,
341
- client: "mysql2",
342
- connection: { host, port, user, password }
343
- }
344
- }
345
- })
346
- );
347
- return {
348
- stopContainer: stop,
349
- databaseManager,
350
- databaseNames: [],
351
- connections: []
352
- };
353
- }
354
- async initSqlite(properties) {
355
- const databaseManager = backendCommon.DatabaseManager.fromConfig(
356
- new config.ConfigReader({
357
- backend: {
358
- database: {
359
- client: properties.driver,
360
- connection: ":memory:"
361
- }
362
- }
363
- })
364
- );
365
- return {
366
- databaseManager,
367
- databaseNames: [],
368
- connections: []
369
- };
787
+ return await engine.createDatabaseInstance();
370
788
  }
371
789
  async shutdown() {
372
- const instances = [...this.instanceById.values()];
373
- this.instanceById.clear();
374
- for (const {
375
- stopContainer,
376
- dropDatabases,
377
- connections,
378
- databaseManager
379
- } of instances) {
380
- for (const connection of connections) {
381
- try {
382
- await connection.destroy();
383
- } catch (error) {
384
- console.warn(`TestDatabases: Failed to destroy connection`, {
385
- connection,
386
- error
387
- });
388
- }
389
- }
790
+ const engines = [...this.engineByTestDatabaseId.values()];
791
+ this.engineByTestDatabaseId.clear();
792
+ for (const engine of engines) {
390
793
  try {
391
- await dropDatabases?.();
794
+ await engine.shutdown();
392
795
  } catch (error) {
393
- console.warn(`TestDatabases: Failed to drop databases`, {
394
- error
395
- });
396
- }
397
- try {
398
- await stopContainer?.();
399
- } catch (error) {
400
- console.warn(`TestDatabases: Failed to stop container`, {
401
- databaseManager,
796
+ console.warn(`TestDatabases: Failed to shutdown engine`, {
797
+ engine,
402
798
  error
403
799
  });
404
800
  }
@@ -697,10 +1093,14 @@ exports.mockCredentials = void 0;
697
1093
  }
698
1094
  limitedUser2.invalidCookie = invalidCookie;
699
1095
  })(limitedUser = mockCredentials2.limitedUser || (mockCredentials2.limitedUser = {}));
700
- function service(subject = DEFAULT_MOCK_SERVICE_SUBJECT) {
1096
+ function service(subject = DEFAULT_MOCK_SERVICE_SUBJECT, accessRestrictions) {
701
1097
  return {
702
1098
  $$type: "@backstage/BackstageCredentials",
703
- principal: { type: "service", subject }
1099
+ principal: {
1100
+ type: "service",
1101
+ subject,
1102
+ ...accessRestrictions ? { accessRestrictions } : {}
1103
+ }
704
1104
  };
705
1105
  }
706
1106
  mockCredentials2.service = service;
@@ -1876,6 +2276,7 @@ class ServiceFactoryTester {
1876
2276
  }
1877
2277
 
1878
2278
  exports.ServiceFactoryTester = ServiceFactoryTester;
2279
+ exports.TestCaches = TestCaches;
1879
2280
  exports.TestDatabases = TestDatabases;
1880
2281
  exports.createMockDirectory = createMockDirectory;
1881
2282
  exports.isDockerDisabledForTests = isDockerDisabledForTests;