Skip to content
markpaper

src/rest/rest.test.ts

v0.1.0 · 4.2 KB

Download file
import { describe, expect, it } from 'vitest';
import { FIXTURE_ACCOUNT, FIXTURE_KEYS } from '../testing/fixtures.js';
import {
  computeClockSkew,
  createRestClient,
  decodeQfexOrderbook,
  isCdnCached,
  isCdnEdgeRejection,
  parseRetryAfterMs,
} from './index.js';

const response = (status: number, text: string, headers: Record<string, string> = {}) => ({
  status,
  headers: { get: (key: string) => headers[key] ?? null },
  text: async () => text,
});
function client(sequence: Array<ReturnType<typeof response> | Error>) {
  let calls = 0;
  let now = 1_820_000_000_000;
  const rest = createRestClient({
    baseUrl: 'mainnet',
    fetch: async () => {
      calls++;
      const next = sequence.shift();
      if (next instanceof Error) throw next;
      if (!next) throw new Error('No response');
      return next;
    },
    keys: FIXTURE_KEYS,
    restMaxRps: 9,
    rateLimitDefaultBackoffMs: 370,
    rateBackoff: { minMs: 10, maxMs: 8000 },
    retryDelayMs: () => 0,
    readTries: 2,
    timeoutMs: 90,
    maxWaitMs: 5000,
    skewHistoryLimit: 7,
    readOnly: false,
    now: () => now,
    sleep: async (ms) => {
      now += ms;
    },
  });
  return { rest, calls: () => calls };
}
describe('REST retries and exact reads', () => {
  it('retries reads but keeps uncertain writes distinct and unretried', async () => {
    const read = client([response(503, 'Unavailable'), response(200, '{"value":9007199254741009}')]);
    expect(await read.rest.publicGet('/refdata')).toEqual({ value: '9007199254741009' });
    expect(read.calls()).toBe(2);
    const write = client([response(503, 'Unavailable'), response(200, '{}')]);
    await expect(
      write.rest.userPost('/user/leverage', FIXTURE_ACCOUNT, { symbol: 'BTC-USD', leverage: 8 }),
    ).rejects.toMatchObject({ kind: 'server', outcomeUnknown: true });
    expect(write.calls()).toBe(1);
  });
  it('refreshes a nonce once for signature failure, not forbidden keys', async () => {
    const test = client([response(401, 'invalid signature'), response(200, '{}')]);
    await test.rest.userGet('/user/positions', FIXTURE_ACCOUNT);
    expect(test.calls()).toBe(2);
    const rejected = client([response(403, 'invalid credentials')]);
    await expect(rejected.rest.userGet('/user/positions', FIXTURE_ACCOUNT)).rejects.toMatchObject({ kind: 'auth' });
    expect(rejected.calls()).toBe(1);
  });
  it('does not treat edge HTML as a revoked key or signed cached data as fresh', async () => {
    expect(isCdnEdgeRejection('text/html', 'Forbidden')).toBe(true);
    expect(isCdnCached('Hit from cloudfront', '0')).toBe(true);
    const test = client([response(200, '{}', { age: '9' }), response(200, '{}', { age: '0' })]);
    await test.rest.userGet('/user/positions', FIXTURE_ACCOUNT);
    expect(test.calls()).toBe(2);
  });
  it('parses retry-after units and captures a skew interval', () => {
    expect(parseRetryAfterMs(null, 'retry after 350ms', 0, 900, { minMs: 10, maxMs: 8000 })).toBe(350);
    expect(parseRetryAfterMs('2', null, 0, 900, { minMs: 10, maxMs: 8000 })).toBe(2000);
    const skew = computeClockSkew('Tue, 01 Jan 2030 00:00:00 GMT', 1_893_456_000_030, 1_893_456_000_170)!;
    expect(skew.loMs).toBe(-170);
    expect(skew.hiMs).toBe(970);
  });
  it('masks configured secrets in structured problems and error bodies', async () => {
    const test = client([
      response(
        400,
        JSON.stringify({ detail: 'Rejected ' + FIXTURE_KEYS.secret, errors: [{ message: FIXTURE_KEYS.secret }] }),
      ),
    ]);
    try {
      await test.rest.userGet('/user/positions', FIXTURE_ACCOUNT);
      throw new Error('Expected request rejection');
    } catch (error) {
      expect(String(error)).not.toContain(FIXTURE_KEYS.secret);
      expect(JSON.stringify(error)).not.toContain(FIXTURE_KEYS.secret);
    }
  });
  it('drops zero padding, sorts the book, and preserves exact literals', () => {
    const book = decodeQfexOrderbook(
      {
        ticker_id: 'BTC-USD',
        timestamp: '99',
        bids: [
          ['120.25', '0'],
          ['119.5', '0.38'],
        ],
        asks: [
          ['122', '0.4'],
          ['121.5', '0.6'],
        ],
      },
      'BTC-USD',
    );
    expect(book.zeroLevelsDropped).toBe(1);
    expect(book.asks[0]?.px).toBe('121.5');
  });
});
All files