diff --git a/npmDepsHash b/npmDepsHash index 92a9278b..33eb5338 100644 --- a/npmDepsHash +++ b/npmDepsHash @@ -1 +1 @@ -sha256-/JZlQA4VOEGvT5iFaN2KN4LpKI0RqlFhmFQJlesKRQU= +sha256-WkSKOsHFtWeCLyYr5WICTXG+A4+P6/hdxvlutNudQTg= diff --git a/package-lock.json b/package-lock.json index f9237a30..932d407a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "nexpect": "^0.6.0", "node-gyp-build": "^4.4.0", "nodemon": "^3.0.1", - "polykey": "^1.15.1", + "polykey": "^1.16.0", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", @@ -1668,9 +1668,9 @@ "devOptional": true }, "node_modules/@matrixai/rpc": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@matrixai/rpc/-/rpc-0.5.1.tgz", - "integrity": "sha512-crqC2J7jGSQhPwHOoO0dWmUXx5JVb1Cfl4bXB9dcc9JobetU3k0rf5iC4BF7JGCtIxQPRJnT63y9+6EGzozZgA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@matrixai/rpc/-/rpc-0.6.0.tgz", + "integrity": "sha512-ENjJO2h7CmPLaHhObHs2nvpv98YZPxa79/jf+TqEPEfbhE1BkNCys9pXDE/CYDP9vxb4seS39WkR9cNivQU50A==", "dev": true, "dependencies": { "@matrixai/async-init": "^1.10.0", @@ -7602,9 +7602,9 @@ } }, "node_modules/polykey": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.15.1.tgz", - "integrity": "sha512-BP8KNUjkZmOrj097C2IiwaVWr2qJ+k9kivDcanmmME1Pwi/T0Lg/YuOAKLMca0tetRTzL6Hghma59pkSvKUPDQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.16.0.tgz", + "integrity": "sha512-6yYuGny/cbB0KYH5cxdNkKS8MX7Na8MmFLBkOi1S807Q/MZlVDMvpmIdiiN5NpoISqn1LnKgJ2CDSlFN/q6+mQ==", "dev": true, "dependencies": { "@matrixai/async-cancellable": "^1.1.1", @@ -7619,7 +7619,7 @@ "@matrixai/mdns": "^1.2.6", "@matrixai/quic": "^1.2.10", "@matrixai/resources": "^1.1.5", - "@matrixai/rpc": "^0.5.1", + "@matrixai/rpc": "^0.6.0", "@matrixai/timer": "^1.1.3", "@matrixai/workers": "^1.3.7", "@matrixai/ws": "^1.1.7", diff --git a/package.json b/package.json index 0d50dc26..88cecf6b 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "nexpect": "^0.6.0", "node-gyp-build": "^4.4.0", "nodemon": "^3.0.1", - "polykey": "^1.15.1", + "polykey": "^1.16.0", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", diff --git a/src/errors.ts b/src/errors.ts index bc5a78f9..f147e8fd 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -75,6 +75,15 @@ class ErrorPolykeyCLIAsynchronousDeadlock extends ErrorPolykeyCLI { exitCode = sysexits.SOFTWARE; } +/** + * Unexpected error is a runtime error. + * If these exceptions occur, there is a bug. Most likely in Polykey. + */ +class ErrorPolykeyCLIUnexpectedError extends ErrorPolykeyCLI { + static description = 'An unexpected error occured'; + exitCode = sysexits.SOFTWARE; +} + class ErrorPolykeyCLINodePath extends ErrorPolykeyCLI { static description = 'Cannot derive default node path from unknown platform'; exitCode = sysexits.USAGE; @@ -172,10 +181,16 @@ class ErrorPolykeyCLICatSecret extends ErrorPolykeyCLI { exitCode = 1; } +class ErrorPolykeyCLIEditSecret extends ErrorPolykeyCLI { + static description = 'Failed to edit a secret'; + exitCode = 1; +} + export { ErrorPolykeyCLI, ErrorPolykeyCLIUncaughtException, ErrorPolykeyCLIUnhandledRejection, + ErrorPolykeyCLIUnexpectedError, ErrorPolykeyCLIAsynchronousDeadlock, ErrorPolykeyCLINodePath, ErrorPolykeyCLIClientOptions, @@ -196,4 +211,5 @@ export { ErrorPolykeyCLIRenameSecret, ErrorPolykeyCLIRemoveSecret, ErrorPolykeyCLICatSecret, + ErrorPolykeyCLIEditSecret, }; diff --git a/src/polykey.ts b/src/polykey.ts index 936a80b6..26d722a1 100755 --- a/src/polykey.ts +++ b/src/polykey.ts @@ -75,10 +75,14 @@ async function polykeyAgentMain(): Promise { process.exitCode = e.exitCode; } else { // Unknown error, this should not happen + const wrappedError = new binErrors.ErrorPolykeyCLIUnexpectedError( + binUtils.composeErrorMessage(e), + { cause: e }, + ); process.stderr.write( binUtils.outputFormatter({ type: errFormat, - data: e, + data: wrappedError, }), ); process.exitCode = 255; @@ -217,10 +221,14 @@ async function polykeyMain(argv: Array): Promise { process.exitCode = e.exitCode; } else { // Unknown error, this should not happen + const wrappedError = new binErrors.ErrorPolykeyCLIUnexpectedError( + binUtils.composeErrorMessage(e), + { cause: e }, + ); process.stderr.write( binUtils.outputFormatter({ type: errFormat, - data: e, + data: wrappedError, }), ); process.exitCode = 255; diff --git a/src/secrets/CommandCat.ts b/src/secrets/CommandCat.ts index dd2e99c4..cec05106 100644 --- a/src/secrets/CommandCat.ts +++ b/src/secrets/CommandCat.ts @@ -1,4 +1,6 @@ import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type { ContentOrErrorMessage } from 'polykey/dist/client/types'; +import type { ReadableStream } from 'stream/web'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; import * as binOptions from '../utils/options'; @@ -80,28 +82,44 @@ class CommandGet extends CommandPolykey { } const hasErrored = await binUtils.retryAuthentication(async (auth) => { // Write secret paths to input stream - const response = await pkClient.rpcClient.methods.vaultsSecretsGet(); + const response = await pkClient.rpcClient.methods.vaultsSecretsCat(); const writer = response.writable.getWriter(); let first = true; for (const [vaultName, secretPath] of secretPaths) { await writer.write({ nameOrId: vaultName, secretName: secretPath ?? '/', - metadata: first - ? { ...auth, options: { continueOnError: true } } - : undefined, + metadata: first ? auth : undefined, }); first = false; } await writer.close(); // Print out incoming data to standard out let hasErrored = false; - for await (const chunk of response.readable) { - if (chunk.error) { + // TypeScript cannot properly perform type narrowing on this type, so + // the `as` keyword is used to help it out. + for await (const result of response.readable as ReadableStream) { + if (result.type === 'error') { hasErrored = true; - process.stderr.write(chunk.error); + switch (result.code) { + case 'ENOENT': + // Attempt to cat a non-existent file + process.stderr.write( + `cat: ${result.reason}: No such file or directory\n`, + ); + break; + case 'EISDIR': + // Attempt to cat a directory + process.stderr.write( + `cat: ${result.reason}: Is a directory\n`, + ); + break; + default: + // No other code should be thrown + throw result; + } } else { - process.stdout.write(chunk.secretContent); + process.stdout.write(result.secretContent); } } return hasErrored; diff --git a/src/secrets/CommandEdit.ts b/src/secrets/CommandEdit.ts index f89269e1..afdfe442 100644 --- a/src/secrets/CommandEdit.ts +++ b/src/secrets/CommandEdit.ts @@ -24,7 +24,7 @@ class CommandEdit extends CommandPolykey { this.addOption(binOptions.clientPort); this.action(async (secretPath, options) => { const os = await import('os'); - const { execSync } = await import('child_process'); + const { spawn } = await import('child_process'); const vaultsErrors = await import('polykey/dist/vaults/errors'); const { default: PolykeyClient } = await import( 'polykey/dist/PolykeyClient' @@ -64,17 +64,14 @@ class CommandEdit extends CommandPolykey { const secretExists = await binUtils.retryAuthentication( async (auth) => { let exists = true; - const res = await pkClient.rpcClient.methods.vaultsSecretsGet(); - const writer = res.writable.getWriter(); - await writer.write({ + const response = await pkClient.rpcClient.methods.vaultsSecretsGet({ nameOrId: secretPath[0], secretName: secretPath[1] ?? '/', metadata: auth, }); - await writer.close(); try { let rawSecretContent: string = ''; - for await (const chunk of res.readable) { + for await (const chunk of response) { rawSecretContent += chunk.secretContent; } const secretContent = Buffer.from(rawSecretContent, 'binary'); @@ -83,6 +80,20 @@ class CommandEdit extends CommandPolykey { const [cause, _] = binUtils.remoteErrorCause(e); if (cause instanceof vaultsErrors.ErrorSecretsSecretUndefined) { exists = false; + } else if ( + cause instanceof vaultsErrors.ErrorSecretsIsDirectory + ) { + // First, write the inline error to standard error like other + // secrets commands do. + process.stderr.write( + `edit: ${secretPath[1] ?? '/'}: No such file or directory\n`, + ); + // Then, throw an error to get the non-zero exit code. As this + // command is Polykey-specific, the code doesn't really matter + // that much. + throw new errors.ErrorPolykeyCLIEditSecret( + 'Failed to edit secret', + ); } else { throw e; } @@ -94,7 +105,29 @@ class CommandEdit extends CommandPolykey { // If the editor exited with a code other than zero, then execSync // will throw an error. So, in the case of saving the file but the // editor crashing, the program won't save the updated secret. - execSync(`${process.env.EDITOR} \"${tmpFile}\"`, { stdio: 'inherit' }); + await new Promise((resolve, reject) => { + // If $EDITOR is unset, default to nano. If that doesn't exist, then + // the command will raise an error. + const editorProc = spawn(process.env.EDITOR ?? 'nano', [tmpFile], { + stdio: 'inherit', + }); + editorProc.on('error', (e) => { + const error = new errors.ErrorPolykeyCLIEditSecret( + `Failed to run command ${process.env.EDITOR}`, + { cause: e }, + ); + reject(error); + }); + editorProc.on('close', (code) => { + if (code !== 0) { + const error = new errors.ErrorPolykeyCLIEditSecret( + `Editor exited with code ${code}`, + ); + reject(error); + } + resolve(); + }); + }); let content: string; try { const buffer = await this.fs.promises.readFile(tmpFile); diff --git a/src/secrets/CommandMkdir.ts b/src/secrets/CommandMkdir.ts index 073383f0..37de289a 100644 --- a/src/secrets/CommandMkdir.ts +++ b/src/secrets/CommandMkdir.ts @@ -1,14 +1,12 @@ import type PolykeyClient from 'polykey/dist/PolykeyClient'; -import type { ErrorMessage } from 'polykey/dist/client/types'; +import type { SuccessOrErrorMessage } from 'polykey/dist/client/types'; +import type { ReadableStream } from 'stream/web'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; import * as binOptions from '../utils/options'; import * as binParsers from '../utils/parsers'; import * as binProcessors from '../utils/processors'; -import { - ErrorPolykeyCLIMakeDirectory, - ErrorPolykeyCLIUncaughtException, -} from '../errors'; +import { ErrorPolykeyCLIMakeDirectory } from '../errors'; class CommandMkdir extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -59,6 +57,7 @@ class CommandMkdir extends CommandPolykey { }, logger: this.logger.getChild(PolykeyClient.name), }); + throw new TypeError(); const hasErrored = await binUtils.retryAuthentication(async (auth) => { // Write directory paths to input stream const response = @@ -79,29 +78,28 @@ class CommandMkdir extends CommandPolykey { // Print out incoming data to standard out, or incoming errors to // standard error. let hasErrored = false; - for await (const result of response.readable) { + // TypeScript cannot properly perform type narrowing on this type, so + // the `as` keyword is used to help it out. + for await (const result of response.readable as ReadableStream) { if (result.type === 'error') { - // TS cannot properly evaluate a type this deeply nested, so we use - // the as keyword to help it. Inside this block, the type of data - // is ensured to be 'error'. - const error = result as ErrorMessage; hasErrored = true; - let message: string = ''; - switch (error.code) { + switch (result.code) { case 'ENOENT': - message = 'No such secret or directory'; + // Attempt to create a directory without existing parents + process.stderr.write( + `mkdir: cannot create directory ${result.reason}: No such file or directory\n`, + ); break; case 'EEXIST': - message = 'Secret or directory exists'; + // Attempt to create a directory where something already exists + process.stderr.write( + `mkdir: cannot create directory ${result.reason}: File or directory exists\n`, + ); break; default: - throw new ErrorPolykeyCLIUncaughtException( - `Unexpected error code: ${error.code}`, - ); + // No other code should be thrown + throw result; } - process.stderr.write( - `${error.code}: cannot create directory ${error.reason}: ${message}\n`, - ); } } return hasErrored; diff --git a/src/secrets/CommandRemove.ts b/src/secrets/CommandRemove.ts index a9dcbd35..91df3a9c 100644 --- a/src/secrets/CommandRemove.ts +++ b/src/secrets/CommandRemove.ts @@ -1,4 +1,6 @@ import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type { SuccessOrErrorMessage } from 'polykey/dist/client/types'; +import type { ReadableStream } from 'stream/web'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; import * as binOptions from '../utils/options'; @@ -21,17 +23,9 @@ class CommandRemove extends CommandPolykey { this.addOption(binOptions.clientPort); this.addOption(binOptions.recursive); this.action(async (secretPaths, options) => { - for (let i = 0; i < secretPaths.length; i++) { - const value: string = secretPaths[i]; - const parsedValue = binParsers.parseSecretPath(value); - // The vault root cannot be deleted - if (parsedValue[1] == null) { - throw new errors.ErrorPolykeyCLIRemoveSecret( - 'EPERM: Cannot remove vault root', - ); - } - secretPaths[i] = parsedValue; - } + secretPaths = secretPaths.map((path: string) => + binParsers.parseSecretPath(path), + ); const { default: PolykeyClient } = await import( 'polykey/dist/PolykeyClient' ); @@ -59,15 +53,15 @@ class CommandRemove extends CommandPolykey { options: { nodePath: options.nodePath }, logger: this.logger.getChild(PolykeyClient.name), }); - await binUtils.retryAuthentication(async (auth) => { + const hasErrored = await binUtils.retryAuthentication(async (auth) => { const response = await pkClient.rpcClient.methods.vaultsSecretsRemove(); const writer = response.writable.getWriter(); let first = true; - for (const [vault, path] of secretPaths) { + for (const [vaultName, secretPath] of secretPaths) { await writer.write({ - nameOrId: vault, - secretName: path, + nameOrId: vaultName, + secretName: secretPath ?? '/', metadata: first ? { ...auth, options: { recursive: options.recursive } } : undefined, @@ -75,9 +69,45 @@ class CommandRemove extends CommandPolykey { first = false; } await writer.close(); - // Wait for the program to generate a response (complete processing). - await response.output; + // Check if any errors were raised + let hasErrored = false; + // TypeScript cannot properly perform type narrowing on this type, so + // the `as` keyword is used to help it out. + for await (const result of response.readable as ReadableStream) { + if (result.type === 'error') { + hasErrored = true; + switch (result.code) { + case 'ENOTEMPTY': + // Attempt to delete the directory without recursive set + process.stderr.write( + `rm: cannot remove '${result.reason}': Is a directory\n`, + ); + break; + case 'ENOENT': + // Attempt to delete a non-existent path + process.stderr.write( + `rm: cannot remove '${result.reason}': No such file or directory\n`, + ); + break; + case 'EINVAL': + // Attempt to delete vault root + process.stderr.write( + `rm: cannot remove '${result.reason}': Permission denied\n`, + ); + break; + default: + // No other code should be thrown + throw result; + } + } + } + return hasErrored; }, meta); + if (hasErrored) { + throw new errors.ErrorPolykeyCLIRemoveSecret( + 'Failed to remove one or more secrets', + ); + } } finally { if (pkClient! != null) await pkClient.stop(); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b2ac620d..358b0bc4 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -425,22 +425,31 @@ function outputFormatterJson(json: string): string { return `${JSON.stringify(json, standardErrorReplacer)}\n`; } -function outputFormatterError(err: Error): string { +// Anything is throwable +function outputFormatterError(err: any): string { let output = ''; let indent = ' '; while (err != null) { - if (err instanceof networkErrors.ErrorPolykeyRemote) { + if ( + err instanceof networkErrors.ErrorPolykeyRemote || + err instanceof errors.ErrorPolykeyCLI + ) { output += `${err.name}: ${err.description}`; if (err.message && err.message !== '') { output += ` - ${err.message}`; } - if (err.metadata != null) { + if ( + err instanceof networkErrors.ErrorPolykeyRemote && + err.metadata != null + ) { output += '\n'; for (const [key, value] of Object.entries(err.metadata)) { output += `${indent}${key}\t${value}\n`; } + output += `${indent}timestamp\t${err.timestamp}\n`; + } else { + output += '\n'; } - output += `${indent}timestamp\t${err.timestamp}\n`; output += `${indent}cause: `; err = err.cause; } else if (err instanceof ErrorPolykey) { @@ -473,19 +482,59 @@ function outputFormatterError(err: Error): string { } else { break; } - } else { + } else if (err instanceof Error) { output += `${err.name}`; if (err.message && err.message !== '') { output += `: ${err.message}`; } output += '\n'; break; + } else { + output += composeErrorMessage(err); + if (err.message && err.message !== '') { + output += `: ${err.message}`; + } + output += '\n'; + break; } indent = indent + ' '; } return output; } +function composeErrorMessage(error: any) { + switch (typeof error) { + case 'boolean': + case 'number': + case 'string': + case 'bigint': + case 'symbol': + return `Thrown non-error literal '${String(error)}'`; + case 'object': + if (error == null) break; + if ('name' in error && typeof error.name === 'string') { + return `Thrown '${error.name}'`; + } + if (error.constructor?.name != null) { + if (error.constructor.name === 'Object') { + // If the constructor name is Object, then the error is a JSON + // object. + return `Thrown non-error JSON '${JSON.stringify(error)}'`; + } else { + // Otherwise, it is a regular object. + return `Thrown non-error object '${error.constructor.name}'`; + } + } + break; + } + try { + return `Thrown non-error value '${error}'`; + } catch (e) { + if (e instanceof TypeError) return `Thrown non-error value 'null'`; + else throw e; + } +} + /** * CLI Authentication Retry Loop * Retries unary calls on attended authentication errors @@ -587,6 +636,7 @@ export { outputFormatterDict, outputFormatterJson, outputFormatterError, + composeErrorMessage, retryAuthentication, remoteErrorCause, encodeEscapedWrapped, diff --git a/tests/secrets/cat.test.ts b/tests/secrets/cat.test.ts index 424e0efd..79d669b3 100644 --- a/tests/secrets/cat.test.ts +++ b/tests/secrets/cat.test.ts @@ -39,6 +39,24 @@ describe('commandCatSecret', () => { recursive: true, }); }); + + test('should fail with invalid vault name', async () => { + const vaultName = 'vault' as VaultName; + const secretName = 'secret-name'; + const command = [ + 'secrets', + 'cat', + '-np', + dataDir, + `${vaultName}:${secretName}`, + ]; + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toInclude('ErrorVaultsVaultUndefined'); + }); test('should retrieve a secret', async () => { const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); @@ -61,13 +79,12 @@ describe('commandCatSecret', () => { expect(result.exitCode).toBe(0); expect(result.stdout).toBe(secretContent); }); - test('should fail when reading root without secret path', async () => { + test('should fail without secret path', async () => { const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - const secretName = 'secret-name'; - const secretContent = 'this is the contents of the secret'; + const secretName = 'secret'; await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, secretName, secretContent); + await vaultOps.addSecret(vault, secretName, secretName); }); const command = ['secrets', 'cat', '-np', dataDir, vaultName]; const result = await testUtils.pkStdio(command, { @@ -75,13 +92,13 @@ describe('commandCatSecret', () => { cwd: dataDir, }); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toInclude('ErrorSecretsIsDirectory'); + expect(result.stderr).toInclude('Is a directory'); }); test('should concatenate multiple secrets', async () => { - const vaultName = 'Vault3' as VaultName; + const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - const secretName1 = 'secret-name1'; - const secretName2 = 'secret-name2'; + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { await vaultOps.addSecret(vault, secretName1, secretName1); await vaultOps.addSecret(vault, secretName2, secretName2); @@ -102,13 +119,13 @@ describe('commandCatSecret', () => { expect(result.stdout).toBe(`${secretName1}${secretName2}`); }); test('should concatenate secrets from multiple vaults', async () => { - const vaultName1 = 'Vault3-1'; - const vaultName2 = 'Vault3-2'; + const vaultName1 = 'vault-1'; + const vaultName2 = 'vault-2'; const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName1); const vaultId2 = await polykeyAgent.vaultManager.createVault(vaultName2); - const secretName1 = 'secret-name1'; - const secretName2 = 'secret-name2'; - const secretName3 = 'secret-name3'; + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; + const secretName3 = 'secret3'; await polykeyAgent.vaultManager.withVaults( [vaultId1, vaultId2], async (vault1, vault2) => { @@ -134,10 +151,11 @@ describe('commandCatSecret', () => { expect(result.stdout).toBe(`${secretName1}${secretName2}${secretName3}`); }); test('should ignore missing files when concatenating multiple files', async () => { - const vaultName = 'Vault3'; + const vaultName = 'vault'; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - const secretName1 = 'secret-name1'; - const secretName2 = 'secret-name2'; + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; + const invalidName = 'nosecret'; await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { await vaultOps.addSecret(vault, secretName1, secretName1); await vaultOps.addSecret(vault, secretName2, secretName2); @@ -148,7 +166,7 @@ describe('commandCatSecret', () => { '-np', dataDir, `${vaultName}:${secretName1}`, - `${vaultName}:doesnt-exist`, + `${vaultName}:${invalidName}`, `${vaultName}:${secretName2}`, ]; const result = await testUtils.pkStdio(command, { @@ -157,7 +175,7 @@ describe('commandCatSecret', () => { }); expect(result.exitCode).toBe(1); expect(result.stdout).toBe(`${secretName1}${secretName2}`); - expect(result.stderr).not.toBe(''); + expect(result.stderr).toInclude('No such file or directory'); }); test('should return stdin if no arguments are passed', async () => { const command = ['secrets', 'cat', '-np', dataDir]; diff --git a/tests/secrets/edit.test.ts b/tests/secrets/edit.test.ts index 5eebb276..f9f2d38b 100644 --- a/tests/secrets/edit.test.ts +++ b/tests/secrets/edit.test.ts @@ -91,7 +91,7 @@ describe('commandEditSecret', () => { expect(contents.toString()).toStrictEqual(`${editedContent}\n`); }); }); - test('should fail to edit without a secret path specified', async () => { + test('should fail without secret path', async () => { const vaultName = 'vault' as VaultName; const command = ['secrets', 'edit', '-np', dataDir, vaultName]; const result = await testUtils.pkStdio(command, { @@ -163,6 +163,36 @@ describe('commandEditSecret', () => { expect(list.sort()).toStrictEqual([]); }); }); + test('should fail to write to directory', async () => { + const vaultName = 'vault' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretName = 'secret'; + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(secretName); + }); + }); + const command = [ + 'secrets', + 'edit', + '-np', + dataDir, + `${vaultName}:${secretName}`, + ]; + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password, EDITOR: editorExit }, + cwd: dataDir, + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toInclude('No such file or directory'); + // Check no files were created + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + }); + // First needs to be implemented in the handler itself + test.todo('should fail to write to invalid path'); test('file contents should be fetched correctly', async () => { const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); diff --git a/tests/secrets/mkdir.test.ts b/tests/secrets/mkdir.test.ts index 956facaa..103522e1 100644 --- a/tests/secrets/mkdir.test.ts +++ b/tests/secrets/mkdir.test.ts @@ -65,7 +65,7 @@ describe('commandMkdir', () => { cwd: dataDir, }); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toInclude('EEXIST'); // Root directory is already a directory + expect(result.stderr).toInclude('File or directory exists'); await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { expect(await vaultOps.listSecrets(vault)).toEqual([]); }); @@ -114,7 +114,7 @@ describe('commandMkdir', () => { cwd: dataDir, }); expect(result.exitCode).toBe(1); - expect(result.stderr).toInclude('ENOENT'); + expect(result.stderr).toInclude('No such file or directory'); await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { await vault.readF(async (efs) => { const dirName1P = efs.readdir(dirName1); @@ -139,7 +139,7 @@ describe('commandMkdir', () => { cwd: dataDir, }); expect(result.exitCode).toBe(1); - expect(result.stderr).toInclude('EEXIST'); + expect(result.stderr).toInclude('File or directory exists'); await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { await vault.readF(async (efs) => { const dirP = efs.readdir(dirName); @@ -169,7 +169,7 @@ describe('commandMkdir', () => { cwd: dataDir, }); expect(result.exitCode).toBe(1); - expect(result.stderr).toInclude('EEXIST'); + expect(result.stderr).toInclude('File or directory exists'); await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { await vault.readF(async (efs) => { const stat = await efs.stat(secretName); @@ -237,7 +237,7 @@ describe('commandMkdir', () => { cwd: dataDir, }); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toInclude('ENOENT'); + expect(result.stderr).toInclude('No such file or directory'); await polykeyAgent.vaultManager.withVaults( [vaultId1, vaultId2], async (vault1, vault2) => { diff --git a/tests/secrets/remove.test.ts b/tests/secrets/remove.test.ts index f19bcb72..17143e9e 100644 --- a/tests/secrets/remove.test.ts +++ b/tests/secrets/remove.test.ts @@ -40,15 +40,29 @@ describe('commandRemoveSecret', () => { }); }); + test('should fail with invalid vault name', async () => { + const vaultName = 'vault' as VaultName; + const secretName = 'secret'; + const command = [ + 'secrets', + 'rm', + '-np', + dataDir, + `${vaultName}:${secretName}`, + ]; + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toInclude('ErrorVaultsVaultUndefined'); + }); test('should remove secret', async () => { const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); const secretName = 'secret'; - const secretContent = 'this is the secret'; await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, secretName, secretContent); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([secretName]); + await vaultOps.addSecret(vault, secretName, secretName); }); const command = [ 'secrets', @@ -71,11 +85,8 @@ describe('commandRemoveSecret', () => { const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); const secretName = 'secret'; - const secretContent = 'this is the secret'; await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, secretName, secretContent); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([secretName]); + await vaultOps.addSecret(vault, secretName, secretName); }); const command = ['secrets', 'rm', '-np', dataDir, vaultName]; const result = await testUtils.pkStdio(command, { @@ -83,12 +94,34 @@ describe('commandRemoveSecret', () => { cwd: dataDir, }); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toInclude('EPERM'); + expect(result.stderr).toInclude('Permission denied'); await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { const list = await vaultOps.listSecrets(vault); expect(list.sort()).toStrictEqual([secretName]); }); }); + test('should fail to remove non-existent path', async () => { + const vaultName = 'vault' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretName = 'secret'; + const command = [ + 'secrets', + 'rm', + '-np', + dataDir, + `${vaultName}:${secretName}`, + ]; + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toInclude('No such file or directory'); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + }); test('should remove multiple secrets', async () => { const vaultName = 'vault' as VaultName; const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); @@ -101,13 +134,6 @@ describe('commandRemoveSecret', () => { await vaultOps.addSecret(vault, secretName2, secretName2); await vaultOps.addSecret(vault, secretName3, secretName3); await vaultOps.addSecret(vault, secretName4, secretName4); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([ - secretName1, - secretName2, - secretName3, - secretName4, - ]); }); const command = [ 'secrets', @@ -140,12 +166,6 @@ describe('commandRemoveSecret', () => { await vaultOps.addSecret(vault, secretName2, secretName2); await vaultOps.addSecret(vault, secretName3, secretName3); vaultLogLength = (await vault.log()).length; - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([ - secretName1, - secretName2, - secretName3, - ]); }); const command = [ 'secrets', @@ -182,12 +202,6 @@ describe('commandRemoveSecret', () => { await vaultOps.addSecret(vault, secretPath1, secretPath1); await vaultOps.addSecret(vault, secretPath2, secretPath2); await vaultOps.addSecret(vault, secretPath3, secretPath3); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([ - secretPath1, - secretPath2, - secretPath3, - ]); }); const command = [ 'secrets', @@ -222,12 +236,6 @@ describe('commandRemoveSecret', () => { await vaultOps.addSecret(vault, secretPath1, secretPath1); await vaultOps.addSecret(vault, secretPath2, secretPath2); await vaultOps.addSecret(vault, secretPath3, secretPath3); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([ - secretPath1, - secretPath2, - secretPath3, - ]); }); const command = [ 'secrets', @@ -241,6 +249,7 @@ describe('commandRemoveSecret', () => { cwd: dataDir, }); expect(result.exitCode).not.toBe(0); + expect(result.stderr).toInclude('Is a directory'); await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { const list = await vaultOps.listSecrets(vault); expect(list.sort()).toStrictEqual([ @@ -270,15 +279,6 @@ describe('commandRemoveSecret', () => { await vaultOps.addSecret(vault1, secretName4, secretName4); await vaultOps.addSecret(vault2, secretName5, secretName5); await vaultOps.addSecret(vault2, secretName6, secretName6); - const list1 = await vaultOps.listSecrets(vault1); - expect(list1.sort()).toStrictEqual([ - secretName1, - secretName2, - secretName3, - secretName4, - ]); - const list2 = await vaultOps.listSecrets(vault2); - expect(list2.sort()).toStrictEqual([secretName5, secretName6]); }, ); const command = [