UNPKG

ocpp-rpc

Version:

A client & server implementation of the WAMP-like RPC-over-websocket system defined in the OCPP protocols (e.g. OCPP1.6-J and OCPP2.0.1).

1,454 lines (1,184 loc) 99 kB
const assert = require('assert/strict'); const http = require('http'); const { once } = require('events'); const RPCClient = require("../lib/client"); const { TimeoutError, RPCFrameworkError, RPCError, RPCProtocolError, RPCTypeConstraintViolationError, RPCOccurenceConstraintViolationError, RPCPropertyConstraintViolationError, RPCOccurrenceConstraintViolationError, RPCFormationViolationError } = require('../lib/errors'); const RPCServer = require("../lib/server"); const { setTimeout } = require('timers/promises'); const { createValidator } = require('../lib/validator'); const { createRPCError } = require('../lib/util'); const { NOREPLY } = require('../lib/symbols'); const {CLOSING, CLOSED, CONNECTING} = RPCClient; describe('RPCClient', function(){ this.timeout(500); async function createServer(options = {}, extra = {}) { const server = new RPCServer(options); const httpServer = await server.listen(0); const port = httpServer.address().port; const endpoint = `ws://localhost:${port}`; const close = (...args) => server.close(...args); server.on('client', client => { client.handle('Echo', async ({params}) => { return params; }); client.handle('Sleep', async ({params, signal}) => { await setTimeout(params.ms, null, {signal}); return `Waited ${params.ms}ms`; }); client.handle('Reject', async ({params}) => { const err = Error("Rejecting"); Object.assign(err, params); throw err; }); client.handle('Heartbeat', () => { return {currentTime: new Date().toISOString()}; }); client.handle('TestTenth', ({params}) => { return {val: params.val / 10}; }); if (extra.withClient) { extra.withClient(client); } }); return {server, httpServer, port, endpoint, close}; } function getEchoValidator() { return createValidator('echo1.0', [ { "$schema": "http://json-schema.org/draft-07/schema", "$id": "urn:Echo.req", "type": "object", "properties": { "val": { "type": "string" } }, "additionalProperties": false, "required": ["val"] }, { "$schema": "http://json-schema.org/draft-07/schema", "$id": "urn:Echo.conf", "type": "object", "properties": { "val": { "type": "string" } }, "additionalProperties": false, "required": ["val"] } ]); } function getNumberTestValidator() { return createValidator('numbers1.0', [ { "$schema": "http://json-schema.org/draft-07/schema", "$id": "urn:TestTenth.req", "type": "object", "properties": { "val": { "type": "number", "multipleOf": 0.1 } }, "additionalProperties": false, "required": ["val"] }, { "$schema": "http://json-schema.org/draft-07/schema", "$id": "urn:TestTenth.conf", "type": "object", "properties": { "val": { "type": "number", "multipleOf": 0.01 } }, "additionalProperties": false, "required": ["val"] } ]); } describe('#constructor', function(){ it('should throw on missing identity', async () => { assert.throws(() => { new RPCClient({ endpoint: 'ws://localhost', }); }); }); it('should throw if strictMode = true and not all protocol schemas found', async () => { assert.throws(() => { new RPCClient({ endpoint: 'ws://localhost', identity: 'x', protocols: ['ocpp1.6', 'echo1.0', 'other0.1'], strictMode: true, }); }); assert.throws(() => { new RPCClient({ endpoint: 'ws://localhost', identity: 'x', protocols: ['ocpp1.6', 'echo1.0', 'other0.1'], strictMode: ['ocpp1.6', 'other0.1'], }); }); assert.throws(() => { // trying to use strict mode with no protocols specified new RPCClient({ endpoint: 'ws://localhost', identity: 'x', protocols: [], strictMode: true, }); }); assert.throws(() => { // trying to use strict mode with no protocols specified new RPCClient({ endpoint: 'ws://localhost', identity: 'x', strictMode: true, }); }); assert.doesNotThrow(() => { new RPCClient({ endpoint: 'ws://localhost', identity: 'x', protocols: ['ocpp1.6', 'echo1.0', 'other0.1'], strictModeValidators: [getEchoValidator()], strictMode: ['ocpp1.6', 'echo1.0'], }); }); assert.doesNotThrow(() => { new RPCClient({ endpoint: 'ws://localhost', identity: 'x', protocols: ['ocpp1.6', 'echo1.0'], strictModeValidators: [getEchoValidator()], strictMode: true, }); }); }); }); describe('events', function(){ it('should emit call and response events', async () => { const {endpoint, close} = await createServer({}, { withClient: cli => { cli.call('Test').catch(()=>{}); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); const test = { in: {}, out: {}, }; cli.on('call', call => { test[call.outbound?'out':'in'].call = call; }); cli.on('response', response => { test[response.outbound?'out':'in'].response = response; }); await cli.connect(); await cli.call('Sleep', {ms: 25}); await cli.close(); await close(); assert.ok(test.in.call); assert.ok(test.in.response); assert.ok(test.out.call); assert.ok(test.out.response); assert.equal(test.in.call.payload[1], test.out.response.payload[1]); assert.equal(test.out.call.payload[1], test.in.response.payload[1]); }); it('should emit callResult and callError events for outbound calls', async () => { const {endpoint, close} = await createServer({}, {}); const cli = new RPCClient({ endpoint, identity: 'X', }); let result; let error; cli.on('callResult', evt => { result = evt; }); cli.on('callError', evt => { error = evt; }); await cli.connect(); await cli.call('Echo', {txt: 'Test'}); await cli.call('Reject', {details:{code: 'Test'}}).catch(()=>{}); await cli.close(); await close(); assert.equal(result.method, 'Echo'); assert.equal(result.outbound, true); assert.equal(result.params.txt, 'Test'); assert.equal(result.result.txt, 'Test'); assert.equal(error.method, 'Reject'); assert.equal(error.outbound, true); assert.equal(error.params.details.code, 'Test'); assert.equal(error.error.details.code, 'Test'); }); it('should emit callResult and callError events for inbound calls', async () => { let resolveReceived; let received = new Promise(r => {resolveReceived = r}); const {endpoint, close} = await createServer({}, { withClient: async (cli) => { await cli.call('Echo', {txt: 'Test'}); await cli.call('Reject', {details:{code: 'Test'}}).catch(()=>{}); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); cli.handle('Echo', async ({params}) => { return params; }); cli.handle('Reject', async ({params}) => { const err = Error("Rejecting"); Object.assign(err, params); throw err; }); let result; let error; cli.on('callResult', evt => { result = evt; }); cli.on('callError', evt => { error = evt; resolveReceived(); }); await cli.connect(); await received; await cli.close(); await close(); assert.equal(result.method, 'Echo'); assert.equal(result.outbound, false); assert.equal(result.params.txt, 'Test'); assert.equal(result.result.txt, 'Test'); assert.equal(error.method, 'Reject'); assert.equal(error.outbound, false); assert.equal(error.params.details.code, 'Test'); assert.equal(error.error.details.code, 'Test'); }); it('should emit 2 message events after call', async () => { const {endpoint, close} = await createServer(); const cli = new RPCClient({ endpoint, identity: 'X', }); const messages = []; await cli.connect(); cli.on('message', m => messages.push({ payload: JSON.parse(m.message.toString('utf8')), outbound: m.outbound, })); await cli.call('Echo', {val: 123}); await cli.close(); await close(); const [call, res] = messages; assert.equal(call.outbound, true); assert.equal(res.outbound, false); assert.equal(call.payload[0], 2); assert.equal(res.payload[0], 3); assert.equal(call.payload[1], res.payload[1]); assert.equal(call.payload[2], 'Echo'); }); it('should emit 2 message events after handle', async () => { let complete; let done = new Promise(r=>{complete = r;}); const {endpoint, close} = await createServer({}, { withClient: async (cli) => { await cli.call('Echo', {val: 123}); cli.close(); complete(); } }); const cli = new RPCClient({ endpoint, identity: 'X', reconnect: false, }); cli.handle('Echo', ({params}) => params); const messages = []; await cli.connect(); cli.on('message', m => messages.push({ payload: JSON.parse(m.message.toString('utf8')), outbound: m.outbound, })); await done; await close(); const [call, res] = messages; assert.equal(call.outbound, false); assert.equal(res.outbound, true); assert.equal(call.payload[0], 2); assert.equal(res.payload[0], 3); assert.equal(call.payload[1], res.payload[1]); assert.equal(call.payload[2], 'Echo'); }); it("should emit 'badMessage' with 'RpcFrameworkError' when message is not a JSON structure", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { cli.sendRaw('{]'); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'RpcFrameworkError'); } finally { await cli.close(); close(); } }); it("should emit 'badMessage' with 'RpcFrameworkError' when message is not an array", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { cli.sendRaw('{}'); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'RpcFrameworkError'); } finally { await cli.close(); close(); } }); it("should emit 'badMessage' with 'RpcFrameworkError' when message type is not a number", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { cli.sendRaw('["a", "123", "Echo", {}]'); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'RpcFrameworkError'); } finally { await cli.close(); close(); } }); it("should emit 'badMessage' with 'MessageTypeNotSupported' when message type unrecognised", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { cli.sendRaw('[0, "123", "Echo", {}]'); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'MessageTypeNotSupported'); } finally { await cli.close(); close(); } }); it("should emit 'badMessage' with 'RpcFrameworkError' when message ID is not a string", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { cli.sendRaw('[2, 123, "Echo", {}]'); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'RpcFrameworkError'); } finally { await cli.close(); close(); } }); it("should emit 'badMessage' with 'RpcFrameworkError' when method is not a string", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { cli.sendRaw('[2, "123", 123, {}]'); } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'RpcFrameworkError'); } finally { await cli.close(); close(); } }); it("should emit 'badMessage' with 'RpcFrameworkError' when message ID is repeated", async () => { const {endpoint, close, server} = await createServer({}, { withClient: cli => { } }); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); cli.sendRaw('[2, "123", "Sleep", {"ms":20}]'); cli.sendRaw('[2, "123", "Sleep", {"ms":20}]'); const [badMsg] = await once(cli, 'badMessage'); assert.equal(badMsg.error.rpcErrorCode, 'RpcFrameworkError'); assert.equal(badMsg.error.details.msgId, '123'); assert.equal(badMsg.error.details.errorCode, 'RpcFrameworkError'); assert.equal(badMsg.error.details.errorDescription, 'Already processing a call with message ID: 123'); } finally { await cli.close(); close(); } }); }); describe('#connect', function(){ it('should connect to an RPCServer', async () => { const {endpoint, close} = await createServer(); const cli = new RPCClient({ endpoint, identity: 'X', }); await cli.connect(); await cli.close(); close(); }); it('should reject on non-websocket server', async () => { const httpServer = http.createServer((req, res) => { res.end(); }); const httpServerAbort = new AbortController(); await new Promise((resolve, reject) => { httpServer.listen({ port: 0, host: 'localhost', signal: httpServerAbort.signal, }, err => err ? reject(err) : resolve()); }); const port = httpServer.address().port; const endpoint = `ws://localhost:${port}`; const cli = new RPCClient({endpoint, identity: 'X'}); try { await assert.rejects(cli.connect()); } finally { await cli.close(); httpServerAbort.abort(); } }); it('should reject on non-ws endpoint URL', async () => { const {close, port} = await createServer(); const cli = new RPCClient({ endpoint: `http://localhost:${port}`, identity: 'X', }); try { await assert.rejects(cli.connect()); } finally { await cli.close(); close(); } }); it('should reject on malformed endpoint URL', async () => { const {close} = await createServer(); const cli = new RPCClient({ endpoint: 'x', identity: 'X', }); try { await assert.rejects(cli.connect()); } finally { await cli.close(); close(); } }); it('should reject on unreachable address', async () => { const {close} = await createServer(); const cli = new RPCClient({ endpoint: 'ws://0.0.0.0:0', identity: 'X', }); try { await assert.rejects(cli.connect()); } finally { await cli.close(); close(); } }); it('should reject when no subprotocols match', async () => { const {endpoint, close} = await createServer({protocols: ['one', 'two']}); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['three', 'four'] }); try { await assert.rejects(cli.connect()); } finally { await cli.close(); close(); } }); it('should select first matching subprotocol', async () => { const {endpoint, close} = await createServer({protocols: ['one', 'two', 'three', 'four', 'x']}); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['test', 'three', 'four'], }); try { await cli.connect(); assert.equal(cli.protocol, 'three'); } finally { await cli.close(); close(); } }); it('should pass query string to server (as object)', async () => { let shake; const {endpoint, close, server} = await createServer({}, { withClient: client => { client.handle('GetQuery', () => client.handshake.query.toString()); } }); server.auth((accept, reject, handshake) => { shake = handshake; accept(); }); const cli = new RPCClient({ endpoint, identity: 'X', query: {'x-test': 'abc', '?=': '123'}, }); try { await cli.connect(); const query = new URLSearchParams(await cli.call('GetQuery')); assert.equal(query.get('x-test'), 'abc'); assert.equal(shake.query.get('?='), '123'); } finally { cli.close(); close(); } }); it('should pass query string to server (as string)', async () => { let shake; const {endpoint, close, server} = await createServer(); server.auth((accept, reject, handshake) => { shake = handshake; accept(); }); const cli = new RPCClient({ endpoint, identity: 'X', query: 'x-test=abc' }); try { await cli.connect(); assert.equal(shake.query.get('x-test'), 'abc'); } finally { cli.close(); close(); } }); it('should pass headers to server', async () => { let shake; const {endpoint, close, server} = await createServer({}, { withClient: client => { client.handle('GetHeaders', () => client.handshake.headers); } }); server.auth((accept, reject, handshake) => { shake = handshake; accept(); }); const cli = new RPCClient({ endpoint, identity: 'X', headers: { 'x-test': 'abc', 'x-test2': 'Token xxx', } }); try { await cli.connect(); const headers = await cli.call('GetHeaders'); assert.equal(headers['x-test'], 'abc'); assert.equal(shake.headers['x-test2'], 'Token xxx'); } finally { cli.close(); close(); } }); it('should also pass headers to server via wsOpts (but can be overridden by headers option)', async () => { let shake; const {endpoint, close, server} = await createServer({}, { withClient: client => { client.handle('GetHeaders', () => client.handshake.headers); } }); server.auth((accept, reject, handshake) => { shake = handshake; accept(); }); const cli = new RPCClient({ endpoint, identity: 'X', headers: { 'x-test': 'abc', 'x-test2': 'Token xxx', }, wsOpts: { headers: { 'x-test2': 'Token yyy', 'x-test3': 'Token zzz', } } }); try { await cli.connect(); const headers = await cli.call('GetHeaders'); assert.equal(headers['x-test'], 'abc'); assert.equal(headers['x-test2'], 'Token xxx'); assert.equal(headers['x-test3'], 'Token zzz'); assert.equal(shake.headers['x-test'], 'abc'); assert.equal(shake.headers['x-test2'], 'Token xxx'); assert.equal(shake.headers['x-test3'], 'Token zzz'); } finally { cli.close(); close(); } }); it('should reject while closing', async () => { const {endpoint, close, server} = await createServer(); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await cli.connect(); const [call, closed, connected] = await Promise.allSettled([ cli.call('Sleep', {ms: 30}), cli.close({awaitPending: true}), cli.connect(), ]); assert.equal(call.status, 'fulfilled'); assert.equal(closed.status, 'fulfilled'); assert.equal(connected.status, 'rejected'); } finally { cli.close(); close(); } }); it('should do nothing if already connected', async () => { const {endpoint, close, server} = await createServer(); const cli = new RPCClient({ endpoint, identity: 'X', }); try { await assert.doesNotReject(cli.connect()); await assert.doesNotReject(cli.connect()); } finally { cli.close(); close(); } }); it('should resolve to the same result when called simultaneously', async () => { const {endpoint, close, server} = await createServer(); const cli = new RPCClient({ endpoint, identity: 'X', }); try { const c1 = cli.connect(); const c2 = cli.connect(); await c1; await c2; assert.deepEqual(c1, c2); } finally { cli.close(); close(); } }); it('should authenticate with string passwords', async () => { const password = 'hunter2'; let recPass; const {endpoint, close, server} = await createServer(); server.auth((accept, reject, handshake) => { recPass = handshake.password; accept(); }); const cli = new RPCClient({ endpoint, identity: 'X', password, }); try { await cli.connect(); assert.equal(password, recPass.toString('utf8')); } finally { cli.close(); close(); } }); it('should authenticate with binary passwords', async () => { const password = Buffer.from([ 0,1,2,3,4,5,6,7,8,9, 65,66,67,68,69, 251,252,253,254,255, ]); let recPass; const {endpoint, close, server} = await createServer(); server.auth((accept, reject, handshake) => { recPass = handshake.password; accept(); }); const cli = new RPCClient({ endpoint, identity: 'X', password, }); try { await cli.connect(); // console.log(Buffer.from(recPass, 'ascii')); assert.equal(password.toString('hex'), recPass.toString('hex')); } finally { cli.close(); close(); } }); }); describe('#close', function() { it('should pass code and reason to server', async () => { const {server, endpoint, close} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { const serverClientPromise = once(server, 'client'); await cli.connect(); const [serverClient] = await serverClientPromise; const serverClosePromise = once(serverClient, 'close'); await cli.close({code: 4001, reason: 'TEST'}); const [serverClose] = await serverClosePromise; assert.equal(serverClose.code, 4001); assert.equal(serverClose.reason, 'TEST'); } finally { close(); } }); it('should treat invalid/reserved close codes as 1000', async () => { const {server, endpoint, close} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { const testCodes = [-1000,0,1,1005,10000]; for (const testCode of testCodes) { const serverClientPromise = once(server, 'client'); await cli.connect(); const [serverClient] = await serverClientPromise; const serverClosePromise = once(serverClient, 'close'); await cli.close({code: testCode}); const [serverClose] = await serverClosePromise; assert.equal(serverClose.code, 1000); } } finally { close(); } }); it('should return the close code of the first close() call', async () => { const {endpoint, close} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { await cli.connect(); const p1 = cli.close({code: 4001, reason: 'FIRST'}); const p2 = cli.close({code: 4002, reason: 'SECOND'}); const [v1, v2] = await Promise.all([p1, p2]); assert.equal(v1.code, 4001); assert.equal(v1.reason, 'FIRST'); assert.equal(v2.code, 4001); assert.equal(v2.reason, 'FIRST'); const v3 = await cli.close({code: 4003, reason: 'THIRD'}); assert.equal(v3.code, 4001); assert.equal(v3.reason, 'FIRST'); } finally { close(); } }); it('should abort #connect if connection in progress, with code 1001', async () => { const {endpoint, close} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { const connPromise = cli.connect(); const closePromise = cli.close({code: 4001}); // 4001 should be ignored const [connResult, closeResult] = await Promise.allSettled([connPromise, closePromise]); assert.equal(connResult.status, 'rejected'); assert.equal(connResult.reason.name, 'AbortError'); assert.equal(closeResult.status, 'fulfilled'); assert.equal(closeResult.value?.code, 1001); } finally { close(); } }); it('should not throw if already closed', async () => { const {endpoint, close} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { await cli.connect(); await cli.close(); await assert.doesNotReject(cli.close()); } finally { close(); } }); it('should abort all outbound calls when {awaitPending: false}', async () => { const {endpoint, close, server} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { await cli.connect(); const [callResult, closeResult] = await Promise.allSettled([ cli.call('Sleep', {ms: 100}), cli.close({awaitPending: false}) ]); assert.equal(callResult.status, 'rejected'); assert.equal(closeResult.status, 'fulfilled'); } finally { close(); } }); it('should abort all inbound calls when {awaitPending: false}', async () => { let serverInitiatedCall = null; const {endpoint, close, server} = await createServer({}, { withClient: client => { serverInitiatedCall = client.call('Sleep', {ms: 50}); } }); const cli = new RPCClient({endpoint, identity: 'X'}); try { cli.handle('Sleep', async ({params, signal}) => { await setTimeout(params.ms, null, {signal}); }); await cli.connect(); const [callResult, closeResult] = await Promise.allSettled([ serverInitiatedCall, cli.close({awaitPending: false}) ]); assert.equal(callResult.status, 'rejected'); assert.equal(closeResult.status, 'fulfilled'); } finally { close(); } }); it('should wait for all outbound calls to settle when {awaitPending: true}', async () => { const {endpoint, close, server} = await createServer({respondWithDetailedErrors: true}); const cli = new RPCClient({endpoint, identity: 'X', callConcurrency: 2}); try { await cli.connect(); const [rejectResult, sleepResult, closeResult] = await Promise.allSettled([ cli.call('Reject', {code: 'TEST'}), cli.call('Sleep', {ms: 50}), cli.close({awaitPending: true}) ]); assert.equal(rejectResult.status, 'rejected'); assert.equal(rejectResult.reason.details.code, 'TEST'); assert.equal(sleepResult.status, 'fulfilled'); assert.equal(closeResult.status, 'fulfilled'); } finally { close(); } }); it('should wait for all inbound calls to settle when {awaitPending: true}', async () => { const echoVal = 'TEST123'; let serverInitiatedCall = null; const {endpoint, close, server} = await createServer({}, { withClient: client => { serverInitiatedCall = client.call('SlowEcho', {ms: 50, val: echoVal}); } }); const cli = new RPCClient({endpoint, identity: 'X'}); try { cli.handle('SlowEcho', async ({params}) => { await setTimeout(params.ms); return params.val; }); await cli.connect(); const [callResult, closeResult] = await Promise.allSettled([ serverInitiatedCall, setTimeout(1).then(() => cli.close({awaitPending: true})), ]); assert.equal(callResult.status, 'fulfilled'); assert.equal(callResult.value, echoVal); assert.equal(closeResult.status, 'fulfilled'); } finally { close(); } }); it('should close immediately with code 1006 when {force: true}', async () => { const {endpoint, close, server} = await createServer(); const cli = new RPCClient({endpoint, identity: 'X'}); try { await cli.connect(); cli.close({code: 4000, force: true}); const [dc] = await once(cli, 'close'); assert.equal(dc.code, 1006); } finally { close(); } }); it('should immediately reject any in-flight calls when {force: true}', async () => { let serverInitiatedCall = null; const {endpoint, close, server} = await createServer({}, { withClient: client => { serverInitiatedCall = client.call('Sleep', {ms: 5000}); } }); const cli = new RPCClient({endpoint, identity: 'X'}); cli.handle('Sleep', async ({params, signal}) => { await setTimeout(params.ms, null, {signal}); return `Waited ${params.ms}ms`; }); try { await cli.connect(); const clientInitiatedCall = cli.call('Sleep', {ms: 5000}); cli.close({code: 4000, force: true}); const dcp = once(cli, 'close'); await assert.rejects(clientInitiatedCall); await assert.rejects(serverInitiatedCall); const [dc] = await dcp; assert.equal(dc.code, 1006); } finally { close(); } }); it('should not reconnect even when {reconnect: true}', async () => { const {endpoint, close, server} = await createServer({ protocols: ['a'], }); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['a'], reconnect: true, maxReconnects: Infinity, }); let connectCount = 0; cli.on('connecting', () => { connectCount++; }); try { await cli.connect(); const dc = await cli.close({code: 4000}); assert.equal(dc.code, 4000); assert.equal(connectCount, 1); } finally { close(); } }); }); describe('#call', function() { it("should reject with 'RPCError' after invalid payload with client strictMode", async () => { const {endpoint, close, server} = await createServer({ protocols: ['echo1.0'] }); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['echo1.0'], strictModeValidators: [getEchoValidator()], strictMode: true, }); try { await cli.connect(); const [c1, c2, c3] = await Promise.allSettled([ cli.call('Echo', {val: '123'}), cli.call('Echo', {val: 123}), cli.call('Unknown'), ]); assert.equal(c1.status, 'fulfilled'); assert.equal(c1.value.val, '123'); assert.equal(c2.status, 'rejected'); assert.ok(c2.reason instanceof RPCTypeConstraintViolationError); assert.equal(c2.reason.rpcErrorCode, 'TypeConstraintViolation'); assert.equal(c3.status, 'rejected'); assert.ok(c3.reason instanceof RPCProtocolError); assert.equal(c3.reason.rpcErrorCode, 'ProtocolError'); } finally { await cli.close(); close(); } }); it("should reject with 'RPCError' after invalid payload with server strictMode", async () => { const {endpoint, close, server} = await createServer({ protocols: ['ocpp1.6'], strictMode: true, }, {withClient: cli => { cli.handle(() => {}); }}); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['ocpp1.6'], }); try { await cli.connect(); const [c1, c2, c3] = await Promise.allSettled([ cli.call('UpdateFirmware', {}), cli.call('Heartbeat', {a:123}), cli.call('UpdateFirmware', {location: "a", retrieveDate: "a"}), ]); assert.equal(c1.status, 'rejected'); assert.equal(c1.reason.rpcErrorCode, 'OccurrenceConstraintViolation'); assert.ok(c1.reason instanceof RPCOccurrenceConstraintViolationError); assert.equal(c2.status, 'rejected'); assert.equal(c2.reason.rpcErrorCode, 'PropertyConstraintViolation'); assert.ok(c2.reason instanceof RPCPropertyConstraintViolationError); assert.equal(c3.status, 'rejected'); assert.equal(c3.reason.rpcErrorCode, 'FormationViolation'); assert.ok(c3.reason instanceof RPCFormationViolationError); } finally { await cli.close(); close(); } }); it("should reject with 'RPCProtocolError' after invalid response with strictMode", async () => { const {endpoint, close, server} = await createServer({ protocols: ['echo1.0'] }, {withClient: cli => { cli.handle('Echo', async ({params}) => { switch (params.val) { case '1': return {bad: true}; case '2': return 123; case '3': return [1,2,3]; case '4': return null; case '5': return {val: params.val}; } }); }}); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['echo1.0'], strictModeValidators: [getEchoValidator()], strictMode: true, }); try { await cli.connect(); const [c1, c2, c3, c4, c5] = await Promise.allSettled([ cli.call('Echo', {val: '1'}), cli.call('Echo', {val: '2'}), cli.call('Echo', {val: '3'}), cli.call('Echo', {val: '4'}), cli.call('Echo', {val: '5'}), ]); assert.equal(c1.status, 'rejected'); assert.ok(c1.reason instanceof RPCOccurenceConstraintViolationError); assert.equal(c2.status, 'rejected'); assert.ok(c2.reason instanceof RPCTypeConstraintViolationError); assert.equal(c3.status, 'rejected'); assert.ok(c3.reason instanceof RPCTypeConstraintViolationError); assert.equal(c4.status, 'rejected'); assert.ok(c4.reason instanceof RPCTypeConstraintViolationError); assert.equal(c5.status, 'fulfilled'); assert.equal(c5.value.val, '5'); } finally { await cli.close(); close(); } }); it("should emit 'strictValidationFailure' when incoming call is rejected by strictMode", async () => { const {endpoint, close, server} = await createServer({ protocols: ['echo1.0'] }, {withClient: async (cli) => { await cli.call('Echo', {bad: true}).catch(()=>{}); await cli.call('Echo').catch(()=>{}); await cli.call('Unknown').catch(()=>{}); }}); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['echo1.0'], strictModeValidators: [getEchoValidator()], strictMode: true, }); try { let uks = 0; cli.handle('Echo', ({params}) => params); cli.handle('Unknown', () => ++uks); await cli.connect(); let calls = 0; let responses = 0; cli.on('call', () => calls++); cli.on('response', () => responses++); const [c1] = await once(cli, 'strictValidationFailure'); const [c2] = await once(cli, 'strictValidationFailure'); const [c3] = await once(cli, 'strictValidationFailure'); assert.equal(c1.error.rpcErrorCode, 'OccurenceConstraintViolation'); assert.equal(c2.error.rpcErrorCode, 'TypeConstraintViolation'); assert.equal(c3.error.rpcErrorCode, 'ProtocolError'); assert.equal(calls, 3); assert.equal(responses, 3); assert.equal(uks, 0); // 'Unknown' handler should not be called } finally { await cli.close(); close(); } }); it("should emit 'strictValidationFailure' when outgoing response is discarded by strictMode", async () => { const {endpoint, close, server} = await createServer({ protocols: ['echo1.0'] }, {withClient: cli => { cli.handle('Echo', async ({params}) => { switch (params.val) { case '1': return {bad: true}; case '2': return 123; case '3': return [1,2,3]; case '4': return null; case '5': return {val: params.val}; } }); }}); const cli = new RPCClient({ endpoint, identity: 'X', protocols: ['echo1.0'], strictModeValidators: [getEchoValidator()], strictMode: true, }); try { await cli.connect(); const [c1, c2, c3, c4, c5] = await Promise.allSettled([ cli.call('Echo', {val: '1'}), cli.call('Echo', {val: '2'}), cli.call('Echo', {val: '3'}), cli.call('Echo', {val: '4'}), cli.call('Echo', {val: '5'}), ]); assert.equal(c1.status, 'rejected'); assert.ok(c1.reason instanceof RPCOccurenceConstraintViolationError); assert.equal(c2.status, 'rejected'); assert.ok(c2.reason instanceof RPCTypeConstraintViolationError); assert.equal(c3.status, 'rejected'); assert.ok(c3.reason instanceof RPCTypeConstraintViolationError); assert.equ