@red-hat-developer-hub/e2e-test-utils 2.1.14 → 2.1.16

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 (79) hide show
  1. package/dist/deployment/rhdh/constants.d.ts +0 -1
  2. package/dist/deployment/rhdh/constants.d.ts.map +1 -1
  3. package/dist/deployment/rhdh/constants.js +0 -1
  4. package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
  5. package/dist/deployment/rhdh/deployment.js +0 -3
  6. package/dist/secrets/bitwarden.d.ts +43 -0
  7. package/dist/secrets/bitwarden.d.ts.map +1 -1
  8. package/dist/secrets/bitwarden.js +465 -53
  9. package/dist/secrets/cli.d.ts +48 -1
  10. package/dist/secrets/cli.d.ts.map +1 -1
  11. package/dist/secrets/cli.js +455 -28
  12. package/dist/secrets/command.d.ts +7 -0
  13. package/dist/secrets/command.d.ts.map +1 -1
  14. package/dist/secrets/command.js +103 -26
  15. package/dist/secrets/config.d.ts +11 -1
  16. package/dist/secrets/config.d.ts.map +1 -1
  17. package/dist/secrets/config.js +32 -1
  18. package/dist/secrets/environment-name.d.ts +2 -0
  19. package/dist/secrets/environment-name.d.ts.map +1 -0
  20. package/dist/secrets/environment-name.js +4 -0
  21. package/dist/secrets/environment.d.ts +10 -0
  22. package/dist/secrets/environment.d.ts.map +1 -1
  23. package/dist/secrets/environment.js +35 -4
  24. package/dist/secrets/exec.d.ts +9 -1
  25. package/dist/secrets/exec.d.ts.map +1 -1
  26. package/dist/secrets/exec.js +318 -38
  27. package/dist/secrets/gsm-wrapper.d.ts +42 -0
  28. package/dist/secrets/gsm-wrapper.d.ts.map +1 -0
  29. package/dist/secrets/gsm-wrapper.js +238 -0
  30. package/dist/secrets/gsm.d.ts +33 -0
  31. package/dist/secrets/gsm.d.ts.map +1 -0
  32. package/dist/secrets/gsm.js +172 -0
  33. package/dist/secrets/index.d.ts +9 -4
  34. package/dist/secrets/index.d.ts.map +1 -1
  35. package/dist/secrets/index.js +7 -2
  36. package/dist/secrets/lock.d.ts +3 -0
  37. package/dist/secrets/lock.d.ts.map +1 -0
  38. package/dist/secrets/lock.js +28 -0
  39. package/dist/secrets/mutation.d.ts +71 -0
  40. package/dist/secrets/mutation.d.ts.map +1 -0
  41. package/dist/secrets/mutation.js +210 -0
  42. package/dist/secrets/secret-input.d.ts +17 -0
  43. package/dist/secrets/secret-input.d.ts.map +1 -0
  44. package/dist/secrets/secret-input.js +40 -0
  45. package/dist/secrets/stream.d.ts +9 -0
  46. package/dist/secrets/stream.d.ts.map +1 -0
  47. package/dist/secrets/stream.js +211 -0
  48. package/dist/secrets/temporary-secret.d.ts +13 -0
  49. package/dist/secrets/temporary-secret.d.ts.map +1 -0
  50. package/dist/secrets/temporary-secret.js +87 -0
  51. package/dist/secrets/tests/bitwarden.test.js +640 -0
  52. package/dist/secrets/tests/cli.integration.test.js +131 -3
  53. package/dist/secrets/tests/cli.test.js +281 -6
  54. package/dist/secrets/tests/command.test.d.ts +2 -0
  55. package/dist/secrets/tests/command.test.d.ts.map +1 -0
  56. package/dist/secrets/tests/command.test.js +39 -0
  57. package/dist/secrets/tests/config.test.js +20 -1
  58. package/dist/secrets/tests/environment.test.js +145 -3
  59. package/dist/secrets/tests/exec.test.js +469 -2
  60. package/dist/secrets/tests/gsm-wrapper.test.d.ts +2 -0
  61. package/dist/secrets/tests/gsm-wrapper.test.d.ts.map +1 -0
  62. package/dist/secrets/tests/gsm-wrapper.test.js +166 -0
  63. package/dist/secrets/tests/gsm.test.d.ts +2 -0
  64. package/dist/secrets/tests/gsm.test.d.ts.map +1 -0
  65. package/dist/secrets/tests/gsm.test.js +181 -0
  66. package/dist/secrets/tests/mutation.test.d.ts +2 -0
  67. package/dist/secrets/tests/mutation.test.d.ts.map +1 -0
  68. package/dist/secrets/tests/mutation.test.js +390 -0
  69. package/dist/secrets/tests/secret-input.test.d.ts +2 -0
  70. package/dist/secrets/tests/secret-input.test.d.ts.map +1 -0
  71. package/dist/secrets/tests/secret-input.test.js +71 -0
  72. package/dist/secrets/tests/stream.test.d.ts +2 -0
  73. package/dist/secrets/tests/stream.test.d.ts.map +1 -0
  74. package/dist/secrets/tests/stream.test.js +88 -0
  75. package/dist/secrets/tests/temporary-secret.test.d.ts +2 -0
  76. package/dist/secrets/tests/temporary-secret.test.d.ts.map +1 -0
  77. package/dist/secrets/tests/temporary-secret.test.js +76 -0
  78. package/package.json +1 -1
  79. package/dist/deployment/rhdh/config/new-frontend-system/secrets.yaml +0 -8
@@ -0,0 +1,390 @@
1
+ /* eslint-disable playwright/expect-expect -- node:test assertions */
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+ import { executeMutation, } from "../mutation.js";
8
+ function item(value, storage = "note") {
9
+ return {
10
+ id: "item-id",
11
+ name: "rhdh/test",
12
+ value,
13
+ storage,
14
+ revisionDate: "revision-1",
15
+ raw: {},
16
+ attachments: storage === "attachment"
17
+ ? [{ id: "attachment-id", fileName: "test" }]
18
+ : [],
19
+ ...(storage === "attachment"
20
+ ? { attachment: { id: "attachment-id", fileName: "test" } }
21
+ : {}),
22
+ };
23
+ }
24
+ function gsm(overrides = {}) {
25
+ return {
26
+ exists: async () => false,
27
+ create: async () => undefined,
28
+ update: async () => undefined,
29
+ delete: async () => undefined,
30
+ ...overrides,
31
+ };
32
+ }
33
+ test("dry-run creates one value-free plan and performs no writes", async () => {
34
+ const calls = [];
35
+ const bitwarden = {
36
+ findItem: async () => {
37
+ calls.push("bitwarden-find");
38
+ return undefined;
39
+ },
40
+ createItem: async () => {
41
+ calls.push("bitwarden-create");
42
+ throw new Error("must not write");
43
+ },
44
+ updateItem: async () => {
45
+ calls.push("bitwarden-update");
46
+ throw new Error("must not write");
47
+ },
48
+ deleteItem: async () => {
49
+ calls.push("bitwarden-delete");
50
+ throw new Error("must not write");
51
+ },
52
+ };
53
+ const result = await executeMutation({
54
+ command: "create",
55
+ collection: "rhdh-qe",
56
+ bitwardenPath: "rhdh/test",
57
+ fromStdin: true,
58
+ stdin: ["new-value"],
59
+ dryRun: true,
60
+ bitwarden,
61
+ gsm: gsm(),
62
+ });
63
+ assert.deepEqual(result, {
64
+ state: "dry-run",
65
+ plan: {
66
+ command: "create",
67
+ collection: "rhdh-qe",
68
+ bitwardenCollection: "Rhdh Qe Ci Secrets",
69
+ gsmCollection: "rhdh-qe",
70
+ bitwardenPath: "rhdh/test",
71
+ gsmPath: "rhdh/test",
72
+ byteLength: 9,
73
+ storage: "note",
74
+ bitwardenAction: "create",
75
+ gsmAction: "create",
76
+ },
77
+ });
78
+ assert.deepEqual(calls, ["bitwarden-find"]);
79
+ });
80
+ test("decodes GSM dot encoding only for Bitwarden lookups", async () => {
81
+ const calls = [];
82
+ const result = await executeMutation({
83
+ command: "create",
84
+ collection: "rhdh-qe",
85
+ bitwardenPath: "rhdh/rds-db-certificates--dot--pem",
86
+ fromStdin: true,
87
+ stdin: ["new-value"],
88
+ dryRun: true,
89
+ bitwarden: {
90
+ findItem: async (_collection, name) => {
91
+ calls.push(`bitwarden:${name}`);
92
+ return undefined;
93
+ },
94
+ createItem: async () => item("new-value"),
95
+ updateItem: async () => item("new-value"),
96
+ deleteItem: async () => undefined,
97
+ },
98
+ gsm: gsm({
99
+ exists: async (_collection, name) => {
100
+ calls.push(`gsm:${name}`);
101
+ return false;
102
+ },
103
+ }),
104
+ });
105
+ assert.deepEqual(calls, [
106
+ "bitwarden:rhdh/rds-db-certificates.pem",
107
+ "gsm:rhdh/rds-db-certificates--dot--pem",
108
+ ]);
109
+ assert.equal(result.plan.bitwardenPath, "rhdh/rds-db-certificates.pem");
110
+ assert.equal(result.plan.gsmPath, "rhdh/rds-db-certificates--dot--pem");
111
+ });
112
+ test("forced create reconciles existing providers and converts Bitwarden storage", async () => {
113
+ const calls = [];
114
+ const bitwarden = {
115
+ findItem: async () => item("old-value", "note"),
116
+ createItem: async () => {
117
+ calls.push("bitwarden-create");
118
+ return item("new-value", "attachment");
119
+ },
120
+ updateItem: async (_item, value, storage) => {
121
+ calls.push(`bitwarden-update:${value}:${storage}`);
122
+ return item(value, storage);
123
+ },
124
+ deleteItem: async () => {
125
+ calls.push("bitwarden-delete");
126
+ },
127
+ };
128
+ const gsmClient = gsm({
129
+ exists: async () => true,
130
+ update: async () => {
131
+ calls.push("gsm-update");
132
+ },
133
+ });
134
+ const result = await executeMutation({
135
+ command: "create",
136
+ collection: "rhdh-qe",
137
+ bitwardenPath: "rhdh/test",
138
+ fromFile: "replacement.txt",
139
+ readFile: async () => Buffer.from("replacement-value"),
140
+ force: true,
141
+ bitwarden,
142
+ gsm: gsmClient,
143
+ });
144
+ assert.equal(result.state, "applied");
145
+ assert.deepEqual(result.plan, {
146
+ command: "create",
147
+ collection: "rhdh-qe",
148
+ bitwardenCollection: "Rhdh Qe Ci Secrets",
149
+ gsmCollection: "rhdh-qe",
150
+ bitwardenPath: "rhdh/test",
151
+ gsmPath: "rhdh/test",
152
+ byteLength: 17,
153
+ storage: "attachment",
154
+ existingStorage: "note",
155
+ bitwardenAction: "update",
156
+ gsmAction: "update",
157
+ });
158
+ assert.deepEqual(calls, [
159
+ "bitwarden-update:replacement-value:attachment",
160
+ "gsm-update",
161
+ ]);
162
+ });
163
+ test("checks the interactive terminal before writing Bitwarden for GSM create", async () => {
164
+ const calls = [];
165
+ await executeMutation({
166
+ command: "create",
167
+ collection: "rhdh-qe",
168
+ bitwardenPath: "rhdh/test",
169
+ fromStdin: true,
170
+ stdin: ["new-value"],
171
+ bitwarden: {
172
+ findItem: async () => undefined,
173
+ createItem: async () => {
174
+ calls.push("bitwarden-create");
175
+ return item("new-value");
176
+ },
177
+ updateItem: async () => item("new-value"),
178
+ deleteItem: async () => undefined,
179
+ },
180
+ gsm: gsm({
181
+ ensureInteractive: async () => {
182
+ calls.push("gsm-ensure-interactive");
183
+ },
184
+ create: async () => {
185
+ calls.push("gsm-create");
186
+ },
187
+ }),
188
+ });
189
+ assert.deepEqual(calls, [
190
+ "gsm-ensure-interactive",
191
+ "bitwarden-create",
192
+ "gsm-create",
193
+ ]);
194
+ });
195
+ test("normal create rejects a one-sided existing target before writing", async () => {
196
+ const calls = [];
197
+ await assert.rejects(() => executeMutation({
198
+ command: "create",
199
+ collection: "rhdh-qe",
200
+ bitwardenPath: "rhdh/test",
201
+ fromStdin: true,
202
+ stdin: ["new-value"],
203
+ bitwarden: {
204
+ findItem: async () => item("old-value"),
205
+ createItem: async () => {
206
+ calls.push("bitwarden-create");
207
+ return item("new-value");
208
+ },
209
+ updateItem: async () => item("new-value"),
210
+ deleteItem: async () => undefined,
211
+ },
212
+ gsm: gsm(),
213
+ }), /already exists.*force/i);
214
+ assert.deepEqual(calls, []);
215
+ });
216
+ test("update writes Bitwarden before GSM and reports a retry without a resume id", async () => {
217
+ const calls = [];
218
+ await assert.rejects(() => executeMutation({
219
+ command: "update",
220
+ collection: "rhdh-qe",
221
+ bitwardenPath: "rhdh/test",
222
+ fromStdin: true,
223
+ stdin: ["new-value"],
224
+ bitwarden: {
225
+ findItem: async () => item("old-value"),
226
+ createItem: async () => item("new-value"),
227
+ updateItem: async (_item, value) => {
228
+ calls.push("bitwarden-update");
229
+ return item(value);
230
+ },
231
+ deleteItem: async () => undefined,
232
+ },
233
+ gsm: gsm({
234
+ exists: async () => true,
235
+ update: async () => {
236
+ calls.push("gsm-update");
237
+ throw new Error("GSM unavailable");
238
+ },
239
+ }),
240
+ }), (error) => {
241
+ assert(error instanceof Error);
242
+ assert.match(error.message, /GSM unavailable/);
243
+ assert.match(error.message, /retry/i);
244
+ assert.doesNotMatch(error.message, /resume|operation id/i);
245
+ return true;
246
+ });
247
+ assert.deepEqual(calls, ["bitwarden-update", "gsm-update"]);
248
+ });
249
+ test("preserves force guidance after a forced partial delete fails", async () => {
250
+ await assert.rejects(() => executeMutation({
251
+ command: "delete",
252
+ collection: "rhdh-qe",
253
+ bitwardenPath: "rhdh/test",
254
+ force: true,
255
+ bitwarden: {
256
+ findItem: async () => undefined,
257
+ createItem: async () => item("unused"),
258
+ updateItem: async () => item("unused"),
259
+ deleteItem: async () => undefined,
260
+ },
261
+ gsm: gsm({
262
+ exists: async () => true,
263
+ delete: async () => {
264
+ throw new Error("GSM unavailable");
265
+ },
266
+ }),
267
+ }), (error) => {
268
+ assert(error instanceof Error);
269
+ assert.match(error.message, /retry the delete command with --force/i);
270
+ return true;
271
+ });
272
+ });
273
+ test("reports reconciliation guidance when Bitwarden may have changed", async () => {
274
+ await assert.rejects(() => executeMutation({
275
+ command: "create",
276
+ collection: "rhdh-qe",
277
+ bitwardenPath: "rhdh/test",
278
+ fromStdin: true,
279
+ stdin: ["new-value"],
280
+ force: true,
281
+ bitwarden: {
282
+ findItem: async () => undefined,
283
+ createItem: async () => {
284
+ throw new Error("Bitwarden response lost after create");
285
+ },
286
+ updateItem: async () => item("unused"),
287
+ deleteItem: async () => undefined,
288
+ },
289
+ gsm: gsm(),
290
+ }), (error) => {
291
+ assert(error instanceof Error);
292
+ assert.match(error.message, /may have been updated|Bitwarden/i);
293
+ assert.match(error.message, /--force/);
294
+ return true;
295
+ });
296
+ });
297
+ test("rejects delete input options at the library boundary", async () => {
298
+ await assert.rejects(() => executeMutation({
299
+ command: "delete",
300
+ collection: "rhdh-qe",
301
+ bitwardenPath: "rhdh/test",
302
+ fromFile: "secret.txt",
303
+ bitwarden: {
304
+ findItem: async () => undefined,
305
+ createItem: async () => item("unused"),
306
+ updateItem: async () => item("unused"),
307
+ deleteItem: async () => undefined,
308
+ },
309
+ gsm: gsm(),
310
+ }), /delete does not accept input options/i);
311
+ });
312
+ test("uses one lock for equivalent GSM path spellings", async () => {
313
+ const lockDirectory = await mkdtemp(path.join(os.tmpdir(), "rhdh-e2e-mutation-lock-test-"));
314
+ let active = 0;
315
+ let maximumActive = 0;
316
+ const bitwarden = {
317
+ findItem: async () => undefined,
318
+ createItem: async () => {
319
+ active++;
320
+ maximumActive = Math.max(maximumActive, active);
321
+ await new Promise((resolve) => setTimeout(resolve, 25));
322
+ active--;
323
+ return item("new-value");
324
+ },
325
+ updateItem: async () => item("new-value"),
326
+ deleteItem: async () => undefined,
327
+ };
328
+ const options = {
329
+ command: "create",
330
+ collection: "rhdh-qe",
331
+ fromStdin: true,
332
+ stdin: ["new-value"],
333
+ force: true,
334
+ lockDirectory,
335
+ bitwarden,
336
+ gsm: gsm(),
337
+ };
338
+ try {
339
+ const results = await Promise.allSettled([
340
+ executeMutation({ ...options, bitwardenPath: "rhdh/test.pem" }),
341
+ executeMutation({ ...options, bitwardenPath: "rhdh/test--dot--pem" }),
342
+ ]);
343
+ assert.equal(maximumActive, 1);
344
+ assert.equal(results.filter((result) => result.status === "rejected").length, 1);
345
+ }
346
+ finally {
347
+ await rm(lockDirectory, { recursive: true, force: true });
348
+ }
349
+ });
350
+ test("forced delete removes existing targets and accepts an absent provider", async () => {
351
+ const calls = [];
352
+ const result = await executeMutation({
353
+ command: "delete",
354
+ collection: "rhdh-qe",
355
+ bitwardenPath: "rhdh/test",
356
+ force: true,
357
+ bitwarden: {
358
+ findItem: async () => item("old-value"),
359
+ createItem: async () => item("unused"),
360
+ updateItem: async () => item("unused"),
361
+ deleteItem: async () => {
362
+ calls.push("bitwarden-delete");
363
+ },
364
+ },
365
+ gsm: gsm({
366
+ exists: async () => false,
367
+ delete: async () => {
368
+ calls.push("gsm-delete");
369
+ },
370
+ }),
371
+ });
372
+ assert.equal(result.state, "applied");
373
+ assert.equal(result.plan.bitwardenAction, "delete");
374
+ assert.equal(result.plan.gsmAction, "skip");
375
+ assert.deepEqual(calls, ["bitwarden-delete"]);
376
+ });
377
+ test("normal delete rejects an absent provider unless forced", async () => {
378
+ await assert.rejects(() => executeMutation({
379
+ command: "delete",
380
+ collection: "rhdh-qe",
381
+ bitwardenPath: "rhdh/test",
382
+ bitwarden: {
383
+ findItem: async () => undefined,
384
+ createItem: async () => item("unused"),
385
+ updateItem: async () => item("unused"),
386
+ deleteItem: async () => undefined,
387
+ },
388
+ gsm: gsm({ exists: async () => true }),
389
+ }), /absent.*force/i);
390
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=secret-input.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-input.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/secret-input.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ /* eslint-disable playwright/expect-expect -- node:test assertions */
2
+ import assert from "node:assert/strict";
3
+ import test from "node:test";
4
+ import { Readable } from "node:stream";
5
+ import { readSecretInput } from "../secret-input.js";
6
+ test("reads a UTF-8 file and reports its byte size", async () => {
7
+ const result = await readSecretInput({
8
+ fromFile: "synthetic.txt",
9
+ readFile: async () => Buffer.from("line one\nline two\n", "utf8"),
10
+ });
11
+ assert.deepEqual(result, {
12
+ value: "line one\nline two\n",
13
+ byteLength: 18,
14
+ storage: "attachment",
15
+ });
16
+ });
17
+ test("reads stdin when selected and rejects a second input source", async () => {
18
+ const result = await readSecretInput({
19
+ fromStdin: true,
20
+ stdin: Readable.from([Buffer.from("synthetic")]),
21
+ });
22
+ assert.equal(result.value, "synthetic");
23
+ assert.equal(result.byteLength, 9);
24
+ assert.equal(result.storage, "note");
25
+ await assert.rejects(() => readSecretInput({
26
+ fromFile: "secret.txt",
27
+ fromStdin: true,
28
+ readFile: async () => Buffer.from("ignored"),
29
+ }), /exactly one/i);
30
+ });
31
+ test("rejects terminal stdin before waiting for input", async () => {
32
+ let iterated = false;
33
+ const terminalStdin = {
34
+ isTTY: true,
35
+ [Symbol.asyncIterator]() {
36
+ iterated = true;
37
+ return {
38
+ next: async () => ({ done: true, value: undefined }),
39
+ };
40
+ },
41
+ };
42
+ await assert.rejects(() => readSecretInput({ fromStdin: true, stdin: terminalStdin }), /piped.*stdin|stdin.*piped/i);
43
+ assert.equal(iterated, false);
44
+ });
45
+ test("rejects missing input and invalid UTF-8", async () => {
46
+ await assert.rejects(() => readSecretInput({ readFile: async () => Buffer.from("") }), /exactly one/i);
47
+ await assert.rejects(() => readSecretInput({
48
+ fromFile: "secret.txt",
49
+ readFile: async () => Buffer.from([0xc3, 0x28]),
50
+ }), /UTF-8/i);
51
+ });
52
+ test("requires explicit confirmation for an empty value", async () => {
53
+ await assert.rejects(() => readSecretInput({
54
+ fromFile: "empty.txt",
55
+ readFile: async () => Buffer.alloc(0),
56
+ }), /allow-empty/i);
57
+ const result = await readSecretInput({
58
+ fromFile: "empty.txt",
59
+ allowEmpty: true,
60
+ readFile: async () => Buffer.alloc(0),
61
+ });
62
+ assert.deepEqual(result, { value: "", byteLength: 0, storage: "attachment" });
63
+ });
64
+ test("preserves a UTF-8 BOM during validation", async () => {
65
+ const result = await readSecretInput({
66
+ fromFile: "bom.txt",
67
+ readFile: async () => Buffer.from([0xef, 0xbb, 0xbf, 0x76]),
68
+ });
69
+ assert.equal(result.value, "\ufeffv");
70
+ assert.equal(Buffer.byteLength(result.value, "utf8"), 4);
71
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stream.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/stream.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,88 @@
1
+ /* eslint-disable playwright/expect-expect -- node:test assertions */
2
+ import assert from "node:assert/strict";
3
+ import { Writable } from "node:stream";
4
+ import test from "node:test";
5
+ import { decodeSecretStream, writeSecretStream, } from "../stream.js";
6
+ async function encode(entries) {
7
+ const chunks = [];
8
+ const stream = new Writable({
9
+ write(chunk, _encoding, callback) {
10
+ chunks.push(Buffer.from(chunk));
11
+ callback();
12
+ },
13
+ });
14
+ await writeSecretStream(stream, entries);
15
+ return Buffer.concat(chunks);
16
+ }
17
+ test("round-trips sorted multiline and empty values", async () => {
18
+ const entries = [
19
+ { name: "ZETA_VALUE", value: "last" },
20
+ { name: "EMPTY_VALUE", value: "" },
21
+ { name: "ALPHA_VALUE", value: "first line\nsecond line\n" },
22
+ ];
23
+ const encoded = await encode(entries);
24
+ assert.deepEqual(decodeSecretStream(encoded), [
25
+ { name: "ALPHA_VALUE", value: "first line\nsecond line\n" },
26
+ { name: "EMPTY_VALUE", value: "" },
27
+ { name: "ZETA_VALUE", value: "last" },
28
+ ]);
29
+ });
30
+ test("round-trips a value larger than an execve environment string", async () => {
31
+ const value = "x".repeat(160 * 1024);
32
+ const encoded = await encode([{ name: "LARGE_VALUE", value }]);
33
+ assert.deepEqual(decodeSecretStream(encoded), [
34
+ { name: "LARGE_VALUE", value },
35
+ ]);
36
+ });
37
+ test("rejects truncated and trailing data", async () => {
38
+ const encoded = await encode([{ name: "VALUE", value: "synthetic" }]);
39
+ assert.throws(() => decodeSecretStream(encoded.subarray(0, encoded.length - 1)), /truncated|incomplete/i);
40
+ assert.throws(() => decodeSecretStream(Buffer.concat([encoded, Buffer.from([0])])), /trailing|end of input/i);
41
+ });
42
+ test("rejects duplicate, invalid, NUL-containing, and oversized entries", async () => {
43
+ await assert.rejects(() => encode([
44
+ { name: "DUPLICATE", value: "first" },
45
+ { name: "DUPLICATE", value: "second" },
46
+ ]), /duplicate/i);
47
+ await assert.rejects(() => encode([{ name: "NOT-VALID", value: "synthetic" }]), /environment name|invalid name/i);
48
+ await assert.rejects(() => encode([{ name: "NUL\0NAME", value: "synthetic" }]), /nul/i);
49
+ await assert.rejects(() => encode([{ name: "NUL_VALUE", value: "nul\0value" }]), /nul/i);
50
+ await assert.rejects(() => encode([{ name: "OVERSIZED", value: "x".repeat(8 * 1024 * 1024 + 1) }]), /field|large|size/i);
51
+ });
52
+ test("rejects an invalid header, footer, count, and field length", async () => {
53
+ const encoded = await encode([{ name: "VALUE", value: "synthetic" }]);
54
+ const invalidHeader = Buffer.from(encoded);
55
+ invalidHeader[0] = invalidHeader[0] ^ 1;
56
+ assert.throws(() => decodeSecretStream(invalidHeader), /header/i);
57
+ const invalidFooter = Buffer.from(encoded);
58
+ invalidFooter[invalidFooter.length - 1] =
59
+ invalidFooter[invalidFooter.length - 1] ^ 1;
60
+ assert.throws(() => decodeSecretStream(invalidFooter), /footer/i);
61
+ const invalidCount = Buffer.from(encoded);
62
+ invalidCount.writeUInt32BE(65536, 8);
63
+ assert.throws(() => decodeSecretStream(invalidCount), /count|entries/i);
64
+ const invalidFieldLength = Buffer.from(encoded);
65
+ invalidFieldLength.writeUInt32BE(0xffffffff, 12);
66
+ assert.throws(() => decodeSecretStream(invalidFieldLength), /field|length|truncated/i);
67
+ });
68
+ test("rejects an oversized stream before writing any bytes", async () => {
69
+ const chunks = [];
70
+ let ended = false;
71
+ const stream = {
72
+ write(chunk) {
73
+ chunks.push(chunk);
74
+ return true;
75
+ },
76
+ end() {
77
+ ended = true;
78
+ },
79
+ };
80
+ const value = "x".repeat(8 * 1024 * 1024 - 1);
81
+ const entries = Array.from({ length: 8 }, (_, index) => ({
82
+ name: `VALUE_${index}`,
83
+ value,
84
+ }));
85
+ await assert.rejects(() => writeSecretStream(stream, entries), /stream|size/i);
86
+ assert.deepEqual(chunks, []);
87
+ assert.equal(ended, false);
88
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=temporary-secret.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temporary-secret.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/temporary-secret.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,76 @@
1
+ /* eslint-disable playwright/expect-expect -- node:test assertions */
2
+ import assert from "node:assert/strict";
3
+ import { access, lstat, mkdir, mkdtemp, readFile, rm, stat, symlink, } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+ import { createTemporarySecretFile } from "../temporary-secret.js";
8
+ test("creates a private temporary secret file and removes it", async () => {
9
+ const root = await mkdtemp(path.join(os.tmpdir(), "temporary-secret-test-"));
10
+ try {
11
+ const secret = await createTemporarySecretFile("synthetic-secret", "value", {
12
+ rootDirectory: root,
13
+ });
14
+ const rootStats = await stat(root);
15
+ const directoryStats = await stat(path.dirname(secret.path));
16
+ const fileStats = await stat(secret.path);
17
+ assert.equal(rootStats.mode & 0o777, 0o700);
18
+ assert.equal(directoryStats.mode & 0o777, 0o700);
19
+ assert.equal(fileStats.mode & 0o777, 0o600);
20
+ assert.equal(await readFile(secret.path, "utf8"), "synthetic-secret");
21
+ await secret.remove();
22
+ await assert.rejects(() => access(secret.path), /ENOENT/);
23
+ }
24
+ finally {
25
+ await rm(root, { recursive: true, force: true });
26
+ }
27
+ });
28
+ test("removes abandoned directories for dead processes but preserves active ones", async () => {
29
+ const root = await mkdtemp(path.join(os.tmpdir(), "temporary-secret-test-"));
30
+ const deadDirectory = path.join(root, "424242-dead");
31
+ const activeDirectory = path.join(root, "424243-active");
32
+ try {
33
+ await mkdir(deadDirectory);
34
+ await mkdir(activeDirectory);
35
+ const secret = await createTemporarySecretFile("value", "value", {
36
+ rootDirectory: root,
37
+ processId: 424243,
38
+ processExists: (pid) => pid === 424243,
39
+ });
40
+ await assert.rejects(() => lstat(deadDirectory), /ENOENT/);
41
+ await access(activeDirectory);
42
+ await secret.remove();
43
+ }
44
+ finally {
45
+ await rm(root, { recursive: true, force: true });
46
+ }
47
+ });
48
+ test("rejects a symlink used as the temporary secret root", async () => {
49
+ const parent = await mkdtemp(path.join(os.tmpdir(), "temporary-secret-test-"));
50
+ const target = path.join(parent, "target");
51
+ const link = path.join(parent, "link");
52
+ try {
53
+ await mkdir(target);
54
+ await symlink(target, link);
55
+ await assert.rejects(() => createTemporarySecretFile("value", "value", {
56
+ rootDirectory: link,
57
+ }), /symlink/i);
58
+ }
59
+ finally {
60
+ await rm(parent, { recursive: true, force: true });
61
+ }
62
+ });
63
+ test("reports temporary cleanup failure when file creation fails", async () => {
64
+ const root = await mkdtemp(path.join(os.tmpdir(), "temporary-secret-test-"));
65
+ try {
66
+ await assert.rejects(() => createTemporarySecretFile("value", ".", {
67
+ rootDirectory: root,
68
+ removeDirectory: async () => {
69
+ throw new Error("cleanup failed");
70
+ },
71
+ }), /creation and cleanup failed/i);
72
+ }
73
+ finally {
74
+ await rm(root, { recursive: true, force: true });
75
+ }
76
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/e2e-test-utils",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "Test utilities for RHDH E2E tests",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,8 +0,0 @@
1
- apiVersion: v1
2
- kind: Secret
3
- metadata:
4
- name: rhdh-secrets
5
- type: Opaque
6
- stringData:
7
- APP_CONFIG_app_packageName: app-next
8
- ENABLE_STANDARD_MODULE_FEDERATION: "true"