498 lines
16 KiB
TypeScript
498 lines
16 KiB
TypeScript
|
|
/**
|
||
|
|
* Layer 4: RPC Unit Tests - Internal Message Parsing
|
||
|
|
*
|
||
|
|
* Tests for the internal message format parsers and normalizers.
|
||
|
|
* These form the foundation of the RPC layer.
|
||
|
|
*/
|
||
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import {
|
||
|
|
parseInternalRequest,
|
||
|
|
parseInternalReply,
|
||
|
|
normalizeInternalReply,
|
||
|
|
isJsonNullPayload,
|
||
|
|
type InternalRequest,
|
||
|
|
type InternalReply,
|
||
|
|
} from '@open-matrix/federation/runtime/internal_messages';
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// parseInternalRequest Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('parseInternalRequest: valid mxenv1 request', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'corr-123',
|
||
|
|
replyTo: '_local.replies.emits.corr-123',
|
||
|
|
params: { key: 'value' },
|
||
|
|
_mx: { fromRealm: 'flowpad', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = parseInternalRequest(json);
|
||
|
|
|
||
|
|
assert.ok(result !== null);
|
||
|
|
assert.equal(result.correlationId, 'corr-123');
|
||
|
|
assert.equal(result.replyTo, '_local.replies.emits.corr-123');
|
||
|
|
assert.deepEqual(result.params, { key: 'value' });
|
||
|
|
assert.equal(result._mx.fromRealm, 'flowpad');
|
||
|
|
assert.equal(result._mx.incomingProfile, 'mxenv1');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: valid matrix3 request', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'corr-456',
|
||
|
|
replyTo: 'flowpad._local.replies.emits.corr-456',
|
||
|
|
params: [1, 2, 3],
|
||
|
|
_mx: { fromRealm: 'daemon@matrix-3', incomingProfile: 'matrix3' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = parseInternalRequest(json);
|
||
|
|
|
||
|
|
assert.ok(result !== null);
|
||
|
|
assert.equal(result.correlationId, 'corr-456');
|
||
|
|
assert.equal(result._mx.fromRealm, 'daemon@matrix-3');
|
||
|
|
assert.equal(result._mx.incomingProfile, 'matrix3');
|
||
|
|
assert.deepEqual(result.params, [1, 2, 3]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: params can be any JSON value', () => {
|
||
|
|
// null params
|
||
|
|
const json1 = JSON.stringify({
|
||
|
|
correlationId: 'c1',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: null,
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
assert.ok(parseInternalRequest(json1) !== null);
|
||
|
|
|
||
|
|
// string params
|
||
|
|
const json2 = JSON.stringify({
|
||
|
|
correlationId: 'c2',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: 'just a string',
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
const result2 = parseInternalRequest(json2);
|
||
|
|
assert.ok(result2 !== null);
|
||
|
|
assert.equal(result2.params, 'just a string');
|
||
|
|
|
||
|
|
// number params
|
||
|
|
const json3 = JSON.stringify({
|
||
|
|
correlationId: 'c3',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: 42,
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
const result3 = parseInternalRequest(json3);
|
||
|
|
assert.ok(result3 !== null);
|
||
|
|
assert.equal(result3.params, 42);
|
||
|
|
|
||
|
|
// boolean params
|
||
|
|
const json4 = JSON.stringify({
|
||
|
|
correlationId: 'c4',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: false,
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
const result4 = parseInternalRequest(json4);
|
||
|
|
assert.ok(result4 !== null);
|
||
|
|
assert.equal(result4.params, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects invalid JSON', () => {
|
||
|
|
assert.equal(parseInternalRequest('not json'), null);
|
||
|
|
assert.equal(parseInternalRequest('{malformed'), null);
|
||
|
|
assert.equal(parseInternalRequest(''), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects non-object JSON', () => {
|
||
|
|
assert.equal(parseInternalRequest('"string"'), null);
|
||
|
|
assert.equal(parseInternalRequest('123'), null);
|
||
|
|
assert.equal(parseInternalRequest('null'), null);
|
||
|
|
assert.equal(parseInternalRequest('true'), null);
|
||
|
|
assert.equal(parseInternalRequest('[]'), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects missing correlationId', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: {},
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects non-string correlationId', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 123,
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: {},
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects missing replyTo', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'c',
|
||
|
|
params: {},
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects non-string replyTo', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'c',
|
||
|
|
replyTo: { url: 'topic' },
|
||
|
|
params: {},
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects missing _mx', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'c',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: {},
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects non-object _mx', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'c',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: {},
|
||
|
|
_mx: 'not an object',
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects missing _mx.fromRealm', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'c',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: {},
|
||
|
|
_mx: { incomingProfile: 'mxenv1' },
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalRequest: rejects invalid _mx.incomingProfile', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'c',
|
||
|
|
replyTo: 'reply/topic',
|
||
|
|
params: {},
|
||
|
|
_mx: { fromRealm: 'r', incomingProfile: 'invalid' },
|
||
|
|
});
|
||
|
|
assert.equal(parseInternalRequest(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// parseInternalReply Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('parseInternalReply: valid success reply', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'corr-123',
|
||
|
|
ok: true,
|
||
|
|
body: { data: 'result' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = parseInternalReply(json);
|
||
|
|
|
||
|
|
assert.ok(result !== null);
|
||
|
|
assert.equal(result.correlationId, 'corr-123');
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
assert.deepEqual(result.body, { data: 'result' });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: valid error reply', () => {
|
||
|
|
const json = JSON.stringify({
|
||
|
|
correlationId: 'corr-456',
|
||
|
|
ok: false,
|
||
|
|
body: null,
|
||
|
|
error: { code: 'MX.TIMEOUT', message: 'Request timed out' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = parseInternalReply(json);
|
||
|
|
|
||
|
|
assert.ok(result !== null);
|
||
|
|
assert.equal(result.correlationId, 'corr-456');
|
||
|
|
assert.equal(result.ok, false);
|
||
|
|
assert.deepEqual(result.error, { code: 'MX.TIMEOUT', message: 'Request timed out' });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: rejects invalid JSON', () => {
|
||
|
|
assert.equal(parseInternalReply('not json'), null);
|
||
|
|
assert.equal(parseInternalReply('{malformed'), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: rejects non-object JSON', () => {
|
||
|
|
assert.equal(parseInternalReply('"string"'), null);
|
||
|
|
assert.equal(parseInternalReply('123'), null);
|
||
|
|
assert.equal(parseInternalReply('null'), null);
|
||
|
|
assert.equal(parseInternalReply('[]'), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: rejects missing correlationId', () => {
|
||
|
|
const json = JSON.stringify({ ok: true, body: {} });
|
||
|
|
assert.equal(parseInternalReply(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: rejects non-string correlationId', () => {
|
||
|
|
const json = JSON.stringify({ correlationId: 123, ok: true, body: {} });
|
||
|
|
assert.equal(parseInternalReply(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: rejects missing ok', () => {
|
||
|
|
const json = JSON.stringify({ correlationId: 'c', body: {} });
|
||
|
|
assert.equal(parseInternalReply(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('parseInternalReply: rejects non-boolean ok', () => {
|
||
|
|
const json = JSON.stringify({ correlationId: 'c', ok: 'true', body: {} });
|
||
|
|
assert.equal(parseInternalReply(json), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// normalizeInternalReply Tests (Tolerant Reader)
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('normalizeInternalReply: full InternalReply format', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
correlationId: 'corr-full',
|
||
|
|
ok: true,
|
||
|
|
body: { value: 42 },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fallback');
|
||
|
|
|
||
|
|
assert.equal(result.correlationId, 'corr-full'); // Uses actual, not fallback
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
assert.deepEqual(result.body, { value: 42 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: semi reply with ok:true and body', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
body: { semi: 'reply' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fallback-corr');
|
||
|
|
|
||
|
|
assert.equal(result.correlationId, 'fallback-corr'); // Uses fallback
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
assert.deepEqual(result.body, { semi: 'reply' });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: semi reply with ok:false and error', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
ok: false,
|
||
|
|
error: { code: 'ERR', message: 'Something failed' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fallback-corr');
|
||
|
|
|
||
|
|
assert.equal(result.ok, false);
|
||
|
|
// Implementation includes details: undefined when not provided
|
||
|
|
assert.deepEqual(result.error, { code: 'ERR', message: 'Something failed', details: undefined });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: simple result object { result: ... }', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
result: [1, 2, 3],
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fallback-corr');
|
||
|
|
|
||
|
|
assert.equal(result.correlationId, 'fallback-corr');
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
assert.deepEqual(result.body, [1, 2, 3]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: when no ok field, result field triggers success path', () => {
|
||
|
|
// When ok is NOT present but result IS present, the implementation
|
||
|
|
// uses the dedicated 'result' handler path which returns result directly
|
||
|
|
// (doesn't go through pickBody which would prefer body)
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
result: 'from-result',
|
||
|
|
body: 'from-body', // This is ignored when no 'ok' field
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fb');
|
||
|
|
|
||
|
|
// Per implementation: if (!'ok' in parsed && 'result' in parsed) => body = result
|
||
|
|
assert.equal(result.body, 'from-result');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: arbitrary object is treated as successful body', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
foo: 'bar',
|
||
|
|
num: 123,
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fallback');
|
||
|
|
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
assert.deepEqual(result.body, { foo: 'bar', num: 123 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: object with error shape but no ok:false is still success', () => {
|
||
|
|
// Per the implementation comment: "we do NOT infer failure without ok=false"
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
error: { code: 'X', message: 'Y' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fb');
|
||
|
|
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
// The whole object becomes the body since no ok field
|
||
|
|
assert.deepEqual(result.body, { error: { code: 'X', message: 'Y' } });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: non-object JSON values', () => {
|
||
|
|
// Array
|
||
|
|
const r1 = normalizeInternalReply('[1,2,3]', 'fb');
|
||
|
|
assert.equal(r1.ok, true);
|
||
|
|
assert.deepEqual(r1.body, [1, 2, 3]);
|
||
|
|
|
||
|
|
// Number
|
||
|
|
const r2 = normalizeInternalReply('42', 'fb');
|
||
|
|
assert.equal(r2.ok, true);
|
||
|
|
assert.equal(r2.body, 42);
|
||
|
|
|
||
|
|
// String
|
||
|
|
const r3 = normalizeInternalReply('"hello"', 'fb');
|
||
|
|
assert.equal(r3.ok, true);
|
||
|
|
assert.equal(r3.body, 'hello');
|
||
|
|
|
||
|
|
// Boolean
|
||
|
|
const r4 = normalizeInternalReply('true', 'fb');
|
||
|
|
assert.equal(r4.ok, true);
|
||
|
|
assert.equal(r4.body, true);
|
||
|
|
|
||
|
|
// JSON null
|
||
|
|
const r5 = normalizeInternalReply('null', 'fb');
|
||
|
|
assert.equal(r5.ok, true);
|
||
|
|
assert.equal(r5.body, null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: invalid JSON treated as raw string body', () => {
|
||
|
|
const result = normalizeInternalReply('not valid json at all', 'fb');
|
||
|
|
|
||
|
|
assert.equal(result.correlationId, 'fb');
|
||
|
|
assert.equal(result.ok, true);
|
||
|
|
assert.equal(result.body, 'not valid json at all');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: error details are preserved', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
ok: false,
|
||
|
|
error: {
|
||
|
|
code: 'MX.AUTH.FORBIDDEN',
|
||
|
|
message: 'Access denied',
|
||
|
|
details: { requiredScope: 'admin', actualScope: 'user' },
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fb');
|
||
|
|
|
||
|
|
assert.equal(result.ok, false);
|
||
|
|
assert.deepEqual(result.error, {
|
||
|
|
code: 'MX.AUTH.FORBIDDEN',
|
||
|
|
message: 'Access denied',
|
||
|
|
details: { requiredScope: 'admin', actualScope: 'user' },
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('normalizeInternalReply: incomplete error (missing code/message) is not picked', () => {
|
||
|
|
const raw = JSON.stringify({
|
||
|
|
ok: false,
|
||
|
|
error: { details: 'no code or message' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = normalizeInternalReply(raw, 'fb');
|
||
|
|
|
||
|
|
assert.equal(result.ok, false);
|
||
|
|
assert.equal(result.error, undefined); // Error not picked due to missing required fields
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// isJsonNullPayload Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('isJsonNullPayload: detects JSON null', () => {
|
||
|
|
assert.equal(isJsonNullPayload('null'), true);
|
||
|
|
assert.equal(isJsonNullPayload(' null '), true); // With whitespace
|
||
|
|
assert.equal(isJsonNullPayload('\tnull\n'), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isJsonNullPayload: rejects non-null values', () => {
|
||
|
|
assert.equal(isJsonNullPayload('true'), false);
|
||
|
|
assert.equal(isJsonNullPayload('false'), false);
|
||
|
|
assert.equal(isJsonNullPayload('0'), false);
|
||
|
|
assert.equal(isJsonNullPayload('""'), false);
|
||
|
|
assert.equal(isJsonNullPayload('{}'), false);
|
||
|
|
assert.equal(isJsonNullPayload('[]'), false);
|
||
|
|
assert.equal(isJsonNullPayload('"null"'), false); // String "null", not JSON null
|
||
|
|
assert.equal(isJsonNullPayload('undefined'), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Protocol Compliance Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('protocol: InternalRequest must include _mx for routing metadata', () => {
|
||
|
|
// This test verifies the protocol requirement that all internal requests
|
||
|
|
// must carry routing metadata via the _mx field
|
||
|
|
const validRequest: InternalRequest = {
|
||
|
|
correlationId: 'test-123',
|
||
|
|
replyTo: '_local.replies.emits.test-123',
|
||
|
|
params: { action: 'test' },
|
||
|
|
_mx: {
|
||
|
|
fromRealm: 'source-realm',
|
||
|
|
incomingProfile: 'mxenv1',
|
||
|
|
msgId: 'optional-msg-id',
|
||
|
|
traceId: 'optional-trace-id',
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const json = JSON.stringify(validRequest);
|
||
|
|
const parsed = parseInternalRequest(json);
|
||
|
|
|
||
|
|
assert.ok(parsed !== null);
|
||
|
|
assert.equal(parsed._mx.fromRealm, 'source-realm');
|
||
|
|
assert.equal(parsed._mx.incomingProfile, 'mxenv1');
|
||
|
|
assert.equal(parsed._mx.msgId, 'optional-msg-id');
|
||
|
|
assert.equal(parsed._mx.traceId, 'optional-trace-id');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('protocol: InternalReply error shape follows MATRIX-PROTOCOL-6', () => {
|
||
|
|
// Per MATRIX-PROTOCOL-6 Section 52, errors must have code and message
|
||
|
|
const errorReply: InternalReply = {
|
||
|
|
correlationId: 'err-001',
|
||
|
|
ok: false,
|
||
|
|
body: null,
|
||
|
|
error: {
|
||
|
|
code: 'MX.TIMEOUT',
|
||
|
|
message: 'Request timed out after 5000ms',
|
||
|
|
details: { timeoutMs: 5000, target: 'realm/component.accepts.method' },
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const json = JSON.stringify(errorReply);
|
||
|
|
const parsed = parseInternalReply(json);
|
||
|
|
|
||
|
|
assert.ok(parsed !== null);
|
||
|
|
assert.equal(parsed.ok, false);
|
||
|
|
assert.ok(parsed.error !== undefined);
|
||
|
|
|
||
|
|
// Verify normalized version also preserves error structure
|
||
|
|
const normalized = normalizeInternalReply(json, 'fb');
|
||
|
|
assert.equal(normalized.ok, false);
|
||
|
|
assert.equal(normalized.error?.code, 'MX.TIMEOUT');
|
||
|
|
assert.equal(normalized.error?.message, 'Request timed out after 5000ms');
|
||
|
|
});
|