thenavisapp
Version:
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
440 lines (350 loc) • 12.1 kB
text/typescript
import process from 'node:process';
import {Buffer} from 'node:buffer';
import stream from 'node:stream';
import {pipeline as streamPipeline} from 'node:stream/promises';
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import test from 'ava';
import delay from 'delay';
import {pEvent} from 'p-event';
import type {Handler} from 'express';
import {parse, Body, isBodyFile, type BodyEntryPath, type BodyEntryRawValue} from 'then-busboy';
import {FormData as FormDataNode, Blob, File} from 'formdata-node';
import {fileFromPath} from 'formdata-node/file-from-path';
import getStream from 'get-stream';
import FormData from 'form-data';
import got, {UploadError} from '../source/index.js';
import withServer from './helpers/with-server.js';
const defaultEndpoint: Handler = async (request, response) => {
response.setHeader('method', request.method);
await streamPipeline(request, response);
};
const echoHeaders: Handler = (request, response) => {
response.end(JSON.stringify(request.headers));
};
const echoMultipartBody: Handler = async (request, response) => {
const body = await parse(request);
const entries = await Promise.all(
[...body.entries()].map<Promise<[BodyEntryPath, BodyEntryRawValue]>>(
async ([name, value]) => [name, isBodyFile(value) ? await value.text() : value],
),
);
response.json(Body.json(entries));
};
test('GET cannot have body without the `allowGetBody` option', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
await t.throwsAsync(got.get({body: 'hi'}), {message: 'The `GET` method cannot be used with a body'});
});
test('GET can have body with option allowGetBody', withServer, async (t, server, got) => {
server.get('/', defaultEndpoint);
await t.notThrowsAsync(got.get({body: 'hi', allowGetBody: true}));
});
test('invalid body', async t => {
await t.throwsAsync(
// @ts-expect-error Error tests
got.post('https://example.com', {body: {}}),
);
});
test('sends strings', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post({body: 'wow'});
t.is(body, 'wow');
});
test('sends Buffers', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post({body: Buffer.from('wow')});
t.is(body, 'wow');
});
test('sends Streams', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post({body: stream.Readable.from('wow')});
t.is(body, 'wow');
});
test('sends plain objects as forms', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post({
form: {such: 'wow'},
});
t.is(body, 'such=wow');
});
test('does not support sending arrays as forms', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
await t.throwsAsync(got.post({
form: ['such', 'wow'],
}));
});
test('sends plain objects as JSON', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post({
json: {such: 'wow'},
responseType: 'json',
});
t.deepEqual(body, {such: 'wow'});
});
test('sends arrays as JSON', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post({
json: ['such', 'wow'],
responseType: 'json',
});
t.deepEqual(body, ['such', 'wow']);
});
test('works with empty post response', withServer, async (t, server, got) => {
server.post('/empty', (_request, response) => {
response.end();
});
const {body} = await got.post('empty', {body: 'wow'});
t.is(body, '');
});
test('`content-length` header with string body', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({body: 'wow'});
const headers = JSON.parse(body);
t.is(headers['content-length'], '3');
});
test('`content-length` header with json body', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({json: {foo: 'bar'}});
const headers = JSON.parse(body);
t.is(headers['content-length'], '13');
});
test('`content-length` header with form body', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({form: {foo: 'bar'}});
const headers = JSON.parse(body);
t.is(headers['content-length'], '7');
});
test('`content-length` header with Buffer body', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({body: Buffer.from('wow')});
const headers = JSON.parse(body);
t.is(headers['content-length'], '3');
});
test('`content-length` header with Stream body', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({body: stream.Readable.from('wow')});
const headers = JSON.parse(body);
t.is(headers['transfer-encoding'], 'chunked', 'likely failed to get headers at all');
t.is(headers['content-length'], undefined);
});
test('`content-length` header is not overriden', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({
body: 'wow',
headers: {
'content-length': '10',
},
});
const headers = JSON.parse(body);
t.is(headers['content-length'], '10');
});
test('`content-length` header is present when using custom content-type', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({
json: {foo: 'bar'},
headers: {
'content-type': 'custom',
},
});
const headers = JSON.parse(body);
t.is(headers['content-length'], '13');
});
test('`content-length` header disabled for chunked transfer-encoding', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body} = await got.post({
body: '3\r\nwow\r\n0\r\n',
headers: {
'transfer-encoding': 'chunked',
},
});
const headers = JSON.parse(body);
t.is(headers['transfer-encoding'], 'chunked', 'likely failed to get headers at all');
t.is(headers['content-length'], undefined);
});
test('`content-type` header is not overriden when object in `options.body`', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const {body: headers} = await got.post<Record<string, string>>({
headers: {
'content-type': 'doge',
},
json: {
such: 'wow',
},
responseType: 'json',
});
t.is(headers['content-type'], 'doge');
});
test('throws when form body is not a plain object or array', async t => {
// @ts-expect-error Manual test
await t.throwsAsync(got.post('https://example.com', {form: 'such=wow'}));
});
// See https://github.com/sindresorhus/got/issues/897
test('the `json` payload is not touched', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const {body} = await got.post<{context: {foo: true}}>({
json: {
context: {
foo: true,
},
},
responseType: 'json',
});
t.true('context' in body);
t.true(body.context.foo);
});
test('the `body` payload is not touched', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const buffer = Buffer.from('Hello, Got!') as Buffer & {context?: unknown};
buffer.context = {foo: 'bar'};
const body = await got.post({body: buffer}).text();
t.is(body, 'Hello, Got!');
});
test('the `form` payload is not touched', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const form = {
context: true,
};
const body = await got.post({form}).text();
t.is(body, 'context=true');
});
test('DELETE method sends plain objects as JSON', withServer, async (t, server, got) => {
server.delete('/', defaultEndpoint);
const {body} = await got.delete({
json: {such: 'wow'},
responseType: 'json',
});
t.deepEqual(body, {such: 'wow'});
});
test('catches body errors before calling pipeline() - promise', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
await t.throwsAsync(got.post({
body: fs.createReadStream('./file-that-does-not-exist.txt'),
}), {
message: /ENOENT: no such file or directory/,
});
// Wait for unhandled errors
await delay(100);
});
test('catches body errors before calling pipeline() - stream', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
await t.throwsAsync(getStream(got.stream.post({
body: fs.createReadStream('./file-that-does-not-exist.txt'),
})), {
message: /ENOENT: no such file or directory/,
});
// Wait for unhandled errors
await delay(100);
});
test('body - file read stream', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const fullPath = path.resolve('test/fixtures/ok');
const toSend = await getStream(fs.createReadStream(fullPath));
const body = await got.post({
body: fs.createReadStream(fullPath),
}).text();
t.is(toSend, body);
});
test('body - file read stream, wait for `ready` event', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const fullPath = path.resolve('test/fixtures/ok');
const toSend = await getStream(fs.createReadStream(fullPath));
const ifStream = fs.createReadStream(fullPath);
await pEvent(ifStream, 'ready');
const body = await got.post({
body: ifStream,
}).text();
t.is(toSend, body);
});
test('body - sends spec-compliant FormData', withServer, async (t, server, got) => {
server.post('/', echoMultipartBody);
const form = new FormDataNode();
form.set('a', 'b');
const body = await got.post({body: form}).json<{a: string}>();
t.is(body.a, 'b');
});
test('body - sends files with spec-compliant FormData', withServer, async (t, server, got) => {
server.post('/', echoMultipartBody);
const fullPath = path.resolve('test/fixtures/ok');
const blobContent = 'Blob content';
const fileContent = 'File content';
const anotherFileContent = await fsPromises.readFile(fullPath, 'utf8');
const expected = {
blob: blobContent,
file: fileContent,
anotherFile: anotherFileContent,
};
const form = new FormDataNode();
form.set('blob', new Blob([blobContent]));
form.set('file', new File([fileContent], 'file.txt', {type: 'text/plain'}));
form.set('anotherFile', await fileFromPath(fullPath, {type: 'text/plain'}));
const body = await got.post({body: form}).json<typeof expected>();
t.deepEqual(body, expected);
});
test('body - sends form-data with without known length', withServer, async (t, server, got) => {
server.post('/', echoMultipartBody);
const fullPath = path.resolve('test/fixtures/ok');
function getFileStream() {
const fileStream = fs.createReadStream(fullPath);
const passThrough = new stream.PassThrough();
fileStream.pipe(passThrough);
return passThrough;
}
const expected = {
file: await fsPromises.readFile(fullPath, 'utf8'),
};
const form = new FormDataNode();
form.set('file', {
[Symbol.toStringTag]: 'File',
type: 'text/plain',
name: 'file.txt',
stream() {
return getFileStream();
},
});
const body = await got.post({body: form}).json<typeof expected>();
t.deepEqual(body, expected);
});
test('throws on upload error', withServer, async (t, server, got) => {
server.post('/', defaultEndpoint);
const body = new stream.PassThrough();
const message = 'oh no';
await t.throwsAsync(getStream(got.stream.post({
body,
hooks: {
beforeRequest: [
() => {
process.nextTick(() => {
body.destroy(new Error(message));
});
},
],
},
})), {
instanceOf: UploadError,
message,
code: 'ERR_UPLOAD',
});
});
test('formdata retry', withServer, async (t, server, got) => {
server.post('/', echoHeaders);
const instance = got.extend({
hooks: {
afterResponse: [
async (_response, retryWithMergedOptions) => retryWithMergedOptions({
headers: {
foo: 'bar',
},
}),
],
},
});
const form = new FormData();
form.append('hello', 'world');
await t.throwsAsync(instance.post({
body: form,
headers: form.getHeaders(),
}).json<{foo?: string}>(), {
message: 'Cannot retry with consumed body stream',
});
});