@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/CHANGELOG.md +30 -0
- package/dist/index.cjs.js +608 -207
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +63 -11
- package/package.json +13 -8
package/dist/index.cjs.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
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
|
|
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
|
|
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
|
|
39
|
+
async function attemptMemcachedConnection(connection) {
|
|
32
40
|
const startTime = Date.now();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
db.destroy();
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
|
-
async function
|
|
55
|
-
const
|
|
56
|
-
|
|
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(
|
|
71
|
+
const container = await new GenericContainer(image).withExposedPorts(11211).start();
|
|
59
72
|
const host = container.getHost();
|
|
60
|
-
const port = container.getMappedPort(
|
|
61
|
-
const
|
|
62
|
-
|
|
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
|
|
87
|
+
async function attemptRedisConnection(connection) {
|
|
69
88
|
const startTime = Date.now();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
88
|
-
db.destroy();
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
89
106
|
}
|
|
90
107
|
}
|
|
91
|
-
async function
|
|
92
|
-
const
|
|
93
|
-
|
|
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(
|
|
119
|
+
const container = await new GenericContainer(image).withExposedPorts(6379).start();
|
|
96
120
|
const host = container.getHost();
|
|
97
|
-
const port = container.getMappedPort(
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
255
|
-
if (!
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
373
|
-
this.
|
|
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
|
|
794
|
+
await engine.shutdown();
|
|
392
795
|
} catch (error) {
|
|
393
|
-
console.warn(`TestDatabases: Failed to
|
|
394
|
-
|
|
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: {
|
|
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;
|