Skip to content

Commit

Permalink
chore: updated behaviour of mkdir command
Browse files Browse the repository at this point in the history
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
  • Loading branch information
aryanjassal committed Oct 15, 2024
1 parent 71973d0 commit 97ed38c
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 98 deletions.
2 changes: 1 addition & 1 deletion npmDepsHash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-k4xOpxG18ymouzdPMKiyOkaIg8hN29OJ8TFcRsCQc3g=
sha256-Wp7VGWLaWozJWyUpJENIsD9Vq5RWgBguALIhZ8Hq+vc=
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ class ErrorPolykeyCLIDuplicateEnvName<T> extends ErrorPolykeyCLI<T> {
exitCode = sysexits.USAGE;
}

class ErrorPolykeyCLIMakeDirectory<T> extends ErrorPolykeyCLI<T> {
static description = 'Failed to create one or more directories';
exitCode = 1;
}

export {
ErrorPolykeyCLI,
ErrorPolykeyCLIUncaughtException,
Expand All @@ -172,4 +177,5 @@ export {
ErrorPolykeyCLINodePingFailed,
ErrorPolykeyCLIInvalidEnvName,
ErrorPolykeyCLIDuplicateEnvName,
ErrorPolykeyCLIMakeDirectory,
};
79 changes: 64 additions & 15 deletions src/secrets/CommandMkdir.ts
Original file line number Diff line number Diff line change
@@ -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<typeof CommandPolykey>) {
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(
'<secretPath>',
'<secretPath...>',
'Path to where the directory to be created, specified as <vaultName>:<directoryPath>',
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'
);
Expand Down Expand Up @@ -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();
}
Expand Down
240 changes: 240 additions & 0 deletions tests/secrets/mkdir.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>;

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();
},
);
});
});
Loading

0 comments on commit 97ed38c

Please sign in to comment.