Migrate to Jest (#102)

Migrates all tests to Jest and TypeScript per the module template. Care was taken to modify the existing tests as little as possible. Therefore, the tests unfortunately make prodigious use of casts to `any`. Nevertheless, to minimize such casts, a pair of assertion type guards were added for successful and failed JSON-RPC responses. They use the existing boolean type guards under the hood, and are fully tested.

Assertion type guards are tremendously helpful in situations like this, where boolean type guards don't help since e.g. `expect(typeGuard(value)).toBe(true);` doesn't tell TypeScript anything, and we have lint rules preventing us from calling `expect` conditionally.
This commit is contained in:
Erik Marks
2022-05-15 18:06:36 -07:00
committed by GitHub
parent 75d2d37c7a
commit 07f76045ff
20 changed files with 3475 additions and 2128 deletions

View File

@@ -18,13 +18,10 @@ module.exports = {
},
{
files: ['test/*'],
extends: ['@metamask/eslint-config-mocha'],
parserOptions: {
ecmaVersion: 2020,
},
files: ['*.test.ts', '*.test.js'],
extends: ['@metamask/eslint-config-jest'],
},
],
ignorePatterns: ['!.eslintrc.js', '.nyc*', 'coverage/', 'dist/'],
ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js', 'dist/'],
};

208
jest.config.ts Normal file
View File

@@ -0,0 +1,208 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from '@jest/types';
const config: Config.InitialOptions = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/fk/c3y07g0576j8_2s9m01pk4qw0000gn/T/jest_dx",
// Automatically clear mock calls, instances and results before every test.
// This does not remove any mock implementation that may have been provided,
// so we disable it.
// clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ['./src/**/*.ts'],
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'babel',
// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ['text', 'html'],
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// "resetMocks" resets all mocks, including mocked modules, to jest.fn(),
// between each test case.
resetMocks: true,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// "restoreMocks" restores all mocks created using jest.spyOn to their
// original implementations, between each test. It does not affect mocked
// modules.
restoreMocks: true,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// Reduce the default test timeout from 5s to 2.5s
testTimeout: 2500,
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
export default config;

View File

@@ -27,8 +27,8 @@
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern",
"prepublishOnly": "yarn build:clean && yarn lint && yarn test:coverage",
"setup": "yarn install && yarn allow-scripts",
"test": "mocha ./test",
"test:coverage": "nyc --check-coverage yarn test"
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"@metamask/safe-event-emitter": "^2.0.0",
@@ -38,25 +38,26 @@
"@lavamoat/allow-scripts": "^1.0.5",
"@metamask/auto-changelog": "^2.3.0",
"@metamask/eslint-config": "^9.0.0",
"@metamask/eslint-config-mocha": "^6.0.0",
"@metamask/eslint-config-jest": "^9.0.0",
"@metamask/eslint-config-nodejs": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.1",
"@types/jest": "^26.0.13",
"@types/node": "^17.0.23",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "^7.23.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.4",
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-mocha": "^8.1.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
"mocha": "^7.1.1",
"nyc": "^15.1.0",
"jest": "^27.5.1",
"prettier": "^2.2.1",
"prettier-plugin-packagejson": "^2.2.11",
"rimraf": "^3.0.2",
"sinon": "^9.0.2",
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"typescript": "^4.2.4"
},
"engines": {

254
src/asMiddleware.test.ts Normal file
View File

@@ -0,0 +1,254 @@
import {
assertIsJsonRpcSuccess,
isJsonRpcSuccess,
JsonRpcEngine,
JsonRpcRequest,
} from '.';
const jsonrpc = '2.0' as const;
describe('asMiddleware', () => {
it('basic', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
let originalReq: JsonRpcRequest<unknown>;
subengine.push(function (req, res, _next, end) {
originalReq = req;
res.result = 'saw subengine';
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(originalReq.id).toStrictEqual(res.id);
expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc);
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual('saw subengine');
resolve();
});
});
});
it('decorate res', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
let originalReq: JsonRpcRequest<unknown>;
subengine.push(function (req, res, _next, end) {
originalReq = req;
(res as any).xyz = true;
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(originalReq.id).toStrictEqual(res.id);
expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc);
expect((res as any).xyz).toBe(true);
resolve();
});
});
});
it('decorate req', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
let originalReq: JsonRpcRequest<unknown>;
subengine.push(function (req, res, _next, end) {
originalReq = req;
(req as any).xyz = true;
(res as any).xyz = true;
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(originalReq.id).toStrictEqual(res.id);
expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc);
expect((originalReq as any).xyz).toBe(true);
resolve();
});
});
});
it('should not error even if end not called', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, _res, next, _end) => next());
engine.push(subengine.asMiddleware());
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
resolve();
});
});
});
it('handles next handler correctly when nested', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, res, next, _end) => {
next((cb) => {
(res as any).copy = res.result;
cb();
});
});
engine.push(subengine.asMiddleware());
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual((res as any).copy);
resolve();
});
});
});
it('handles next handler correctly when flat', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, res, next, _end) => {
next((cb) => {
(res as any).copy = res.result;
cb();
});
});
subengine.push((_req, res, _next, end) => {
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual((res as any).copy);
resolve();
});
});
});
it('handles error thrown in middleware', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push(function (_req, _res, _next, _end) {
throw new Error('foo');
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect((err as any).message).toStrictEqual('foo');
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('handles next handler error correctly when nested', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, _res, next, _end) => {
next((_cb) => {
throw new Error('foo');
});
});
engine.push(subengine.asMiddleware());
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect((err as any).message).toStrictEqual('foo');
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('handles next handler error correctly when flat', async () => {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, _res, next, _end) => {
next((_cb) => {
throw new Error('foo');
});
});
subengine.push((_req, res, _next, end) => {
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect((err as any).message).toStrictEqual('foo');
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
});

View File

@@ -0,0 +1,136 @@
import {
JsonRpcEngine,
createAsyncMiddleware,
assertIsJsonRpcSuccess,
} from '.';
const jsonrpc = '2.0' as const;
describe('createAsyncMiddleware', () => {
it('basic middleware test', async () => {
const engine = new JsonRpcEngine();
engine.push(
createAsyncMiddleware(async (_req, res, _next) => {
res.result = 42;
}),
);
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
resolve();
});
});
});
it('next middleware test', async () => {
const engine = new JsonRpcEngine();
engine.push(
createAsyncMiddleware(async (_req, res, next) => {
expect(res.result).not.toBeDefined();
await next(); // eslint-disable-line node/callback-return
expect(res.result).toStrictEqual(1234);
// override value
res.result = 42; // eslint-disable-line require-atomic-updates
}),
);
engine.push(function (_req, res, _next, end) {
res.result = 1234;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
resolve();
});
});
});
it('basic throw test', async () => {
const engine = new JsonRpcEngine();
const error = new Error('bad boy');
engine.push(
createAsyncMiddleware(async (_req, _res, _next) => {
throw error;
}),
);
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, _res) {
expect(err).toBeDefined();
expect(err).toStrictEqual(error);
resolve();
});
});
});
it('throw after next test', async () => {
const engine = new JsonRpcEngine();
const error = new Error('bad boy');
engine.push(
createAsyncMiddleware(async (_req, _res, next) => {
await next(); // eslint-disable-line node/callback-return
throw error;
}),
);
engine.push(function (_req, res, _next, end) {
res.result = 1234;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, _res) {
expect(err).toBeDefined();
expect(err).toStrictEqual(error);
resolve();
});
});
});
it("doesn't await next", async () => {
const engine = new JsonRpcEngine();
engine.push(
createAsyncMiddleware(async (_req, _res, next) => {
next();
}),
);
engine.push(function (_req, res, _next, end) {
res.result = 1234;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, _res) {
expect(err).toBeDefined();
resolve();
});
});
});
});

View File

@@ -0,0 +1,53 @@
import {
JsonRpcEngine,
createScaffoldMiddleware,
JsonRpcMiddleware,
assertIsJsonRpcSuccess,
assertIsJsonRpcFailure,
} from '.';
describe('createScaffoldMiddleware', () => {
it('basic middleware test', async () => {
const engine = new JsonRpcEngine();
const scaffold: Record<
string,
string | JsonRpcMiddleware<unknown, unknown>
> = {
method1: 'foo',
method2: (_req, res, _next, end) => {
res.result = 42;
end();
},
method3: (_req, res, _next, end) => {
res.error = new Error('method3');
end();
},
};
engine.push(createScaffoldMiddleware(scaffold));
engine.push((_req, res, _next, end) => {
res.result = 'passthrough';
end();
});
const payload = { id: 1, jsonrpc: '2.0' as const };
const response1 = await engine.handle({ ...payload, method: 'method1' });
const response2 = await engine.handle({ ...payload, method: 'method2' });
const response3 = await engine.handle({ ...payload, method: 'method3' });
const response4 = await engine.handle({ ...payload, method: 'unknown' });
assertIsJsonRpcSuccess(response1);
expect(response1.result).toStrictEqual('foo');
assertIsJsonRpcSuccess(response2);
expect(response2.result).toStrictEqual(42);
assertIsJsonRpcFailure(response3);
expect(response3.error.message).toStrictEqual('method3');
assertIsJsonRpcSuccess(response4);
expect(response4.result).toStrictEqual('passthrough');
});
});

529
src/engine.test.ts Normal file
View File

@@ -0,0 +1,529 @@
import { isJsonRpcFailure, isJsonRpcSuccess } from './utils';
import {
JsonRpcEngine,
assertIsJsonRpcSuccess,
assertIsJsonRpcFailure,
} from '.';
const jsonrpc = '2.0' as const;
describe('JsonRpcEngine', () => {
it('handle: throws on truthy, non-function callback', () => {
const engine: any = new JsonRpcEngine();
expect(() => engine.handle({}, true)).toThrow(
'"callback" must be a function if provided.',
);
});
it('handle: returns error for invalid request parameter', async () => {
const engine = new JsonRpcEngine();
let response: any = await engine.handle(null as any);
expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
response = await engine.handle(true as any);
expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
});
it('handle: returns error for invalid request method', async () => {
const engine = new JsonRpcEngine();
let response: any = await engine.handle({ method: null } as any);
expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
response = await engine.handle({ method: true } as any);
expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
});
it('handle: basic middleware test 1', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
resolve();
});
});
});
it('handle: basic middleware test 2', async () => {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
req.method = 'banana';
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
expect(payload.method).toStrictEqual('hello');
resolve();
});
});
});
it('handle (async): basic middleware test', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
const res = await engine.handle(payload);
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
});
it('allow null result', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = null;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toBeNull();
resolve();
});
});
});
it('interacting middleware test', async () => {
const engine = new JsonRpcEngine();
engine.push(function (req: any, _res, next, _end) {
req.resultShouldBe = 42;
next();
});
engine.push(function (req: any, res, _next, end) {
res.result = req.resultShouldBe;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
resolve();
});
});
});
it('middleware ending request before all middlewares applied', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
engine.push(function (_req, _res, _next, _end) {
throw new Error('Test should have ended already.');
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual(42);
resolve();
});
});
});
it('erroring middleware test: end(error)', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, _res, _next, end) {
end(new Error('no bueno'));
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect(res).toBeDefined();
assertIsJsonRpcFailure(res);
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('erroring middleware test: res.error -> next()', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, next, _end) {
res.error = new Error('no bueno');
next();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect(res).toBeDefined();
assertIsJsonRpcFailure(res);
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('erroring middleware test: res.error -> end()', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.error = new Error('no bueno');
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect(res).toBeDefined();
expect(isJsonRpcFailure(res)).toBe(true);
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('erroring middleware test: non-function passsed to next()', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, _res, next, _end) {
next(true as any);
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeDefined();
expect(res).toBeDefined();
assertIsJsonRpcFailure(res);
expect(res.error.code).toStrictEqual(-32603);
expect(
res.error.message.startsWith(
'JsonRpcEngine: "next" return handlers must be functions.',
),
).toBe(true);
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('empty middleware test', async () => {
const engine = new JsonRpcEngine();
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, _res) {
expect(err).toBeDefined();
resolve();
});
});
});
it('handle: batch payloads', async () => {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
if (req.id === 4) {
delete res.result;
res.error = new Error('foobar');
return end(res.error);
}
res.result = req.id;
return end();
});
const payloadA = { id: 1, jsonrpc, method: 'hello' };
const payloadB = { id: 2, jsonrpc, method: 'hello' };
const payloadC = { id: 3, jsonrpc, method: 'hello' };
const payloadD = { id: 4, jsonrpc, method: 'hello' };
const payloadE = { id: 5, jsonrpc, method: 'hello' };
const payload = [payloadA, payloadB, payloadC, payloadD, payloadE];
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res: any) {
expect(err).toBeNull();
expect(res).toBeInstanceOf(Array);
expect(res[0].result).toStrictEqual(1);
expect(res[1].result).toStrictEqual(2);
expect(res[2].result).toStrictEqual(3);
expect(isJsonRpcSuccess(res[3])).toBe(false);
expect(res[3].error.code).toStrictEqual(-32603);
expect(res[4].result).toStrictEqual(5);
resolve();
});
});
});
it('handle: batch payloads (async signature)', async () => {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
if (req.id === 4) {
delete res.result;
res.error = new Error('foobar');
return end(res.error);
}
res.result = req.id;
return end();
});
const payloadA = { id: 1, jsonrpc, method: 'hello' };
const payloadB = { id: 2, jsonrpc, method: 'hello' };
const payloadC = { id: 3, jsonrpc, method: 'hello' };
const payloadD = { id: 4, jsonrpc, method: 'hello' };
const payloadE = { id: 5, jsonrpc, method: 'hello' };
const payload = [payloadA, payloadB, payloadC, payloadD, payloadE];
const res: any = await engine.handle(payload);
expect(res).toBeInstanceOf(Array);
expect(res[0].result).toStrictEqual(1);
expect(res[1].result).toStrictEqual(2);
expect(res[2].result).toStrictEqual(3);
expect(isJsonRpcSuccess(res[3])).toBe(false);
expect(res[3].error.code).toStrictEqual(-32603);
expect(res[4].result).toStrictEqual(5);
});
it('handle: batch payload with bad request object', async () => {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
res.result = req.id;
return end();
});
const payloadA = { id: 1, jsonrpc, method: 'hello' };
const payloadB = true;
const payloadC = { id: 3, jsonrpc, method: 'hello' };
const payload = [payloadA, payloadB, payloadC];
const res: any = await engine.handle(payload as any);
expect(res).toBeInstanceOf(Array);
expect(res[0].result).toStrictEqual(1);
expect(isJsonRpcSuccess(res[1])).toBe(false);
expect(res[1].error.code).toStrictEqual(-32600);
expect(res[2].result).toStrictEqual(3);
});
it('basic notifications', async () => {
const engine = new JsonRpcEngine();
await new Promise<void>((resolve) => {
engine.once('notification', (notif) => {
expect(notif.method).toStrictEqual('test_notif');
resolve();
});
engine.emit('notification', { jsonrpc, method: 'test_notif' });
});
});
it('return handlers test', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res: any, next, _end) {
next(function (cb) {
res.sawReturnHandler = true;
cb();
});
});
engine.push(function (_req, res, _next, end) {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res: any) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(res.sawReturnHandler).toBe(true);
resolve();
});
});
});
it('return order of events', async () => {
const engine = new JsonRpcEngine();
const events: string[] = [];
engine.push(function (_req, _res, next, _end) {
events.push('1-next');
next(function (cb) {
events.push('1-return');
cb();
});
});
engine.push(function (_req, _res, next, _end) {
events.push('2-next');
next(function (cb) {
events.push('2-return');
cb();
});
});
engine.push(function (_req, res, _next, end) {
events.push('3-end');
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, _res) {
expect(err).toBeNull();
expect(events[0]).toStrictEqual('1-next');
expect(events[1]).toStrictEqual('2-next');
expect(events[2]).toStrictEqual('3-end');
expect(events[3]).toStrictEqual('2-return');
expect(events[4]).toStrictEqual('1-return');
resolve();
});
});
});
it('calls back next handler even if error', async () => {
const engine = new JsonRpcEngine();
let sawNextReturnHandlerCalled = false;
engine.push(function (_req, _res, next, _end) {
next(function (cb) {
sawNextReturnHandlerCalled = true;
cb();
});
});
engine.push(function (_req, _res, _next, end) {
end(new Error('boom'));
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, (err, _res) => {
expect(err).toBeDefined();
expect(sawNextReturnHandlerCalled).toBe(true);
resolve();
});
});
});
it('handles error in next handler', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, _res, next, _end) {
next(function (_cb) {
throw new Error('foo');
});
});
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, (err: any, _res) => {
expect(err).toBeDefined();
expect(err.message).toStrictEqual('foo');
resolve();
});
});
});
it('handles failure to end request', async () => {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, next, _end) {
res.result = 42;
next();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, (err: any, res) => {
expect(
err.message.startsWith('JsonRpcEngine: Nothing ended request:'),
).toBe(true);
expect(isJsonRpcSuccess(res)).toBe(false);
resolve();
});
});
});
it('handles batch request processing error', async () => {
const engine = new JsonRpcEngine();
jest
.spyOn(engine as any, '_promiseHandle')
.mockRejectedValue(new Error('foo'));
await new Promise<void>((resolve) => {
engine.handle([{}] as any, (err: any) => {
expect(err.message).toStrictEqual('foo');
resolve();
});
});
});
it('handles batch request processing error (async)', async () => {
const engine = new JsonRpcEngine();
jest
.spyOn(engine as any, '_promiseHandle')
.mockRejectedValue(new Error('foo'));
await expect(engine.handle([{}] as any)).rejects.toThrow('foo');
});
});

View File

@@ -0,0 +1,51 @@
import { JsonRpcEngine, createIdRemapMiddleware } from '.';
describe('idRemapMiddleware', () => {
it('basic middleware test', async () => {
const engine = new JsonRpcEngine();
const observedIds: Record<string, Record<string, unknown>> = {
before: {},
after: {},
};
engine.push(function (req, res, next, _end) {
observedIds.before.req = req.id;
observedIds.before.res = res.id;
next();
});
engine.push(createIdRemapMiddleware());
engine.push(function (req, res, _next, end) {
observedIds.after.req = req.id;
observedIds.after.res = res.id;
// set result so it doesnt error
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0' as const, method: 'hello' };
const payloadCopy = { ...payload };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
// collected data
expect(observedIds.before.req).toBeDefined();
expect(observedIds.before.res).toBeDefined();
expect(observedIds.after.req).toBeDefined();
expect(observedIds.after.res).toBeDefined();
// data matches expectations
expect(observedIds.before.req).toStrictEqual(observedIds.before.res);
expect(observedIds.after.req).toStrictEqual(observedIds.after.res);
// correct behavior
expect(observedIds.before.req).not.toStrictEqual(observedIds.after.req);
expect(observedIds.before.req).toStrictEqual(res.id);
expect(payload.id).toStrictEqual(res.id);
expect(payloadCopy.id).toStrictEqual(res.id);
resolve();
});
});
});
});

176
src/mergeMiddleware.test.ts Normal file
View File

@@ -0,0 +1,176 @@
import {
assertIsJsonRpcSuccess,
JsonRpcEngine,
JsonRpcRequest,
mergeMiddleware,
} from '.';
const jsonrpc = '2.0' as const;
describe('mergeMiddleware', () => {
it('basic', async () => {
const engine = new JsonRpcEngine();
let originalReq: JsonRpcRequest<unknown>;
engine.push(
mergeMiddleware([
function (req, res, _next, end) {
originalReq = req;
res.result = 'saw merged middleware';
end();
},
]),
);
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(originalReq.id).toStrictEqual(res.id);
expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc);
expect('result' in res).toBe(true);
resolve();
});
});
});
it('handles next handler correctly for multiple merged', async () => {
const engine = new JsonRpcEngine();
engine.push(
mergeMiddleware([
(_req, res, next, _end) => {
next((cb) => {
(res as any).copy = res.result;
cb();
});
},
(_req, res, _next, end) => {
res.result = true;
end();
},
]),
);
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual((res as any).copy);
resolve();
});
});
});
it('decorate res', async () => {
const engine = new JsonRpcEngine();
let originalReq: JsonRpcRequest<unknown>;
engine.push(
mergeMiddleware([
function (req, res, _next, end) {
originalReq = req;
(res as any).xyz = true;
res.result = true;
end();
},
]),
);
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(originalReq.id).toStrictEqual(res.id);
expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc);
expect((res as any).xyz).toBe(true);
resolve();
});
});
});
it('decorate req', async () => {
const engine = new JsonRpcEngine();
let originalReq: JsonRpcRequest<unknown>;
engine.push(
mergeMiddleware([
function (req, res, _next, end) {
originalReq = req;
(req as any).xyz = true;
res.result = true;
end();
},
]),
);
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
expect(originalReq.id).toStrictEqual(res.id);
expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc);
expect((originalReq as any).xyz).toBe(true);
resolve();
});
});
});
it('should not error even if end not called', async () => {
const engine = new JsonRpcEngine();
engine.push(mergeMiddleware([(_req, _res, next, _end) => next()]));
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
expect(res).toBeDefined();
resolve();
});
});
});
it('handles next handler correctly across middleware', async () => {
const engine = new JsonRpcEngine();
engine.push(
mergeMiddleware([
(_req, res, next, _end) => {
next((cb) => {
(res as any).copy = res.result;
cb();
});
},
]),
);
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc, method: 'hello' };
await new Promise<void>((resolve) => {
engine.handle(payload, function (err, res) {
expect(err).toBeNull();
assertIsJsonRpcSuccess(res);
expect(res.result).toStrictEqual((res as any).copy);
resolve();
});
});
});
});

145
src/utils.test.ts Normal file
View File

@@ -0,0 +1,145 @@
import {
isJsonRpcFailure,
isJsonRpcSuccess,
getJsonRpcIdValidator,
assertIsJsonRpcSuccess,
assertIsJsonRpcFailure,
} from '.';
describe('isJsonRpcSuccess', () => {
it('correctly identifies JSON-RPC response objects', () => {
(
[
[{ result: 'success' }, true],
[{ result: null }, true],
[{ error: new Error('foo') }, false],
[{}, false],
] as [any, boolean][]
).forEach(([input, expectedResult]) => {
expect(isJsonRpcSuccess(input)).toBe(expectedResult);
});
});
});
describe('isJsonRpcFailure', () => {
it('correctly identifies JSON-RPC response objects', () => {
(
[
[{ error: 'failure' }, true],
[{ error: null }, true],
[{ result: 'success' }, false],
[{}, false],
] as [any, boolean][]
).forEach(([input, expectedResult]) => {
expect(isJsonRpcFailure(input)).toBe(expectedResult);
});
});
});
describe('assertIsJsonRpcSuccess', () => {
it('correctly identifies JSON-RPC response objects', () => {
([{ result: 'success' }, { result: null }] as any[]).forEach((input) => {
expect(() => assertIsJsonRpcSuccess(input)).not.toThrow();
});
([{ error: new Error('foo') }, {}] as any[]).forEach((input) => {
expect(() => assertIsJsonRpcSuccess(input)).toThrow(
'Not a successful JSON-RPC response.',
);
});
});
});
describe('assertIsJsonRpcFailure', () => {
it('correctly identifies JSON-RPC response objects', () => {
([{ error: 'failure' }, { error: null }] as any[]).forEach((input) => {
expect(() => assertIsJsonRpcFailure(input)).not.toThrow();
});
([{ result: 'success' }, {}] as any[]).forEach((input) => {
expect(() => assertIsJsonRpcFailure(input)).toThrow(
'Not a failed JSON-RPC response.',
);
});
});
});
describe('getJsonRpcIdValidator', () => {
const getInputs = () => {
return {
// invariant with respect to options
fractionString: { value: '1.2', expected: true },
negativeInteger: { value: -1, expected: true },
object: { value: {}, expected: false },
positiveInteger: { value: 1, expected: true },
string: { value: 'foo', expected: true },
undefined: { value: undefined, expected: false },
zero: { value: 0, expected: true },
// variant with respect to options
emptyString: { value: '', expected: true },
fraction: { value: 1.2, expected: false },
null: { value: null, expected: true },
};
};
const validateAll = (
validate: ReturnType<typeof getJsonRpcIdValidator>,
inputs: ReturnType<typeof getInputs>,
) => {
for (const input of Object.values(inputs)) {
expect(validate(input.value)).toStrictEqual(input.expected);
}
};
it('performs as expected with default options', () => {
const inputs = getInputs();
// The default options are:
// permitEmptyString: true,
// permitFractions: false,
// permitNull: true,
expect(() => validateAll(getJsonRpcIdValidator(), inputs)).not.toThrow();
});
it('performs as expected with "permitEmptyString: false"', () => {
const inputs = getInputs();
inputs.emptyString.expected = false;
expect(() =>
validateAll(
getJsonRpcIdValidator({
permitEmptyString: false,
}),
inputs,
),
).not.toThrow();
});
it('performs as expected with "permitFractions: true"', () => {
const inputs = getInputs();
inputs.fraction.expected = true;
expect(() =>
validateAll(
getJsonRpcIdValidator({
permitFractions: true,
}),
inputs,
),
).not.toThrow();
});
it('performs as expected with "permitNull: false"', () => {
const inputs = getInputs();
inputs.null.expected = false;
expect(() =>
validateAll(
getJsonRpcIdValidator({
permitNull: false,
}),
inputs,
),
).not.toThrow();
});
});

View File

@@ -5,6 +5,11 @@ import type {
JsonRpcSuccess,
} from './JsonRpcEngine';
export const hasProperty = (
object: Object, // eslint-disable-line @typescript-eslint/ban-types
name: string | number | symbol,
): boolean => Object.hasOwnProperty.call(object, name);
/**
* ATTN: Assumes that only one of the `result` and `error` properties is
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
@@ -18,7 +23,23 @@ import type {
export function isJsonRpcSuccess<T>(
response: JsonRpcResponse<T>,
): response is JsonRpcSuccess<T> {
return 'result' in response;
return hasProperty(response, 'result');
}
/**
* ATTN: Assumes that only one of the `result` and `error` properties is
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
*
* Type assertion to narrow a JsonRpcResponse object to a success (or failure).
*
* @param response - The response object to check.
*/
export function assertIsJsonRpcSuccess<T>(
response: JsonRpcResponse<T>,
): asserts response is JsonRpcSuccess<T> {
if (!isJsonRpcSuccess(response)) {
throw new Error('Not a successful JSON-RPC response.');
}
}
/**
@@ -34,7 +55,23 @@ export function isJsonRpcSuccess<T>(
export function isJsonRpcFailure(
response: JsonRpcResponse<unknown>,
): response is JsonRpcFailure {
return 'error' in response;
return hasProperty(response, 'error');
}
/**
* ATTN: Assumes that only one of the `result` and `error` properties is
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
*
* Type assertion to narrow a JsonRpcResponse object to a failure (or success).
*
* @param response - The response object to check.
*/
export function assertIsJsonRpcFailure(
response: JsonRpcResponse<unknown>,
): asserts response is JsonRpcFailure {
if (!isJsonRpcFailure(response)) {
throw new Error('Not a failed JSON-RPC response.');
}
}
interface JsonRpcValidatorOptions {

View File

@@ -1,233 +0,0 @@
/* eslint-env mocha */
'use strict';
const { strict: assert } = require('assert');
const { JsonRpcEngine } = require('../dist');
describe('asMiddleware', function () {
it('basic', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
let originalReq;
subengine.push(function (req, res, _next, end) {
originalReq = req;
res.result = 'saw subengine';
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(originalReq.id, res.id, 'id matches');
assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches');
assert.equal(
res.result,
'saw subengine',
'response was handled by nested engine',
);
done();
});
});
it('decorate res', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
let originalReq;
subengine.push(function (req, res, _next, end) {
originalReq = req;
res.xyz = true;
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(originalReq.id, res.id, 'id matches');
assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches');
assert.ok(res.xyz, 'res non-result prop was transfered');
done();
});
});
it('decorate req', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
let originalReq;
subengine.push(function (req, res, _next, end) {
originalReq = req;
req.xyz = true;
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(originalReq.id, res.id, 'id matches');
assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches');
assert.ok(originalReq.xyz, 'req prop was transfered');
done();
});
});
it('should not error even if end not called', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, _res, next, _end) => next());
engine.push(subengine.asMiddleware());
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
done();
});
});
it('handles next handler correctly when nested', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, res, next, _end) => {
next((cb) => {
res.copy = res.result;
cb();
});
});
engine.push(subengine.asMiddleware());
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, res.copy, 'copied result correctly');
done();
});
});
it('handles next handler correctly when flat', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, res, next, _end) => {
next((cb) => {
res.copy = res.result;
cb();
});
});
subengine.push((_req, res, _next, end) => {
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, res.copy, 'copied result correctly');
done();
});
});
it('handles error thrown in middleware', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push(function (_req, _res, _next, _end) {
throw new Error('foo');
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'should have errored');
assert.equal(err.message, 'foo', 'should have expected error');
assert.ifError(res.result, 'should not have result');
done();
});
});
it('handles next handler error correctly when nested', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, _res, next, _end) => {
next((_cb) => {
throw new Error('foo');
});
});
engine.push(subengine.asMiddleware());
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'should have errored');
assert.equal(err.message, 'foo', 'should have expected error');
assert.ifError(res.result, 'should not have result');
done();
});
});
it('handles next handler error correctly when flat', function (done) {
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
subengine.push((_req, _res, next, _end) => {
next((_cb) => {
throw new Error('foo');
});
});
subengine.push((_req, res, _next, end) => {
res.result = true;
end();
});
engine.push(subengine.asMiddleware());
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'should have errored');
assert.equal(err.message, 'foo', 'should have expected error');
assert.ifError(res.result, 'should not have result');
done();
});
});
});

View File

@@ -1,123 +0,0 @@
/* eslint-env mocha */
/* eslint require-await: off */
'use strict';
const { strict: assert } = require('assert');
const { JsonRpcEngine, createAsyncMiddleware } = require('../dist');
describe('createAsyncMiddleware', function () {
it('basic middleware test', function (done) {
const engine = new JsonRpcEngine();
engine.push(
createAsyncMiddleware(async (_req, res, _next) => {
res.result = 42;
}),
);
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
done();
});
});
it('next middleware test', function (done) {
const engine = new JsonRpcEngine();
engine.push(
createAsyncMiddleware(async (_req, res, next) => {
assert.ifError(res.result, 'does not have result');
await next(); // eslint-disable-line node/callback-return
assert.equal(res.result, 1234, 'value was set as expected');
// override value
res.result = 42; // eslint-disable-line require-atomic-updates
}),
);
engine.push(function (_req, res, _next, end) {
res.result = 1234;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
done();
});
});
it('basic throw test', function (done) {
const engine = new JsonRpcEngine();
const error = new Error('bad boy');
engine.push(
createAsyncMiddleware(async (_req, _res, _next) => {
throw error;
}),
);
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, _res) {
assert.ok(err, 'has err');
assert.equal(err, error, 'has expected result');
done();
});
});
it('throw after next test', function (done) {
const engine = new JsonRpcEngine();
const error = new Error('bad boy');
engine.push(
createAsyncMiddleware(async (_req, _res, next) => {
await next(); // eslint-disable-line node/callback-return
throw error;
}),
);
engine.push(function (_req, res, _next, end) {
res.result = 1234;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, _res) {
assert.ok(err, 'has err');
assert.equal(err, error, 'has expected result');
done();
});
});
it("doesn't await next", function (done) {
const engine = new JsonRpcEngine();
engine.push(
createAsyncMiddleware(async (_req, _res, next) => {
next();
}),
);
engine.push(function (_req, res, _next, end) {
res.result = 1234;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, _res) {
assert.ifError(err, 'has err');
done();
});
});
});

View File

@@ -1,50 +0,0 @@
/* eslint-env mocha */
'use strict';
const { strict: assert } = require('assert');
const { JsonRpcEngine, createScaffoldMiddleware } = require('../dist');
describe('createScaffoldMiddleware', function () {
it('basic middleware test', async function () {
const engine = new JsonRpcEngine();
const scaffold = {
method1: 'foo',
method2: (_req, res, _next, end) => {
res.result = 42;
end();
},
method3: (_req, res, _next, end) => {
res.error = new Error('method3');
end();
},
};
engine.push(createScaffoldMiddleware(scaffold));
engine.push((_req, res, _next, end) => {
res.result = 'passthrough';
end();
});
const payload = { id: 1, jsonrpc: '2.0' };
const response1 = await engine.handle({ ...payload, method: 'method1' });
const response2 = await engine.handle({ ...payload, method: 'method2' });
const response3 = await engine.handle({ ...payload, method: 'method3' });
const response4 = await engine.handle({ ...payload, method: 'unknown' });
assert.equal(response1.result, 'foo', 'should have expected result');
assert.equal(response2.result, 42, 'should have expected result');
assert.equal(
response3.error.message,
'method3',
'should have expected error',
);
assert.equal(
response4.result,
'passthrough',
'should have expected result',
);
});
});

View File

@@ -1,508 +0,0 @@
/* eslint-env mocha */
'use strict';
const { strict: assert } = require('assert');
const { stub } = require('sinon');
const { JsonRpcEngine } = require('../dist');
describe('JsonRpcEngine', function () {
it('handle: throws on truthy, non-function callback', function () {
const engine = new JsonRpcEngine();
assert.throws(
() => engine.handle({}, true),
{ message: '"callback" must be a function if provided.' },
'should throw expected error',
);
});
it('handle: returns error for invalid request parameter', async function () {
const engine = new JsonRpcEngine();
let response = await engine.handle(null);
assert.equal(response.error.code, -32600, 'should have expected error');
assert.equal(response.result, undefined, 'should have no results');
response = await engine.handle(true);
assert.equal(response.error.code, -32600, 'should have expected error');
assert.equal(response.result, undefined, 'should have no results');
});
it('handle: returns error for invalid request method', async function () {
const engine = new JsonRpcEngine();
let response = await engine.handle({ method: null });
assert.equal(response.error.code, -32600, 'should have expected error');
assert.equal(response.result, undefined, 'should have no results');
response = await engine.handle({ method: true });
assert.equal(response.error.code, -32600, 'should have expected error');
assert.equal(response.result, undefined, 'should have no results');
});
it('handle: basic middleware test 1', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
done();
});
});
it('handle: basic middleware test 2', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
req.method = 'banana';
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
assert.equal(
payload.method,
'hello',
'original request object is not mutated by middleware',
);
done();
});
});
it('handle (async): basic middleware test', async function () {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
const res = await engine.handle(payload);
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
});
it('allow null result', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = null;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, null, 'has expected result');
done();
});
});
it('interacting middleware test', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (req, _res, next, _end) {
req.resultShouldBe = 42;
next();
});
engine.push(function (req, res, _next, end) {
res.result = req.resultShouldBe;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
done();
});
});
it('middleware ending request before all middlewares applied', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
engine.push(function (_req, _res, _next, _end) {
assert.fail('should not have called second middleware');
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, 42, 'has expected result');
done();
});
});
it('erroring middleware test: end(error)', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, _res, _next, end) {
end(new Error('no bueno'));
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'did error');
assert.ok(res, 'does have response');
assert.ok(res.error, 'does have error on response');
assert.ok(!res.result, 'does not have result on response');
done();
});
});
it('erroring middleware test: res.error -> next()', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, next, _end) {
res.error = new Error('no bueno');
next();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'did error');
assert.ok(res, 'does have response');
assert.ok(res.error, 'does have error on response');
assert.ok(!res.result, 'does not have result on response');
done();
});
});
it('erroring middleware test: res.error -> end()', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, _next, end) {
res.error = new Error('no bueno');
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'did error');
assert.ok(res, 'does have response');
assert.ok(res.error, 'does have error on response');
assert.ok(!res.result, 'does not have result on response');
done();
});
});
it('erroring middleware test: non-function passsed to next()', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, _res, next, _end) {
next(true);
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ok(err, 'should error');
assert.ok(res, 'should have response');
assert.ok(res.error, 'should have error on response');
assert.equal(res.error.code, -32603, 'should have expected error');
assert.ok(
res.error.message.startsWith(
'JsonRpcEngine: "next" return handlers must be functions.',
),
'should have expected error',
);
assert.ok(!res.result, 'should not have result on response');
done();
});
});
it('empty middleware test', function (done) {
const engine = new JsonRpcEngine();
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, _res) {
assert.ok(err, 'did error');
done();
});
});
it('handle: batch payloads', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
if (req.id === 4) {
delete res.result;
res.error = new Error('foobar');
return end(res.error);
}
res.result = req.id;
return end();
});
const payloadA = { id: 1, jsonrpc: '2.0', method: 'hello' };
const payloadB = { id: 2, jsonrpc: '2.0', method: 'hello' };
const payloadC = { id: 3, jsonrpc: '2.0', method: 'hello' };
const payloadD = { id: 4, jsonrpc: '2.0', method: 'hello' };
const payloadE = { id: 5, jsonrpc: '2.0', method: 'hello' };
const payload = [payloadA, payloadB, payloadC, payloadD, payloadE];
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.ok(Array.isArray(res), 'res is array');
assert.equal(res[0].result, 1, 'has expected result');
assert.equal(res[1].result, 2, 'has expected result');
assert.equal(res[2].result, 3, 'has expected result');
assert.ok(!res[3].result, 'has no result');
assert.equal(res[3].error.code, -32603, 'has expected error');
assert.equal(res[4].result, 5, 'has expected result');
done();
});
});
it('handle: batch payloads (async signature)', async function () {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
if (req.id === 4) {
delete res.result;
res.error = new Error('foobar');
return end(res.error);
}
res.result = req.id;
return end();
});
const payloadA = { id: 1, jsonrpc: '2.0', method: 'hello' };
const payloadB = { id: 2, jsonrpc: '2.0', method: 'hello' };
const payloadC = { id: 3, jsonrpc: '2.0', method: 'hello' };
const payloadD = { id: 4, jsonrpc: '2.0', method: 'hello' };
const payloadE = { id: 5, jsonrpc: '2.0', method: 'hello' };
const payload = [payloadA, payloadB, payloadC, payloadD, payloadE];
const res = await engine.handle(payload);
assert.ok(res, 'has res');
assert.ok(Array.isArray(res), 'res is array');
assert.equal(res[0].result, 1, 'has expected result');
assert.equal(res[1].result, 2, 'has expected result');
assert.equal(res[2].result, 3, 'has expected result');
assert.ok(!res[3].result, 'has no result');
assert.equal(res[3].error.code, -32603, 'has expected error');
assert.equal(res[4].result, 5, 'has expected result');
});
it('handle: batch payload with bad request object', async function () {
const engine = new JsonRpcEngine();
engine.push(function (req, res, _next, end) {
res.result = req.id;
return end();
});
const payloadA = { id: 1, jsonrpc: '2.0', method: 'hello' };
const payloadB = true;
const payloadC = { id: 3, jsonrpc: '2.0', method: 'hello' };
const payload = [payloadA, payloadB, payloadC];
const res = await engine.handle(payload);
assert.ok(res, 'has res');
assert.ok(Array.isArray(res), 'res is array');
assert.equal(res[0].result, 1, 'should have expected result');
assert.equal(res[1].error.code, -32600, 'should have expected error');
assert.ok(!res[1].result, 'should have no result');
assert.equal(res[2].result, 3, 'should have expected result');
});
it('basic notifications', function (done) {
const engine = new JsonRpcEngine();
engine.once('notification', (notif) => {
assert.equal(notif.method, 'test_notif');
done();
});
const payload = { jsonrpc: '2.0', method: 'test_notif' };
engine.emit('notification', payload);
});
it('return handlers test', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, next, _end) {
next(function (cb) {
res.sawReturnHandler = true;
cb();
});
});
engine.push(function (_req, res, _next, end) {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.ok(res.sawReturnHandler, 'saw return handler');
done();
});
});
it('return order of events', function (done) {
const engine = new JsonRpcEngine();
const events = [];
engine.push(function (_req, _res, next, _end) {
events.push('1-next');
next(function (cb) {
events.push('1-return');
cb();
});
});
engine.push(function (_req, _res, next, _end) {
events.push('2-next');
next(function (cb) {
events.push('2-return');
cb();
});
});
engine.push(function (_req, res, _next, end) {
events.push('3-end');
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, _res) {
assert.ifError(err, 'did not error');
assert.equal(events[0], '1-next', '(event 0) was "1-next"');
assert.equal(events[1], '2-next', '(event 1) was "2-next"');
assert.equal(events[2], '3-end', '(event 2) was "3-end"');
assert.equal(events[3], '2-return', '(event 3) was "2-return"');
assert.equal(events[4], '1-return', '(event 4) was "1-return"');
done();
});
});
it('calls back next handler even if error', function (done) {
const engine = new JsonRpcEngine();
let sawNextReturnHandlerCalled = false;
engine.push(function (_req, _res, next, _end) {
next(function (cb) {
sawNextReturnHandlerCalled = true;
cb();
});
});
engine.push(function (_req, _res, _next, end) {
end(new Error('boom'));
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, (err, _res) => {
assert.ok(err, 'did error');
assert.ok(sawNextReturnHandlerCalled, 'saw next return handler called');
done();
});
});
it('handles error in next handler', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, _res, next, _end) {
next(function (_cb) {
throw new Error('foo');
});
});
engine.push(function (_req, res, _next, end) {
res.result = 42;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, (err, _res) => {
assert.ok(err, 'did error');
assert.equal(err.message, 'foo', 'error has expected message');
done();
});
});
it('handles failure to end request', function (done) {
const engine = new JsonRpcEngine();
engine.push(function (_req, res, next, _end) {
res.result = 42;
next();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, (err, res) => {
assert.ok(err, 'should have errored');
assert.ok(
err.message.startsWith('JsonRpcEngine: Nothing ended request:'),
'should have expected error message',
);
assert.ok(!res.result, 'should not have result');
done();
});
});
it('handles batch request processing error', function (done) {
const engine = new JsonRpcEngine();
stub(engine, '_promiseHandle').throws(new Error('foo'));
engine.handle([{}], (err) => {
assert.ok(err, 'did error');
assert.equal(err.message, 'foo', 'error has expected message');
done();
});
});
it('handles batch request processing error (async)', async function () {
const engine = new JsonRpcEngine();
stub(engine, '_promiseHandle').throws(new Error('foo'));
try {
await engine.handle([{}]);
assert.fail('should have errored');
} catch (err) {
assert.ok(err, 'did error');
assert.equal(err.message, 'foo', 'error has expected message');
}
});
});

View File

@@ -1,61 +0,0 @@
/* eslint-env mocha */
'use strict';
const { strict: assert } = require('assert');
const { JsonRpcEngine, createIdRemapMiddleware } = require('../dist');
describe('idRemapMiddleware', function () {
it('basic middleware test', function (done) {
const engine = new JsonRpcEngine();
const observedIds = {
before: {},
after: {},
};
engine.push(function (req, res, next, _end) {
observedIds.before.req = req.id;
observedIds.before.res = res.id;
next();
});
engine.push(createIdRemapMiddleware());
engine.push(function (req, res, _next, end) {
observedIds.after.req = req.id;
observedIds.after.res = res.id;
// set result so it doesnt error
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
const payloadCopy = { ...payload };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
// collected data
assert.ok(observedIds.before.req, 'captured ids');
assert.ok(observedIds.before.res, 'captured ids');
assert.ok(observedIds.after.req, 'captured ids');
assert.ok(observedIds.after.res, 'captured ids');
// data matches expectations
assert.equal(observedIds.before.req, observedIds.before.res, 'ids match');
assert.equal(observedIds.after.req, observedIds.after.res, 'ids match');
// correct behavior
assert.notEqual(
observedIds.before.req,
observedIds.after.req,
'ids are different',
);
assert.equal(
observedIds.before.req,
res.id,
'result id matches original',
);
assert.equal(payload.id, res.id, 'result id matches original');
assert.equal(payloadCopy.id, res.id, 'result id matches original');
done();
});
});
});

View File

@@ -1,165 +0,0 @@
/* eslint-env mocha */
'use strict';
const { strict: assert } = require('assert');
const { JsonRpcEngine, mergeMiddleware } = require('../dist');
describe('mergeMiddleware', function () {
it('basic', function (done) {
const engine = new JsonRpcEngine();
let originalReq;
engine.push(
mergeMiddleware([
function (req, res, _next, end) {
originalReq = req;
res.result = 'saw merged middleware';
end();
},
]),
);
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(originalReq.id, res.id, 'id matches');
assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches');
assert.equal(
res.result,
'saw merged middleware',
'response was handled by nested middleware',
);
done();
});
});
it('handles next handler correctly for multiple merged', function (done) {
const engine = new JsonRpcEngine();
engine.push(
mergeMiddleware([
(_req, res, next, _end) => {
next((cb) => {
res.copy = res.result;
cb();
});
},
(_req, res, _next, end) => {
res.result = true;
end();
},
]),
);
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, res.copy, 'copied result correctly');
done();
});
});
it('decorate res', function (done) {
const engine = new JsonRpcEngine();
let originalReq;
engine.push(
mergeMiddleware([
function (req, res, _next, end) {
originalReq = req;
res.xyz = true;
res.result = true;
end();
},
]),
);
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(originalReq.id, res.id, 'id matches');
assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches');
assert.ok(res.xyz, 'res non-result prop was transfered');
done();
});
});
it('decorate req', function (done) {
const engine = new JsonRpcEngine();
let originalReq;
engine.push(
mergeMiddleware([
function (req, res, _next, end) {
originalReq = req;
req.xyz = true;
res.result = true;
end();
},
]),
);
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(originalReq.id, res.id, 'id matches');
assert.equal(originalReq.jsonrpc, res.jsonrpc, 'jsonrpc version matches');
assert.ok(originalReq.xyz, 'req prop was transfered');
done();
});
});
it('should not error even if end not called', function (done) {
const engine = new JsonRpcEngine();
engine.push(mergeMiddleware([(_req, _res, next, _end) => next()]));
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
done();
});
});
it('handles next handler correctly across middleware', function (done) {
const engine = new JsonRpcEngine();
engine.push(
mergeMiddleware([
(_req, res, next, _end) => {
next((cb) => {
res.copy = res.result;
cb();
});
},
]),
);
engine.push((_req, res, _next, end) => {
res.result = true;
end();
});
const payload = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(payload, function (err, res) {
assert.ifError(err, 'did not error');
assert.ok(res, 'has res');
assert.equal(res.result, res.copy, 'copied result correctly');
done();
});
});
});

View File

@@ -1,102 +0,0 @@
/* eslint-env mocha */
'use strict';
const { strict: assert } = require('assert');
const {
isJsonRpcFailure,
isJsonRpcSuccess,
getJsonRpcIdValidator,
} = require('../dist');
describe('isJsonRpcSuccess', function () {
it('correctly identifies JSON-RPC response objects', function () {
assert.equal(isJsonRpcSuccess({ result: 'success' }), true);
assert.equal(isJsonRpcSuccess({ result: null }), true);
assert.equal(isJsonRpcSuccess({ error: new Error('foo') }), false);
assert.equal(isJsonRpcSuccess({}), false);
});
});
describe('isJsonRpcFailure', function () {
it('correctly identifies JSON-RPC response objects', function () {
assert.equal(isJsonRpcFailure({ error: 'failure' }), true);
assert.equal(isJsonRpcFailure({ error: null }), true);
assert.equal(isJsonRpcFailure({ result: 'success' }), false);
assert.equal(isJsonRpcFailure({}), false);
});
});
describe('getJsonRpcIdValidator', function () {
const getInputs = () => {
return {
// invariant with respect to options
fractionString: { value: '1.2', expected: true },
negativeInteger: { value: -1, expected: true },
object: { value: {}, expected: false },
positiveInteger: { value: 1, expected: true },
string: { value: 'foo', expected: true },
undefined: { value: undefined, expected: false },
zero: { value: 0, expected: true },
// variant with respect to options
emptyString: { value: '', expected: true },
fraction: { value: 1.2, expected: false },
null: { value: null, expected: true },
};
};
const validateAll = (validate, inputs) => {
for (const input of Object.values(inputs)) {
assert.equal(
validate(input.value),
input.expected,
`should output "${input.expected}" for "${input.value}"`,
);
}
};
it('performs as expected with default options', function () {
const inputs = getInputs();
// The default options are:
// permitEmptyString: true,
// permitFractions: false,
// permitNull: true,
validateAll(getJsonRpcIdValidator(), inputs);
});
it('performs as expected with "permitEmptyString: false"', function () {
const inputs = getInputs();
inputs.emptyString.expected = false;
validateAll(
getJsonRpcIdValidator({
permitEmptyString: false,
}),
inputs,
);
});
it('performs as expected with "permitFractions: true"', function () {
const inputs = getInputs();
inputs.fraction.expected = true;
validateAll(
getJsonRpcIdValidator({
permitFractions: true,
}),
inputs,
);
});
it('performs as expected with "permitNull: false"', function () {
const inputs = getInputs();
inputs.null.expected = false;
validateAll(
getJsonRpcIdValidator({
permitNull: false,
}),
inputs,
);
});
});

View File

@@ -12,6 +12,6 @@
"strict": true,
"target": "ES2017"
},
"exclude": ["./test"],
"exclude": ["./src/**/*.test.ts"],
"include": ["./src/**/*.ts"]
}

2742
yarn.lock

File diff suppressed because it is too large Load Diff