From 97ed38cd9f7afeddcaf0fb8699a7b5173101346e Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Fri, 4 Oct 2024 15:28:08 +1000 Subject: [PATCH] chore: updated behaviour of mkdir command chore: updated tests for mkdir chore: updated mkdir and added tests chore: added warning for untracked directory fix: removed write.yml chore: working on adding non-zero exit codes chore: updated tests to be more accurate chore: addressed review fix: manually updated type of error chore: simplified streaming paths to handler chore: updated polykey version fix: lint fix: updated tests fix: lint --- npmDepsHash | 2 +- package-lock.json | 8 +- package.json | 2 +- src/errors.ts | 6 + src/secrets/CommandMkdir.ts | 79 +++++++++--- tests/secrets/mkdir.test.ts | 240 +++++++++++++++++++++++++++++++++++ tests/secrets/newDir.test.ts | 77 ----------- 7 files changed, 316 insertions(+), 98 deletions(-) create mode 100644 tests/secrets/mkdir.test.ts delete mode 100644 tests/secrets/newDir.test.ts diff --git a/npmDepsHash b/npmDepsHash index 63ec0445..135c128f 100644 --- a/npmDepsHash +++ b/npmDepsHash @@ -1 +1 @@ -sha256-k4xOpxG18ymouzdPMKiyOkaIg8hN29OJ8TFcRsCQc3g= +sha256-Wp7VGWLaWozJWyUpJENIsD9Vq5RWgBguALIhZ8Hq+vc= diff --git a/package-lock.json b/package-lock.json index 055e508b..c4f6b080 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.14.0", + "polykey": "^1.15.0", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", @@ -7602,9 +7602,9 @@ } }, "node_modules/polykey": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.14.0.tgz", - "integrity": "sha512-XHQ2h5VdcXhLpq6qL9FwHAP1k5WfezAipTRQmkLICujfkXz9wDTgu8Mn+Ubr4FfOnu267VQxsygqevHx+eJGBA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.15.0.tgz", + "integrity": "sha512-a7E9enAUUY9R5iwhkuuY5orzLzeRUWvujzdlMy446o1rfsz58VjzAIwpS2WNhf8YhN2r50oGObYMTyu/zJr8Sg==", "dev": true, "dependencies": { "@matrixai/async-cancellable": "^1.1.1", diff --git a/package.json b/package.json index fef5239f..51801684 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.14.0", + "polykey": "^1.15.0", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", diff --git a/src/errors.ts b/src/errors.ts index 1d6de160..c229393e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -152,6 +152,11 @@ class ErrorPolykeyCLIDuplicateEnvName extends ErrorPolykeyCLI { exitCode = sysexits.USAGE; } +class ErrorPolykeyCLIMakeDirectory extends ErrorPolykeyCLI { + static description = 'Failed to create one or more directories'; + exitCode = 1; +} + export { ErrorPolykeyCLI, ErrorPolykeyCLIUncaughtException, @@ -172,4 +177,5 @@ export { ErrorPolykeyCLINodePingFailed, ErrorPolykeyCLIInvalidEnvName, ErrorPolykeyCLIDuplicateEnvName, + ErrorPolykeyCLIMakeDirectory, }; diff --git a/src/secrets/CommandMkdir.ts b/src/secrets/CommandMkdir.ts index 90ea406c..4d1ce2da 100644 --- a/src/secrets/CommandMkdir.ts +++ b/src/secrets/CommandMkdir.ts @@ -1,25 +1,34 @@ import type PolykeyClient from 'polykey/dist/PolykeyClient'; +import type { ErrorMessage } from 'polykey/dist/client/types'; 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'; class CommandMkdir extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('mkdir'); - this.description('Create a Directory within a Vault'); + this.description( + 'Create a Directory within a Vault. Empty directories are not a part of the vault and will not be shared when cloning a Vault.', + ); this.argument( - '', + '', 'Path to where the directory to be created, specified as :', - binParsers.parseSecretPathValue, ); - this.option('-r, --recursive', 'Create the directory recursively'); this.addOption(binOptions.nodeId); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); - this.action(async (secretPath, options) => { + this.addOption(binOptions.recursive); + this.action(async (secretPaths, options) => { + secretPaths = secretPaths.map((path: string) => + binParsers.parseSecretPath(path), + ); const { default: PolykeyClient } = await import( 'polykey/dist/PolykeyClient' ); @@ -50,16 +59,56 @@ class CommandMkdir extends CommandPolykey { }, logger: this.logger.getChild(PolykeyClient.name), }); - await binUtils.retryAuthentication( - (auth) => - pkClient.rpcClient.methods.vaultsSecretsMkdir({ - metadata: auth, - nameOrId: secretPath[0], - dirName: secretPath[1], - recursive: options.recursive, - }), - meta, - ); + const response = await binUtils.retryAuthentication(async (auth) => { + const response = + await pkClient.rpcClient.methods.vaultsSecretsMkdir(); + const writer = response.writable.getWriter(); + let first = true; + for (const [vault, path] of secretPaths) { + await writer.write({ + nameOrId: vault, + dirName: path, + metadata: first + ? { ...auth, options: { recursive: options.recursive } } + : undefined, + }); + first = false; + } + await writer.close(); + return response; + }, meta); + + let hasErrored = false; + for await (const result of response.readable) { + 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) { + case 'ENOENT': + message = 'No such secret or directory'; + break; + case 'EEXIST': + message = 'Secret or directory exists'; + break; + default: + throw new ErrorPolykeyCLIUncaughtException( + `Unexpected error code: ${error.code}`, + ); + } + process.stderr.write( + `${error.code}: cannot create directory ${error.reason}: ${message}\n`, + ); + } + } + if (hasErrored) { + throw new ErrorPolykeyCLIMakeDirectory( + 'Failed to create one or more directories', + ); + } } finally { if (pkClient! != null) await pkClient.stop(); } diff --git a/tests/secrets/mkdir.test.ts b/tests/secrets/mkdir.test.ts new file mode 100644 index 00000000..831386a6 --- /dev/null +++ b/tests/secrets/mkdir.test.ts @@ -0,0 +1,240 @@ +import type { VaultName } from 'polykey/dist/vaults/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { vaultOps } from 'polykey/dist/vaults'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('commandMkdir', () => { + const password = 'password'; + const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + let polykeyAgent: PolykeyAgent; + let command: Array; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + options: { + nodePath: dataDir, + agentServiceHost: '127.0.0.1', + clientServiceHost: '127.0.0.1', + keys: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }, + logger: logger, + }); + }); + afterEach(async () => { + await polykeyAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('should make a directory', async () => { + const vaultName = 'vault' as VaultName; + const dirName = 'dir'; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + command = ['secrets', 'mkdir', '-np', dataDir, `${vaultName}:${dirName}`]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const stat = await vaultOps.statSecret(vault, dirName); + expect(stat.isDirectory()).toBeTruthy(); + }); + }); + test('should make directories recursively', async () => { + const vaultName = 'vault' as VaultName; + const dirName1 = 'dir1'; + const dirName2 = 'dir2'; + const dirNameNested = path.join(dirName1, dirName2); + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + command = [ + 'secrets', + 'mkdir', + '-np', + dataDir, + `${vaultName}:${dirNameNested}`, + '--recursive', + ]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const stat1 = await vaultOps.statSecret(vault, dirName1); + expect(stat1.isDirectory()).toBeTruthy(); + const stat2 = await vaultOps.statSecret(vault, dirNameNested); + expect(stat2.isDirectory()).toBeTruthy(); + }); + }); + test('should fail without recursive set', async () => { + const vaultName = 'vault' as VaultName; + const dirName1 = 'dir1'; + const dirName2 = 'dir2'; + const dirNameNested = path.join(dirName1, dirName2); + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + command = [ + 'secrets', + 'mkdir', + '-np', + dataDir, + `${vaultName}:${dirNameNested}`, + ]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toInclude('ENOENT'); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const dirName1P = efs.readdir(dirName1); + await expect(dirName1P).rejects.toThrow('ENOENT'); + const dirNameNestedP = efs.readdir(dirNameNested); + await expect(dirNameNestedP).rejects.toThrow('ENOENT'); + }); + }); + }); + test('should fail to make existing directory', async () => { + const vaultName = 'vault' as VaultName; + const dirName = 'dir-exists'; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(dirName); + }); + }); + command = ['secrets', 'mkdir', '-np', dataDir, `${vaultName}:${dirName}`]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toInclude('EEXIST'); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const dirP = efs.readdir(dirName); + await expect(dirP).toResolve(); + }); + }); + }); + test('should fail to make existing secret', async () => { + const vaultName = 'vault' as VaultName; + const secretName = 'secret-exists'; + const secretContent = 'secret-content'; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName, secretContent); + }); + }); + command = [ + 'secrets', + 'mkdir', + '-np', + dataDir, + `${vaultName}:${secretName}`, + ]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toInclude('EEXIST'); + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const stat = await efs.stat(secretName); + expect(stat.isFile()).toBeTruthy(); + const contents = await efs.readFile(secretName); + expect(contents.toString()).toEqual(secretContent); + }); + }); + }); + test('should make directories in multiple vaults', async () => { + const vaultName1 = 'vault1' as VaultName; + const vaultName2 = 'vault2' as VaultName; + const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName1); + const vaultId2 = await polykeyAgent.vaultManager.createVault(vaultName2); + const dirName1 = 'dir1'; + const dirName2 = 'dir2'; + const dirName3 = 'dir3'; + command = [ + 'secrets', + 'mkdir', + '-np', + dataDir, + `${vaultName1}:${dirName1}`, + `${vaultName2}:${dirName2}`, + `${vaultName1}:${dirName3}`, + ]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + await polykeyAgent.vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + const stat1 = await vaultOps.statSecret(vault1, dirName1); + expect(stat1.isDirectory()).toBeTruthy(); + const stat2 = await vaultOps.statSecret(vault2, dirName2); + expect(stat2.isDirectory()).toBeTruthy(); + const stat3 = await vaultOps.statSecret(vault1, dirName3); + expect(stat3.isDirectory()).toBeTruthy(); + }, + ); + }); + test('should continue after error', async () => { + const vaultName1 = 'vault1' as VaultName; + const vaultName2 = 'vault2' as VaultName; + const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName1); + const vaultId2 = await polykeyAgent.vaultManager.createVault(vaultName2); + const dirName1 = 'dir1'; + const dirName2 = 'nodir/dir2'; + const dirName3 = 'dir3'; + const dirName4 = 'dir4'; + command = [ + 'secrets', + 'mkdir', + '-np', + dataDir, + `${vaultName1}:${dirName1}`, + `${vaultName2}:${dirName2}`, + `${vaultName2}:${dirName3}`, + `${vaultName1}:${dirName4}`, + ]; + const result = await testUtils.pkStdio([...command], { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toInclude('ENOENT'); + await polykeyAgent.vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + const stat1 = await vaultOps.statSecret(vault1, dirName1); + expect(stat1.isDirectory()).toBeTruthy(); + await expect(vaultOps.statSecret(vault2, dirName2)).toReject(); + const stat3 = await vaultOps.statSecret(vault2, dirName3); + expect(stat3.isDirectory()).toBeTruthy(); + const stat4 = await vaultOps.statSecret(vault1, dirName4); + expect(stat4.isDirectory()).toBeTruthy(); + }, + ); + }); +}); diff --git a/tests/secrets/newDir.test.ts b/tests/secrets/newDir.test.ts deleted file mode 100644 index 5809ab23..00000000 --- a/tests/secrets/newDir.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { VaultName } from 'polykey/dist/vaults/types'; -import path from 'path'; -import fs from 'fs'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import PolykeyAgent from 'polykey/dist/PolykeyAgent'; -import { vaultOps } from 'polykey/dist/vaults'; -import * as keysUtils from 'polykey/dist/keys/utils'; -import * as testUtils from '../utils'; - -describe('commandNewDir', () => { - const password = 'password'; - const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]); - let dataDir: string; - let polykeyAgent: PolykeyAgent; - let command: Array; - - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(globalThis.tmpDir, 'polykey-test-'), - ); - polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password, - options: { - nodePath: dataDir, - agentServiceHost: '127.0.0.1', - clientServiceHost: '127.0.0.1', - keys: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, - }, - logger: logger, - }); - }); - afterEach(async () => { - await polykeyAgent.stop(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - test('should make a directory', async () => { - const vaultName = 'Vault5' as VaultName; - const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - - command = [ - 'secrets', - 'mkdir', - '-np', - dataDir, - `${vaultName}:dir1/dir2`, - '-r', - ]; - - const result = await testUtils.pkStdio([...command], { - env: { PK_PASSWORD: password }, - cwd: dataDir, - }); - expect(result.exitCode).toBe(0); - - await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, 'dir1/MySecret1', 'this is the secret 1'); - await vaultOps.addSecret( - vault, - 'dir1/dir2/MySecret2', - 'this is the secret 2', - ); - - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual( - ['dir1/MySecret1', 'dir1/dir2/MySecret2'].sort(), - ); - }); - }); -});