@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.
- package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +5 -0
- package/dist/deployment/rhdh/constants.d.ts +0 -5
- package/dist/deployment/rhdh/constants.d.ts.map +1 -1
- package/dist/deployment/rhdh/constants.js +0 -5
- package/dist/deployment/rhdh/deployment.d.ts +2 -2
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +9 -25
- package/dist/deployment/rhdh/deployment.test.js +9 -9
- package/dist/deployment/rhdh/types.d.ts +0 -4
- package/dist/deployment/rhdh/types.d.ts.map +1 -1
- package/dist/playwright/helpers/github-session.test.js +1 -1
- package/dist/secrets/bitwarden.d.ts +43 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -1
- package/dist/secrets/bitwarden.js +465 -53
- package/dist/secrets/cli.d.ts +48 -1
- package/dist/secrets/cli.d.ts.map +1 -1
- package/dist/secrets/cli.js +455 -28
- package/dist/secrets/command.d.ts +7 -0
- package/dist/secrets/command.d.ts.map +1 -1
- package/dist/secrets/command.js +103 -26
- package/dist/secrets/config.d.ts +11 -1
- package/dist/secrets/config.d.ts.map +1 -1
- package/dist/secrets/config.js +32 -1
- package/dist/secrets/environment-name.d.ts +2 -0
- package/dist/secrets/environment-name.d.ts.map +1 -0
- package/dist/secrets/environment-name.js +4 -0
- package/dist/secrets/environment.d.ts +10 -0
- package/dist/secrets/environment.d.ts.map +1 -1
- package/dist/secrets/environment.js +35 -4
- package/dist/secrets/exec.d.ts +9 -1
- package/dist/secrets/exec.d.ts.map +1 -1
- package/dist/secrets/exec.js +318 -38
- package/dist/secrets/gsm-wrapper.d.ts +42 -0
- package/dist/secrets/gsm-wrapper.d.ts.map +1 -0
- package/dist/secrets/gsm-wrapper.js +238 -0
- package/dist/secrets/gsm.d.ts +33 -0
- package/dist/secrets/gsm.d.ts.map +1 -0
- package/dist/secrets/gsm.js +172 -0
- package/dist/secrets/index.d.ts +9 -4
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +7 -2
- package/dist/secrets/lock.d.ts +3 -0
- package/dist/secrets/lock.d.ts.map +1 -0
- package/dist/secrets/lock.js +28 -0
- package/dist/secrets/mutation.d.ts +71 -0
- package/dist/secrets/mutation.d.ts.map +1 -0
- package/dist/secrets/mutation.js +210 -0
- package/dist/secrets/secret-input.d.ts +17 -0
- package/dist/secrets/secret-input.d.ts.map +1 -0
- package/dist/secrets/secret-input.js +40 -0
- package/dist/secrets/stream.d.ts +9 -0
- package/dist/secrets/stream.d.ts.map +1 -0
- package/dist/secrets/stream.js +211 -0
- package/dist/secrets/temporary-secret.d.ts +13 -0
- package/dist/secrets/temporary-secret.d.ts.map +1 -0
- package/dist/secrets/temporary-secret.js +87 -0
- package/dist/secrets/tests/bitwarden.test.js +640 -0
- package/dist/secrets/tests/cli.integration.test.js +131 -3
- package/dist/secrets/tests/cli.test.js +281 -6
- package/dist/secrets/tests/command.test.d.ts +2 -0
- package/dist/secrets/tests/command.test.d.ts.map +1 -0
- package/dist/secrets/tests/command.test.js +39 -0
- package/dist/secrets/tests/config.test.js +20 -1
- package/dist/secrets/tests/environment.test.js +145 -3
- package/dist/secrets/tests/exec.test.js +469 -2
- package/dist/secrets/tests/gsm-wrapper.test.d.ts +2 -0
- package/dist/secrets/tests/gsm-wrapper.test.d.ts.map +1 -0
- package/dist/secrets/tests/gsm-wrapper.test.js +166 -0
- package/dist/secrets/tests/gsm.test.d.ts +2 -0
- package/dist/secrets/tests/gsm.test.d.ts.map +1 -0
- package/dist/secrets/tests/gsm.test.js +181 -0
- package/dist/secrets/tests/mutation.test.d.ts +2 -0
- package/dist/secrets/tests/mutation.test.d.ts.map +1 -0
- package/dist/secrets/tests/mutation.test.js +390 -0
- package/dist/secrets/tests/secret-input.test.d.ts +2 -0
- package/dist/secrets/tests/secret-input.test.d.ts.map +1 -0
- package/dist/secrets/tests/secret-input.test.js +71 -0
- package/dist/secrets/tests/stream.test.d.ts +2 -0
- package/dist/secrets/tests/stream.test.d.ts.map +1 -0
- package/dist/secrets/tests/stream.test.js +88 -0
- package/dist/secrets/tests/temporary-secret.test.d.ts +2 -0
- package/dist/secrets/tests/temporary-secret.test.d.ts.map +1 -0
- package/dist/secrets/tests/temporary-secret.test.js +76 -0
- package/dist/utils/plugin-metadata.d.ts +5 -0
- package/dist/utils/plugin-metadata.d.ts.map +1 -1
- package/dist/utils/plugin-metadata.js +19 -0
- package/dist/utils/tests/plugin-metadata.test.js +17 -1
- package/dist/utils/workspace-paths.d.ts +0 -2
- package/dist/utils/workspace-paths.d.ts.map +1 -1
- package/dist/utils/workspace-paths.js +0 -4
- package/package.json +1 -1
- package/dist/deployment/rhdh/config/new-frontend-system/dynamic-plugins.yaml +0 -5
- 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("
|
|
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
|
-
|
|
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.
|
|
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("
|
|
27
|
-
assert.
|
|
28
|
-
|
|
29
|
-
|
|
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("
|
|
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 @@
|
|
|
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
|
});
|