@red-hat-developer-hub/e2e-test-utils 2.1.13 → 2.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deployment/rhdh/constants.d.ts +0 -1
- package/dist/deployment/rhdh/constants.d.ts.map +1 -1
- package/dist/deployment/rhdh/constants.js +0 -1
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +0 -3
- package/dist/playwright/global-setup.d.ts +5 -0
- package/dist/playwright/global-setup.d.ts.map +1 -1
- package/dist/playwright/global-setup.js +2 -4
- package/dist/playwright/helpers/github-session.test.js +1 -1
- package/dist/playwright/tests/global-setup.test.d.ts +2 -0
- package/dist/playwright/tests/global-setup.test.d.ts.map +1 -0
- package/dist/playwright/tests/global-setup.test.js +68 -0
- package/dist/secrets/bitwarden.d.ts +34 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -0
- package/dist/secrets/bitwarden.js +193 -0
- package/dist/secrets/cli.d.ts +11 -0
- package/dist/secrets/cli.d.ts.map +1 -0
- package/dist/secrets/cli.js +98 -0
- package/dist/secrets/command.d.ts +15 -0
- package/dist/secrets/command.d.ts.map +1 -0
- package/dist/secrets/command.js +30 -0
- package/dist/secrets/config.d.ts +33 -0
- package/dist/secrets/config.d.ts.map +1 -0
- package/dist/secrets/config.js +156 -0
- package/dist/secrets/environment.d.ts +10 -0
- package/dist/secrets/environment.d.ts.map +1 -0
- package/dist/secrets/environment.js +53 -0
- package/dist/secrets/exec.d.ts +18 -0
- package/dist/secrets/exec.d.ts.map +1 -0
- package/dist/secrets/exec.js +50 -0
- package/dist/secrets/index.d.ts +5 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +4 -0
- package/dist/secrets/tests/bitwarden.integration.test.d.ts +2 -0
- package/dist/secrets/tests/bitwarden.integration.test.d.ts.map +1 -0
- package/dist/secrets/tests/bitwarden.integration.test.js +50 -0
- package/dist/secrets/tests/bitwarden.test.d.ts +2 -0
- package/dist/secrets/tests/bitwarden.test.d.ts.map +1 -0
- package/dist/secrets/tests/bitwarden.test.js +418 -0
- package/dist/secrets/tests/cli.integration.test.d.ts +2 -0
- package/dist/secrets/tests/cli.integration.test.d.ts.map +1 -0
- package/dist/secrets/tests/cli.integration.test.js +70 -0
- package/dist/secrets/tests/cli.test.d.ts +2 -0
- package/dist/secrets/tests/cli.test.d.ts.map +1 -0
- package/dist/secrets/tests/cli.test.js +42 -0
- package/dist/secrets/tests/config.test.d.ts +2 -0
- package/dist/secrets/tests/config.test.d.ts.map +1 -0
- package/dist/secrets/tests/config.test.js +105 -0
- package/dist/secrets/tests/environment.test.d.ts +2 -0
- package/dist/secrets/tests/environment.test.d.ts.map +1 -0
- package/dist/secrets/tests/environment.test.js +119 -0
- package/dist/secrets/tests/exec.test.d.ts +2 -0
- package/dist/secrets/tests/exec.test.d.ts.map +1 -0
- package/dist/secrets/tests/exec.test.js +87 -0
- package/package.json +9 -2
- package/dist/deployment/rhdh/config/new-frontend-system/secrets.yaml +0 -8
- package/dist/utils/vault.d.ts +0 -16
- package/dist/utils/vault.d.ts.map +0 -1
- package/dist/utils/vault.js +0 -94
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/naming-convention, playwright/expect-expect, playwright/no-conditional-in-test -- node:test fixtures model CLI and environment keys */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { BitwardenClient } from "../bitwarden.js";
|
|
5
|
+
const selectors = [
|
|
6
|
+
{
|
|
7
|
+
prefix: "global/",
|
|
8
|
+
optional: false,
|
|
9
|
+
destination: {
|
|
10
|
+
kind: "environment",
|
|
11
|
+
stripPrefix: "global/",
|
|
12
|
+
requirePrefix: "VAULT_",
|
|
13
|
+
keyTransform: "legacy-env",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
prefix: "workspaces/backstage/",
|
|
18
|
+
optional: true,
|
|
19
|
+
destination: {
|
|
20
|
+
kind: "environment",
|
|
21
|
+
stripPrefix: "workspaces/backstage/",
|
|
22
|
+
requirePrefix: "VAULT_",
|
|
23
|
+
keyTransform: "legacy-env",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
function result(stdout = "", status = 0) {
|
|
28
|
+
return { status, stdout, stderr: status === 0 ? "" : "synthetic failure" };
|
|
29
|
+
}
|
|
30
|
+
test("requires an unlocked BW_SESSION before any Bitwarden command", async () => {
|
|
31
|
+
let called = false;
|
|
32
|
+
const runner = async () => {
|
|
33
|
+
called = true;
|
|
34
|
+
return result();
|
|
35
|
+
};
|
|
36
|
+
await assert.rejects(() => new BitwardenClient({
|
|
37
|
+
env: { PATH: "/bin" },
|
|
38
|
+
runner,
|
|
39
|
+
}).read("rhdh-plugin-export-overlays", selectors), /BW_SESSION.*unlocked/i);
|
|
40
|
+
assert.equal(called, false);
|
|
41
|
+
});
|
|
42
|
+
test("rejects the denied AWS collection before any Bitwarden command", async () => {
|
|
43
|
+
let called = false;
|
|
44
|
+
const runner = async () => {
|
|
45
|
+
called = true;
|
|
46
|
+
return result();
|
|
47
|
+
};
|
|
48
|
+
await assert.rejects(() => new BitwardenClient({
|
|
49
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
50
|
+
runner,
|
|
51
|
+
}).read("rhdh-aws-credentials", [selectors[0]]), /rhdh-aws-credentials.*Bitwarden/i);
|
|
52
|
+
assert.equal(called, false);
|
|
53
|
+
});
|
|
54
|
+
test("syncs and reads only exact-prefix items from the selected collection", async () => {
|
|
55
|
+
const calls = [];
|
|
56
|
+
const runner = async (_command, args, options) => {
|
|
57
|
+
calls.push({ args, env: options?.env });
|
|
58
|
+
if (args[0] === "--version")
|
|
59
|
+
return result("2026.5.0\n");
|
|
60
|
+
if (args[0] === "status")
|
|
61
|
+
return result(JSON.stringify({ status: "unlocked" }));
|
|
62
|
+
if (args[0] === "sync")
|
|
63
|
+
return result();
|
|
64
|
+
if (args[0] === "list" && args[1] === "collections") {
|
|
65
|
+
return result(JSON.stringify([
|
|
66
|
+
{
|
|
67
|
+
id: "other-collection",
|
|
68
|
+
name: "Other",
|
|
69
|
+
organizationId: "other-org",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "collection-id",
|
|
73
|
+
name: "Rhdh Plugin Export Overlays Ci Secrets",
|
|
74
|
+
organizationId: "org-id",
|
|
75
|
+
},
|
|
76
|
+
]));
|
|
77
|
+
}
|
|
78
|
+
if (args[0] === "list" && args[1] === "items") {
|
|
79
|
+
assert.deepEqual(args.slice(0, 4), [
|
|
80
|
+
"list",
|
|
81
|
+
"items",
|
|
82
|
+
"--collectionid",
|
|
83
|
+
"collection-id",
|
|
84
|
+
]);
|
|
85
|
+
return result(JSON.stringify(args.at(-1) === "global/"
|
|
86
|
+
? [{ id: "global-id" }, { id: "false-positive" }]
|
|
87
|
+
: [{ id: "workspace-id" }]));
|
|
88
|
+
}
|
|
89
|
+
if (args[0] === "get" && args[1] === "item" && args[2] === "global-id") {
|
|
90
|
+
return result(JSON.stringify({
|
|
91
|
+
id: "global-id",
|
|
92
|
+
name: "global/VAULT_GITHUB_TOKEN",
|
|
93
|
+
notes: "synthetic-token",
|
|
94
|
+
type: 2,
|
|
95
|
+
collectionIds: ["collection-id"],
|
|
96
|
+
organizationId: "org-id",
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
if (args[0] === "get" &&
|
|
100
|
+
args[1] === "item" &&
|
|
101
|
+
args[2] === "false-positive") {
|
|
102
|
+
return result(JSON.stringify({
|
|
103
|
+
id: "false-positive",
|
|
104
|
+
name: "global-other/VAULT_NOT_SELECTED",
|
|
105
|
+
notes: "not-selected",
|
|
106
|
+
type: 2,
|
|
107
|
+
collectionIds: ["collection-id"],
|
|
108
|
+
organizationId: "org-id",
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
if (args[0] === "get" && args[1] === "item" && args[2] === "workspace-id") {
|
|
112
|
+
return result(JSON.stringify({
|
|
113
|
+
id: "workspace-id",
|
|
114
|
+
name: "workspaces/backstage/VAULT_GH_USER_ID",
|
|
115
|
+
notes: "synthetic-user",
|
|
116
|
+
type: 2,
|
|
117
|
+
collectionIds: ["collection-id"],
|
|
118
|
+
organizationId: "org-id",
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Unexpected command: ${args.join(" ")}`);
|
|
122
|
+
};
|
|
123
|
+
const secrets = await new BitwardenClient({
|
|
124
|
+
env: { BW_SESSION: "synthetic-session", PATH: "/bin" },
|
|
125
|
+
runner,
|
|
126
|
+
}).read("rhdh-plugin-export-overlays", selectors);
|
|
127
|
+
assert.deepEqual(secrets, [
|
|
128
|
+
{
|
|
129
|
+
id: "global-id",
|
|
130
|
+
name: "global/VAULT_GITHUB_TOKEN",
|
|
131
|
+
value: "synthetic-token",
|
|
132
|
+
selector: selectors[0],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "workspace-id",
|
|
136
|
+
name: "workspaces/backstage/VAULT_GH_USER_ID",
|
|
137
|
+
value: "synthetic-user",
|
|
138
|
+
selector: selectors[1],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
assert.equal(calls[0]?.args[0], "--version");
|
|
142
|
+
assert.equal(calls.every((call) => call.env?.BW_SESSION === "synthetic-session"), true);
|
|
143
|
+
});
|
|
144
|
+
test("rejects duplicate exact item names before returning secrets", async () => {
|
|
145
|
+
const runner = async (_command, args) => {
|
|
146
|
+
if (args[0] === "--version")
|
|
147
|
+
return result("2026.5.0");
|
|
148
|
+
if (args[0] === "status")
|
|
149
|
+
return result(JSON.stringify({ status: "unlocked" }));
|
|
150
|
+
if (args[0] === "sync")
|
|
151
|
+
return result();
|
|
152
|
+
if (args[1] === "collections") {
|
|
153
|
+
return result(JSON.stringify([
|
|
154
|
+
{
|
|
155
|
+
id: "collection-id",
|
|
156
|
+
name: "Rhdh Qe Ci Secrets",
|
|
157
|
+
organizationId: "org-id",
|
|
158
|
+
},
|
|
159
|
+
]));
|
|
160
|
+
}
|
|
161
|
+
if (args[1] === "items") {
|
|
162
|
+
return result(JSON.stringify([{ id: "one" }, { id: "two" }]));
|
|
163
|
+
}
|
|
164
|
+
return result(JSON.stringify({
|
|
165
|
+
id: args[2],
|
|
166
|
+
name: "global/VAULT_DUPLICATE",
|
|
167
|
+
notes: "synthetic",
|
|
168
|
+
type: 2,
|
|
169
|
+
collectionIds: ["collection-id"],
|
|
170
|
+
organizationId: "org-id",
|
|
171
|
+
}));
|
|
172
|
+
};
|
|
173
|
+
await assert.rejects(() => new BitwardenClient({
|
|
174
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
175
|
+
runner,
|
|
176
|
+
}).read("rhdh-qe", [selectors[0]]), /duplicate.*item name/i);
|
|
177
|
+
});
|
|
178
|
+
test("rejects non-note items and items assigned to multiple collections", async () => {
|
|
179
|
+
const runner = async (_command, args) => {
|
|
180
|
+
if (args[0] === "--version")
|
|
181
|
+
return result("2026.5.0");
|
|
182
|
+
if (args[0] === "status")
|
|
183
|
+
return result(JSON.stringify({ status: "unlocked" }));
|
|
184
|
+
if (args[0] === "sync")
|
|
185
|
+
return result();
|
|
186
|
+
if (args[1] === "collections") {
|
|
187
|
+
return result(JSON.stringify([
|
|
188
|
+
{
|
|
189
|
+
id: "collection-id",
|
|
190
|
+
name: "Rhdh Qe Ci Secrets",
|
|
191
|
+
organizationId: "org-id",
|
|
192
|
+
},
|
|
193
|
+
]));
|
|
194
|
+
}
|
|
195
|
+
if (args[1] === "items")
|
|
196
|
+
return result(JSON.stringify([{ id: "item-id" }]));
|
|
197
|
+
return result(JSON.stringify({
|
|
198
|
+
id: "item-id",
|
|
199
|
+
name: "global/VAULT_TOKEN",
|
|
200
|
+
notes: "synthetic",
|
|
201
|
+
type: 1,
|
|
202
|
+
collectionIds: ["collection-id", "other-id"],
|
|
203
|
+
organizationId: "org-id",
|
|
204
|
+
}));
|
|
205
|
+
};
|
|
206
|
+
await assert.rejects(() => new BitwardenClient({
|
|
207
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
208
|
+
runner,
|
|
209
|
+
}).read("rhdh-qe", [selectors[0]]), /secure note.*selected collection/i);
|
|
210
|
+
});
|
|
211
|
+
test("fails required selectors and permits empty optional selectors", async () => {
|
|
212
|
+
const runner = async (_command, args) => {
|
|
213
|
+
if (args[0] === "--version")
|
|
214
|
+
return result("2026.5.0");
|
|
215
|
+
if (args[0] === "status")
|
|
216
|
+
return result(JSON.stringify({ status: "unlocked" }));
|
|
217
|
+
if (args[0] === "sync")
|
|
218
|
+
return result();
|
|
219
|
+
if (args[1] === "collections") {
|
|
220
|
+
return result(JSON.stringify([
|
|
221
|
+
{
|
|
222
|
+
id: "collection-id",
|
|
223
|
+
name: "Rhdh Qe Ci Secrets",
|
|
224
|
+
organizationId: "org-id",
|
|
225
|
+
},
|
|
226
|
+
]));
|
|
227
|
+
}
|
|
228
|
+
return result("[]");
|
|
229
|
+
};
|
|
230
|
+
await assert.rejects(() => new BitwardenClient({
|
|
231
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
232
|
+
runner,
|
|
233
|
+
}).read("rhdh-qe", [selectors[0]]), /required prefix.*global\//i);
|
|
234
|
+
const secrets = await new BitwardenClient({
|
|
235
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
236
|
+
runner,
|
|
237
|
+
}).read("rhdh-qe", [selectors[1]]);
|
|
238
|
+
assert.deepEqual(secrets, []);
|
|
239
|
+
});
|
|
240
|
+
function attachmentSelector() {
|
|
241
|
+
return selectors[0];
|
|
242
|
+
}
|
|
243
|
+
function attachmentRunner(item, attachmentResult = result("synthetic-attachment"), calls = []) {
|
|
244
|
+
return async (_command, args) => {
|
|
245
|
+
calls.push(args);
|
|
246
|
+
if (args[0] === "--version")
|
|
247
|
+
return result("2026.5.0\n");
|
|
248
|
+
if (args[0] === "status")
|
|
249
|
+
return result(JSON.stringify({ status: "unlocked" }));
|
|
250
|
+
if (args[0] === "sync")
|
|
251
|
+
return result();
|
|
252
|
+
if (args[0] === "list" && args[1] === "collections") {
|
|
253
|
+
return result(JSON.stringify([
|
|
254
|
+
{
|
|
255
|
+
id: "collection-id",
|
|
256
|
+
name: "Rhdh Qe Ci Secrets",
|
|
257
|
+
organizationId: "org-id",
|
|
258
|
+
},
|
|
259
|
+
]));
|
|
260
|
+
}
|
|
261
|
+
if (args[0] === "list" && args[1] === "items") {
|
|
262
|
+
return result(JSON.stringify([{ id: item.id }]));
|
|
263
|
+
}
|
|
264
|
+
if (args[0] === "get" && args[1] === "item") {
|
|
265
|
+
return result(JSON.stringify(item));
|
|
266
|
+
}
|
|
267
|
+
if (args[0] === "get" && args[1] === "attachment") {
|
|
268
|
+
return attachmentResult;
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`Unexpected command: ${args.join(" ")}`);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
test("reads one attachment through the raw Bitwarden command", async () => {
|
|
274
|
+
const calls = [];
|
|
275
|
+
const runner = attachmentRunner({
|
|
276
|
+
id: "item-id",
|
|
277
|
+
name: "global/VAULT_CERT_PEM",
|
|
278
|
+
notes: null,
|
|
279
|
+
type: 2,
|
|
280
|
+
collectionIds: ["collection-id"],
|
|
281
|
+
organizationId: "org-id",
|
|
282
|
+
attachments: [{ id: "attachment-id", fileName: "VAULT_CERT_PEM" }],
|
|
283
|
+
}, result("synthetic-attachment"), calls);
|
|
284
|
+
const [secret] = await new BitwardenClient({
|
|
285
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
286
|
+
runner,
|
|
287
|
+
}).read("rhdh-qe", [attachmentSelector()]);
|
|
288
|
+
assert.equal(secret?.value, "synthetic-attachment");
|
|
289
|
+
assert.deepEqual(calls.at(-1), [
|
|
290
|
+
"get",
|
|
291
|
+
"attachment",
|
|
292
|
+
"VAULT_CERT_PEM",
|
|
293
|
+
"--itemid",
|
|
294
|
+
"item-id",
|
|
295
|
+
"--raw",
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
test("preserves multiline attachment contents", async () => {
|
|
299
|
+
const contents = "-----BEGIN CERTIFICATE-----\nline\n-----END CERTIFICATE-----\n";
|
|
300
|
+
const runner = attachmentRunner({
|
|
301
|
+
id: "item-id",
|
|
302
|
+
name: "global/VAULT_CERT_PEM",
|
|
303
|
+
notes: "",
|
|
304
|
+
type: 2,
|
|
305
|
+
collectionIds: ["collection-id"],
|
|
306
|
+
organizationId: "org-id",
|
|
307
|
+
attachments: [{ id: "attachment-id", fileName: "VAULT_CERT_PEM" }],
|
|
308
|
+
}, result(contents));
|
|
309
|
+
const [secret] = await new BitwardenClient({
|
|
310
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
311
|
+
runner,
|
|
312
|
+
}).read("rhdh-qe", [attachmentSelector()]);
|
|
313
|
+
assert.equal(secret?.value, contents);
|
|
314
|
+
});
|
|
315
|
+
test("rejects items with multiple attachments", async () => {
|
|
316
|
+
const runner = attachmentRunner({
|
|
317
|
+
id: "item-id",
|
|
318
|
+
name: "global/VAULT_CERT_PEM",
|
|
319
|
+
notes: null,
|
|
320
|
+
type: 2,
|
|
321
|
+
collectionIds: ["collection-id"],
|
|
322
|
+
organizationId: "org-id",
|
|
323
|
+
attachments: [
|
|
324
|
+
{ id: "attachment-id", fileName: "VAULT_CERT_PEM" },
|
|
325
|
+
{ id: "other-attachment-id", fileName: "other" },
|
|
326
|
+
],
|
|
327
|
+
});
|
|
328
|
+
await assert.rejects(() => new BitwardenClient({
|
|
329
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
330
|
+
runner,
|
|
331
|
+
}).read("rhdh-qe", [attachmentSelector()]), /exactly one attachment/i);
|
|
332
|
+
});
|
|
333
|
+
test("rejects invalid attachment metadata", async () => {
|
|
334
|
+
const runner = attachmentRunner({
|
|
335
|
+
id: "item-id",
|
|
336
|
+
name: "global/VAULT_CERT_PEM",
|
|
337
|
+
notes: null,
|
|
338
|
+
type: 2,
|
|
339
|
+
collectionIds: ["collection-id"],
|
|
340
|
+
organizationId: "org-id",
|
|
341
|
+
attachments: [{ id: "attachment-id" }],
|
|
342
|
+
});
|
|
343
|
+
await assert.rejects(() => new BitwardenClient({
|
|
344
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
345
|
+
runner,
|
|
346
|
+
}).read("rhdh-qe", [attachmentSelector()]), /invalid attachment metadata/i);
|
|
347
|
+
});
|
|
348
|
+
test("rejects an attachment filename that does not match the sanitized item basename", async () => {
|
|
349
|
+
const runner = attachmentRunner({
|
|
350
|
+
id: "item-id",
|
|
351
|
+
name: "global/VAULT_CERT key",
|
|
352
|
+
notes: null,
|
|
353
|
+
type: 2,
|
|
354
|
+
collectionIds: ["collection-id"],
|
|
355
|
+
organizationId: "org-id",
|
|
356
|
+
attachments: [{ id: "attachment-id", fileName: "VAULT_CERT-key" }],
|
|
357
|
+
});
|
|
358
|
+
await assert.rejects(() => new BitwardenClient({
|
|
359
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
360
|
+
runner,
|
|
361
|
+
}).read("rhdh-qe", [attachmentSelector()]), /attachment filename.*basename/i);
|
|
362
|
+
});
|
|
363
|
+
test("rejects a non-empty note combined with an attachment", async () => {
|
|
364
|
+
const runner = attachmentRunner({
|
|
365
|
+
id: "item-id",
|
|
366
|
+
name: "global/VAULT_CERT_PEM",
|
|
367
|
+
notes: "synthetic-note",
|
|
368
|
+
type: 2,
|
|
369
|
+
collectionIds: ["collection-id"],
|
|
370
|
+
organizationId: "org-id",
|
|
371
|
+
attachments: [{ id: "attachment-id", fileName: "VAULT_CERT_PEM" }],
|
|
372
|
+
});
|
|
373
|
+
await assert.rejects(() => new BitwardenClient({
|
|
374
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
375
|
+
runner,
|
|
376
|
+
}).read("rhdh-qe", [attachmentSelector()]), /notes.*attachment/i);
|
|
377
|
+
});
|
|
378
|
+
test("rejects non-string note metadata on attachment-backed items", async () => {
|
|
379
|
+
const runner = attachmentRunner({
|
|
380
|
+
id: "item-id",
|
|
381
|
+
name: "global/VAULT_CERT_PEM",
|
|
382
|
+
notes: 42,
|
|
383
|
+
type: 2,
|
|
384
|
+
collectionIds: ["collection-id"],
|
|
385
|
+
organizationId: "org-id",
|
|
386
|
+
attachments: [{ id: "attachment-id", fileName: "VAULT_CERT_PEM" }],
|
|
387
|
+
});
|
|
388
|
+
await assert.rejects(() => new BitwardenClient({
|
|
389
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
390
|
+
runner,
|
|
391
|
+
}).read("rhdh-qe", [attachmentSelector()]), (error) => {
|
|
392
|
+
assert(error instanceof Error);
|
|
393
|
+
assert.equal(error.message, "Bitwarden item notes must be null or empty when an attachment is present for global/");
|
|
394
|
+
assert.doesNotMatch(error.message, /synthetic-attachment/);
|
|
395
|
+
return true;
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
test("does not expose attachment command output when the command fails", async () => {
|
|
399
|
+
const attachmentPayload = "synthetic-private-attachment";
|
|
400
|
+
const runner = attachmentRunner({
|
|
401
|
+
id: "item-id",
|
|
402
|
+
name: "global/VAULT_CERT_PEM",
|
|
403
|
+
notes: null,
|
|
404
|
+
type: 2,
|
|
405
|
+
collectionIds: ["collection-id"],
|
|
406
|
+
organizationId: "org-id",
|
|
407
|
+
attachments: [{ id: "attachment-id", fileName: "VAULT_CERT_PEM" }],
|
|
408
|
+
}, { status: 1, stdout: attachmentPayload, stderr: attachmentPayload });
|
|
409
|
+
await assert.rejects(() => new BitwardenClient({
|
|
410
|
+
env: { BW_SESSION: "synthetic-session" },
|
|
411
|
+
runner,
|
|
412
|
+
}).read("rhdh-qe", [attachmentSelector()]), (error) => {
|
|
413
|
+
assert(error instanceof Error);
|
|
414
|
+
assert.match(error.message, /attachment read failed/i);
|
|
415
|
+
assert.doesNotMatch(error.message, /synthetic-private-attachment/);
|
|
416
|
+
return true;
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.integration.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/cli.integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/naming-convention, playwright/expect-expect -- node:test fixture models CLI environment keys */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { chmod, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
test("runs the package bin with a fake bw executable and redacted child auth", async () => {
|
|
9
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "secrets-cli-test-"));
|
|
10
|
+
const command = path.join(directory, "bw");
|
|
11
|
+
const profile = path.join(directory, "profile.json");
|
|
12
|
+
const entrypoint = path.join(directory, "rhdh-e2e-secrets");
|
|
13
|
+
await symlink(path.resolve("dist/secrets/cli.js"), entrypoint);
|
|
14
|
+
await writeFile(command, `#!/usr/bin/env node
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
if (args[0] === "--version") process.stdout.write("2026.5.0\\n");
|
|
17
|
+
else if (args[0] === "status") process.stdout.write(JSON.stringify({ status: "unlocked" }));
|
|
18
|
+
else if (args[0] === "sync") {}
|
|
19
|
+
else if (args[0] === "list" && args[1] === "collections") process.stdout.write(JSON.stringify([{ id: "collection-id", name: "Rhdh Qe Ci Secrets", organizationId: "organization-id" }]));
|
|
20
|
+
else if (args[0] === "list" && args[1] === "items") process.stdout.write(JSON.stringify([{ id: "note-item-id" }, { id: "attachment-item-id" }]));
|
|
21
|
+
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
|
+
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");
|
|
24
|
+
else process.exitCode = 1;
|
|
25
|
+
`);
|
|
26
|
+
await chmod(command, 0o755);
|
|
27
|
+
await writeFile(profile, JSON.stringify({
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
collection: "rhdh-qe",
|
|
30
|
+
selectors: [
|
|
31
|
+
{
|
|
32
|
+
prefix: "global/",
|
|
33
|
+
destination: {
|
|
34
|
+
kind: "environment",
|
|
35
|
+
stripPrefix: "global/",
|
|
36
|
+
requirePrefix: "VAULT_",
|
|
37
|
+
keyTransform: "legacy-env",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}));
|
|
42
|
+
try {
|
|
43
|
+
const result = spawnSync(entrypoint, [
|
|
44
|
+
"exec",
|
|
45
|
+
"--profile",
|
|
46
|
+
profile,
|
|
47
|
+
"--",
|
|
48
|
+
process.execPath,
|
|
49
|
+
"-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)",
|
|
51
|
+
], {
|
|
52
|
+
cwd: path.resolve("."),
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
PATH: `${directory}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
57
|
+
BW_SESSION: "synthetic-session",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
assert.equal(result.status, 0, result.stderr);
|
|
61
|
+
assert.match(result.stdout, /child-ran/);
|
|
62
|
+
assert.doesNotMatch(result.stdout, /synthetic-(note|attachment)-value/);
|
|
63
|
+
assert.doesNotMatch(result.stderr, /synthetic-(note|attachment)-value/);
|
|
64
|
+
assert.doesNotMatch(result.stdout, /synthetic-session/);
|
|
65
|
+
assert.doesNotMatch(result.stderr, /synthetic-session/);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await rm(directory, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/cli.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* eslint-disable playwright/expect-expect -- this file uses node:test assertions */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { parseCliArguments } from "../cli.js";
|
|
5
|
+
test("parses the exec profile, repeated workspaces, and command after --", () => {
|
|
6
|
+
const parsed = parseCliArguments([
|
|
7
|
+
"exec",
|
|
8
|
+
"--profile",
|
|
9
|
+
"e2e-secrets.profile.json",
|
|
10
|
+
"--workspace",
|
|
11
|
+
"backstage",
|
|
12
|
+
"--workspace=extensions",
|
|
13
|
+
"--",
|
|
14
|
+
"playwright",
|
|
15
|
+
"test",
|
|
16
|
+
"--headed",
|
|
17
|
+
]);
|
|
18
|
+
assert.deepEqual(parsed, {
|
|
19
|
+
command: "exec",
|
|
20
|
+
profilePath: "e2e-secrets.profile.json",
|
|
21
|
+
workspaces: ["backstage", "extensions"],
|
|
22
|
+
executable: "playwright",
|
|
23
|
+
args: ["test", "--headed"],
|
|
24
|
+
});
|
|
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);
|
|
30
|
+
});
|
|
31
|
+
test("rejects unknown commands and malformed options", () => {
|
|
32
|
+
assert.throws(() => parseCliArguments(["rotate"]), /unsupported command/i);
|
|
33
|
+
assert.throws(() => parseCliArguments(["exec", "--profile", "--", "playwright"]), /requires a value/i);
|
|
34
|
+
assert.throws(() => parseCliArguments([
|
|
35
|
+
"exec",
|
|
36
|
+
"--profile",
|
|
37
|
+
"profile.json",
|
|
38
|
+
"--workspace",
|
|
39
|
+
"--",
|
|
40
|
+
"playwright",
|
|
41
|
+
]), /requires a value/i);
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* eslint-disable playwright/expect-expect -- this file uses node:test assertions */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { expandProfile, getCollectionMapping, parseProfile, } from "../config.js";
|
|
5
|
+
const overlayProfile = {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
collection: "rhdh-plugin-export-overlays",
|
|
8
|
+
selectors: [
|
|
9
|
+
{
|
|
10
|
+
prefix: "global/",
|
|
11
|
+
destination: {
|
|
12
|
+
kind: "environment",
|
|
13
|
+
stripPrefix: "global/",
|
|
14
|
+
requirePrefix: "VAULT_",
|
|
15
|
+
keyTransform: "legacy-env",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
prefix: "workspaces/${workspace}/",
|
|
20
|
+
optional: true,
|
|
21
|
+
destination: {
|
|
22
|
+
kind: "environment",
|
|
23
|
+
stripPrefix: "workspaces/${workspace}/",
|
|
24
|
+
requirePrefix: "VAULT_",
|
|
25
|
+
keyTransform: "legacy-env",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
test("maps each approved collection to its exact Bitwarden collection name", () => {
|
|
31
|
+
assert.deepEqual(getCollectionMapping("rhdh-qe"), {
|
|
32
|
+
id: "rhdh-qe",
|
|
33
|
+
bitwardenCollection: "Rhdh Qe Ci Secrets",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
test("rejects the GSM-only AWS collection before Bitwarden access", () => {
|
|
37
|
+
assert.throws(() => parseProfile({ ...overlayProfile, collection: "rhdh-aws-credentials" }), /rhdh-aws-credentials.*Bitwarden/i);
|
|
38
|
+
});
|
|
39
|
+
test("expands workspace selectors once for every requested workspace", () => {
|
|
40
|
+
assert.deepEqual(expandProfile(overlayProfile, ["backstage", "extensions"]).selectors.map((selector) => selector.prefix), ["global/", "workspaces/backstage/", "workspaces/extensions/"]);
|
|
41
|
+
});
|
|
42
|
+
test("allows an optional workspace selector to have no matching items", () => {
|
|
43
|
+
const expanded = expandProfile(overlayProfile, ["extensions"]);
|
|
44
|
+
assert.equal(expanded.selectors[1]?.optional, true);
|
|
45
|
+
});
|
|
46
|
+
test("rejects workspace arguments for a profile without a workspace token", () => {
|
|
47
|
+
assert.throws(() => expandProfile(parseProfile({
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
collection: "rhdh-qe",
|
|
50
|
+
selectors: [
|
|
51
|
+
{
|
|
52
|
+
prefix: "rhdh/",
|
|
53
|
+
destination: {
|
|
54
|
+
kind: "environment",
|
|
55
|
+
stripPrefix: "rhdh/",
|
|
56
|
+
keyTransform: "identity",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}), ["backstage"]), /does not accept workspace arguments/);
|
|
61
|
+
});
|
|
62
|
+
test("rejects invalid workspace names and duplicate transformed selectors", () => {
|
|
63
|
+
assert.throws(() => expandProfile(overlayProfile, ["../secrets"]), /invalid workspace/i);
|
|
64
|
+
assert.throws(() => parseProfile({
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
collection: "rhdh-qe",
|
|
67
|
+
selectors: [
|
|
68
|
+
{
|
|
69
|
+
prefix: "rhdh/",
|
|
70
|
+
destination: {
|
|
71
|
+
kind: "environment",
|
|
72
|
+
stripPrefix: "rhdh/",
|
|
73
|
+
keyTransform: "identity",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
prefix: "rhdh/",
|
|
78
|
+
destination: {
|
|
79
|
+
kind: "environment",
|
|
80
|
+
stripPrefix: "rhdh/",
|
|
81
|
+
keyTransform: "identity",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}), /duplicate selector/i);
|
|
86
|
+
});
|
|
87
|
+
test("rejects a strip prefix that cannot apply to the selected item prefix", () => {
|
|
88
|
+
assert.throws(() => parseProfile({
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
collection: "rhdh-qe",
|
|
91
|
+
selectors: [
|
|
92
|
+
{
|
|
93
|
+
prefix: "global/",
|
|
94
|
+
destination: {
|
|
95
|
+
kind: "environment",
|
|
96
|
+
stripPrefix: "workspaces/",
|
|
97
|
+
keyTransform: "identity",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}), /stripPrefix.*prefix/i);
|
|
102
|
+
});
|
|
103
|
+
test("rejects duplicate workspace arguments", () => {
|
|
104
|
+
assert.throws(() => expandProfile(overlayProfile, ["backstage", "backstage"]), /duplicate workspace/i);
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.test.d.ts","sourceRoot":"","sources":["../../../src/secrets/tests/environment.test.ts"],"names":[],"mappings":""}
|