From cb1ff9d2bad81e8abc539f6c83950b474e83d35e Mon Sep 17 00:00:00 2001 From: Emma Casolin Date: Mon, 23 May 2022 15:35:18 +1000 Subject: [PATCH] feat: format json affects error logging Specifying `--format=json` will cause errors to be logged in json format. ErrorPolykeyRemote errors also now contain additional metadata about the origin node of the error. #323 --- package-lock.json | 112 ++++++++++++++++++- package.json | 3 +- src/agent/GRPCClientAgent.ts | 10 ++ src/agent/service/nodesCrossSignClaim.ts | 6 +- src/agent/service/vaultsGitPackGet.ts | 6 +- src/bin/CommandPolykey.ts | 11 +- src/bin/agent/CommandStart.ts | 1 + src/bin/polykey-agent.ts | 23 ++-- src/bin/polykey.ts | 12 +-- src/bin/types.ts | 1 + src/bin/utils/ExitHandlers.ts | 27 +++-- src/bin/utils/utils.ts | 77 +++++++++++-- src/client/GRPCClientClient.ts | 64 +++++++++++ src/errors.ts | 53 +++++++-- src/grpc/utils/utils.ts | 79 +++++++------- src/types.ts | 10 ++ tests/bin/agent/lock.test.ts | 2 +- tests/bin/agent/lockall.test.ts | 14 ++- tests/bin/agent/start.test.ts | 40 ++++--- tests/bin/agent/stop.test.ts | 16 ++- tests/bin/bootstrap.test.ts | 85 +++++++-------- tests/bin/sessions.test.ts | 26 ++--- tests/bin/utils.retryAuthentication.test.ts | 2 +- tests/bin/utils.test.ts | 114 +++++++++++++++++++- tests/bin/utils.ts | 24 +++-- 25 files changed, 609 insertions(+), 209 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70219d0c7..b832b042f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,8 @@ "ts-node": "^10.4.0", "tsconfig-paths": "^3.9.0", "typedoc": "^0.22.15", - "typescript": "^4.5.2" + "typescript": "^4.5.2", + "typescript-cached-transpile": "0.0.6" } }, "node_modules/@ampproject/remapping": { @@ -11146,6 +11147,64 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-cached-transpile": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typescript-cached-transpile/-/typescript-cached-transpile-0.0.6.tgz", + "integrity": "sha512-bfPc7YUW0PrVkQHU0xN0ANRuxdPgoYYXtZEW6PNkH5a97/AOM+kPPxSTMZbpWA3BG1do22JUkfC60KoCKJ9VZQ==", + "dev": true, + "dependencies": { + "@types/node": "^12.12.7", + "fs-extra": "^8.1.0", + "tslib": "^1.10.0" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/typescript-cached-transpile/node_modules/@types/node": { + "version": "12.20.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.52.tgz", + "integrity": "sha512-cfkwWw72849SNYp3Zx0IcIs25vABmFh73xicxhCkTcvtZQeIez15PpwQN8fY3RD7gv1Wrxlc9MEtfMORZDEsGw==", + "dev": true + }, + "node_modules/typescript-cached-transpile/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/typescript-cached-transpile/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/typescript-cached-transpile/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/typescript-cached-transpile/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/uglify-js": { "version": "3.15.5", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", @@ -19883,6 +19942,57 @@ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true }, + "typescript-cached-transpile": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typescript-cached-transpile/-/typescript-cached-transpile-0.0.6.tgz", + "integrity": "sha512-bfPc7YUW0PrVkQHU0xN0ANRuxdPgoYYXtZEW6PNkH5a97/AOM+kPPxSTMZbpWA3BG1do22JUkfC60KoCKJ9VZQ==", + "dev": true, + "requires": { + "@types/node": "^12.12.7", + "fs-extra": "^8.1.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "@types/node": { + "version": "12.20.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.52.tgz", + "integrity": "sha512-cfkwWw72849SNYp3Zx0IcIs25vABmFh73xicxhCkTcvtZQeIez15PpwQN8fY3RD7gv1Wrxlc9MEtfMORZDEsGw==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, "uglify-js": { "version": "3.15.5", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", diff --git a/package.json b/package.json index 29bc7b603..9909dbe4d 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "ts-node": "^10.4.0", "tsconfig-paths": "^3.9.0", "typedoc": "^0.22.15", - "typescript": "^4.5.2" + "typescript": "^4.5.2", + "typescript-cached-transpile": "0.0.6" } } diff --git a/src/agent/GRPCClientAgent.ts b/src/agent/GRPCClientAgent.ts index bfc1c4d65..bb4593785 100644 --- a/src/agent/GRPCClientAgent.ts +++ b/src/agent/GRPCClientAgent.ts @@ -83,6 +83,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.echo.name, }, this.client.echo, )(...args); @@ -101,6 +102,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsGitInfoGet.name, }, this.client.vaultsGitInfoGet, )(...args); @@ -120,6 +122,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsGitPackGet.name, }, this.client.vaultsGitPackGet, )(...args); @@ -138,6 +141,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsScan.name, }, this.client.vaultsScan, )(...args); @@ -151,6 +155,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesClosestLocalNodesGet.name, }, this.client.nodesClosestLocalNodesGet, )(...args); @@ -164,6 +169,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesClaimsGet.name, }, this.client.nodesClaimsGet, )(...args); @@ -177,6 +183,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesChainDataGet.name, }, this.client.nodesChainDataGet, )(...args); @@ -190,6 +197,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesHolePunchMessageSend.name, }, this.client.nodesHolePunchMessageSend, )(...args); @@ -203,6 +211,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.notificationsSend.name, }, this.client.notificationsSend, )(...args); @@ -225,6 +234,7 @@ class GRPCClientAgent extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesCrossSignClaim.name, }, this.client.nodesCrossSignClaim, )(...args); diff --git a/src/agent/service/nodesCrossSignClaim.ts b/src/agent/service/nodesCrossSignClaim.ts index f0a3e2a9a..f9fbb1bbb 100644 --- a/src/agent/service/nodesCrossSignClaim.ts +++ b/src/agent/service/nodesCrossSignClaim.ts @@ -32,7 +32,11 @@ function nodesCrossSignClaim({ call: grpc.ServerDuplexStream, ) => { const nodeId = keyManager.getNodeId(); - const genClaims = grpcUtils.generatorDuplex(call, { nodeId }, true); + const genClaims = grpcUtils.generatorDuplex( + call, + { nodeId, command: nodesCrossSignClaim.name }, + true, + ); try { await db.withTransactionF(async (tran) => { const readStatus = await genClaims.read(); diff --git a/src/agent/service/vaultsGitPackGet.ts b/src/agent/service/vaultsGitPackGet.ts index 8d9561512..c7ec95dbd 100644 --- a/src/agent/service/vaultsGitPackGet.ts +++ b/src/agent/service/vaultsGitPackGet.ts @@ -34,7 +34,11 @@ function vaultsGitPackGet({ call: grpc.ServerDuplexStream, ): Promise => { const nodeId = keyManager.getNodeId(); - const genDuplex = grpcUtils.generatorDuplex(call, { nodeId }, true); + const genDuplex = grpcUtils.generatorDuplex( + call, + { nodeId, command: vaultsGitPackGet.name }, + true, + ); try { const clientBodyBuffers: Uint8Array[] = []; const clientRequest = (await genDuplex.read()).value; diff --git a/src/bin/CommandPolykey.ts b/src/bin/CommandPolykey.ts index 9dd84a95f..436dfdbdd 100644 --- a/src/bin/CommandPolykey.ts +++ b/src/bin/CommandPolykey.ts @@ -61,6 +61,8 @@ class CommandPolykey extends commander.Command { public action(fn: (...args: any[]) => void | Promise): this { return super.action(async (...args: any[]) => { const opts = this.opts(); + // Set the format for error logging for the exit handlers + this.exitHandlers.errFormat = opts.format === 'json' ? 'json' : 'error'; // Set the logger according to the verbosity this.logger.setLevel(binUtils.verboseToLogLevel(opts.verbose)); // Set the global upstream GRPC logger @@ -70,14 +72,7 @@ class CommandPolykey extends commander.Command { if (opts.nodePath == null) { throw new binErrors.ErrorCLINodePath(); } - try { - await fn(...args); - } catch (e) { - const [errorCause, remoteLevel] = binUtils.remoteErrorCause(e); - console.log(remoteLevel); - console.error(errorCause); - throw errorCause; - } + await fn(...args); }); } } diff --git a/src/bin/agent/CommandStart.ts b/src/bin/agent/CommandStart.ts index e4b863a47..6ccc4e9c0 100644 --- a/src/bin/agent/CommandStart.ts +++ b/src/bin/agent/CommandStart.ts @@ -185,6 +185,7 @@ class CommandStart extends CommandPolykey { }); const messageIn: AgentChildProcessInput = { logLevel: this.logger.getEffectiveLevel(), + format: options.format, workers: options.workers, agentConfig, }; diff --git a/src/bin/polykey-agent.ts b/src/bin/polykey-agent.ts index d56d49220..de65ebc86 100644 --- a/src/bin/polykey-agent.ts +++ b/src/bin/polykey-agent.ts @@ -44,6 +44,8 @@ async function main(_argv = process.argv): Promise { resolveMessageInP(data); }); const messageIn = await messageInP; + const errFormat = messageIn.format === 'json' ? 'json' : 'error'; + exitHandlers.errFormat = errFormat; logger.setLevel(messageIn.logLevel); // Set the global upstream GRPC logger grpcSetLogger(logger.getChild('grpc')); @@ -71,10 +73,8 @@ async function main(_argv = process.argv): Promise { if (e instanceof ErrorPolykey) { process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.description, - message: e.message, + type: errFormat, + data: e, }), ); process.exitCode = e.exitCode; @@ -82,9 +82,8 @@ async function main(_argv = process.argv): Promise { // Unknown error, this should not happen process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.message, + type: errFormat, + data: e, }), ); process.exitCode = 255; @@ -107,9 +106,8 @@ async function main(_argv = process.argv): Promise { // There's no point attempting to propagate the error to the parent process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.message, + type: errFormat, + data: e, }), ); process.exitCode = 255; @@ -137,9 +135,8 @@ async function main(_argv = process.argv): Promise { // There's no point attempting to propagate the error to the parent process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.message, + type: errFormat, + data: e, }), ); process.exitCode = 255; diff --git a/src/bin/polykey.ts b/src/bin/polykey.ts index ac40d5373..bb4d49f8a 100644 --- a/src/bin/polykey.ts +++ b/src/bin/polykey.ts @@ -55,6 +55,7 @@ async function main(argv = process.argv): Promise { // Successful execution (even if the command was non-terminating) process.exitCode = 0; } catch (e) { + const errFormat = rootCommand.opts().format === 'json' ? 'json' : 'error'; if (e instanceof commander.CommanderError) { // Commander writes help and error messages on stderr automatically if ( @@ -78,10 +79,8 @@ async function main(argv = process.argv): Promise { } else if (e instanceof ErrorPolykey) { process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.description, - message: e.message, + type: errFormat, + data: e, }), ); process.exitCode = e.exitCode; @@ -89,9 +88,8 @@ async function main(argv = process.argv): Promise { // Unknown error, this should not happen process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.message, + type: errFormat, + data: e, }), ); process.exitCode = 255; diff --git a/src/bin/types.ts b/src/bin/types.ts index ef2451661..0517f08f1 100644 --- a/src/bin/types.ts +++ b/src/bin/types.ts @@ -17,6 +17,7 @@ type AgentStatusLiveData = Omit & { */ type AgentChildProcessInput = { logLevel: LogLevel; + format: 'human' | 'json'; workers?: number; agentConfig: { password: string; diff --git a/src/bin/utils/ExitHandlers.ts b/src/bin/utils/ExitHandlers.ts index 84b981d80..2fdd74f03 100644 --- a/src/bin/utils/ExitHandlers.ts +++ b/src/bin/utils/ExitHandlers.ts @@ -9,6 +9,7 @@ class ExitHandlers { */ public handlers: Array<(signal?: NodeJS.Signals) => Promise>; protected _exiting: boolean = false; + protected _errFormat: 'json' | 'error'; /** * Handles synchronous and asynchronous exceptions * This prints out appropriate error message on STDERR @@ -23,10 +24,8 @@ class ExitHandlers { if (e instanceof ErrorPolykey) { process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.description, - message: e.message, + type: this._errFormat, + data: e, }), ); process.exitCode = e.exitCode; @@ -34,9 +33,8 @@ class ExitHandlers { // Unknown error, this should not happen process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.message, + type: this._errFormat, + data: e, }), ); process.exitCode = 255; @@ -65,19 +63,16 @@ class ExitHandlers { if (e instanceof ErrorPolykey) { process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.description, - message: e.message, + type: this._errFormat, + data: e, }), ); } else { // Unknown error, this should not happen process.stderr.write( binUtils.outputFormatter({ - type: 'error', - name: e.name, - description: e.message, + type: this._errFormat, + data: e, }), ); } @@ -103,6 +98,10 @@ class ExitHandlers { return this._exiting; } + set errFormat(errFormat: 'json' | 'error') { + this._errFormat = errFormat; + } + public install() { process.on('SIGINT', this.signalHandler); process.on('SIGTERM', this.signalHandler); diff --git a/src/bin/utils/utils.ts b/src/bin/utils/utils.ts index 5477228a9..0e66b8e23 100644 --- a/src/bin/utils/utils.ts +++ b/src/bin/utils/utils.ts @@ -2,11 +2,14 @@ import type { POJO } from '../../types'; import process from 'process'; import { LogLevel } from '@matrixai/logger'; import * as grpc from '@grpc/grpc-js'; +import { AbstractError } from '@matrixai/errors'; import * as binProcessors from './processors'; import * as binErrors from '../errors'; import * as clientUtils from '../../client/utils'; import * as clientErrors from '../../client/errors'; import * as errors from '../../errors'; +import * as nodesUtils from '../../nodes/utils'; +import * as utils from '../../utils'; /** * Convert verbosity to LogLevel @@ -40,9 +43,7 @@ type OutputObject = } | { type: 'error'; - name: string; - description: string; - message?: string; + data: Error; }; function outputFormatter(msg: OutputObject): string { @@ -92,14 +93,72 @@ function outputFormatter(msg: OutputObject): string { output += `${key}\t${value}\n`; } } else if (msg.type === 'json') { + if (msg.data instanceof Error && !(msg.data instanceof AbstractError)) { + msg.data = { + type: msg.data.name, + data: { message: msg.data.message, stack: msg.data.stack }, + }; + } output = JSON.stringify(msg.data); output += '\n'; } else if (msg.type === 'error') { - output += `${msg.name}: ${msg.description}`; - if (msg.message) { - output += ` - ${msg.message}`; + let currError = msg.data; + let indent = ' '; + while (currError != null) { + if (currError instanceof errors.ErrorPolykeyRemote) { + output += `${currError.name}: ${currError.description}`; + if (currError.message && currError.message !== '') { + output += ` - ${currError.message}`; + } + output += '\n'; + output += `${indent}command\t${currError.metadata.command}\n`; + output += `${indent}nodeId\t${nodesUtils.encodeNodeId( + currError.metadata.nodeId, + )}\n`; + output += `${indent}host\t${currError.metadata.host}\n`; + output += `${indent}port\t${currError.metadata.port}\n`; + output += `${indent}timestamp\t${currError.timestamp}\n`; + output += `${indent}remote error: `; + currError = currError.cause; + } else if (currError instanceof errors.ErrorPolykey) { + output += `${currError.name}: ${currError.description}`; + if (currError.message && currError.message !== '') { + output += ` - ${currError.message}`; + } + output += '\n'; + output += `${indent}exitCode\t${currError.exitCode}\n`; + output += `${indent}timestamp\t${currError.timestamp}\n`; + if (currError.data && !utils.isEmptyObject(currError.data)) { + output += `${indent}data\t${JSON.stringify(currError.data)}\n`; + } + if (currError.cause) { + output += `${indent}cause: `; + if (currError.cause instanceof errors.ErrorPolykey) { + currError = currError.cause; + } else if (currError.cause instanceof Error) { + output += `${currError.cause.name}`; + if (currError.cause.message && currError.cause.message !== '') { + output += `: ${currError.cause.message}`; + } + output += '\n'; + break; + } else { + output += `${JSON.stringify(currError.cause)}\n`; + break; + } + } else { + break; + } + } else { + output += `${currError.name}`; + if (currError.message && currError.message !== '') { + output += `: ${currError.message}`; + } + output += '\n'; + break; + } + indent = indent + ' '; } - output += '\n'; } return output; } @@ -155,8 +214,8 @@ async function retryAuthentication( function remoteErrorCause(e: any): [any, number] { let errorCause = e; let depth = 0; - while (e instanceof errors.ErrorPolykeyRemote) { - errorCause = e.cause; + while (errorCause instanceof errors.ErrorPolykeyRemote) { + errorCause = errorCause.cause; depth++; } return [errorCause, depth]; diff --git a/src/client/GRPCClientClient.ts b/src/client/GRPCClientClient.ts index e866ec475..9492841ed 100644 --- a/src/client/GRPCClientClient.ts +++ b/src/client/GRPCClientClient.ts @@ -95,6 +95,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.agentStatus.name, }, this.client.agentStatus, )(...args); @@ -108,6 +109,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.agentStop.name, }, this.client.agentStop, )(...args); @@ -121,6 +123,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.agentUnlock.name, }, this.client.agentUnlock, )(...args); @@ -134,6 +137,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.agentLockAll.name, }, this.client.agentLockAll, )(...args); @@ -152,6 +156,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsList.name, }, this.client.vaultsList, )(...args); @@ -165,6 +170,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsCreate.name, }, this.client.vaultsCreate, )(...args); @@ -178,6 +184,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsRename.name, }, this.client.vaultsRename, )(...args); @@ -191,6 +198,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsDelete.name, }, this.client.vaultsDelete, )(...args); @@ -204,6 +212,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsClone.name, }, this.client.vaultsClone, )(...args); @@ -217,6 +226,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsPull.name, }, this.client.vaultsPull, )(...args); @@ -235,6 +245,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsScan.name, }, this.client.vaultsScan, )(...args); @@ -248,6 +259,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsPermissionGet.name, }, this.client.vaultsPermissionGet, )(...args); @@ -261,6 +273,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsPermissionSet.name, }, this.client.vaultsPermissionSet, )(...args); @@ -274,6 +287,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsPermissionUnset.name, }, this.client.vaultsPermissionUnset, )(...args); @@ -292,6 +306,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsList.name, }, this.client.vaultsSecretsList, )(...args); @@ -305,6 +320,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsMkdir.name, }, this.client.vaultsSecretsMkdir, )(...args); @@ -318,6 +334,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsDelete.name, }, this.client.vaultsSecretsDelete, )(...args); @@ -331,6 +348,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsEdit.name, }, this.client.vaultsSecretsEdit, )(...args); @@ -344,6 +362,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsGet.name, }, this.client.vaultsSecretsGet, )(...args); @@ -357,6 +376,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsStat.name, }, this.client.vaultsSecretsStat, )(...args); @@ -370,6 +390,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsRename.name, }, this.client.vaultsSecretsRename, )(...args); @@ -383,6 +404,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsNew.name, }, this.client.vaultsSecretsNew, )(...args); @@ -396,6 +418,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsSecretsNewDir.name, }, this.client.vaultsSecretsNewDir, )(...args); @@ -409,6 +432,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsVersion.name, }, this.client.vaultsVersion, )(...args); @@ -427,6 +451,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.vaultsLog.name, }, this.client.vaultsLog, )(...args); @@ -440,6 +465,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysKeyPairRoot.name, }, this.client.keysKeyPairRoot, )(...args); @@ -453,6 +479,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysKeyPairReset.name, }, this.client.keysKeyPairReset, )(...args); @@ -466,6 +493,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysKeyPairRenew.name, }, this.client.keysKeyPairRenew, )(...args); @@ -479,6 +507,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysEncrypt.name, }, this.client.keysEncrypt, )(...args); @@ -492,6 +521,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysDecrypt.name, }, this.client.keysDecrypt, )(...args); @@ -505,6 +535,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysSign.name, }, this.client.keysSign, )(...args); @@ -518,6 +549,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysVerify.name, }, this.client.keysVerify, )(...args); @@ -531,6 +563,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysPasswordChange.name, }, this.client.keysPasswordChange, )(...args); @@ -544,6 +577,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysCertsGet.name, }, this.client.keysCertsGet, )(...args); @@ -562,6 +596,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.keysCertsGet.name, }, this.client.keysCertsChainGet, )(...args); @@ -580,6 +615,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsGestaltList.name, }, this.client.gestaltsGestaltList, )(...args); @@ -593,6 +629,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsGestaltGetByIdentity.name, }, this.client.gestaltsGestaltGetByIdentity, )(...args); @@ -606,6 +643,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsGestaltGetByNode.name, }, this.client.gestaltsGestaltGetByNode, )(...args); @@ -619,6 +657,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsDiscoveryByNode.name, }, this.client.gestaltsDiscoveryByNode, )(...args); @@ -632,6 +671,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsDiscoveryByIdentity.name, }, this.client.gestaltsDiscoveryByIdentity, )(...args); @@ -645,6 +685,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsActionsGetByNode.name, }, this.client.gestaltsActionsGetByNode, )(...args); @@ -658,6 +699,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsActionsGetByIdentity.name, }, this.client.gestaltsActionsGetByIdentity, )(...args); @@ -671,6 +713,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsActionsSetByNode.name, }, this.client.gestaltsActionsSetByNode, )(...args); @@ -684,6 +727,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsActionsSetByIdentity.name, }, this.client.gestaltsActionsSetByIdentity, )(...args); @@ -697,6 +741,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsActionsUnsetByNode.name, }, this.client.gestaltsActionsUnsetByNode, )(...args); @@ -710,6 +755,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsActionsUnsetByIdentity.name, }, this.client.gestaltsActionsUnsetByIdentity, )(...args); @@ -723,6 +769,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsGestaltTrustByNode.name, }, this.client.gestaltsGestaltTrustByNode, )(...args); @@ -736,6 +783,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.gestaltsGestaltTrustByIdentity.name, }, this.client.gestaltsGestaltTrustByIdentity, )(...args); @@ -749,6 +797,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesTokenPut.name, }, this.client.identitiesTokenPut, )(...args); @@ -762,6 +811,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesTokenGet.name, }, this.client.identitiesTokenGet, )(...args); @@ -775,6 +825,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesTokenDelete.name, }, this.client.identitiesTokenDelete, )(...args); @@ -788,6 +839,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesProvidersList.name, }, this.client.identitiesProvidersList, )(...args); @@ -801,6 +853,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesAdd.name, }, this.client.nodesAdd, )(...args); @@ -814,6 +867,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesPing.name, }, this.client.nodesPing, )(...args); @@ -827,6 +881,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesClaim.name, }, this.client.nodesClaim, )(...args); @@ -840,6 +895,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.nodesFind.name, }, this.client.nodesFind, )(...args); @@ -853,6 +909,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesAuthenticate.name, }, this.client.identitiesAuthenticate, )(...args); @@ -866,6 +923,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesInfoConnectedGet.name, }, this.client.identitiesInfoConnectedGet, )(...args); @@ -879,6 +937,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesInfoGet.name, }, this.client.identitiesInfoGet, )(...args); @@ -892,6 +951,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesClaim.name, }, this.client.identitiesClaim, )(...args); @@ -905,6 +965,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.identitiesAuthenticatedGet.name, }, this.client.identitiesAuthenticatedGet, )(...args); @@ -918,6 +979,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.notificationsSend.name, }, this.client.notificationsSend, )(...args); @@ -931,6 +993,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.notificationsRead.name, }, this.client.notificationsRead, )(...args); @@ -944,6 +1007,7 @@ class GRPCClientClient extends GRPCClient { nodeId: this.nodeId, host: this.host, port: this.port, + command: this.notificationsClear.name, }, this.client.notificationsClear, )(...args); diff --git a/src/errors.ts b/src/errors.ts index 53ec7b8ed..139744171 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,6 +1,52 @@ +import type { Class } from '@matrixai/errors'; +import type { ClientMetadata } from './types'; import ErrorPolykey from './ErrorPolykey'; import sysexits from './utils/sysexits'; +class ErrorPolykeyRemote extends ErrorPolykey { + static description = 'Remote error from RPC call'; + exitCode = sysexits.UNAVAILABLE; + metadata: ClientMetadata; + + constructor(metadata: ClientMetadata, message?: string, options?) { + super(message, options); + this.metadata = metadata; + } + + public static fromJSON>( + this: T, + json: any, + ): InstanceType { + if ( + typeof json !== 'object' || + json.type !== this.name || + typeof json.data !== 'object' || + typeof json.data.message !== 'string' || + isNaN(Date.parse(json.data.timestamp)) || + typeof json.data.metadata !== 'object' || + typeof json.data.data !== 'object' || + typeof json.data.exitCode !== 'number' || + ('stack' in json.data && typeof json.data.stack !== 'string') + ) { + throw new TypeError(`Cannot decode JSON to ${this.name}`); + } + const e = new this(json.data.metadata, json.data.message, { + timestamp: new Date(json.data.timestamp), + data: json.data.data, + cause: json.data.cause, + }); + e.exitCode = json.data.exitCode; + e.stack = json.data.stack; + return e; + } + + public toJSON(): any { + const json = super.toJSON(); + json.data.metadata = this.metadata; + return json; + } +} + class ErrorPolykeyUnimplemented extends ErrorPolykey { static description = 'This is an unimplemented functionality'; exitCode = sysexits.UNAVAILABLE; @@ -8,12 +54,7 @@ class ErrorPolykeyUnimplemented extends ErrorPolykey { class ErrorPolykeyUnknown extends ErrorPolykey { static description = 'Unable to deserialise to known error'; - exitCode = sysexits.UNKNOWN; -} - -class ErrorPolykeyRemote extends ErrorPolykey { - static description = 'Remote error from RPC call'; - exitCode = sysexits.UNAVAILABLE; + exitCode = sysexits.PROTOCOL; } class ErrorPolykeyAgentRunning extends ErrorPolykey { diff --git a/src/grpc/utils/utils.ts b/src/grpc/utils/utils.ts index f99331585..f37442e41 100644 --- a/src/grpc/utils/utils.ts +++ b/src/grpc/utils/utils.ts @@ -28,14 +28,14 @@ import type { AsyncGeneratorDuplexStreamClient, } from '../types'; import type { CertificatePemChain, PrivateKeyPem } from '../../keys/types'; -import type { POJO } from '../../types'; +import type { POJO, ClientMetadata } from '../../types'; +import type { NodeId } from '../../nodes/types'; import { Buffer } from 'buffer'; import { AbstractError } from '@matrixai/errors'; import * as grpc from '@grpc/grpc-js'; import * as grpcErrors from '../errors'; import * as errors from '../../errors'; import * as networkUtils from '../../network/utils'; -import * as nodesUtils from '../../nodes/utils'; import { promisify, promise, never } from '../../utils/utils'; /** @@ -184,9 +184,11 @@ function fromError( * Deserialized GRPC errors into ErrorPolykey * Use this on the receiving side to receive exceptions */ -function toError(e: ServiceError): errors.ErrorPolykey { +function toError( + e: ServiceError, + metadata: ClientMetadata, +): errors.ErrorPolykey { const errorData = e.metadata.get('error')[0].toString(); - const connInfo = e.metadata.get('metadata')[0].toString(); // Grpc.status is an enum // this will iterate the enum values then enum keys // they will all be of string type @@ -197,10 +199,17 @@ function toError(e: ServiceError): errors.ErrorPolykey { if (isNaN(parseInt(key)) && e.code === grpc.status[key]) { if (key === 'UNKNOWN' && errorData != null) { const error: Error = JSON.parse(errorData, reviver); - return new errors.ErrorPolykeyRemote(error.message, { - data: JSON.parse(connInfo), - cause: error, - }); + const remoteError = new errors.ErrorPolykeyRemote( + metadata, + error.message, + { + cause: error, + }, + ); + if (error instanceof errors.ErrorPolykey) { + remoteError.exitCode = error.exitCode; + } + return remoteError; } else { return new grpcErrors.ErrorGRPCClientCall(e.message, { data: { @@ -344,14 +353,6 @@ function reviver(key: string, value: any): any { }, }); return error; - } else if (key === 'timestamp') { - // Encode timestamps - const timestampParsed = Date.parse(value); - if (!isNaN(timestampParsed)) { - return new Date(timestampParsed); - } else { - return undefined; - } } else { return value; } @@ -364,7 +365,7 @@ function reviver(key: string, value: any): any { */ function promisifyUnaryCall( client: Client, - metadata: POJO, + metadata: ClientMetadata, f: (...args: any[]) => ClientUnaryCall, ): (...args: any[]) => PromiseUnaryCall { return (...args) => { @@ -376,11 +377,7 @@ function promisifyUnaryCall( const { p: pMeta, resolveP: resolveMetaP } = promise(); const callback = (error: ServiceError, ...values) => { if (error != null) { - if ('nodeId' in metadata) { - metadata.nodeId = nodesUtils.encodeNodeId(metadata.nodeId); - } - error.metadata.set('metadata', JSON.stringify(metadata)); - rejectP(toError(error)); + rejectP(toError(error, metadata)); return; } resolveP(values.length === 1 ? values[0] : values); @@ -405,15 +402,15 @@ function promisifyUnaryCall( */ function generatorReadable( stream: ClientReadableStream, - metadata: POJO, + metadata: { nodeId: NodeId; command: string } & POJO, ): AsyncGeneratorReadableStream>; function generatorReadable( stream: ServerReadableStream, - metadata: POJO, + metadata: { nodeId: NodeId; command: string } & POJO, ): AsyncGeneratorReadableStream>; function generatorReadable( stream: ClientReadableStream | ServerReadableStream, - metadata: POJO, + metadata: { nodeId: NodeId; command: string } & POJO, ) { const peerAddress = stream .getPeer() @@ -426,9 +423,12 @@ function generatorReadable( if (!('port' in metadata) && peerAddress != null) { metadata.port = networkUtils.parseAddress(peerAddress[0])[1]; } - if ('nodeId' in metadata) { - metadata.nodeId = nodesUtils.encodeNodeId(metadata.nodeId); - } + const clientMetadata: ClientMetadata = { + nodeId: metadata.nodeId, + host: metadata.host, + port: metadata.port, + command: metadata.command, + }; const gf = async function* () { try { let vR = yield; @@ -449,8 +449,7 @@ function generatorReadable( } } catch (e) { stream.destroy(); - e.metadata.set('metadata', JSON.stringify(metadata)); - throw toError(e); + throw toError(e, clientMetadata); } }; const g: any = gf(); @@ -467,7 +466,7 @@ function generatorReadable( */ function promisifyReadableStreamCall( client: grpc.Client, - metadata: POJO, + metadata: ClientMetadata, f: (...args: any[]) => ClientReadableStream, ): ( ...args: any[] @@ -539,7 +538,7 @@ function generatorWritable( */ function promisifyWritableStreamCall( client: grpc.Client, - metadata: POJO, + metadata: ClientMetadata, f: (...args: any[]) => ClientWritableStream, ): ( ...args: any[] @@ -555,11 +554,7 @@ function promisifyWritableStreamCall( }; const callback = (error, ...values) => { if (error != null) { - if ('nodeId' in metadata) { - metadata.nodeId = nodesUtils.encodeNodeId(metadata.nodeId); - } - error.metadata.set('metadata', JSON.stringify(metadata)); - return rejectP(toError(error)); + return rejectP(toError(error, metadata)); } return resolveP(values.length === 1 ? values[0] : values); }; @@ -591,17 +586,17 @@ function promisifyWritableStreamCall( */ function generatorDuplex( stream: ClientDuplexStream, - metadata: POJO, + metadata: { nodeId: NodeId; command: string } & POJO, sensitive: boolean, ): AsyncGeneratorDuplexStream>; function generatorDuplex( stream: ServerDuplexStream, - metadata: POJO, + metadata: { nodeId: NodeId; command: string } & POJO, sensitive: boolean, ): AsyncGeneratorDuplexStream>; function generatorDuplex( stream: ClientDuplexStream | ServerDuplexStream, - metadata: POJO, + metadata: { nodeId: NodeId; command: string } & POJO, sensitive: boolean = false, ) { const gR = generatorReadable(stream as any, metadata); @@ -654,7 +649,7 @@ function generatorDuplex( */ function promisifyDuplexStreamCall( client: grpc.Client, - metadata: POJO, + metadata: ClientMetadata, f: (...args: any[]) => ClientDuplexStream, ): ( ...args: any[] @@ -699,4 +694,6 @@ export { promisifyDuplexStreamCall, toError, fromError, + replacer, + reviver, }; diff --git a/src/types.ts b/src/types.ts index 8fba1fc08..0e41f366f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ // eslint-disable-next-line no-restricted-imports -- Interim types for FileSystem import type fs from 'fs'; +import type { Host, Port } from './network/types'; +import type { NodeId } from './nodes/types'; /** * Plain data dictionary @@ -77,6 +79,13 @@ interface FileSystem { type FileHandle = fs.promises.FileHandle; +type ClientMetadata = { + nodeId: NodeId; + host: Host; + port: Port; + command: string; +} & POJO; + export type { POJO, Opaque, @@ -89,4 +98,5 @@ export type { Timer, FileSystem, FileHandle, + ClientMetadata, }; diff --git a/tests/bin/agent/lock.test.ts b/tests/bin/agent/lock.test.ts index 012bbcaf1..a35719991 100644 --- a/tests/bin/agent/lock.test.ts +++ b/tests/bin/agent/lock.test.ts @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs'; import prompts from 'prompts'; -import { mocked } from 'ts-jest/utils'; +import { mocked } from 'jest-mock'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import Session from '@/sessions/Session'; import config from '@/config'; diff --git a/tests/bin/agent/lockall.test.ts b/tests/bin/agent/lockall.test.ts index c0096ff14..1f39d4b9e 100644 --- a/tests/bin/agent/lockall.test.ts +++ b/tests/bin/agent/lockall.test.ts @@ -1,11 +1,11 @@ import path from 'path'; import fs from 'fs'; import prompts from 'prompts'; -import { mocked } from 'ts-jest/utils'; +import { mocked } from 'jest-mock'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import Session from '@/sessions/Session'; import config from '@/config'; -import * as clientErrors from '@/client/errors'; +import * as errors from '@/errors'; import * as testBinUtils from '../utils'; import * as testUtils from '../../utils'; @@ -113,17 +113,15 @@ describe('lockall', () => { ); // Old token is invalid const { exitCode, stderr } = await testBinUtils.pkStdio( - ['agent', 'status'], + ['agent', 'status', '--format', 'json'], { PK_NODE_PATH: globalAgentDir, PK_TOKEN: token, }, globalAgentDir, ); - testBinUtils.expectProcessError( - exitCode, - stderr, - new clientErrors.ErrorClientAuthDenied(), - ); + testBinUtils.expectProcessError(exitCode, stderr, [ + new errors.ErrorClientAuthDenied(), + ]); }); }); diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts index 06a83f544..6b419fde0 100644 --- a/tests/bin/agent/start.test.ts +++ b/tests/bin/agent/start.test.ts @@ -218,6 +218,8 @@ describe('start', () => { '--workers', '0', '--verbose', + '--format', + 'json', ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -239,6 +241,8 @@ describe('start', () => { '--workers', '0', '--verbose', + '--format', + 'json', ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -274,21 +278,17 @@ describe('start', () => { const errorStatusLocked = new statusErrors.ErrorStatusLocked(); // It's either the first or second process if (index === 0) { - testBinUtils.expectProcessError( - exitCode!, - stdErrLine1, + testBinUtils.expectProcessError(exitCode!, stdErrLine1, [ errorStatusLocked, - ); + ]); agentProcess2.kill('SIGQUIT'); [exitCode, signal] = await testBinUtils.processExit(agentProcess2); expect(exitCode).toBe(null); expect(signal).toBe('SIGQUIT'); } else if (index === 1) { - testBinUtils.expectProcessError( - exitCode!, - stdErrLine2, + testBinUtils.expectProcessError(exitCode!, stdErrLine2, [ errorStatusLocked, - ); + ]); agentProcess1.kill('SIGQUIT'); [exitCode, signal] = await testBinUtils.processExit(agentProcess1); expect(exitCode).toBe(null); @@ -316,6 +316,8 @@ describe('start', () => { '--workers', '0', '--verbose', + '--format', + 'json', ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -325,7 +327,15 @@ describe('start', () => { logger.getChild('agentProcess'), ), testBinUtils.pkSpawn( - ['bootstrap', '--fresh', '--root-key-pair-bits', '1024', '--verbose'], + [ + 'bootstrap', + '--fresh', + '--root-key-pair-bits', + '1024', + '--verbose', + '--format', + 'json', + ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, @@ -360,21 +370,17 @@ describe('start', () => { const errorStatusLocked = new statusErrors.ErrorStatusLocked(); // It's either the first or second process if (index === 0) { - testBinUtils.expectProcessError( - exitCode!, - stdErrLine1, + testBinUtils.expectProcessError(exitCode!, stdErrLine1, [ errorStatusLocked, - ); + ]); bootstrapProcess.kill('SIGTERM'); [exitCode, signal] = await testBinUtils.processExit(bootstrapProcess); expect(exitCode).toBe(null); expect(signal).toBe('SIGTERM'); } else if (index === 1) { - testBinUtils.expectProcessError( - exitCode!, - stdErrLine2, + testBinUtils.expectProcessError(exitCode!, stdErrLine2, [ errorStatusLocked, - ); + ]); agentProcess.kill('SIGTERM'); [exitCode, signal] = await testBinUtils.processExit(agentProcess); expect(exitCode).toBe(null); diff --git a/tests/bin/agent/stop.test.ts b/tests/bin/agent/stop.test.ts index c22843e45..b56f9b42c 100644 --- a/tests/bin/agent/stop.test.ts +++ b/tests/bin/agent/stop.test.ts @@ -197,17 +197,15 @@ describe('stop', () => { ); await status.waitFor('STARTING'); const { exitCode, stderr } = await testBinUtils.pkStdio( - ['agent', 'stop'], + ['agent', 'stop', '--format', 'json'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), }, dataDir, ); - testBinUtils.expectProcessError( - exitCode, - stderr, + testBinUtils.expectProcessError(exitCode, stderr, [ new binErrors.ErrorCLIPolykeyAgentStatus('agent is starting'), - ); + ]); await status.waitFor('LIVE'); await testBinUtils.pkStdio( ['agent', 'stop'], @@ -256,18 +254,16 @@ describe('stop', () => { logger, }); const { exitCode, stderr } = await testBinUtils.pkStdio( - ['agent', 'stop'], + ['agent', 'stop', '--format', 'json'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: 'wrong password', }, dataDir, ); - testBinUtils.expectProcessError( - exitCode, - stderr, + testBinUtils.expectProcessError(exitCode, stderr, [ new clientErrors.ErrorClientAuthDenied(), - ); + ]); // Should still be LIVE await sleep(500); const statusInfo = await status.readStatus(); diff --git a/tests/bin/bootstrap.test.ts b/tests/bin/bootstrap.test.ts index c108d0345..409afc4f7 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -5,7 +5,6 @@ import readline from 'readline'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { errors as statusErrors } from '@/status'; import { errors as bootstrapErrors } from '@/bootstrap'; -import * as binUtils from '@/bin/utils'; import * as testBinUtils from './utils'; describe('bootstrap', () => { @@ -68,6 +67,8 @@ describe('bootstrap', () => { '--root-key-pair-bits', '1024', '--verbose', + '--format', + 'json', ], { PK_PASSWORD: password, @@ -76,17 +77,9 @@ describe('bootstrap', () => { )); const errorBootstrapExistingState = new bootstrapErrors.ErrorBootstrapExistingState(); - expect(exitCode).toBe(errorBootstrapExistingState.exitCode); - const stdErrLine = stderr.trim().split('\n').pop(); - const eOutput = binUtils - .outputFormatter({ - type: 'error', - name: errorBootstrapExistingState.name, - description: errorBootstrapExistingState.description, - message: errorBootstrapExistingState.message, - }) - .trim(); - expect(stdErrLine).toBe(eOutput); + testBinUtils.expectProcessError(exitCode, stderr, [ + errorBootstrapExistingState, + ]); ({ exitCode, stdout, stderr } = await testBinUtils.pkStdio( [ 'bootstrap', @@ -117,7 +110,14 @@ describe('bootstrap', () => { const password = 'password'; const [bootstrapProcess1, bootstrapProcess2] = await Promise.all([ testBinUtils.pkSpawn( - ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + [ + 'bootstrap', + '--root-key-pair-bits', + '1024', + '--verbose', + '--format', + 'json', + ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, @@ -126,7 +126,14 @@ describe('bootstrap', () => { logger.getChild('bootstrapProcess1'), ), testBinUtils.pkSpawn( - ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + [ + 'bootstrap', + '--root-key-pair-bits', + '1024', + '--verbose', + '--format', + 'json', + ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, @@ -158,27 +165,22 @@ describe('bootstrap', () => { }); }); const errorStatusLocked = new statusErrors.ErrorStatusLocked(); - expect(exitCode).toBe(errorStatusLocked.exitCode); expect(signal).toBe(null); - const eOutput = binUtils - .outputFormatter({ - type: 'error', - name: errorStatusLocked.name, - description: errorStatusLocked.description, - message: errorStatusLocked.message, - }) - .trim(); // It's either the first or second process if (index === 0) { expect(stdErrLine1).toBeDefined(); - expect(stdErrLine1).toBe(eOutput); - const [exitCode] = await testBinUtils.processExit(bootstrapProcess2); - expect(exitCode).toBe(0); + testBinUtils.expectProcessError(exitCode!, stdErrLine1, [ + errorStatusLocked, + ]); + const [exitCode2] = await testBinUtils.processExit(bootstrapProcess2); + expect(exitCode2).toBe(0); } else if (index === 1) { expect(stdErrLine2).toBeDefined(); - expect(stdErrLine2).toBe(eOutput); - const [exitCode] = await testBinUtils.processExit(bootstrapProcess1); - expect(exitCode).toBe(0); + testBinUtils.expectProcessError(exitCode!, stdErrLine2, [ + errorStatusLocked, + ]); + const [exitCode2] = await testBinUtils.processExit(bootstrapProcess1); + expect(exitCode2).toBe(0); } }, global.defaultTimeout * 2, @@ -217,28 +219,27 @@ describe('bootstrap', () => { expect(signal).toBe('SIGINT'); // Attempting to bootstrap should fail with existing state const bootstrapProcess2 = await testBinUtils.pkStdio( - ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + [ + 'bootstrap', + '--root-key-pair-bits', + '1024', + '--verbose', + '--format', + 'json', + ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, }, dataDir, ); - const stdErrLine = bootstrapProcess2.stderr.trim().split('\n').pop(); const errorBootstrapExistingState = new bootstrapErrors.ErrorBootstrapExistingState(); - expect(bootstrapProcess2.exitCode).toBe( - errorBootstrapExistingState.exitCode, + testBinUtils.expectProcessError( + bootstrapProcess2.exitCode, + bootstrapProcess2.stderr, + [errorBootstrapExistingState], ); - const eOutput = binUtils - .outputFormatter({ - type: 'error', - name: errorBootstrapExistingState.name, - description: errorBootstrapExistingState.description, - message: errorBootstrapExistingState.message, - }) - .trim(); - expect(stdErrLine).toBe(eOutput); // Attempting to bootstrap with --fresh should succeed const bootstrapProcess3 = await testBinUtils.pkStdio( ['bootstrap', '--root-key-pair-bits', '1024', '--fresh', '--verbose'], diff --git a/tests/bin/sessions.test.ts b/tests/bin/sessions.test.ts index b688eb2ef..0487b9f97 100644 --- a/tests/bin/sessions.test.ts +++ b/tests/bin/sessions.test.ts @@ -6,7 +6,7 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; -import { mocked } from 'ts-jest/utils'; +import { mocked } from 'jest-mock'; import prompts from 'prompts'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { Session } from '@/sessions'; @@ -83,7 +83,7 @@ describe('sessions', () => { let exitCode, stderr; // Password and Token set ({ exitCode, stderr } = await testBinUtils.pkStdio( - ['agent', 'status'], + ['agent', 'status', '--format', 'json'], { PK_NODE_PATH: globalAgentDir, PK_PASSWORD: 'invalid', @@ -91,14 +91,12 @@ describe('sessions', () => { }, globalAgentDir, )); - testBinUtils.expectProcessError( - exitCode, - stderr, + testBinUtils.expectProcessError(exitCode, stderr, [ new clientErrors.ErrorClientAuthDenied(), - ); + ]); // Password set ({ exitCode, stderr } = await testBinUtils.pkStdio( - ['agent', 'status'], + ['agent', 'status', '--format', 'json'], { PK_NODE_PATH: globalAgentDir, PK_PASSWORD: 'invalid', @@ -106,14 +104,12 @@ describe('sessions', () => { }, globalAgentDir, )); - testBinUtils.expectProcessError( - exitCode, - stderr, + testBinUtils.expectProcessError(exitCode, stderr, [ new clientErrors.ErrorClientAuthDenied(), - ); + ]); // Token set ({ exitCode, stderr } = await testBinUtils.pkStdio( - ['agent', 'status'], + ['agent', 'status', '--format', 'json'], { PK_NODE_PATH: globalAgentDir, PK_PASSWORD: undefined, @@ -121,11 +117,9 @@ describe('sessions', () => { }, globalAgentDir, )); - testBinUtils.expectProcessError( - exitCode, - stderr, + testBinUtils.expectProcessError(exitCode, stderr, [ new clientErrors.ErrorClientAuthDenied(), - ); + ]); }); test('prompt for password to authenticate attended commands', async () => { const password = globalAgentPassword; diff --git a/tests/bin/utils.retryAuthentication.test.ts b/tests/bin/utils.retryAuthentication.test.ts index 9a97f050f..32e45eee3 100644 --- a/tests/bin/utils.retryAuthentication.test.ts +++ b/tests/bin/utils.retryAuthentication.test.ts @@ -1,5 +1,5 @@ import prompts from 'prompts'; -import { mocked } from 'ts-jest/utils'; +import { mocked } from 'jest-mock'; import mockedEnv from 'mocked-env'; import { utils as clientUtils, errors as clientErrors } from '@/client'; import * as binUtils from '@/bin/utils'; diff --git a/tests/bin/utils.test.ts b/tests/bin/utils.test.ts index 42661b0af..45e6bd870 100644 --- a/tests/bin/utils.test.ts +++ b/tests/bin/utils.test.ts @@ -1,4 +1,8 @@ -import * as binUtils from '@/bin/utils'; +import type { Host, Port } from '@/network/types'; +import * as binUtils from '@/bin/utils/utils'; +import * as nodesUtils from '@/nodes/utils'; +import * as errors from '@/errors'; +import * as testUtils from '../utils'; describe('bin/utils', () => { test('list in human and json format', () => { @@ -70,4 +74,112 @@ describe('bin/utils', () => { }), ).toBe('{"key1":"value1","key2":"value2"}\n'); }); + test('errors in human and json format', () => { + const timestamp = new Date(); + const data = { string: 'one', number: 1 }; + const host = '127.0.0.1' as Host; + const port = 55555 as Port; + const nodeId = testUtils.generateRandomNodeId(); + const standardError = new TypeError('some error'); + const pkError = new errors.ErrorPolykey('some pk error', { + timestamp, + data, + }); + const remoteError = new errors.ErrorPolykeyRemote( + { + nodeId, + host, + port, + command: 'some command', + }, + 'some remote error', + { timestamp, cause: pkError }, + ); + const twoRemoteErrors = new errors.ErrorPolykeyRemote( + { + nodeId, + host, + port, + command: 'command 2', + }, + 'remote error', + { + timestamp, + cause: new errors.ErrorPolykeyRemote( + { + nodeId, + host, + port, + command: 'command 1', + }, + undefined, + { + timestamp, + cause: new errors.ErrorPolykey('pk error', { + timestamp, + cause: standardError, + }), + }, + ), + }, + ); + // Error + expect( + binUtils.outputFormatter({ type: 'error', data: standardError }), + ).toBe(`${standardError.name}: ${standardError.message}\n`); + expect(binUtils.outputFormatter({ type: 'error', data: pkError })).toBe( + `${pkError.name}: ${pkError.description} - ${pkError.message}\n` + + ` exitCode\t${pkError.exitCode}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` data\t${JSON.stringify(data)}\n`, + ); + expect(binUtils.outputFormatter({ type: 'error', data: remoteError })).toBe( + `${remoteError.name}: ${remoteError.description} - ${remoteError.message}\n` + + ` command\t${remoteError.metadata.command}\n` + + ` nodeId\t${nodesUtils.encodeNodeId(nodeId)}\n` + + ` host\t${host}\n` + + ` port\t${port}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` remote error: ${remoteError.cause.name}: ${remoteError.cause.description} - ${remoteError.cause.message}\n` + + ` exitCode\t${pkError.exitCode}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` data\t${JSON.stringify(data)}\n`, + ); + expect( + binUtils.outputFormatter({ type: 'error', data: twoRemoteErrors }), + ).toBe( + `${twoRemoteErrors.name}: ${twoRemoteErrors.description} - ${twoRemoteErrors.message}\n` + + ` command\t${twoRemoteErrors.metadata.command}\n` + + ` nodeId\t${nodesUtils.encodeNodeId(nodeId)}\n` + + ` host\t${host}\n` + + ` port\t${port}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` remote error: ${twoRemoteErrors.cause.name}: ${twoRemoteErrors.cause.description}\n` + + ` command\t${twoRemoteErrors.cause.metadata.command}\n` + + ` nodeId\t${nodesUtils.encodeNodeId(nodeId)}\n` + + ` host\t${host}\n` + + ` port\t${port}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` remote error: ${twoRemoteErrors.cause.cause.name}: ${twoRemoteErrors.cause.cause.description} - ${twoRemoteErrors.cause.cause.message}\n` + + ` exitCode\t${pkError.exitCode}\n` + + ` timestamp\t${timestamp.toString()}\n` + + ` cause: ${standardError.name}: ${standardError.message}\n`, + ); + expect( + binUtils.outputFormatter({ type: 'json', data: standardError }), + ).toBe( + `{"type":"${standardError.name}","data":{"message":"${ + standardError.message + }","stack":"${standardError.stack?.replaceAll('\n', '\\n')}"}}\n`, + ); + expect(binUtils.outputFormatter({ type: 'json', data: pkError })).toBe( + JSON.stringify(pkError.toJSON()) + '\n', + ); + expect(binUtils.outputFormatter({ type: 'json', data: remoteError })).toBe( + JSON.stringify(remoteError.toJSON()) + '\n', + ); + expect( + binUtils.outputFormatter({ type: 'json', data: twoRemoteErrors }), + ).toBe(JSON.stringify(twoRemoteErrors.toJSON()) + '\n'); + }); }); diff --git a/tests/bin/utils.ts b/tests/bin/utils.ts index 3b552cf64..2464b9d82 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -12,6 +12,7 @@ import nexpect from 'nexpect'; import Logger from '@matrixai/logger'; import main from '@/bin/polykey'; import * as binUtils from '@/bin/utils'; +import * as grpcUtils from '@/grpc/utils'; /** * Runs pk command functionally @@ -339,23 +340,24 @@ async function processExit( /** * Checks exit code and stderr against ErrorPolykey + * Errors should contain all of the errors in the expected error chain + * starting with the outermost error (excluding ErrorPolykeyRemote) + * When using this function, the command must be run with --format=json */ function expectProcessError( exitCode: number, stderr: string, - error: ErrorPolykey, + errors: Array>, ) { - expect(exitCode).toBe(error.exitCode); + expect(exitCode).toBe(errors[0].exitCode); const stdErrLine = stderr.trim().split('\n').pop(); - const errorOutput = binUtils - .outputFormatter({ - type: 'error', - name: error.name, - description: error.description, - message: error.message, - }) - .trim(); - expect(stdErrLine).toBe(errorOutput); + const receivedError = JSON.parse(stdErrLine!, grpcUtils.reviver); + let [currentError] = binUtils.remoteErrorCause(receivedError); + for (const error of errors) { + expect(currentError.name).toBe(error.name); + expect(currentError.message).toBe(error.message); + currentError = currentError.cause; + } } export {