ts / test / http.test.ts
1,232 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createClient } from '../src/client.js';
import { isApiError, isTransportError } from '../src/errors.js';
import { buildRequest, paginate, paginateOffset, queryString, request, uploadBytes } from '../src/http.js';
import { bodyJson, captureClient, caught, fakeFetch } from './helpers.js';

const keyed = createClient({ apiKey: 'ird_test-key' });
const anon = createClient();

test('buildRequest', async (t) => {
  await t.test('GET carries bearer auth and accept header, no body', () => {
    const req = buildRequest(keyed, 'get', '/vps/tiers');
    assert.equal(req.method, 'GET');
    assert.equal(req.url, 'https://api.ird.ro/vps/tiers');
    assert.equal(req.headers.Authorization, 'Bearer ird_test-key');
    assert.equal(req.headers.Accept, 'application/json');
    assert.equal(req.body, undefined);
  });
  await t.test('a missing api key sends no Authorization header', () => {
    const req = buildRequest(anon, 'get', '/domains/check', { query: { domain: 'example.ro' } });
    assert.equal(req.headers.Authorization, undefined);
    assert.equal(req.url, 'https://api.ird.ro/domains/check?domain=example.ro');
  });
  await t.test('query params are encoded with %20 and undefined/null skipped', () => {
    assert.equal(queryString({ q: 'web hosting', count: 5, skip: undefined, gone: null }), '?q=web%20hosting&count=5');
    assert.equal(queryString({ status: 'pinned' }), '?status=pinned');
    assert.equal(queryString({}), '');
    assert.equal(queryString(undefined), '');
    assert.equal(queryString({ s: 'a+b/c' }), '?s=a%2Bb%2Fc');
  });
  await t.test('client headers merge into the request without clobbering the computed ones', () => {
    const client = createClient({ apiKey: 'k', headers: { 'User-Agent': 'my-app' }, timeoutMs: 30000 });
    const req = buildRequest(client, 'post', '/jobs', { body: { type: 'embedding', input: 'hi' } });
    assert.equal(req.headers['User-Agent'], 'my-app');
    assert.equal(req.headers.Authorization, 'Bearer k');
    assert.equal(req.headers['Content-Type'], 'application/json');
    assert.equal(client.timeoutMs, 30000);
  });
  await t.test('POST serialises a JSON body', () => {
    const req = buildRequest(keyed, 'post', '/jobs', { body: { type: 'embedding', input: 'hi' } });
    assert.equal(req.headers['Content-Type'], 'application/json');
    assert.deepEqual(JSON.parse(req.body as string), { type: 'embedding', input: 'hi' });
  });
  await t.test('DELETE can carry a JSON body', () => {
    const req = buildRequest(keyed, 'delete', '/dns/domains/{domain}/records', {
      params: { domain: 'x.ro' },
      body: { name: 'www', type: 'A' },
    });
    assert.equal(req.method, 'DELETE');
    assert.equal(req.url, 'https://api.ird.ro/dns/domains/x.ro/records');
    assert.equal(req.headers['Content-Type'], 'application/json');
    assert.equal((JSON.parse(req.body as string) as { name: string }).name, 'www');
  });
  await t.test('path parameters are percent-encoded as one segment', () => {
    const req = buildRequest(keyed, 'get', '/domains/directory/{domain}', { params: { domain: 'a b/c' } });
    assert.equal(req.url, 'https://api.ird.ro/domains/directory/a%20b%2Fc');
  });
  await t.test('apiUrl override loses its trailing slash', () => {
    const client = createClient({ apiKey: 'k', apiUrl: 'http://localhost:8080/' });
    assert.equal(buildRequest(client, 'get', '/credits/balance').url, 'http://localhost:8080/credits/balance');
  });
});

test('request decodes responses', async (t) => {
  await t.test('parses a JSON body keeping snake_case keys', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [{ status: 200, body: { total: '42.50', next_cursor: null } }]);
    const resp = await request(client, 'get', '/credits/balance');
    assert.equal(resp.total, '42.50', 'money stays a decimal string');
  });
  await t.test('202 accepted is a success', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [{ status: 202, body: { job: { id: 'j1', status: 'queued' } } }]);
    const resp = await request(client, 'post', '/jobs', { body: { type: 'embedding', input: 'x' } });
    assert.equal(resp.job.status, 'queued');
  });
  await t.test('an empty 2xx body decodes to {}', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [{ status: 204 }]);
    assert.deepEqual(await request(client, 'delete', '/vps/ssh-keys/{id}', { params: { id: 'k1' } }), {});
  });
  await t.test('non-2xx with the ird error envelope is an api error', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [
      { status: 402, body: { error: { code: 'insufficient_credits', message: 'Not enough credits' } } },
    ]);
    const e = await caught(() => request(client, 'post', '/domains/registrations', { body: { domain: 'x.ro', years: 1 } }));
    assert.ok(isApiError(e));
    assert.equal(e.message, 'Not enough credits');
    assert.equal(e.code, 'insufficient_credits');
    assert.equal(e.status, 402);
    assert.equal(e.name, 'IrdApiError');
    assert.ok(e instanceof Error);
  });
  await t.test('non-2xx with a non-JSON body is an api error carrying the raw text', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [{ status: 503, body: 'upstream unavailable' }]);
    const e = await caught(() => request(client, 'get', '/vps/tiers'));
    assert.ok(isApiError(e));
    assert.equal(e.status, 503);
    assert.equal(e.message, 'upstream unavailable');
    assert.equal(e.code, null);
  });
  await t.test('an empty non-2xx body is an api error too', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [{ status: 500 }]);
    const e = await caught(() => request(client, 'get', '/vps/tiers'));
    assert.ok(isApiError(e));
    assert.equal(e.message, 'Empty error response');
  });
  await t.test('2xx with a non-JSON body is a transport error', async () => {
    const { client } = captureClient({ apiKey: 'k' }, [{ status: 200, body: '<html>nope</html>' }]);
    const e = await caught(() => request(client, 'get', '/vps/tiers'));
    assert.ok(isTransportError(e));
    assert.match(e.message, /Failed to parse JSON/);
  });
  await t.test('a rejected fetch is a transport error with the cause attached', async () => {
    const client = createClient({
      apiKey: 'k',
      fetch: async () => {
        throw new TypeError('fetch failed');
      },
    });
    const e = await caught(() => request(client, 'get', '/vps/tiers'));
    assert.ok(isTransportError(e));
    assert.match(e.message, /Request failed: fetch failed/);
    assert.ok(e.cause instanceof TypeError);
  });
});

test('paginate follows next_cursor', async () => {
  const { client, calls } = captureClient({ apiKey: 'k' }, [
    { status: 200, body: { registrations: [{ id: 'r1' }, { id: 'r2' }], next_cursor: 'c2' } },
    { status: 200, body: { registrations: [{ id: 'r3' }], next_cursor: null } },
  ]);
  const items = await paginate(client, '/domains/registrations', 'registrations', { query: { limit: 2 } });
  assert.deepEqual(
    items.map((r) => r.id),
    ['r1', 'r2', 'r3'],
  );
  assert.equal(calls.length, 2);
  assert.match(calls[1]!.url, /cursor=c2/);
});

test('paginateOffset', async (t) => {
  await t.test('walks pages of limit until a short page ends it', async () => {
    const { client, calls } = captureClient({ apiKey: 'k' }, [
      { status: 200, body: { pins: [{ id: 'p1' }, { id: 'p2' }], total: 5, limit: 2, offset: 0 } },
      { status: 200, body: { pins: [{ id: 'p3' }, { id: 'p4' }], total: 5, limit: 2, offset: 2 } },
      { status: 200, body: { pins: [{ id: 'p5' }], total: 5, limit: 2, offset: 4 } },
    ]);
    const items = await paginateOffset(client, '/ipfs/domains/{id}/pins', 'pins', {
      params: { id: 'd1' },
      query: { limit: 2 },
    });
    assert.deepEqual(
      items.map((p) => p.id),
      ['p1', 'p2', 'p3', 'p4', 'p5'],
    );
    assert.deepEqual(
      calls.map((c) => c.url.slice('https://api.ird.ro'.length)),
      ['/ipfs/domains/d1/pins?limit=2&offset=0', '/ipfs/domains/d1/pins?limit=2&offset=2', '/ipfs/domains/d1/pins?limit=2&offset=4'],
    );
  });
  await t.test('an exactly-full last page needs one more (empty) request', async () => {
    const { client, calls } = captureClient({ apiKey: 'k' }, [
      { status: 200, body: { pins: [{ id: 'p1' }, { id: 'p2' }], total: 2 } },
      { status: 200, body: { pins: [], total: 2 } },
    ]);
    assert.equal((await paginateOffset(client, '/ipfs/pins', 'pins', { query: { limit: 2 } })).length, 2);
    assert.equal(calls.length, 2);
  });
  await t.test('filters ride along and the default page size is 100', async () => {
    const { client, calls } = captureClient({ apiKey: 'k' }, [{ status: 200, body: { pins: [{ id: 'p1' }] } }]);
    assert.equal((await paginateOffset(client, '/ipfs/pins', 'pins', { query: { status: 'pinned' } })).length, 1);
    assert.equal(calls[0]!.url.slice('https://api.ird.ro'.length), '/ipfs/pins?status=pinned&limit=100&offset=0');
  });
});

test('uploadBytes', async (t) => {
  await t.test('posts octet-stream with the grant token to the external URL', async () => {
    const { fetch, calls } = fakeFetch();
    await uploadBytes('https://node1.ird.ro/up/abc', 'grant-tok', new TextEncoder().encode('hello'), { fetch });
    const req = calls[0]!;
    assert.equal(req.method, 'POST');
    assert.equal(req.url, 'https://node1.ird.ro/up/abc');
    assert.equal(req.headers.Authorization, 'Bearer grant-tok');
    assert.equal(req.headers['Content-Type'], 'application/octet-stream');
    assert.ok(req.body instanceof Uint8Array);
    assert.equal(bodyJson(req), undefined);
  });
  await t.test('car format appends the query param; a null token omits auth', async () => {
    const { fetch, calls } = fakeFetch();
    await uploadBytes('https://node1.ird.ro/up/abc', null, new Uint8Array([1]), { format: 'car', fetch });
    assert.equal(calls[0]!.url, 'https://node1.ird.ro/up/abc?format=car');
    assert.equal(calls[0]!.headers.Authorization, undefined);
  });
  await t.test('an unsupported source is a transport error before any request', async () => {
    const { fetch, calls } = fakeFetch();
    const e = await caught(() => uploadBytes('https://n/u', 't', 42 as unknown as Uint8Array, { fetch }));
    assert.ok(isTransportError(e));
    assert.match(e.message, /Unsupported upload source/);
    assert.equal(calls.length, 0);
  });
});

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/ts.git