UNPKG

thenavisapp

Version:

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

755 lines (594 loc) 16.8 kB
import {Buffer} from 'node:buffer'; import test from 'ava'; import delay from 'delay'; import getStream from 'get-stream'; import got, {type Response} from '../source/index.js'; import withServer, {withBodyParsingServer} from './helpers/with-server.js'; import type {ExtendedHttpTestServer} from './helpers/create-http-test-server.js'; const thrower = (): any => { throw new Error('This should not be called'); }; const resetPagination = { paginate: undefined, transform: undefined, filter: undefined, shouldContinue: undefined, }; // eslint-disable-next-line unicorn/no-object-as-default-parameter const attachHandler = (server: ExtendedHttpTestServer, count: number, {relative} = {relative: false}): void => { server.get('/', (request, response) => { // eslint-disable-next-line unicorn/prevent-abbreviations const searchParams = new URLSearchParams(request.url.split('?')[1]); const page = Number(searchParams.get('page')) || 1; if (page < count) { response.setHeader('link', `<${relative ? '' : server.url}/?page=${page + 1}>; rel="next"`); } response.end(`[${page <= count ? page : ''}]`); }); }; test('the link header has no next value', withServer, async (t, server, got) => { const items = [1]; server.get('/', (_request, response) => { response.setHeader('link', '<https://example.com>; rel="prev"'); response.end(JSON.stringify(items)); }); const received = await got.paginate.all<number>(''); t.deepEqual(received, items); }); test('the link header is empty', withServer, async (t, server, got) => { const items = [1]; server.get('/', (_request, response) => { response.setHeader('link', ''); response.end(JSON.stringify(items)); }); const received = await got.paginate.all<number>(''); t.deepEqual(received, items); }); test('retrieves all elements', withServer, async (t, server, got) => { attachHandler(server, 2); const result = await got.paginate.all<number>(''); t.deepEqual(result, [1, 2]); }); test('retrieves all elements with JSON responseType', withServer, async (t, server, got) => { attachHandler(server, 2); const result = await got.extend({ responseType: 'json', }).paginate.all<number>(''); t.deepEqual(result, [1, 2]); }); test('points to defaults when extending Got without custom `pagination`', withServer, async (t, server, got) => { attachHandler(server, 2); const result = await got.extend().paginate.all<number>(''); t.deepEqual(result, [1, 2]); }); test('pagination options can be extended', withServer, async (t, server, got) => { attachHandler(server, 2); const result = await got.extend({ pagination: { shouldContinue: () => false, }, }).paginate.all<number>(''); t.deepEqual(result, []); }); test('filters elements', withServer, async (t, server, got) => { attachHandler(server, 3); const result = await got.paginate.all<number>({ pagination: { filter({item, currentItems, allItems}) { t.true(Array.isArray(allItems)); t.true(Array.isArray(currentItems)); return item !== 2; }, }, }); t.deepEqual(result, [1, 3]); }); test('parses elements', withServer, async (t, server, got) => { attachHandler(server, 100); const result = await got.paginate.all<number, string>('?page=100', { pagination: { transform: response => [response.body.length], }, }); t.deepEqual(result, [5]); }); test('parses elements - async function', withServer, async (t, server, got) => { attachHandler(server, 100); const result = await got.paginate.all<number, string>('?page=100', { pagination: { transform: async response => [response.body.length], }, }); t.deepEqual(result, [5]); }); test('custom paginate function', withServer, async (t, server, got) => { attachHandler(server, 3); const result = await got.paginate.all<number>({ pagination: { paginate({response}) { const url = new URL(response.url); if (url.search === '?page=3') { return false; } url.search = '?page=3'; return {url}; }, }, }); t.deepEqual(result, [1, 3]); }); test('custom paginate function using allItems', withServer, async (t, server, got) => { attachHandler(server, 3); const result = await got.paginate.all<number>({ pagination: { paginate({allItems, response}) { if (allItems.length === 2) { return false; } return { url: new URL('/?page=3', response.url), }; }, stackAllItems: true, }, }); t.deepEqual(result, [1, 3]); }); test('custom paginate function using currentItems', withServer, async (t, server, got) => { attachHandler(server, 3); const result = await got.paginate.all<number>({ pagination: { paginate({currentItems, response}) { if (currentItems[0] === 3) { return false; } return { url: new URL('/?page=3', response.url), }; }, }, }); t.deepEqual(result, [1, 3]); }); test('iterator works', withServer, async (t, server, got) => { attachHandler(server, 5); const results: number[] = []; for await (const item of got.paginate<number>('')) { results.push(item); } t.deepEqual(results, [1, 2, 3, 4, 5]); }); test('iterator works #2', withServer, async (t, server, got) => { attachHandler(server, 5); const results: number[] = []; for await (const item of got.paginate.each<number>('')) { results.push(item); } t.deepEqual(results, [1, 2, 3, 4, 5]); }); test('`shouldContinue` works', withServer, async (t, server, got) => { attachHandler(server, 2); const options = { pagination: { shouldContinue({currentItems, allItems}: {allItems: number[]; currentItems: number[]}) { t.true(Array.isArray(allItems)); t.true(Array.isArray(currentItems)); return false; }, }, }; const results: number[] = []; for await (const item of got.paginate<number>(options)) { results.push(item); } t.deepEqual(results, []); }); test('`countLimit` works', withServer, async (t, server, got) => { attachHandler(server, 2); const options = { pagination: { countLimit: 1, }, }; const results: number[] = []; for await (const item of got.paginate<number>(options)) { results.push(item); } t.deepEqual(results, [1]); }); test('throws if the `pagination` option does not have `transform` property', async t => { const iterator = got.paginate('', { pagination: {...resetPagination}, prefixUrl: 'https://example.com', }); await t.throwsAsync(iterator.next(), // {message: '`options.pagination.transform` must be implemented'} ); }); test('throws if the `pagination` option does not have `shouldContinue` property', async t => { const iterator = got.paginate('', { pagination: { ...resetPagination, transform: thrower, }, prefixUrl: 'https://example.com', }); await t.throwsAsync(iterator.next(), // {message: '`options.pagination.shouldContinue` must be implemented'} ); }); test('throws if the `pagination` option does not have `filter` property', async t => { const iterator = got.paginate('', { pagination: { ...resetPagination, transform: thrower, shouldContinue: thrower, paginate: thrower, }, prefixUrl: 'https://example.com', }); await t.throwsAsync(iterator.next(), // {message: '`options.pagination.filter` must be implemented'} ); }); test('throws if the `pagination` option does not have `paginate` property', async t => { const iterator = got.paginate('', { pagination: { ...resetPagination, transform: thrower, shouldContinue: thrower, filter: thrower, }, prefixUrl: 'https://example.com', }); await t.throwsAsync(iterator.next(), // {message: '`options.pagination.paginate` must be implemented'} ); }); test('ignores the `resolveBodyOnly` option', withServer, async (t, server, got) => { attachHandler(server, 2); const items = await got.paginate.all('', { resolveBodyOnly: true, }); t.deepEqual(items, [1, 2]); }); test('allowGetBody sends json payload with .paginate()', withBodyParsingServer, async (t, server, got) => { server.get('/', (request, response) => { if (request.body.hello !== 'world') { response.statusCode = 400; } response.end(JSON.stringify([1, 2, 3])); }); const iterator = got.paginate<number>({ allowGetBody: true, json: {hello: 'world'}, retry: { limit: 0, }, }); const results: number[] = []; for await (const item of iterator) { results.push(item); } t.deepEqual(results, [1, 2, 3]); }); test('`hooks` are not duplicated', withServer, async (t, server, got) => { let page = 1; server.get('/', (_request, response) => { response.end(JSON.stringify([page++])); }); const nopHook = () => {}; const result = await got.paginate.all<number>({ pagination: { paginate({response}) { if ((response.body as string) === '[3]') { return false; // Stop after page 3 } const {options} = response.request; const {init, beforeRequest, beforeRedirect, beforeRetry, afterResponse, beforeError} = options.hooks; const hooksCount = [init, beforeRequest, beforeRedirect, beforeRetry, afterResponse, beforeError].map(a => a.length); t.deepEqual(hooksCount, [1, 1, 1, 1, 1, 1]); return options; }, }, hooks: { init: [nopHook], beforeRequest: [nopHook], beforeRedirect: [nopHook], beforeRetry: [nopHook], afterResponse: [response => response], beforeError: [error => error], }, }); t.deepEqual(result, [1, 2, 3]); }); test('allowGetBody sends correct json payload with .paginate()', withServer, async (t, server, got) => { let page = 1; server.get('/', async (request, response) => { const payload = await getStream(request); try { JSON.parse(payload); } catch { response.statusCode = 422; } if (request.headers['content-length']) { t.is(Number(request.headers['content-length'] || 0), Buffer.byteLength(payload)); } response.end(JSON.stringify([page++])); }); let body = ''; const iterator = got.paginate<number>({ allowGetBody: true, retry: { limit: 0, }, json: {body}, pagination: { paginate() { if (body.length === 2) { return false; } body += 'a'; return { json: {body}, }; }, }, }); const results: number[] = []; for await (const item of iterator) { results.push(item); } t.deepEqual(results, [1, 2, 3]); }); test('`requestLimit` works', withServer, async (t, server, got) => { attachHandler(server, 2); const options = { pagination: { requestLimit: 1, }, }; const results: number[] = []; for await (const item of got.paginate<number>(options)) { results.push(item); } t.deepEqual(results, [1]); }); test('`backoff` works', withServer, async (t, server, got) => { attachHandler(server, 2); const backoff = 200; const asyncIterator: AsyncIterator<number> = got.paginate<number>('', { pagination: { backoff, }, }); t.is((await asyncIterator.next()).value, 1); let receivedLastOne = false; const start = Date.now(); const promise = asyncIterator.next(); (async () => { await promise; receivedLastOne = true; })(); await delay(backoff / 2); t.false(receivedLastOne); await promise; t.true(Date.now() - start >= backoff); }); test('`stackAllItems` set to true', withServer, async (t, server, got) => { attachHandler(server, 3); let itemCount = 0; const result = await got.paginate.all<number>({ pagination: { stackAllItems: true, filter({allItems}) { t.is(allItems.length, itemCount); return true; }, shouldContinue({allItems}) { t.is(allItems.length, itemCount); return true; }, paginate({response, currentItems, allItems}) { itemCount += 1; t.is(allItems.length, itemCount); return got.defaults.options.pagination.paginate!({response, currentItems, allItems}); }, }, }); t.deepEqual(result, [1, 2, 3]); }); test('`stackAllItems` set to false', withServer, async (t, server, got) => { attachHandler(server, 3); const result = await got.paginate.all<number>({ pagination: { stackAllItems: false, filter({allItems}) { t.is(allItems.length, 0); return true; }, shouldContinue({allItems}) { t.is(allItems.length, 0); return true; }, paginate({response, currentItems, allItems}) { t.is(allItems.length, 0); return got.defaults.options.pagination.paginate!({response, allItems, currentItems}); }, }, }); t.deepEqual(result, [1, 2, 3]); }); test('next url in json response', withServer, async (t, server, got) => { server.get('/', (request, response) => { const parameters = new URLSearchParams(request.url.slice(2)); const page = Number(parameters.get('page') ?? 0); response.end(JSON.stringify({ currentUrl: request.url, next: page < 3 ? `${server.url}/?page=${page + 1}` : undefined, })); }); type Page = { currentUrl: string; next?: string; }; const all = await got.paginate.all('', { searchParams: { page: 0, }, responseType: 'json', pagination: { transform: (response: Response<Page>) => [response.body.currentUrl], paginate({response}) { const {next} = response.body; if (!next) { return false; } return { url: new URL(next), prefixUrl: '', searchParams: undefined, }; }, }, }); t.deepEqual(all, [ '/?page=0', '/?page=1', '/?page=2', '/?page=3', ]); }); test('pagination using searchParams', withServer, async (t, server, got) => { server.get('/', (request, response) => { const parameters = new URLSearchParams(request.url.slice(2)); const page = Number(parameters.get('page') ?? 0); response.end(JSON.stringify({ currentUrl: request.url, next: page < 3, })); }); type Page = { currentUrl: string; next?: string; }; const all = await got.paginate.all('', { searchParams: { page: 0, }, responseType: 'json', pagination: { transform: (response: Response<Page>) => [response.body.currentUrl], paginate({response}) { const {next} = response.body; // eslint-disable-next-line unicorn/prevent-abbreviations const searchParams = response.request.options.searchParams as URLSearchParams; const previousPage = Number(searchParams.get('page')); if (!next) { return false; } return { searchParams: { page: previousPage + 1, }, }; }, }, }); t.deepEqual(all, [ '/?page=0', '/?page=1', '/?page=2', '/?page=3', ]); }); test('pagination using extended searchParams', withServer, async (t, server, got) => { server.get('/', (request, response) => { const parameters = new URLSearchParams(request.url.slice(2)); const page = Number(parameters.get('page') ?? 0); response.end(JSON.stringify({ currentUrl: request.url, next: page < 3, })); }); type Page = { currentUrl: string; next?: string; }; const client = got.extend({ searchParams: { limit: 10, }, }); const all = await client.paginate.all('', { searchParams: { page: 0, }, responseType: 'json', pagination: { transform: (response: Response<Page>) => [response.body.currentUrl], paginate({response}) { const {next} = response.body; // eslint-disable-next-line unicorn/prevent-abbreviations const searchParams = response.request.options.searchParams as URLSearchParams; const previousPage = Number(searchParams.get('page')); if (!next) { return false; } return { searchParams: { page: previousPage + 1, }, }; }, }, }); t.is(all.length, 4); for (let i = 0; i < 4; i++) { t.true(all[i] === `/?page=${i}&limit=10` || all[i] === `/?limit=10&page=${i}`); } }); test('calls init hooks on pagination', withServer, async (t, server) => { server.get('/', (request, response) => { response.end(JSON.stringify([request.url])); }); const instance = got.extend({ hooks: { init: [ options => { options.searchParams = 'foobar'; }, ], }, }); const received = await instance.paginate.all<string>(server.url, { searchParams: 'unicorn', }); t.deepEqual(received, [ '/?foobar=', ]); }); test('retrieves all elements - relative url', withServer, async (t, server, got) => { attachHandler(server, 2, {relative: true}); const result = await got.paginate.all<number>(''); t.deepEqual(result, [1, 2]); }); test('throws if url is not an instance of URL', withServer, async (t, server, got) => { attachHandler(server, 2); await t.throwsAsync(got.paginate.all<number>('', { pagination: { paginate: () => ({ url: 'not an instance of URL', }), }, }), { instanceOf: TypeError, }); }); test('throws when transform does not return an array', withServer, async (t, server) => { server.get('/', (_request, response) => { response.end(JSON.stringify('')); }); await t.throwsAsync(got.paginate.all<string>(server.url)); // TODO: Add assert message above. });