Skip to content

Commit 177e775

Browse files
committed
feat: updated tests for every secret command to allow undefined secret path
1 parent fc7bb0b commit 177e775

15 files changed

+399
-264
lines changed

src/errors.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,21 @@ class ErrorPolykeyCLIMakeDirectory<T> extends ErrorPolykeyCLI<T> {
157157
exitCode = 1;
158158
}
159159

160+
class ErrorPolykeyCLIRenameSecret<T> extends ErrorPolykeyCLI<T> {
161+
static description = 'Failed to rename one or more secrets';
162+
exitCode = 1;
163+
}
164+
165+
class ErrorPolykeyCLIRemoveSecret<T> extends ErrorPolykeyCLI<T> {
166+
static description = 'Failed to remove one or more secrets';
167+
exitCode = 1;
168+
}
169+
170+
class ErrorPolykeyCLICatSecret<T> extends ErrorPolykeyCLI<T> {
171+
static description = 'Failed to concatenate one or more secrets';
172+
exitCode = 1;
173+
}
174+
160175
export {
161176
ErrorPolykeyCLI,
162177
ErrorPolykeyCLIUncaughtException,
@@ -178,4 +193,7 @@ export {
178193
ErrorPolykeyCLIInvalidEnvName,
179194
ErrorPolykeyCLIDuplicateEnvName,
180195
ErrorPolykeyCLIMakeDirectory,
196+
ErrorPolykeyCLIRenameSecret,
197+
ErrorPolykeyCLIRemoveSecret,
198+
ErrorPolykeyCLICatSecret,
181199
};

src/secrets/CommandCat.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as binUtils from '../utils';
44
import * as binOptions from '../utils/options';
55
import * as binParsers from '../utils/parsers';
66
import * as binProcessors from '../utils/processors';
7+
import * as errors from '../errors';
78

89
class CommandGet extends CommandPolykey {
910
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
@@ -21,7 +22,7 @@ class CommandGet extends CommandPolykey {
2122
this.addOption(binOptions.clientPort);
2223
this.action(async (secretPaths, options) => {
2324
secretPaths = secretPaths.map((path: string) =>
24-
binParsers.parseSecretPathValue(path),
25+
binParsers.parseSecretPath(path),
2526
);
2627
const { default: PolykeyClient } = await import(
2728
'polykey/dist/PolykeyClient'
@@ -77,29 +78,37 @@ class CommandGet extends CommandPolykey {
7778
});
7879
return;
7980
}
80-
await binUtils.retryAuthentication(async (auth) => {
81+
const response = await binUtils.retryAuthentication(async (auth) => {
8182
const response = await pkClient.rpcClient.methods.vaultsSecretsGet();
82-
await (async () => {
83-
const writer = response.writable.getWriter();
84-
let first = true;
85-
for (const [vaultName, secretPath] of secretPaths) {
86-
await writer.write({
87-
nameOrId: vaultName,
88-
secretName: secretPath ?? '/',
89-
metadata: first
90-
? { ...auth, options: { continueOnError: true } }
91-
: undefined,
92-
});
93-
first = false;
94-
}
95-
await writer.close();
96-
})();
97-
for await (const chunk of response.readable) {
98-
if (chunk.error) process.stderr.write(chunk.error);
99-
else process.stdout.write(chunk.secretContent);
83+
const writer = response.writable.getWriter();
84+
let first = true;
85+
for (const [vaultName, secretPath] of secretPaths) {
86+
await writer.write({
87+
nameOrId: vaultName,
88+
secretName: secretPath ?? '/',
89+
metadata: first
90+
? { ...auth, options: { continueOnError: true } }
91+
: undefined,
92+
});
93+
first = false;
10094
}
101-
process.stderr.write("\n");
95+
await writer.close();
96+
return response;
10297
}, meta);
98+
let hasErrored = false;
99+
for await (const chunk of response.readable) {
100+
if (chunk.error) {
101+
hasErrored = true;
102+
process.stderr.write(chunk.error);
103+
} else {
104+
process.stdout.write(chunk.secretContent);
105+
}
106+
}
107+
if (hasErrored) {
108+
throw new errors.ErrorPolykeyCLICatSecret(
109+
'Failed to concatenate one or more secrets',
110+
);
111+
}
103112
} finally {
104113
if (pkClient! != null) await pkClient.stop();
105114
}

src/secrets/CommandCreate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class CommandCreate extends CommandPolykey {
7373
pkClient.rpcClient.methods.vaultsSecretsNew({
7474
metadata: auth,
7575
nameOrId: secretPath[0],
76-
secretName: secretPath[1],
76+
secretName: secretPath[1] ?? '/',
7777
secretContent: content.toString('binary'),
7878
}),
7979
meta,

src/secrets/CommandRemove.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as binUtils from '../utils';
44
import * as binOptions from '../utils/options';
55
import * as binParsers from '../utils/parsers';
66
import * as binProcessors from '../utils/processors';
7+
import * as errors from '../errors';
78

89
class CommandRemove extends CommandPolykey {
910
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
@@ -20,9 +21,17 @@ class CommandRemove extends CommandPolykey {
2021
this.addOption(binOptions.clientPort);
2122
this.addOption(binOptions.recursive);
2223
this.action(async (secretPaths, options) => {
23-
secretPaths = secretPaths.map((path: string) =>
24-
binParsers.parseSecretPath(path),
25-
);
24+
for (let i = 0; i < secretPaths.length; i++) {
25+
const value: string = secretPaths[i];
26+
const parsedValue = binParsers.parseSecretPath(value);
27+
// The vault root cannot be deleted
28+
if (parsedValue[1] == null) {
29+
throw new errors.ErrorPolykeyCLIRemoveSecret(
30+
'EPERM: Cannot remove vault root',
31+
);
32+
}
33+
secretPaths[i] = parsedValue;
34+
}
2635
const { default: PolykeyClient } = await import(
2736
'polykey/dist/PolykeyClient'
2837
);

src/secrets/CommandRename.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as binUtils from '../utils';
44
import * as binOptions from '../utils/options';
55
import * as binParsers from '../utils/parsers';
66
import * as binProcessors from '../utils/processors';
7+
import * as errors from '../errors';
78

89
class CommandRename extends CommandPolykey {
910
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
@@ -13,13 +14,19 @@ class CommandRename extends CommandPolykey {
1314
this.argument(
1415
'<secretPath>',
1516
'Path to where the secret to be renamed, specified as <vaultName>:<directoryPath>',
16-
binParsers.parseSecretPathValue,
17+
binParsers.parseSecretPath,
1718
);
1819
this.argument('<newSecretName>', 'New name of the secret');
1920
this.addOption(binOptions.nodeId);
2021
this.addOption(binOptions.clientHost);
2122
this.addOption(binOptions.clientPort);
2223
this.action(async (secretPath, newSecretName, options) => {
24+
// Ensure that a valid secret path is provided
25+
if (secretPath[1] == null) {
26+
throw new errors.ErrorPolykeyCLIRenameSecret(
27+
'EPERM: Cannot rename vault root',
28+
);
29+
}
2330
const { default: PolykeyClient } = await import(
2431
'polykey/dist/PolykeyClient'
2532
);

src/secrets/CommandStat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class CommandStat extends CommandPolykey {
5555
pkClient.rpcClient.methods.vaultsSecretsStat({
5656
metadata: auth,
5757
nameOrId: secretPath[0],
58-
secretName: secretPath[1],
58+
secretName: secretPath[1] ?? '/',
5959
}),
6060
meta,
6161
);

src/utils/parsers.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import * as nodesUtils from 'polykey/dist/nodes/utils';
1010

1111
const vaultNameRegex = /^(?!.*[:])[ -~\t\n]*$/s;
1212
const secretPathRegex = /^(?!.*[=])[ -~\t\n]*$/s;
13-
const vaultNameSecretPathRegex = /^([\w\-\.]+)(?::([^\0\\=]+))?$/;
1413
const secretPathValueRegex = /^([a-zA-Z_][\w]+)?$/;
1514
const environmentVariableRegex = /^([a-zA-Z_]+[a-zA-Z0-9_]*)?$/;
1615

@@ -76,52 +75,54 @@ function parseVaultName(vaultName: string): string {
7675
return vaultName;
7776
}
7877

79-
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
80-
// If 'vault1', ['vault1, undefined] is returned
78+
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c', undefined] is returned
79+
// If 'vault1', ['vault1', undefined, undefined] is returned
80+
// If 'vault1:abc=xyz', ['vault1', 'abc', 'xyz'] is returned
8181
// If 'vault1:', an error is thrown
8282
// If 'a/b/c', an error is thrown
8383
// Splits out everything after an `=` separator
84-
function parseSecretPath(secretPath: string): [string, string?, string?] {
84+
function parseSecretPath(inputPath: string): [string, string?, string?] {
8585
// The colon character `:` is prohibited in vaultName, so it's first occurence
8686
// means that this is the delimiter between vaultName and secretPath.
87-
const colonIndex = secretPath.indexOf(':');
87+
const colonIndex = inputPath.indexOf(':');
8888
// If no colon exists, treat entire string as vault name
8989
if (colonIndex === -1) {
90-
return [parseVaultName(secretPath), undefined, undefined];
90+
return [parseVaultName(inputPath), undefined, undefined];
9191
}
92-
// Calculate contents before the `=` separator
93-
const vaultNamePart = secretPath.substring(0, colonIndex);
94-
const secretPathPart = secretPath.substring(colonIndex + 1);
95-
// Calculate contents after the `=` separator
92+
// Calculate vaultName and secretPath
93+
const vaultNamePart = inputPath.substring(0, colonIndex);
94+
const secretPathPart = inputPath.substring(colonIndex + 1);
95+
// Calculate contents after the `=` separator (value)
9696
const equalIndex = secretPathPart.indexOf('=');
97-
const splitSecretPath =
97+
// If `=` isn't found, then the entire secret path section is the actual path.
98+
// Otherwise, split the path section by the index of `=`. First half is the
99+
// actual secret part, and the second half is the value.
100+
const secretPath =
98101
equalIndex === -1
99102
? secretPathPart
100103
: secretPathPart.substring(0, equalIndex);
101104
const value =
102-
equalIndex === -1 ? undefined : secretPathPart.substring(equalIndex + 1);
103-
if (splitSecretPath != null && !secretPathRegex.test(splitSecretPath)) {
105+
equalIndex !== -1 ? secretPathPart.substring(equalIndex + 1) : undefined;
106+
// If secretPath exists but it doesn't pass the regex test, then the path is
107+
// malformed.
108+
if (secretPath != null && !secretPathRegex.test(secretPath)) {
104109
throw new commander.InvalidArgumentError(
105-
`${secretPath} is not of the format <vaultName>[:<secretPath>][=<value>]`,
110+
`${inputPath} is not of the format <vaultName>[:<secretPath>][=<value>]`,
106111
);
107112
}
108-
const parsedVaultName = parseVaultName(vaultNamePart);
109-
const parsedSecretPath = splitSecretPath.match(secretPathRegex)?.[0];
110-
return [parsedVaultName, parsedSecretPath, value];
113+
// We have already tested if the secretPath is valid, and we can return it
114+
// as-is.
115+
const vaultName = parseVaultName(vaultNamePart);
116+
return [vaultName, secretPath, value];
111117
}
112118

113-
function parseSecretPathValue(secretPath: string): [string, string, string?] {
119+
function parseSecretPathValue(secretPath: string): [string, string?, string?] {
114120
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
115121
if (value != null && !secretPathValueRegex.test(value)) {
116122
throw new commander.InvalidArgumentError(
117123
`${value} is not a valid value name`,
118124
);
119125
}
120-
if (directoryPath == null) {
121-
throw new commander.InvalidArgumentError(
122-
`${secretPath} is not of the format <vaultName>:<directoryPath>[=<value>]`,
123-
);
124-
}
125126
return [vaultName, directoryPath, value];
126127
}
127128

src/vaults/CommandList.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import * as binProcessors from '../utils/processors';
88
class CommandList extends CommandPolykey {
99
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
1010
super(...args);
11-
this.name('list');
12-
this.description('List all Available Vaults');
11+
this.name('ls');
12+
this.alias('list');
13+
this.description('List all available Vaults');
1314
this.addOption(binOptions.nodeId);
1415
this.addOption(binOptions.clientHost);
1516
this.addOption(binOptions.clientPort);

tests/secrets/cat.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('commandCatSecret', () => {
4040
});
4141
});
4242
test('should retrieve a secret', async () => {
43-
const vaultName = 'Vault3' as VaultName;
43+
const vaultName = 'vault' as VaultName;
4444
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
4545
const secretName = 'secret-name';
4646
const secretContent = 'this is the contents of the secret';
@@ -62,7 +62,7 @@ describe('commandCatSecret', () => {
6262
expect(result.stdout).toBe(secretContent);
6363
});
6464
test('should fail when reading root without secret path', async () => {
65-
const vaultName = 'Vault3' as VaultName;
65+
const vaultName = 'vault' as VaultName;
6666
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
6767
const secretName = 'secret-name';
6868
const secretContent = 'this is the contents of the secret';
@@ -75,7 +75,7 @@ describe('commandCatSecret', () => {
7575
cwd: dataDir,
7676
});
7777
expect(result.exitCode).not.toBe(0);
78-
expect(result.stderr).toBeDefined();
78+
expect(result.stderr).toInclude('ErrorSecretsIsDirectory');
7979
});
8080
test('should concatenate multiple secrets', async () => {
8181
const vaultName = 'Vault3' as VaultName;
@@ -148,14 +148,14 @@ describe('commandCatSecret', () => {
148148
'-np',
149149
dataDir,
150150
`${vaultName}:${secretName1}`,
151-
`${vaultName}:doesnt-exist-file`,
151+
`${vaultName}:doesnt-exist`,
152152
`${vaultName}:${secretName2}`,
153153
];
154154
const result = await testUtils.pkStdio(command, {
155155
env: { PK_PASSWORD: password },
156156
cwd: dataDir,
157157
});
158-
expect(result.exitCode).toBe(0);
158+
expect(result.exitCode).toBe(1);
159159
expect(result.stdout).toBe(`${secretName1}${secretName2}`);
160160
expect(result.stderr).not.toBe('');
161161
});

0 commit comments

Comments
 (0)