diff --git a/src/client/handlers/VaultsSecretsEnv.ts b/src/client/handlers/VaultsSecretsEnv.ts index 0f89da5ac..e24ba92bd 100644 --- a/src/client/handlers/VaultsSecretsEnv.ts +++ b/src/client/handlers/VaultsSecretsEnv.ts @@ -5,12 +5,11 @@ import type { ClientRPCRequestParams, ClientRPCResponseResult, SecretIdentifierMessage, - SecretContentMessage, + SecretContentOrErrorMessage, } from '../types.js'; import type VaultManager from '../../vaults/VaultManager.js'; import { DuplexHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils.js'; -import * as vaultsErrors from '../../vaults/errors.js'; class VaultsSecretsEnv extends DuplexHandler< { @@ -18,7 +17,7 @@ class VaultsSecretsEnv extends DuplexHandler< vaultManager: VaultManager; }, ClientRPCRequestParams, - ClientRPCResponseResult + ClientRPCResponseResult > { public handle = async function* ( input: AsyncIterableIterator< @@ -27,64 +26,73 @@ class VaultsSecretsEnv extends DuplexHandler< _cancel: (reason?: any) => void, _meta: Record | undefined, ctx: ContextTimed, - ): AsyncGenerator> { + ): AsyncGenerator< + ClientRPCResponseResult, + void, + void + > { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; return yield* db.withTransactionG(async function* (tran): AsyncGenerator< - ClientRPCResponseResult + ClientRPCResponseResult, + void, + void > { for await (const secretIdentifierMessage of input) { const { nameOrId, secretName } = secretIdentifierMessage; const vaultIdFromName = await vaultManager.getVaultId(nameOrId, tran); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined( - `Vault "${nameOrId}" does not exist`, - ); + yield { + type: 'ErrorMessage', + code: 'EINVAL', + reason: `Vault "${nameOrId}" does not exist`, + data: { secretName: undefined, nameOrId }, + }; + continue; } - const secrets = await vaultManager.withVaults( + yield* vaultManager.withVaultsG( [vaultId], - async (vault) => { - const results: Array<{ - filePath: string; - value: string; - }> = []; - return await vault.readF(async (fs) => { + async function* ( + vault, + ): AsyncGenerator { + yield* vault.readG(async function* (efs): AsyncGenerator< + SecretContentOrErrorMessage, + void, + void + > { try { for await (const filePath of vaultsUtils.walkFs( - fs, + efs, secretName, )) { ctx.signal.throwIfAborted(); - const fileContents = await fs.readFile(filePath); - results.push({ - filePath: filePath, - value: fileContents.toString(), - }); + const fileContents = await efs.readFile(filePath); + yield { + type: 'SuccessMessage', + success: true, + nameOrId: nameOrId, + secretName: filePath, + secretContent: fileContents.toString(), + }; } } catch (e) { if (e.code === 'ENOENT') { - throw new vaultsErrors.ErrorSecretsSecretUndefined( - `Secret with name: ${secretName} does not exist`, - { cause: e }, - ); + yield { + type: 'ErrorMessage', + code: e.code, + reason: `Secret "${secretName}" does not exist`, + data: { secretName, nameOrId }, + }; + } else { + throw e; } - throw e; } - return results; }); }, tran, ctx, ); - for (const { filePath, value } of secrets) { - ctx.signal.throwIfAborted(); - yield { - nameOrId: nameOrId, - secretName: filePath, - secretContent: value, - }; - } } }); }; diff --git a/src/client/types.ts b/src/client/types.ts index bef9ee91b..5b6f30837 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -345,6 +345,13 @@ type ContentOrErrorMessage = ContentSuccessMessage | ErrorMessageTagged; type SecretContentMessage = SecretIdentifierMessage & ContentMessage; +type SecretContentSuccessMessage = SecretIdentifierMessage & + ContentSuccessMessage; + +type SecretContentOrErrorMessage = + | SecretContentSuccessMessage + | ErrorMessageTagged; + type SecretDirMessage = VaultIdentifierMessage & { dirName: string; }; @@ -462,9 +469,11 @@ export type { SecretPathMessage, SecretIdentifierMessage, ContentMessage, + SecretContentMessage, ContentSuccessMessage, ContentOrErrorMessage, - SecretContentMessage, + SecretContentSuccessMessage, + SecretContentOrErrorMessage, SecretDirMessage, SecretRenameMessage, SecretFilesMessage, diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index 91efba32f..bfa39cc44 100644 --- a/tests/client/handlers/vaults.test.ts +++ b/tests/client/handlers/vaults.test.ts @@ -5,8 +5,8 @@ import type { VaultId } from '#ids/index.js'; import type NodeManager from '#nodes/NodeManager.js'; import type { LogEntryMessage, - SecretContentMessage, SecretDirMessage, + SecretContentSuccessMessage, SecretIdentifierMessage, SecretIdentifierMessageTagged, SecretRenameMessage, @@ -1165,8 +1165,8 @@ describe('vaultsSecretsWriteFile', () => { await expect(result).rejects.toThrow(cancelMessage); }); }); -describe('vaultsSecretEnv', () => { - const logger = new Logger('vaultsSecretEnv test', LogLevel.WARN, [ +describe('vaultsSecretsEnv', () => { + const logger = new Logger('vaultsSecretsEnv test', LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -1259,18 +1259,18 @@ describe('vaultsSecretEnv', () => { // Demonstrating we can pull out multiple secrets across separate vaults const vaultName1 = 'vault1'; const vaultName2 = 'vault2'; + const vaultId1 = await vaultManager.createVault(vaultName1); + const vaultId2 = await vaultManager.createVault(vaultName2); const secretName1 = 'secret1'; const secretName2 = 'secret2'; const secretName3 = 'secret3'; const secretName4 = 'secret4'; - const vaultId1 = await vaultManager.createVault(vaultName1); await vaultManager.withVaults([vaultId1], async (vault) => { await vault.writeF(async (efs) => { await efs.writeFile(secretName1, secretName1); await efs.writeFile(secretName2, secretName2); }); }); - const vaultId2 = await vaultManager.createVault(vaultName2); await vaultManager.withVaults([vaultId2], async (vault) => { await vault.writeF(async (efs) => { await efs.writeFile(secretName3, secretName3); @@ -1278,46 +1278,46 @@ describe('vaultsSecretEnv', () => { }); }); - const secrets = [ - [vaultName1, secretName1], - [vaultName1, secretName2], - [vaultName2, secretName3], - [vaultName2, secretName4], - ]; + const response = await rpcClient.methods.vaultsSecretsEnv(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultName1, secretName: secretName1 }); + await writer.write({ nameOrId: vaultName1, secretName: secretName2 }); + await writer.write({ nameOrId: vaultName2, secretName: secretName3 }); + await writer.write({ nameOrId: vaultName2, secretName: secretName4 }); + await writer.close(); - const duplexStream = await rpcClient.methods.vaultsSecretsEnv(); - const writeP = (async () => { - const writer = duplexStream.writable.getWriter(); - for (const [name, secret] of secrets) { - await writer.write({ - nameOrId: name, - secretName: secret, - }); + const results: Array = []; + for await (const value of response.readable) { + if (value.type !== 'SuccessMessage') { + fail('Expected type to be SuccessMessage'); } - await writer.close(); - })(); - const results: Array = []; - for await (const value of duplexStream.readable) { results.push(value); } - await writeP; expect(results[0]).toMatchObject({ + type: 'SuccessMessage', + success: true, nameOrId: vaultName1, secretName: secretName1, secretContent: secretName1, }); expect(results[1]).toMatchObject({ + type: 'SuccessMessage', + success: true, nameOrId: vaultName1, secretName: secretName2, secretContent: secretName2, }); expect(results[2]).toMatchObject({ + type: 'SuccessMessage', + success: true, nameOrId: vaultName2, secretName: secretName3, secretContent: secretName3, }); expect(results[3]).toMatchObject({ + type: 'SuccessMessage', + success: true, nameOrId: vaultName2, secretName: secretName4, secretContent: secretName4, @@ -1325,7 +1325,8 @@ describe('vaultsSecretEnv', () => { }); test('should get secrets by directory', async () => { // Demonstrating we can pull out multiple secrets across separate vaults - const vaultName1 = 'vault1'; + const vaultName = 'vault'; + const vaultId = await vaultManager.createVault(vaultName); const dirName1 = 'dir1'; const dirName2 = 'dir2'; const dirName3 = 'dir3'; @@ -1333,9 +1334,8 @@ describe('vaultsSecretEnv', () => { const secretName2 = 'secret2'; const secretName3 = 'secret3'; const secretName4 = 'secret4'; - const vaultId1 = await vaultManager.createVault(vaultName1); - await vaultManager.withVaults([vaultId1], async (vault) => { + await vaultManager.withVaults([vaultId], async (vault) => { await vault.writeF(async (efs) => { await efs.mkdir(dirName1); await efs.writeFile(`${dirName1}/${secretName1}`, secretName1); @@ -1350,82 +1350,86 @@ describe('vaultsSecretEnv', () => { }); }); - const secrets = [ - [vaultName1, dirName1], - [vaultName1, dirName2], - ]; + const response = await rpcClient.methods.vaultsSecretsEnv(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultName, secretName: dirName1 }); + await writer.write({ nameOrId: vaultName, secretName: dirName2 }); + await writer.close(); - const duplexStream = await rpcClient.methods.vaultsSecretsEnv(); - const writeP = (async () => { - const writer = duplexStream.writable.getWriter(); - for (const [name, secret] of secrets) { - await writer.write({ - nameOrId: name, - secretName: secret, - }); + const results: Map = new Map(); + for await (const value of response.readable) { + if (value.type !== 'SuccessMessage') { + fail('Type should be SuccessMessage'); } - await writer.close(); - })(); - const results: Map = new Map(); - for await (const value of duplexStream.readable) { results.set(value.secretName, value); } - await writeP; expect(results.size).toBe(4); expect(results.has(`${dirName1}/${secretName1}`)).toBeTrue(); expect(results.get(`${dirName1}/${secretName1}`)).toMatchObject({ - nameOrId: vaultName1, + type: 'SuccessMessage', + success: true, + nameOrId: vaultName, secretName: `${dirName1}/${secretName1}`, secretContent: secretName1, }); expect(results.has(`${dirName1}/${secretName2}`)).toBeTrue(); expect(results.get(`${dirName1}/${secretName2}`)).toMatchObject({ - nameOrId: vaultName1, + type: 'SuccessMessage', + success: true, + nameOrId: vaultName, secretName: `${dirName1}/${secretName2}`, secretContent: secretName2, }); expect(results.has(`${dirName2}/${dirName3}/${secretName4}`)).toBeTrue(); expect(results.get(`${dirName2}/${dirName3}/${secretName4}`)).toMatchObject( { - nameOrId: vaultName1, + type: 'SuccessMessage', + success: true, + nameOrId: vaultName, secretName: `${dirName2}/${dirName3}/${secretName4}`, secretContent: secretName4, }, ); expect(results.has(`${dirName2}/${secretName3}`)).toBeTrue(); expect(results.get(`${dirName2}/${secretName3}`)).toMatchObject({ - nameOrId: vaultName1, + type: 'SuccessMessage', + success: true, + nameOrId: vaultName, secretName: `${dirName2}/${secretName3}`, secretContent: secretName3, }); }); - test('errors should be descriptive', async () => { - // Demonstrating we can pull out multiple secrets across separate vaults - const vaultName1 = 'vault1'; - await vaultManager.createVault(vaultName1); + test('should continue on error', async () => { + const vaultName = 'vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; + const wrongSecret = 'noSecret'; - const secrets = [[vaultName1, 'noSecret']]; + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName2, secretName2); + }); + }); - const duplexStream = await rpcClient.methods.vaultsSecretsEnv(); - const writeP = (async () => { - const writer = duplexStream.writable.getWriter(); - for (const [name, secret] of secrets) { - await writer.write({ - nameOrId: name, - secretName: secret, - }); + // Request secrets + const response = await rpcClient.methods.vaultsSecretsEnv(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultName, secretName: secretName1 }); + await writer.write({ nameOrId: vaultName, secretName: wrongSecret }); + await writer.write({ nameOrId: vaultName, secretName: secretName1 }); + await writer.close(); + + // Parse responses + for await (const result of response.readable) { + if (result.type === 'SuccessMessage') { + expect(result.secretName).toBeOneOf([secretName1, secretName2]); + expect(result.secretContent).toBeOneOf([secretName1, secretName2]); + } else { + expect(result.reason).toContain(wrongSecret); } - await writer.close(); - })(); - await testsUtils.expectRemoteError( - (async () => { - for await (const _ of duplexStream.readable) { - // Do nothing until it throws - } - })(), - vaultsErrors.ErrorSecretsSecretUndefined, - ); - await writeP; + } }); test.prop([testsUtils.vaultNameArb(), testsUtils.fileNameLengthSampleArb()], { numRuns: 10,