src/transport/errors.test.ts
v0.2.0 · 6.4 KB
import { describe, expect, it } from 'vitest';
import { parseEnvelope, unwrapEnvelope } from './envelope.js';
import {
isRateLimitError,
isTransientError,
MAX_ERROR_BODY_CHARS,
NadoEnvelopeError,
NadoHttpError,
NadoNetworkError,
NadoNetworkMismatchError,
NadoRejection,
NadoResponseParseError,
NadoTimeoutError,
statusOf,
} from './errors.js';
const URL_ = 'https://gateway.example/v1/query';
describe('parseEnvelope / unwrapEnvelope', () => {
it('unwraps success and failure envelopes (live shapes)', () => {
expect(
parseEnvelope({ status: 'success', data: { digest: '0xabc' }, request_type: 'execute_place_order' }),
).toEqual({
ok: true,
data: { digest: '0xabc' },
requestType: 'execute_place_order',
});
expect(
parseEnvelope(
{
status: 'failure',
error: 'Market is in post-only mode. Only post-only orders are accepted',
error_code: 2117,
},
'place_order',
),
).toEqual({
ok: false,
code: 2117,
error: 'Market is in post-only mode. Only post-only orders are accepted',
requestType: 'place_order',
});
expect(parseEnvelope({ status: 'failure' })).toMatchObject({ ok: false, code: -1, error: 'unknown error' });
expect(
parseEnvelope({ status: 'failure', error_code: '2069', error: 'Trading is blocked for this market' }),
).toMatchObject({ code: 2069 });
});
it('throws NadoEnvelopeError on anything that is not an envelope', () => {
for (const body of [null, 'ok', 42, [], { data: 1 }, { status: 'weird' }]) {
expect(() => parseEnvelope(body)).toThrow(NadoEnvelopeError);
}
});
it('unwrapEnvelope returns data or throws a NadoRejection with code, text, path', () => {
expect(unwrapEnvelope({ status: 'success', data: 7 }, 'query')).toBe(7);
try {
unwrapEnvelope({ status: 'failure', error: 'no', error_code: 2067, request_type: 'place_order' }, 'execute');
expect.unreachable();
} catch (e) {
expect(e).toBeInstanceOf(NadoRejection);
const r = e as NadoRejection;
expect(r.code).toBe(2067);
expect(r.errorText).toBe('no');
expect(r.path).toBe('execute');
expect(r.requestType).toBe('place_order');
expect(r.message).toContain('2067');
}
});
});
describe('error classes', () => {
it('carry status, truncate bodies and keep causes', () => {
const http = new NadoHttpError({
status: 502,
bodyText: 'x'.repeat(MAX_ERROR_BODY_CHARS + 10),
url: URL_,
requestType: 'symbols',
});
expect(http.status).toBe(502);
expect(http.bodyText.length).toBe(MAX_ERROR_BODY_CHARS + 3);
expect(http.message).toContain('502');
const parse = new NadoResponseParseError({
status: 200,
bodyText: 'not json',
url: URL_,
cause: new SyntaxError('x'),
});
expect(parse.transientResponseBody).toBe(true);
expect(parse.cause).toBeInstanceOf(SyntaxError);
const timeout = new NadoTimeoutError({ timeoutMs: 10_000, url: URL_, requestType: 'orders' });
expect(timeout.message).toContain('10000');
const net = new NadoNetworkError({ url: URL_, cause: new Error('ECONNRESET') });
expect(net.message).toContain('ECONNRESET');
const mismatch = new NadoNetworkMismatchError({ expectedChainId: 57073, reportedChainId: 763373 });
expect(mismatch.message).toContain('763373');
expect(mismatch.expectedChainId).toBe(57073);
const env = new NadoEnvelopeError({ body: { a: 1 }, requestType: 'x' });
expect(env.message).toContain('{"a":1}');
});
});
describe('statusOf / isRateLimitError', () => {
it('reads numeric statuses and HTTP nnn in messages', () => {
expect(statusOf(new NadoHttpError({ status: 429, url: URL_ }))).toBe(429);
expect(statusOf({ statusCode: 503 })).toBe(503);
expect(statusOf(new Error('nado orders HTTP 502'))).toBe(502);
expect(statusOf('x')).toBeUndefined();
expect(statusOf(null)).toBeUndefined();
});
it('429 is a rate limit; a rejection whose text contains 429 is not', () => {
expect(isRateLimitError(new NadoHttpError({ status: 429, url: URL_ }))).toBe(true);
expect(isRateLimitError(new Error('too many requests'))).toBe(true);
expect(isRateLimitError(new Error('rate limit'))).toBe(true);
expect(isRateLimitError(new Error('oid 14290 rejected'))).toBe(false);
expect(isRateLimitError(new NadoRejection({ code: 2117, error: 'px 429.5 post only', path: 'execute' }))).toBe(
false,
);
expect(isRateLimitError(new NadoHttpError({ status: 400, bodyText: '429', url: URL_ }))).toBe(false);
});
});
describe('isTransientError', () => {
it('true for 429, 408, 5xx, timeout, network, parse and envelope errors', () => {
expect(isTransientError(new NadoHttpError({ status: 429, url: URL_ }))).toBe(true);
expect(isTransientError(new NadoHttpError({ status: 408, url: URL_ }))).toBe(true);
expect(isTransientError(new NadoHttpError({ status: 503, url: URL_ }))).toBe(true);
expect(isTransientError(new NadoTimeoutError({ timeoutMs: 1, url: URL_ }))).toBe(true);
expect(isTransientError(new NadoNetworkError({ url: URL_, cause: 'x' }))).toBe(true);
expect(isTransientError(new NadoResponseParseError({ status: 200, bodyText: '', url: URL_, cause: 1 }))).toBe(true);
expect(isTransientError(new NadoEnvelopeError({ body: 'plain text' }))).toBe(true);
expect(isTransientError(new Error('fetch failed'))).toBe(true);
expect(isTransientError(new Error('wrapped', { cause: new Error('ECONNRESET') }))).toBe(true);
expect(isTransientError({ name: 'TimeoutError' })).toBe(true);
expect(isTransientError({ status: 200, message: 'html' })).toBe(true);
expect(isTransientError('socket hang up')).toBe(true);
});
it('false for rejections, other 4xx and aborts', () => {
expect(isTransientError(new NadoRejection({ code: 2117, error: 'timeout in text', path: 'execute' }))).toBe(false);
expect(isTransientError(new NadoHttpError({ status: 403, url: URL_ }))).toBe(false);
expect(isTransientError(new NadoHttpError({ status: 400, url: URL_ }))).toBe(false);
expect(isTransientError({ name: 'AbortError', message: 'aborted' })).toBe(false);
expect(isTransientError(new Error('bad address'))).toBe(false);
expect(isTransientError(null)).toBe(false);
expect(
isTransientError(new Error('outer', { cause: new NadoRejection({ code: 1, error: 'network', path: 'query' }) })),
).toBe(false);
});
});