@red-hat-developer-hub/e2e-test-utils 2.1.15 → 2.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.
Files changed (93) hide show
  1. package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +5 -0
  2. package/dist/deployment/rhdh/constants.d.ts +0 -5
  3. package/dist/deployment/rhdh/constants.d.ts.map +1 -1
  4. package/dist/deployment/rhdh/constants.js +0 -5
  5. package/dist/deployment/rhdh/deployment.d.ts +2 -2
  6. package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
  7. package/dist/deployment/rhdh/deployment.js +9 -25
  8. package/dist/deployment/rhdh/deployment.test.js +9 -9
  9. package/dist/deployment/rhdh/types.d.ts +0 -4
  10. package/dist/deployment/rhdh/types.d.ts.map +1 -1
  11. package/dist/playwright/helpers/github-session.test.js +1 -1
  12. package/dist/secrets/bitwarden.d.ts +43 -0
  13. package/dist/secrets/bitwarden.d.ts.map +1 -1
  14. package/dist/secrets/bitwarden.js +465 -53
  15. package/dist/secrets/cli.d.ts +48 -1
  16. package/dist/secrets/cli.d.ts.map +1 -1
  17. package/dist/secrets/cli.js +455 -28
  18. package/dist/secrets/command.d.ts +7 -0
  19. package/dist/secrets/command.d.ts.map +1 -1
  20. package/dist/secrets/command.js +103 -26
  21. package/dist/secrets/config.d.ts +11 -1
  22. package/dist/secrets/config.d.ts.map +1 -1
  23. package/dist/secrets/config.js +32 -1
  24. package/dist/secrets/environment-name.d.ts +2 -0
  25. package/dist/secrets/environment-name.d.ts.map +1 -0
  26. package/dist/secrets/environment-name.js +4 -0
  27. package/dist/secrets/environment.d.ts +10 -0
  28. package/dist/secrets/environment.d.ts.map +1 -1
  29. package/dist/secrets/environment.js +35 -4
  30. package/dist/secrets/exec.d.ts +9 -1
  31. package/dist/secrets/exec.d.ts.map +1 -1
  32. package/dist/secrets/exec.js +318 -38
  33. package/dist/secrets/gsm-wrapper.d.ts +42 -0
  34. package/dist/secrets/gsm-wrapper.d.ts.map +1 -0
  35. package/dist/secrets/gsm-wrapper.js +238 -0
  36. package/dist/secrets/gsm.d.ts +33 -0
  37. package/dist/secrets/gsm.d.ts.map +1 -0
  38. package/dist/secrets/gsm.js +172 -0
  39. package/dist/secrets/index.d.ts +9 -4
  40. package/dist/secrets/index.d.ts.map +1 -1
  41. package/dist/secrets/index.js +7 -2
  42. package/dist/secrets/lock.d.ts +3 -0
  43. package/dist/secrets/lock.d.ts.map +1 -0
  44. package/dist/secrets/lock.js +28 -0
  45. package/dist/secrets/mutation.d.ts +71 -0
  46. package/dist/secrets/mutation.d.ts.map +1 -0
  47. package/dist/secrets/mutation.js +210 -0
  48. package/dist/secrets/secret-input.d.ts +17 -0
  49. package/dist/secrets/secret-input.d.ts.map +1 -0
  50. package/dist/secrets/secret-input.js +40 -0
  51. package/dist/secrets/stream.d.ts +9 -0
  52. package/dist/secrets/stream.d.ts.map +1 -0
  53. package/dist/secrets/stream.js +211 -0
  54. package/dist/secrets/temporary-secret.d.ts +13 -0
  55. package/dist/secrets/temporary-secret.d.ts.map +1 -0
  56. package/dist/secrets/temporary-secret.js +87 -0
  57. package/dist/secrets/tests/bitwarden.test.js +640 -0
  58. package/dist/secrets/tests/cli.integration.test.js +131 -3
  59. package/dist/secrets/tests/cli.test.js +281 -6
  60. package/dist/secrets/tests/command.test.d.ts +2 -0
  61. package/dist/secrets/tests/command.test.d.ts.map +1 -0
  62. package/dist/secrets/tests/command.test.js +39 -0
  63. package/dist/secrets/tests/config.test.js +20 -1
  64. package/dist/secrets/tests/environment.test.js +145 -3
  65. package/dist/secrets/tests/exec.test.js +469 -2
  66. package/dist/secrets/tests/gsm-wrapper.test.d.ts +2 -0
  67. package/dist/secrets/tests/gsm-wrapper.test.d.ts.map +1 -0
  68. package/dist/secrets/tests/gsm-wrapper.test.js +166 -0
  69. package/dist/secrets/tests/gsm.test.d.ts +2 -0
  70. package/dist/secrets/tests/gsm.test.d.ts.map +1 -0
  71. package/dist/secrets/tests/gsm.test.js +181 -0
  72. package/dist/secrets/tests/mutation.test.d.ts +2 -0
  73. package/dist/secrets/tests/mutation.test.d.ts.map +1 -0
  74. package/dist/secrets/tests/mutation.test.js +390 -0
  75. package/dist/secrets/tests/secret-input.test.d.ts +2 -0
  76. package/dist/secrets/tests/secret-input.test.d.ts.map +1 -0
  77. package/dist/secrets/tests/secret-input.test.js +71 -0
  78. package/dist/secrets/tests/stream.test.d.ts +2 -0
  79. package/dist/secrets/tests/stream.test.d.ts.map +1 -0
  80. package/dist/secrets/tests/stream.test.js +88 -0
  81. package/dist/secrets/tests/temporary-secret.test.d.ts +2 -0
  82. package/dist/secrets/tests/temporary-secret.test.d.ts.map +1 -0
  83. package/dist/secrets/tests/temporary-secret.test.js +76 -0
  84. package/dist/utils/plugin-metadata.d.ts +5 -0
  85. package/dist/utils/plugin-metadata.d.ts.map +1 -1
  86. package/dist/utils/plugin-metadata.js +19 -0
  87. package/dist/utils/tests/plugin-metadata.test.js +17 -1
  88. package/dist/utils/workspace-paths.d.ts +0 -2
  89. package/dist/utils/workspace-paths.d.ts.map +1 -1
  90. package/dist/utils/workspace-paths.js +0 -4
  91. package/package.json +1 -1
  92. package/dist/deployment/rhdh/config/new-frontend-system/dynamic-plugins.yaml +0 -5
  93. package/dist/deployment/rhdh/config/new-frontend-system/value_file.yaml +0 -2
@@ -1,10 +1,46 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention, playwright/expect-expect -- node:test fixture models CLI environment keys */
2
2
  import assert from "node:assert/strict";
3
+ import { createHash } from "node:crypto";
3
4
  import { chmod, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
4
5
  import { spawnSync } from "node:child_process";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
7
8
  import test from "node:test";
9
+ import { pathToFileURL } from "node:url";
10
+ const SECRET_METADATA = [
11
+ {
12
+ name: "VAULT_CERT_PEM",
13
+ byteLength: 64 * 1024,
14
+ sha256: createHash("sha256")
15
+ .update("x".repeat(64 * 1024))
16
+ .digest("hex"),
17
+ },
18
+ {
19
+ name: "VAULT_TOKEN",
20
+ byteLength: Buffer.byteLength("synthetic-note-value"),
21
+ sha256: createHash("sha256").update("synthetic-note-value").digest("hex"),
22
+ },
23
+ ];
24
+ test("provides provider-free help for every CLI command", () => {
25
+ const topics = [
26
+ [["--help"], "rhdh-e2e-secrets exec"],
27
+ [["exec", "--help"], "--profile"],
28
+ [["create", "-h"], "--from-stdin"],
29
+ [["update", "--help"], "--from-file"],
30
+ [["delete", "-h"], "--force"],
31
+ [["describe", "--help"], "--output"],
32
+ [["list", "-h"], "--collection"],
33
+ [["gsm-login", "--help"], "Authenticate"],
34
+ [["gsm-clean", "-h"], "Remove"],
35
+ ];
36
+ for (const [args, expected] of topics) {
37
+ const result = spawnSync(process.execPath, [path.resolve("dist/secrets/cli.js"), ...args], { cwd: path.resolve("."), encoding: "utf8", env: process.env });
38
+ assert.equal(result.status, 0, `${args.join(" ")} failed: ${result.stderr}`);
39
+ assert.equal(result.stderr, "", `${args.join(" ")} wrote stderr`);
40
+ assert.match(result.stdout, /Usage:/);
41
+ assert.match(result.stdout, new RegExp(expected));
42
+ }
43
+ });
8
44
  test("runs the package bin with a fake bw executable and redacted child auth", async () => {
9
45
  const directory = await mkdtemp(path.join(os.tmpdir(), "secrets-cli-test-"));
10
46
  const command = path.join(directory, "bw");
@@ -20,7 +56,7 @@ else if (args[0] === "list" && args[1] === "collections") process.stdout.write(J
20
56
  else if (args[0] === "list" && args[1] === "items") process.stdout.write(JSON.stringify([{ id: "note-item-id" }, { id: "attachment-item-id" }]));
21
57
  else if (args[0] === "get" && args[1] === "item" && args[2] === "note-item-id") process.stdout.write(JSON.stringify({ id: "note-item-id", name: "global/VAULT_TOKEN", notes: "synthetic-note-value", type: 2, collectionIds: ["collection-id"], organizationId: "organization-id" }));
22
58
  else if (args[0] === "get" && args[1] === "item" && args[2] === "attachment-item-id") process.stdout.write(JSON.stringify({ id: "attachment-item-id", name: "global/VAULT_CERT_PEM", notes: null, type: 2, collectionIds: ["collection-id"], organizationId: "organization-id", attachments: [{ id: "attachment-id", fileName: "VAULT_CERT_PEM" }] }));
23
- else if (args[0] === "get" && args[1] === "attachment" && args[2] === "VAULT_CERT_PEM" && args[3] === "--itemid" && args[4] === "attachment-item-id" && args[5] === "--raw") process.stdout.write("synthetic-attachment-value");
59
+ else if (args[0] === "get" && args[1] === "attachment" && args[2] === "VAULT_CERT_PEM" && args[3] === "--itemid" && args[4] === "attachment-item-id" && args[5] === "--raw") process.stdout.write("x".repeat(64 * 1024));
24
60
  else process.exitCode = 1;
25
61
  `);
26
62
  await chmod(command, 0o755);
@@ -40,6 +76,7 @@ else process.exitCode = 1;
40
76
  ],
41
77
  }));
42
78
  try {
79
+ const expectedMetadata = JSON.stringify(SECRET_METADATA);
43
80
  const result = spawnSync(entrypoint, [
44
81
  "exec",
45
82
  "--profile",
@@ -47,7 +84,14 @@ else process.exitCode = 1;
47
84
  "--",
48
85
  process.execPath,
49
86
  "-e",
50
- "if (process.env.VAULT_TOKEN === 'synthetic-note-value' && process.env.VAULT_CERT_PEM === 'synthetic-attachment-value' && process.env.BW_SESSION === undefined) process.stdout.write('child-ran'); else process.exit(1)",
87
+ [
88
+ "const { createHash } = require('node:crypto');",
89
+ `const expected = ${expectedMetadata};`,
90
+ "const digest = (value) => createHash('sha256').update(value).digest('hex');",
91
+ "const actual = ['VAULT_CERT_PEM', 'VAULT_TOKEN'].map((name) => ({ name, byteLength: Buffer.byteLength(process.env[name] ?? ''), sha256: typeof process.env[name] === 'string' ? digest(process.env[name]) : '' }));",
92
+ "if (JSON.stringify(actual) !== JSON.stringify(expected) || process.env.BW_SESSION !== undefined) process.exit(1);",
93
+ "process.stdout.write('child-ran');",
94
+ ].join(" "),
51
95
  ], {
52
96
  cwd: path.resolve("."),
53
97
  encoding: "utf8",
@@ -58,11 +102,95 @@ else process.exitCode = 1;
58
102
  },
59
103
  });
60
104
  assert.equal(result.status, 0, result.stderr);
61
- assert.match(result.stdout, /child-ran/);
105
+ assert.equal(result.stdout, "child-ran");
106
+ assert.equal(result.stderr, "");
62
107
  assert.doesNotMatch(result.stdout, /synthetic-(note|attachment)-value/);
63
108
  assert.doesNotMatch(result.stderr, /synthetic-(note|attachment)-value/);
64
109
  assert.doesNotMatch(result.stdout, /synthetic-session/);
65
110
  assert.doesNotMatch(result.stderr, /synthetic-session/);
111
+ const streamModuleUrl = pathToFileURL(path.resolve("dist/secrets/stream.js")).href;
112
+ const streamed = spawnSync(entrypoint, [
113
+ "exec",
114
+ "--profile",
115
+ profile,
116
+ "--stream-secrets",
117
+ "--",
118
+ process.execPath,
119
+ "--input-type=module",
120
+ "-e",
121
+ [
122
+ "import fs from 'node:fs';",
123
+ "import { createHash } from 'node:crypto';",
124
+ `import { decodeSecretStream } from ${JSON.stringify(streamModuleUrl)};`,
125
+ `const expected = ${expectedMetadata};`,
126
+ "const entries = decodeSecretStream(fs.readFileSync(3));",
127
+ "const actual = entries.map(({ name, value }) => ({ name, byteLength: Buffer.byteLength(value), sha256: createHash('sha256').update(value).digest('hex') }));",
128
+ "if (JSON.stringify(actual) !== JSON.stringify(expected) || process.env.VAULT_TOKEN !== undefined || process.env.VAULT_CERT_PEM !== undefined || process.env.RHDH_E2E_SECRET_FD !== '3' || process.env.BW_SESSION !== undefined) process.exit(1);",
129
+ "process.stdout.write('child-ran-with-stream');",
130
+ ].join(" "),
131
+ ], {
132
+ cwd: path.resolve("."),
133
+ encoding: "utf8",
134
+ env: {
135
+ ...process.env,
136
+ PATH: `${directory}${path.delimiter}${process.env.PATH ?? ""}`,
137
+ BW_SESSION: "synthetic-session",
138
+ },
139
+ });
140
+ assert.equal(streamed.status, 0, streamed.stderr);
141
+ assert.equal(streamed.stdout, "child-ran-with-stream");
142
+ assert.equal(streamed.stderr, "");
143
+ assert.doesNotMatch(streamed.stdout, /synthetic-(note|attachment)-value/);
144
+ assert.doesNotMatch(streamed.stderr, /synthetic-(note|attachment)-value/);
145
+ assert.doesNotMatch(streamed.stdout, /synthetic-session/);
146
+ assert.doesNotMatch(streamed.stderr, /synthetic-session/);
147
+ const childEnvironment = {
148
+ ...process.env,
149
+ PATH: `${directory}${path.delimiter}${process.env.PATH ?? ""}`,
150
+ BW_SESSION: "synthetic-session",
151
+ };
152
+ const ignored = spawnSync(entrypoint, [
153
+ "exec",
154
+ "--profile",
155
+ profile,
156
+ "--stream-secrets",
157
+ "--",
158
+ process.execPath,
159
+ "-e",
160
+ "process.exit(0)",
161
+ ], {
162
+ cwd: path.resolve("."),
163
+ encoding: "utf8",
164
+ env: childEnvironment,
165
+ timeout: 2_000,
166
+ });
167
+ assert.equal(ignored.error, undefined);
168
+ assert.notEqual(ignored.status, null);
169
+ assert.doesNotMatch(ignored.stdout, /synthetic-(note|attachment)-value/);
170
+ assert.doesNotMatch(ignored.stderr, /synthetic-(note|attachment)-value/);
171
+ assert.doesNotMatch(ignored.stdout, /synthetic-session/);
172
+ assert.doesNotMatch(ignored.stderr, /synthetic-session/);
173
+ const earlyClosed = spawnSync(entrypoint, [
174
+ "exec",
175
+ "--profile",
176
+ profile,
177
+ "--stream-secrets",
178
+ "--",
179
+ process.execPath,
180
+ "-e",
181
+ "if (process.env.VAULT_CERT_PEM !== undefined || process.env.VAULT_TOKEN !== undefined || process.env.RHDH_E2E_SECRET_FD !== '3' || process.env.BW_SESSION !== undefined) process.exit(18); require('node:fs').closeSync(3); setTimeout(() => process.exit(17), 50)",
182
+ ], {
183
+ cwd: path.resolve("."),
184
+ encoding: "utf8",
185
+ env: childEnvironment,
186
+ timeout: 2_000,
187
+ });
188
+ assert.equal(earlyClosed.error, undefined);
189
+ assert.equal(earlyClosed.status, 17, earlyClosed.stderr);
190
+ assert.doesNotMatch(earlyClosed.stdout, /synthetic-(note|attachment)-value/);
191
+ assert.doesNotMatch(earlyClosed.stderr, /synthetic-(note|attachment)-value/);
192
+ assert.doesNotMatch(earlyClosed.stdout, /synthetic-session/);
193
+ assert.doesNotMatch(earlyClosed.stderr, /synthetic-session/);
66
194
  }
67
195
  finally {
68
196
  await rm(directory, { recursive: true, force: true });
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable playwright/expect-expect -- this file uses node:test assertions */
2
2
  import assert from "node:assert/strict";
3
3
  import test from "node:test";
4
- import { parseCliArguments } from "../cli.js";
4
+ import { formatMutationResult, getHelpText, parseCliArguments, } from "../cli.js";
5
5
  test("parses the exec profile, repeated workspaces, and command after --", () => {
6
6
  const parsed = parseCliArguments([
7
7
  "exec",
@@ -23,13 +23,288 @@ test("parses the exec profile, repeated workspaces, and command after --", () =>
23
23
  args: ["test", "--headed"],
24
24
  });
25
25
  });
26
- test("requires a profile and a command after --", () => {
27
- assert.throws(() => parseCliArguments(["exec", "--", "playwright"]), /profile/i);
28
- assert.throws(() => parseCliArguments(["exec", "--profile", "profile.json"]), /command.*--/i);
29
- assert.throws(() => parseCliArguments(["exec", "--profile", "profile.json", "--", ""]), /command/i);
26
+ test("parses secret streaming before the child command", () => {
27
+ assert.deepEqual(parseCliArguments([
28
+ "exec",
29
+ "--profile",
30
+ "profile.json",
31
+ "--stream-secrets",
32
+ "--",
33
+ "node",
34
+ ]), {
35
+ command: "exec",
36
+ profilePath: "profile.json",
37
+ workspaces: [],
38
+ executable: "node",
39
+ args: [],
40
+ streamSecrets: true,
41
+ });
30
42
  });
31
- test("rejects unknown commands and malformed options", () => {
43
+ test("keeps --stream-secrets after -- as a child argument", () => {
44
+ assert.deepEqual(parseCliArguments([
45
+ "exec",
46
+ "--profile",
47
+ "profile.json",
48
+ "--",
49
+ "node",
50
+ "--stream-secrets",
51
+ ]), {
52
+ command: "exec",
53
+ profilePath: "profile.json",
54
+ workspaces: [],
55
+ executable: "node",
56
+ args: ["--stream-secrets"],
57
+ });
58
+ });
59
+ test("advertises secret streaming and removes secret-name exposure from help", () => {
60
+ assert.doesNotMatch(getHelpText("root"), /--expose-secret-names/);
61
+ assert.doesNotMatch(getHelpText("exec"), /--expose-secret-names/);
62
+ assert.match(getHelpText("root"), /--stream-secrets/);
63
+ assert.match(getHelpText("exec"), /--stream-secrets/);
64
+ });
65
+ test("rejects the removed secret-name exposure option", () => {
66
+ assert.throws(() => parseCliArguments([
67
+ "exec",
68
+ "--profile",
69
+ "profile.json",
70
+ "--expose-secret-names",
71
+ "--",
72
+ "node",
73
+ ]), /Unknown option: --expose-secret-names/);
74
+ });
75
+ test("formats provider actions with their actual collection names", () => {
76
+ const plan = {
77
+ command: "delete",
78
+ collection: "rhdh-qe",
79
+ bitwardenCollection: "Rhdh Qe Ci Secrets",
80
+ gsmCollection: "rhdh-qe",
81
+ bitwardenPath: "rhdh/TEST",
82
+ gsmPath: "rhdh/TEST",
83
+ bitwardenAction: "delete",
84
+ gsmAction: "delete",
85
+ };
86
+ assert.equal(formatMutationResult("applied", plan), [
87
+ "Applied: delete rhdh-qe/rhdh/TEST",
88
+ "Bitwarden: delete Rhdh Qe Ci Secrets/rhdh/TEST",
89
+ "GSM: delete rhdh-qe/rhdh/TEST",
90
+ ].join("\n"));
91
+ });
92
+ test("supports short exec profile and workspace options", () => {
93
+ assert.deepEqual(parseCliArguments([
94
+ "exec",
95
+ "-p",
96
+ "profile.json",
97
+ "-w",
98
+ "backstage",
99
+ "--",
100
+ "node",
101
+ ]), {
102
+ command: "exec",
103
+ profilePath: "profile.json",
104
+ workspaces: ["backstage"],
105
+ executable: "node",
106
+ args: [],
107
+ });
108
+ });
109
+ test("returns root and subcommand help before validating required arguments", () => {
110
+ assert.deepEqual(parseCliArguments(["--help"]), {
111
+ command: "help",
112
+ topic: "root",
113
+ });
114
+ assert.deepEqual(parseCliArguments(["create", "--help"]), {
115
+ command: "help",
116
+ topic: "create",
117
+ });
118
+ assert.deepEqual(parseCliArguments(["update", "-h"]), {
119
+ command: "help",
120
+ topic: "update",
121
+ });
122
+ assert.deepEqual(parseCliArguments(["delete", "--help"]), {
123
+ command: "help",
124
+ topic: "delete",
125
+ });
126
+ assert.deepEqual(parseCliArguments(["describe", "-h"]), {
127
+ command: "help",
128
+ topic: "describe",
129
+ });
130
+ assert.deepEqual(parseCliArguments(["list", "--help"]), {
131
+ command: "help",
132
+ topic: "list",
133
+ });
134
+ assert.deepEqual(parseCliArguments(["gsm-login", "-h"]), {
135
+ command: "help",
136
+ topic: "gsm-login",
137
+ });
138
+ assert.deepEqual(parseCliArguments(["gsm-clean", "--help"]), {
139
+ command: "help",
140
+ topic: "gsm-clean",
141
+ });
142
+ });
143
+ test("keeps --help after exec -- for the child command", () => {
144
+ assert.deepEqual(parseCliArguments([
145
+ "exec",
146
+ "--profile",
147
+ "profile.json",
148
+ "--",
149
+ "node",
150
+ "--help",
151
+ ]), {
152
+ command: "exec",
153
+ profilePath: "profile.json",
154
+ workspaces: [],
155
+ executable: "node",
156
+ args: ["--help"],
157
+ });
158
+ });
159
+ test("parses create with file input and applies by default", () => {
160
+ assert.deepEqual(parseCliArguments([
161
+ "create",
162
+ "-c",
163
+ "rhdh-qe",
164
+ "rhdh/test",
165
+ "-f",
166
+ "replacement.txt",
167
+ ]), {
168
+ command: "create",
169
+ collection: "rhdh-qe",
170
+ bitwardenPath: "rhdh/test",
171
+ fromFile: "replacement.txt",
172
+ fromStdin: false,
173
+ allowEmpty: false,
174
+ force: false,
175
+ dryRun: false,
176
+ gsmTimeoutMs: undefined,
177
+ });
178
+ });
179
+ test("parses update with stdin input and dry-run", () => {
180
+ assert.deepEqual(parseCliArguments([
181
+ "update",
182
+ "--collection=rhdh-qe",
183
+ "rhdh/test",
184
+ "--from-stdin",
185
+ "--allow-empty",
186
+ "--dry-run",
187
+ "--gsm-timeout-seconds",
188
+ "30",
189
+ ]), {
190
+ command: "update",
191
+ collection: "rhdh-qe",
192
+ bitwardenPath: "rhdh/test",
193
+ fromFile: undefined,
194
+ fromStdin: true,
195
+ allowEmpty: true,
196
+ dryRun: true,
197
+ gsmTimeoutMs: 30_000,
198
+ });
199
+ });
200
+ test("supports the short stdin flag and case-insensitive output values", () => {
201
+ assert.deepEqual(parseCliArguments(["create", "-c", "rhdh-qe", "rhdh/test", "-i"]), {
202
+ command: "create",
203
+ collection: "rhdh-qe",
204
+ bitwardenPath: "rhdh/test",
205
+ fromFile: undefined,
206
+ fromStdin: true,
207
+ allowEmpty: false,
208
+ force: false,
209
+ dryRun: false,
210
+ gsmTimeoutMs: undefined,
211
+ });
212
+ assert.equal(parseCliArguments([
213
+ "describe",
214
+ "-c",
215
+ "rhdh-qe",
216
+ "rhdh/test",
217
+ "-o",
218
+ "JSON",
219
+ ]).output, "json");
220
+ });
221
+ test("parses forced delete and does not require input", () => {
222
+ assert.deepEqual(parseCliArguments([
223
+ "delete",
224
+ "--collection",
225
+ "rhdh-qe",
226
+ "rhdh/test",
227
+ "--force",
228
+ ]), {
229
+ command: "delete",
230
+ collection: "rhdh-qe",
231
+ bitwardenPath: "rhdh/test",
232
+ force: true,
233
+ dryRun: false,
234
+ gsmTimeoutMs: undefined,
235
+ });
236
+ });
237
+ test("parses GSM describe and list commands", () => {
238
+ assert.deepEqual(parseCliArguments(["describe", "-c", "rhdh-qe", "rhdh/test", "-o", "json"]), {
239
+ command: "describe",
240
+ collection: "rhdh-qe",
241
+ bitwardenPath: "rhdh/test",
242
+ output: "json",
243
+ });
244
+ assert.deepEqual(parseCliArguments(["list", "--collection", "rhdh-qe"]), {
245
+ command: "list",
246
+ collection: "rhdh-qe",
247
+ output: "text",
248
+ });
249
+ });
250
+ test("rejects GSM timeout values above the Node timer limit", () => {
251
+ assert.throws(() => parseCliArguments([
252
+ "create",
253
+ "-c",
254
+ "rhdh-qe",
255
+ "rhdh/test",
256
+ "-i",
257
+ "--gsm-timeout-seconds",
258
+ "2147484",
259
+ ]), /maximum|timeout/i);
260
+ });
261
+ test("requires exactly one input for create and update", () => {
262
+ assert.throws(() => parseCliArguments(["create", "-c", "rhdh-qe", "rhdh/test"]), /from-file.*from-stdin/i);
263
+ assert.throws(() => parseCliArguments([
264
+ "update",
265
+ "-c",
266
+ "rhdh-qe",
267
+ "rhdh/test",
268
+ "--from-file",
269
+ "one",
270
+ "--from-stdin",
271
+ ]), /exactly one/i);
272
+ assert.throws(() => parseCliArguments([
273
+ "delete",
274
+ "-c",
275
+ "rhdh-qe",
276
+ "rhdh/test",
277
+ "--from-stdin",
278
+ ]), /does not accept input/i);
279
+ assert.throws(() => parseCliArguments([
280
+ "create",
281
+ "-c",
282
+ "rhdh-qe",
283
+ "rhdh/test",
284
+ "--from-literal",
285
+ "secret-value",
286
+ ]), /unknown option/i);
287
+ });
288
+ test("restricts force to create and delete", () => {
289
+ assert.throws(() => parseCliArguments([
290
+ "update",
291
+ "-c",
292
+ "rhdh-qe",
293
+ "rhdh/test",
294
+ "--from-stdin",
295
+ "--force",
296
+ ]), /force.*update/i);
297
+ });
298
+ test("rejects the removed rotation and resume commands", () => {
32
299
  assert.throws(() => parseCliArguments(["rotate"]), /unsupported command/i);
300
+ assert.throws(() => parseCliArguments(["create", "--resume", "operation-id"]), /unknown option|collection/i);
301
+ });
302
+ test("parses GSM credential lifecycle commands", () => {
303
+ assert.deepEqual(parseCliArguments(["gsm-login"]), { command: "gsm-login" });
304
+ assert.deepEqual(parseCliArguments(["gsm-clean"]), { command: "gsm-clean" });
305
+ });
306
+ test("rejects unknown commands and malformed options", () => {
307
+ assert.throws(() => parseCliArguments(["unknown"]), /unsupported command/i);
33
308
  assert.throws(() => parseCliArguments(["exec", "--profile", "--", "playwright"]), /requires a value/i);
34
309
  assert.throws(() => parseCliArguments([
35
310
  "exec",
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=command.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/command.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ /* eslint-disable playwright/expect-expect, playwright/no-conditional-in-test -- node:test assertions */
2
+ import assert from "node:assert/strict";
3
+ import test from "node:test";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { runCommand, shouldDetachCommand } from "../command.js";
6
+ test("keeps terminal-backed commands in the parent session", () => {
7
+ if (process.platform === "win32")
8
+ return;
9
+ assert.equal(shouldDetachCommand({}), true);
10
+ assert.equal(shouldDetachCommand({ tty: true }), false);
11
+ });
12
+ test("terminates timed-out commands and marks the result indeterminate", async () => {
13
+ const result = await runCommand(process.execPath, ["-e", "setTimeout(() => {}, 10_000)"], { timeoutMs: 25 });
14
+ assert.equal(result.timedOut, true);
15
+ assert.equal(result.status, null);
16
+ });
17
+ test("cancels forced termination after a timed-out child exits", async () => {
18
+ if (process.platform === "win32")
19
+ return;
20
+ const originalKill = process.kill;
21
+ const signals = [];
22
+ process.kill = ((pid, signal) => {
23
+ if (signal !== undefined)
24
+ signals.push(signal);
25
+ return originalKill(pid, signal);
26
+ });
27
+ try {
28
+ await runCommand(process.execPath, ["-e", "setTimeout(() => {}, 10_000)"], {
29
+ timeoutMs: 25,
30
+ });
31
+ const signalCountAfterExit = signals.length;
32
+ await delay(1_100);
33
+ assert.equal(signals.length, signalCountAfterExit);
34
+ assert.doesNotMatch(signals.slice(signalCountAfterExit).join(","), /SIGKILL/);
35
+ }
36
+ finally {
37
+ process.kill = originalKill;
38
+ }
39
+ });
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable playwright/expect-expect -- this file uses node:test assertions */
2
2
  import assert from "node:assert/strict";
3
3
  import test from "node:test";
4
- import { expandProfile, getCollectionMapping, parseProfile, } from "../config.js";
4
+ import { bitwardenPathFromGsmPath, expandProfile, getCollectionMapping, gsmPathFromBitwardenPath, parseProfile, } from "../config.js";
5
5
  const overlayProfile = {
6
6
  schemaVersion: 1,
7
7
  collection: "rhdh-plugin-export-overlays",
@@ -31,8 +31,27 @@ test("maps each approved collection to its exact Bitwarden collection name", ()
31
31
  assert.deepEqual(getCollectionMapping("rhdh-qe"), {
32
32
  id: "rhdh-qe",
33
33
  bitwardenCollection: "Rhdh Qe Ci Secrets",
34
+ gsmCollection: "rhdh-qe",
34
35
  });
35
36
  });
37
+ test("keeps legacy collection mappings constructible without a GSM name", () => {
38
+ const mapping = {
39
+ id: "rhdh-qe",
40
+ bitwardenCollection: "Rhdh Qe Ci Secrets",
41
+ };
42
+ assert.equal(mapping.gsmCollection, undefined);
43
+ });
44
+ test("maps dotted Bitwarden paths to the GSM punctuation encoding", () => {
45
+ assert.equal(gsmPathFromBitwardenPath("rhdh/azure-db-certificates.pem"), "rhdh/azure-db-certificates--dot--pem");
46
+ });
47
+ test("normalizes a GSM-encoded path for Bitwarden and preserves GSM encoding", () => {
48
+ const gsmPath = "rhdh/already--dot--encoded";
49
+ assert.equal(bitwardenPathFromGsmPath(gsmPath), "rhdh/already.encoded");
50
+ assert.equal(gsmPathFromBitwardenPath(gsmPath), gsmPath);
51
+ });
52
+ test("rejects overlong rotation path segments", () => {
53
+ assert.throws(() => gsmPathFromBitwardenPath(`rhdh/${"a".repeat(256)}`), /too long/i);
54
+ });
36
55
  test("rejects the GSM-only AWS collection before Bitwarden access", () => {
37
56
  assert.throws(() => parseProfile({ ...overlayProfile, collection: "rhdh-aws-credentials" }), /rhdh-aws-credentials.*Bitwarden/i);
38
57
  });