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
JavaScript
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