@red-hat-developer-hub/e2e-test-utils 1.1.15 → 1.1.17
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/README.md +7 -0
- package/dist/deployment/keycloak/deployment.d.ts +4 -0
- package/dist/deployment/keycloak/deployment.d.ts.map +1 -1
- package/dist/deployment/keycloak/deployment.js +16 -0
- package/dist/deployment/keycloak/index.d.ts +1 -0
- package/dist/deployment/keycloak/index.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +9 -4
- package/dist/deployment/rhdh/deployment.test.d.ts +2 -0
- package/dist/deployment/rhdh/deployment.test.d.ts.map +1 -0
- package/dist/deployment/rhdh/deployment.test.js +41 -0
- package/dist/eslint/base.config.d.ts.map +1 -1
- package/dist/eslint/base.config.js +7 -0
- package/dist/utils/merge-yamls.d.ts +3 -0
- package/dist/utils/merge-yamls.d.ts.map +1 -1
- package/dist/utils/merge-yamls.js +29 -18
- package/dist/utils/merge-yamls.test.d.ts +2 -0
- package/dist/utils/merge-yamls.test.d.ts.map +1 -0
- package/dist/utils/merge-yamls.test.js +54 -0
- package/dist/utils/plugin-metadata.d.ts +12 -0
- package/dist/utils/plugin-metadata.d.ts.map +1 -1
- package/dist/utils/plugin-metadata.js +16 -0
- package/dist/utils/plugin-metadata.test.d.ts +2 -0
- package/dist/utils/plugin-metadata.test.d.ts.map +1 -0
- package/dist/utils/plugin-metadata.test.js +39 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,3 +4,10 @@ Documentation is available online:
|
|
|
4
4
|
|
|
5
5
|
- Package documentation: https://redhat-developer.github.io/rhdh-e2e-test-utils/
|
|
6
6
|
- Overlay testing documentation: https://redhat-developer.github.io/rhdh-e2e-test-utils/overlay/
|
|
7
|
+
|
|
8
|
+
## Testing
|
|
9
|
+
|
|
10
|
+
Unit tests use Node’s built-in test runner (`node:test`) and are discovered by pattern.
|
|
11
|
+
|
|
12
|
+
- **Run tests:** `yarn test` (builds then runs all tests under `dist/` matching `**/*.test.js`).
|
|
13
|
+
- **Add or change tests:** Add a `*.test.ts` file next to the code under `src/` (e.g. `src/utils/foo.test.ts`). It is compiled to `dist/` and picked up automatically; no need to update the test script.
|
|
@@ -54,6 +54,10 @@ export declare class KeycloakHelper {
|
|
|
54
54
|
* Get all groups in a realm
|
|
55
55
|
*/
|
|
56
56
|
getGroups(realm: string): Promise<KeycloakGroupConfig[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Get groups for a user in a realm (user resolved by username).
|
|
59
|
+
*/
|
|
60
|
+
getGroupsOfUser(realm: string, username: string): Promise<KeycloakGroupConfig[]>;
|
|
57
61
|
/**
|
|
58
62
|
* Delete a user from a realm
|
|
59
63
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/keycloak/deployment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAY1E,OAAO,KAAK,EACV,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,gBAAgB,EAAE,wBAAwB,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAM;IACzB,KAAK,EAAE,MAAM,CAAM;IACnB,QAAQ,EAAE,MAAM,CAAM;IACtB,YAAY,EAAE,MAAM,CAAM;IACjC,OAAO,CAAC,YAAY,CAAoC;gBAE5C,OAAO,GAAE,yBAA8B;IAInD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAa7B;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAUnC;;OAEG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;QACvC,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAC;QAC/B,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCjB;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB9D;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB7D;;OAEG;IACG,YAAY,CAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IAqChB;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5E;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA8C1E;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAe5D;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAQ9D;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWhE;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlE;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/C;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B;;OAEG;IACG,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,OAAO,CAAC,sBAAsB;YAahB,eAAe;YAWf,YAAY;IAwBpB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;YAO3B,gBAAgB;YAoBhB,sBAAsB;YActB,kBAAkB;YAQlB,0BAA0B;YAmD1B,eAAe;IAqB7B,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,IAAI;CAGb"}
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/keycloak/deployment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAY1E,OAAO,KAAK,EACV,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,gBAAgB,EAAE,wBAAwB,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAM;IACzB,KAAK,EAAE,MAAM,CAAM;IACnB,QAAQ,EAAE,MAAM,CAAM;IACtB,YAAY,EAAE,MAAM,CAAM;IACjC,OAAO,CAAC,YAAY,CAAoC;gBAE5C,OAAO,GAAE,yBAA8B;IAInD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAa7B;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAUnC;;OAEG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;QACvC,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAC;QAC/B,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCjB;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB9D;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB7D;;OAEG;IACG,YAAY,CAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IAqChB;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5E;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA8C1E;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAe5D;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAQ9D;;OAEG;IACG,eAAe,CACnB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAejC;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWhE;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlE;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/C;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B;;OAEG;IACG,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,OAAO,CAAC,sBAAsB;YAahB,eAAe;YAWf,YAAY;IAwBpB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;YAO3B,gBAAgB;YAoBhB,sBAAsB;YActB,kBAAkB;YAQlB,0BAA0B;YAmD1B,eAAe;IAqB7B,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,IAAI;CAGb"}
|
|
@@ -245,6 +245,22 @@ export class KeycloakHelper {
|
|
|
245
245
|
const groups = await this._adminClient.groups.find();
|
|
246
246
|
return groups.map((g) => ({ name: g.name }));
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Get groups for a user in a realm (user resolved by username).
|
|
250
|
+
*/
|
|
251
|
+
async getGroupsOfUser(realm, username) {
|
|
252
|
+
await this._ensureAdminClient();
|
|
253
|
+
this._adminClient.setConfig({ realmName: realm });
|
|
254
|
+
const users = await this._adminClient.users.find({ username });
|
|
255
|
+
if (users.length === 0) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const user = users[0];
|
|
259
|
+
const groups = await this._adminClient.users.listGroups({
|
|
260
|
+
id: user.id,
|
|
261
|
+
});
|
|
262
|
+
return groups.map((g) => ({ name: g.name }));
|
|
263
|
+
}
|
|
248
264
|
/**
|
|
249
265
|
* Delete a user from a realm
|
|
250
266
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deployment/keycloak/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deployment/keycloak/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAmB1E,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;gBAE9B,SAAS,EAAE,MAAM;IAKvB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAuCpD,eAAe;YAmBf,aAAa;IAqB3B;;;OAGG;YACW,0BAA0B;YA6C1B,oBAAoB;YAWpB,eAAe;YAmCf,mBAAmB;IAiD3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAWrC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IAoCxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
|
|
@@ -3,7 +3,7 @@ import { $ } from "../../utils/bash.js";
|
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
4
|
import { test } from "@playwright/test";
|
|
5
5
|
import { mergeYamlFilesIfExists, deepMerge } from "../../utils/merge-yamls.js";
|
|
6
|
-
import { loadAndInjectPluginMetadata, generateDynamicPluginsConfigFromMetadata, } from "../../utils/plugin-metadata.js";
|
|
6
|
+
import { loadAndInjectPluginMetadata, generateDynamicPluginsConfigFromMetadata, getNormalizedPluginMergeKey, } from "../../utils/plugin-metadata.js";
|
|
7
7
|
import { envsubst } from "../../utils/common.js";
|
|
8
8
|
import { runOnce } from "../../playwright/run-once.js";
|
|
9
9
|
import cloneDeepWith from "lodash.clonedeepwith";
|
|
@@ -81,10 +81,15 @@ export class RHDHDeployment {
|
|
|
81
81
|
if (!userConfigExists) {
|
|
82
82
|
this._log(`Dynamic plugins config not found at '${userConfigPath}', auto-generating from metadata...`);
|
|
83
83
|
const metadataConfig = await generateDynamicPluginsConfigFromMetadata();
|
|
84
|
-
// Merge with package defaults and auth config
|
|
84
|
+
// Merge with package defaults and auth config. Use normalized plugin key so
|
|
85
|
+
// the same logical plugin (e.g. keycloak from metadata OCI + auth local path)
|
|
86
|
+
// is deduplicated; metadata (source) wins so OCI URL is kept on PR builds.
|
|
85
87
|
const authPlugins = await mergeYamlFilesIfExists([DEFAULT_CONFIG_PATHS.dynamicPlugins, authConfig.dynamicPlugins], { arrayMergeStrategy: { byKey: "package" } });
|
|
86
|
-
return deepMerge(
|
|
87
|
-
arrayMergeStrategy: {
|
|
88
|
+
return deepMerge(authPlugins, metadataConfig, {
|
|
89
|
+
arrayMergeStrategy: {
|
|
90
|
+
byKey: "package",
|
|
91
|
+
normalizeKey: (item) => getNormalizedPluginMergeKey(item),
|
|
92
|
+
},
|
|
88
93
|
});
|
|
89
94
|
}
|
|
90
95
|
// User config exists - merge provided configs and inject metadata for listed plugins only
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment.test.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { deepMerge } from "../../utils/merge-yamls.js";
|
|
4
|
+
import { getNormalizedPluginMergeKey } from "../../utils/plugin-metadata.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tests the merge behavior used when user dynamic-plugins config does not exist:
|
|
7
|
+
* auth config (e.g. keycloak) is merged with metadata config using normalized plugin key.
|
|
8
|
+
* Result must have exactly one entry per logical plugin; metadata (source) wins so OCI URL is kept.
|
|
9
|
+
*/
|
|
10
|
+
describe("dynamic-plugins merge (no user config path)", () => {
|
|
11
|
+
it("yields one keycloak plugin with OCI package when auth has local path and metadata has OCI", () => {
|
|
12
|
+
const authPlugins = {
|
|
13
|
+
plugins: [
|
|
14
|
+
{
|
|
15
|
+
package: "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic",
|
|
16
|
+
disabled: false,
|
|
17
|
+
pluginConfig: {},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
includes: ["dynamic-plugins.default.yaml"],
|
|
21
|
+
};
|
|
22
|
+
const metadataConfig = {
|
|
23
|
+
plugins: [
|
|
24
|
+
{
|
|
25
|
+
package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-catalog-backend-module-keycloak:pr_1980__3.16.0!backstage-community-plugin-catalog-backend-module-keycloak",
|
|
26
|
+
disabled: false,
|
|
27
|
+
pluginConfig: { catalog: { providers: { keycloakOrg: {} } } },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
const merged = deepMerge(authPlugins, metadataConfig, {
|
|
32
|
+
arrayMergeStrategy: {
|
|
33
|
+
byKey: "package",
|
|
34
|
+
normalizeKey: (item) => getNormalizedPluginMergeKey(item),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const plugins = merged.plugins;
|
|
38
|
+
assert.strictEqual(plugins.length, 1, "merged config must have exactly one keycloak plugin");
|
|
39
|
+
assert.ok(plugins[0].package?.startsWith("oci://"), "metadata (OCI) must win over auth local path");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.config.d.ts","sourceRoot":"","sources":["../../src/eslint/base.config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"base.config.d.ts","sourceRoot":"","sources":["../../src/eslint/base.config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAmN3E"}
|
|
@@ -209,5 +209,12 @@ export function createEslintConfig(tsconfigRootDir) {
|
|
|
209
209
|
"check-file/filename-naming-convention": "off",
|
|
210
210
|
},
|
|
211
211
|
},
|
|
212
|
+
// Node test runner (*.test.ts) - describe/it return promises the runner handles
|
|
213
|
+
{
|
|
214
|
+
files: ["**/*.test.ts"],
|
|
215
|
+
rules: {
|
|
216
|
+
"@typescript-eslint/no-floating-promises": "off",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
212
219
|
];
|
|
213
220
|
}
|
|
@@ -4,6 +4,8 @@ import yaml from "js-yaml";
|
|
|
4
4
|
*/
|
|
5
5
|
export type ArrayMergeStrategy = "replace" | "concat" | {
|
|
6
6
|
byKey: string;
|
|
7
|
+
/** Optional: normalize key for matching so different values (e.g. OCI vs local path) map to the same entry. Source wins when merging. */
|
|
8
|
+
normalizeKey?: (item: unknown) => string;
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
9
11
|
* Options for YAML merging.
|
|
@@ -14,6 +16,7 @@ export interface MergeOptions {
|
|
|
14
16
|
* - "replace": Replace arrays entirely (default)
|
|
15
17
|
* - "concat": Concatenate arrays
|
|
16
18
|
* - { byKey: "keyName" }: Merge arrays of objects by a specific key
|
|
19
|
+
* - { byKey: "keyName", normalizeKey }: Same, but match by normalized key (e.g. for plugin deduplication)
|
|
17
20
|
*/
|
|
18
21
|
arrayMergeStrategy?: ArrayMergeStrategy;
|
|
19
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-yamls.d.ts","sourceRoot":"","sources":["../../src/utils/merge-yamls.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,SAAS,CAAC;AAG3B;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,QAAQ,GACR;
|
|
1
|
+
{"version":3,"file":"merge-yamls.d.ts","sourceRoot":"","sources":["../../src/utils/merge-yamls.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,SAAS,CAAC;AAG3B;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,QAAQ,GACR;IACE,KAAK,EAAE,MAAM,CAAC;IACd,yIAAyI;IACzI,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,CAAC;CAC1C,CAAC;AAEN;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AA2DD;;;GAGG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,GAAE,YAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAUlC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CASlC;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,IAAI,CAAC,WAA+B,EACjD,YAAY,GAAE,YAAiB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
|
@@ -2,28 +2,39 @@ import fs from "fs-extra";
|
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
3
|
import mergeWith from "lodash.mergewith";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Returns the merge key for an item: normalized if normalizeKey is provided, else raw key value.
|
|
6
|
+
* Returns null if the item is not an object or has no key (and no normalizer is provided).
|
|
7
7
|
*/
|
|
8
|
-
function
|
|
8
|
+
function getMergeKey(item, key, normalizeKey) {
|
|
9
|
+
if (typeof item !== "object" || item === null) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (normalizeKey) {
|
|
13
|
+
return normalizeKey(item);
|
|
14
|
+
}
|
|
15
|
+
if (key in item) {
|
|
16
|
+
return String(item[key]);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Merges two arrays of objects by a specific key (optionally normalized).
|
|
22
|
+
* Objects with matching keys are deeply merged, new objects are appended. Source wins.
|
|
23
|
+
*/
|
|
24
|
+
function mergeArraysByKey(target, source, keyStrategy, mergeOptions) {
|
|
25
|
+
const { byKey: key, normalizeKey } = keyStrategy;
|
|
9
26
|
const result = [...target];
|
|
10
27
|
for (const srcItem of source) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
// Append new object
|
|
22
|
-
result.push(srcItem);
|
|
23
|
-
}
|
|
28
|
+
const srcKeyValue = getMergeKey(srcItem, key, normalizeKey);
|
|
29
|
+
if (srcKeyValue === null) {
|
|
30
|
+
result.push(srcItem);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const existingIndex = result.findIndex((item) => getMergeKey(item, key, normalizeKey) === srcKeyValue);
|
|
34
|
+
if (existingIndex !== -1) {
|
|
35
|
+
result[existingIndex] = deepMerge(result[existingIndex], srcItem, mergeOptions);
|
|
24
36
|
}
|
|
25
37
|
else {
|
|
26
|
-
// Non-object or missing key, append as-is
|
|
27
38
|
result.push(srcItem);
|
|
28
39
|
}
|
|
29
40
|
}
|
|
@@ -44,7 +55,7 @@ export function deepMerge(target, source, options = {}) {
|
|
|
44
55
|
return [...objValue, ...srcValue];
|
|
45
56
|
}
|
|
46
57
|
else if (typeof strategy === "object" && "byKey" in strategy) {
|
|
47
|
-
return mergeArraysByKey(objValue, srcValue, strategy
|
|
58
|
+
return mergeArraysByKey(objValue, srcValue, strategy, options);
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-yamls.test.d.ts","sourceRoot":"","sources":["../../src/utils/merge-yamls.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { deepMerge } from "./merge-yamls.js";
|
|
4
|
+
import { getNormalizedPluginMergeKey } from "./plugin-metadata.js";
|
|
5
|
+
describe("deepMerge with arrayMergeStrategy byKey", () => {
|
|
6
|
+
it("keeps two plugin entries when package values differ and no normalizeKey", () => {
|
|
7
|
+
const target = {
|
|
8
|
+
plugins: [
|
|
9
|
+
{
|
|
10
|
+
package: "oci://ghcr.io/org/repo/backstage-community-plugin-catalog-backend-module-keycloak:tag!alias",
|
|
11
|
+
disabled: false,
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
const source = {
|
|
16
|
+
plugins: [
|
|
17
|
+
{
|
|
18
|
+
package: "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic",
|
|
19
|
+
disabled: false,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
const result = deepMerge(target, source, {
|
|
24
|
+
arrayMergeStrategy: { byKey: "package" },
|
|
25
|
+
});
|
|
26
|
+
const plugins = result.plugins;
|
|
27
|
+
assert.strictEqual(plugins.length, 2, "without normalizeKey both entries are kept");
|
|
28
|
+
});
|
|
29
|
+
it("merges into one plugin when normalizeKey maps both to same key and source wins", () => {
|
|
30
|
+
const target = {
|
|
31
|
+
plugins: [
|
|
32
|
+
{
|
|
33
|
+
package: "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic",
|
|
34
|
+
disabled: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
const source = {
|
|
39
|
+
plugins: [
|
|
40
|
+
{
|
|
41
|
+
package: "oci://ghcr.io/org/repo/backstage-community-plugin-catalog-backend-module-keycloak:pr_1__1.0!keycloak",
|
|
42
|
+
disabled: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const normalizeKey = (item) => getNormalizedPluginMergeKey(item);
|
|
47
|
+
const result = deepMerge(target, source, {
|
|
48
|
+
arrayMergeStrategy: { byKey: "package", normalizeKey },
|
|
49
|
+
});
|
|
50
|
+
const plugins = result.plugins;
|
|
51
|
+
assert.strictEqual(plugins.length, 1, "same normalized key yields one entry");
|
|
52
|
+
assert.ok(plugins[0].package?.startsWith("oci://"), "source (OCI) wins over target (local path)");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -33,6 +33,18 @@ export declare function shouldInjectPluginMetadata(): boolean;
|
|
|
33
33
|
* @returns The extracted plugin name
|
|
34
34
|
*/
|
|
35
35
|
export declare function extractPluginName(packageRef: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Returns a stable merge key for a plugin entry so OCI and local path for the same
|
|
38
|
+
* logical plugin match when merging dynamic-plugins configs. Strips a trailing
|
|
39
|
+
* "-dynamic" so e.g. backstage-community-plugin-catalog-backend-module-keycloak-dynamic
|
|
40
|
+
* and ...-keycloak (from OCI) map to the same key.
|
|
41
|
+
*
|
|
42
|
+
* @param entry Plugin entry with optional package reference
|
|
43
|
+
* @returns Normalized key for merge deduplication, or empty string if package is missing
|
|
44
|
+
*/
|
|
45
|
+
export declare function getNormalizedPluginMergeKey(entry: {
|
|
46
|
+
package?: string;
|
|
47
|
+
}): string;
|
|
36
48
|
/**
|
|
37
49
|
* Default metadata directory path relative to the e2e-tests directory.
|
|
38
50
|
* Follows the same pattern as user config paths (e.g., tests/config/dynamic-plugins.yaml).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AA8HA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;CACpB;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAmBpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CA0BzB;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA4CD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,oBAAoB,EAAE,oBAAoB,EAC1C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACvC,oBAAoB,CAsCtB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wCAAwC,CAC5D,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAsD/B;AAED;;;;;;;;GAQG;AACH,wBAAsB,2BAA2B,CAC/C,oBAAoB,EAAE,oBAAoB,EAC1C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAuC/B"}
|
|
1
|
+
{"version":3,"file":"plugin-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.ts"],"names":[],"mappings":"AA8HA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;CACpB;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAmBpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAMT;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,GAAE,MAA8B,GAC3C,MAAM,GAAG,IAAI,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CA0BzB;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwBtC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA4CD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,oBAAoB,EAAE,oBAAoB,EAC1C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACvC,oBAAoB,CAsCtB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wCAAwC,CAC5D,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAsD/B;AAED;;;;;;;;GAQG;AACH,wBAAsB,2BAA2B,CAC/C,oBAAoB,EAAE,oBAAoB,EAC1C,YAAY,GAAE,MAA8B,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAuC/B"}
|
|
@@ -114,6 +114,22 @@ export function extractPluginName(packageRef) {
|
|
|
114
114
|
const match = ref.match(/\/([^/:@]+)(?:[:@].*)?$/);
|
|
115
115
|
return match?.[1] || packageRef;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Returns a stable merge key for a plugin entry so OCI and local path for the same
|
|
119
|
+
* logical plugin match when merging dynamic-plugins configs. Strips a trailing
|
|
120
|
+
* "-dynamic" so e.g. backstage-community-plugin-catalog-backend-module-keycloak-dynamic
|
|
121
|
+
* and ...-keycloak (from OCI) map to the same key.
|
|
122
|
+
*
|
|
123
|
+
* @param entry Plugin entry with optional package reference
|
|
124
|
+
* @returns Normalized key for merge deduplication, or empty string if package is missing
|
|
125
|
+
*/
|
|
126
|
+
export function getNormalizedPluginMergeKey(entry) {
|
|
127
|
+
const pkg = entry?.package;
|
|
128
|
+
if (pkg === undefined || pkg === "") {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
return extractPluginName(pkg).replace(/-dynamic$/, "");
|
|
132
|
+
}
|
|
117
133
|
/**
|
|
118
134
|
* Default metadata directory path relative to the e2e-tests directory.
|
|
119
135
|
* Follows the same pattern as user config paths (e.g., tests/config/dynamic-plugins.yaml).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-metadata.test.d.ts","sourceRoot":"","sources":["../../src/utils/plugin-metadata.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { extractPluginName, getNormalizedPluginMergeKey, } from "./plugin-metadata.js";
|
|
4
|
+
describe("getNormalizedPluginMergeKey", () => {
|
|
5
|
+
it("returns same key for OCI keycloak and local keycloak-dynamic (same logical plugin)", () => {
|
|
6
|
+
const oci = getNormalizedPluginMergeKey({
|
|
7
|
+
package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-catalog-backend-module-keycloak:pr_1980__3.16.0!backstage-community-plugin-catalog-backend-module-keycloak",
|
|
8
|
+
});
|
|
9
|
+
const local = getNormalizedPluginMergeKey({
|
|
10
|
+
package: "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic",
|
|
11
|
+
});
|
|
12
|
+
assert.strictEqual(oci, local, "same logical plugin has same merge key");
|
|
13
|
+
assert.strictEqual(oci, "backstage-community-plugin-catalog-backend-module-keycloak");
|
|
14
|
+
});
|
|
15
|
+
it("returns different keys for different plugins", () => {
|
|
16
|
+
const keycloak = getNormalizedPluginMergeKey({
|
|
17
|
+
package: "./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic",
|
|
18
|
+
});
|
|
19
|
+
const techRadar = getNormalizedPluginMergeKey({
|
|
20
|
+
package: "./dynamic-plugins/dist/backstage-community-plugin-tech-radar-dynamic",
|
|
21
|
+
});
|
|
22
|
+
assert.notStrictEqual(keycloak, techRadar);
|
|
23
|
+
});
|
|
24
|
+
it("returns empty string for missing or empty package", () => {
|
|
25
|
+
assert.strictEqual(getNormalizedPluginMergeKey({}), "");
|
|
26
|
+
assert.strictEqual(getNormalizedPluginMergeKey({ package: "" }), "");
|
|
27
|
+
assert.strictEqual(getNormalizedPluginMergeKey({ package: undefined }), "");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe("extractPluginName", () => {
|
|
31
|
+
it("extracts name from OCI URL with tag and alias", () => {
|
|
32
|
+
const name = extractPluginName("oci://ghcr.io/org/repo/backstage-community-plugin-catalog-backend-module-keycloak:pr_1__1.0!alias");
|
|
33
|
+
assert.strictEqual(name, "backstage-community-plugin-catalog-backend-module-keycloak");
|
|
34
|
+
});
|
|
35
|
+
it("extracts name from local path with -dynamic suffix", () => {
|
|
36
|
+
const name = extractPluginName("./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic");
|
|
37
|
+
assert.strictEqual(name, "backstage-community-plugin-catalog-backend-module-keycloak-dynamic");
|
|
38
|
+
});
|
|
39
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/e2e-test-utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "Test utilities for RHDH E2E tests",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"prepublishOnly": "yarn build",
|
|
67
67
|
"prettier:check": "prettier --check . '!dist' '!README.md' '!docs' '!.github/workflows/deploy-docs.yml'",
|
|
68
68
|
"prettier:fix": "prettier --write . '!dist' '!README.md' '!docs' '!.github/workflows/deploy-docs.yml'",
|
|
69
|
+
"test": "node --test \"dist/**/*.test.js\"",
|
|
69
70
|
"typecheck": "tsc --noEmit"
|
|
70
71
|
},
|
|
71
72
|
"keywords": [
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
"test-utils"
|
|
76
77
|
],
|
|
77
78
|
"engines": {
|
|
78
|
-
"node": ">=22",
|
|
79
|
+
"node": ">=22.18.0",
|
|
79
80
|
"yarn": ">=3"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|