From aff09378295fe88cef3d3caab101ae3b9c45e7e5 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 31 Oct 2022 18:43:36 +0100 Subject: [PATCH] Add RPC error validation functions --- src/__fixtures__/json.ts | 70 ++++++++++++++++++++++++++++++++++++++++ src/json.test.ts | 55 +++++++++++++++++++++++++++++++ src/json.ts | 31 ++++++++++++++++-- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/src/__fixtures__/json.ts b/src/__fixtures__/json.ts index ca869f5a..aa1b4116 100644 --- a/src/__fixtures__/json.ts +++ b/src/__fixtures__/json.ts @@ -660,6 +660,76 @@ export const JSON_RPC_FAILURE_FIXTURES = { ], }; +export const JSON_RPC_ERROR_FIXTURES = { + valid: JSON_RPC_FAILURE_FIXTURES.valid.map((fixture) => fixture.error), + invalid: [ + {}, + [], + true, + false, + null, + undefined, + 1, + 'foo', + { + code: {}, + message: 'Internal error', + }, + { + code: [], + message: 'Internal error', + }, + { + code: true, + message: 'Internal error', + }, + { + code: false, + message: 'Internal error', + }, + { + code: null, + message: 'Internal error', + }, + { + code: undefined, + message: 'Internal error', + }, + { + code: 'foo', + message: 'Internal error', + }, + { + code: -32000, + message: {}, + }, + { + code: -32000, + message: [], + }, + { + code: -32000, + message: true, + }, + { + code: -32000, + message: false, + }, + { + code: -32000, + message: null, + }, + { + code: -32000, + message: undefined, + }, + { + code: -32000.5, + message: undefined, + }, + ], +}; + export const JSON_RPC_RESPONSE_FIXTURES = { valid: [ ...JSON_RPC_SUCCESS_FIXTURES.valid, diff --git a/src/json.test.ts b/src/json.test.ts index 05bd02e8..98361ab9 100644 --- a/src/json.test.ts +++ b/src/json.test.ts @@ -4,6 +4,7 @@ import { ARRAY_OF_MIXED_SPECIAL_OBJECTS, COMPLEX_OBJECT, JSON_FIXTURES, + JSON_RPC_ERROR_FIXTURES, JSON_RPC_FAILURE_FIXTURES, JSON_RPC_NOTIFICATION_FIXTURES, JSON_RPC_PENDING_RESPONSE_FIXTURES, @@ -29,6 +30,8 @@ import { isPendingJsonRpcResponse, isValidJson, validateJsonAndGetSize, + isJsonRpcError, + assertIsJsonRpcError, } from '.'; describe('json', () => { @@ -257,6 +260,58 @@ describe('json', () => { }); }); + describe('isJsonRpcError', () => { + it.each(JSON_RPC_ERROR_FIXTURES.valid)( + 'returns true for a valid JSON-RPC error', + (error) => { + expect(isJsonRpcError(error)).toBe(true); + }, + ); + + it.each(JSON_RPC_ERROR_FIXTURES.invalid)( + 'returns false for an invalid JSON-RPC error', + (error) => { + expect(isJsonRpcError(error)).toBe(false); + }, + ); + }); + + describe('assertIsJsonRpcError', () => { + it.each(JSON_RPC_ERROR_FIXTURES.valid)( + 'does not throw an error for valid JSON-RPC error', + (error) => { + expect(() => assertIsJsonRpcError(error)).not.toThrow(); + }, + ); + + it.each(JSON_RPC_ERROR_FIXTURES.invalid)( + 'throws an error for invalid JSON-RPC error', + (error) => { + expect(() => assertIsJsonRpcError(error)).toThrow( + 'Not a JSON-RPC error', + ); + }, + ); + + it('includes the reason in the error message', () => { + expect(() => + assertIsJsonRpcError(JSON_RPC_ERROR_FIXTURES.invalid[0]), + ).toThrow( + 'Not a JSON-RPC error: At path: code -- Expected an integer, but received: undefined.', + ); + }); + + it('includes the value thrown in the message if it is not an error', () => { + jest.spyOn(superstructModule, 'assert').mockImplementation(() => { + throw 'oops'; + }); + + expect(() => + assertIsJsonRpcError(JSON_RPC_ERROR_FIXTURES.invalid[0]), + ).toThrow('Not a JSON-RPC error: oops'); + }); + }); + describe('isPendingJsonRpcResponse', () => { it.each(JSON_RPC_PENDING_RESPONSE_FIXTURES.valid)( 'returns true for a valid pending JSON-RPC response', diff --git a/src/json.ts b/src/json.ts index f1aaaff9..0f0e0e2f 100644 --- a/src/json.ts +++ b/src/json.ts @@ -4,6 +4,7 @@ import { assert, boolean, Infer, + integer, is, lazy, literal, @@ -94,9 +95,9 @@ export const JsonRpcIdStruct = nullable(union([number(), string()])); export type JsonRpcId = Infer; export const JsonRpcErrorStruct = object({ - code: number(), + code: integer(), message: string(), - data: optional(unknown()), + data: optional(JsonStruct), stack: optional(string()), }); @@ -400,6 +401,32 @@ export function assertIsJsonRpcFailure( } } +/** + * Type guard to validate whether an object is a valid JSON-RPC error. + * + * @param value - The value object to check. + * @returns Whether the response object is a valid JSON-RPC error. + */ +export function isJsonRpcError(value: unknown): value is JsonRpcError { + return is(value, JsonRpcErrorStruct); +} + +/** + * Type assertion to validate whether an object is a valid JSON-RPC error. + * + * @param value - The value object to check. + */ +export function assertIsJsonRpcError( + value: unknown, +): asserts value is JsonRpcError { + try { + assert(value, JsonRpcErrorStruct); + } catch (error) { + const message = isErrorWithMessage(error) ? error.message : error; + throw new Error(`Not a JSON-RPC error: ${message}.`); + } +} + type JsonRpcValidatorOptions = { permitEmptyString?: boolean; permitFractions?: boolean;