Skip to content

Commit

Permalink
Fix types & increase coverage (#1)
Browse files Browse the repository at this point in the history
Fix types & increase coverage
  • Loading branch information
pmmmwh authored Nov 30, 2019
2 parents 55676ea + f7bbd9f commit 85712d7
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 80 deletions.
2 changes: 1 addition & 1 deletion source/as-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const parseBody = (body: Response['body'], responseType: NormalizedOptions['resp
return body;
}

throw new Error(`Failed to parse body of type '${typeof body}' as '${responseType}'`);
throw new Error(`Failed to parse body of type '${typeof body}' as '${responseType!}'`);
};

export default function asPromise<T>(options: NormalizedOptions): CancelableRequest<T> {
Expand Down
11 changes: 6 additions & 5 deletions source/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
CancelableRequest,
ExtendOptions,
HandlerFunction,
NormalizedDefaults,
NormalizedOptions,
Options,
Defaults,
Response,
URLOrOptions
URLOrOptions,
DefaultOptions
} from './utils/types';

export type HTTPAlias =
Expand Down Expand Up @@ -55,7 +56,7 @@ interface GotFunctions {

export interface Got extends Record<HTTPAlias, GotFunctions>, GotFunctions {
stream: GotStream;
defaults: NormalizedDefaults | Readonly<NormalizedDefaults>;
defaults: Defaults | Readonly<Defaults>;
GotError: typeof errors.GotError;
CacheError: typeof errors.CacheError;
RequestError: typeof errors.RequestError;
Expand Down Expand Up @@ -87,7 +88,7 @@ const aliases: readonly HTTPAlias[] = [

export const defaultHandler: HandlerFunction = (options, next) => next(options);

const create = (defaults: NormalizedDefaults): Got => {
const create = (defaults: Defaults): Got => {
// Proxy properties from next handlers
defaults._rawHandlers = defaults.handlers;
defaults.handlers = defaults.handlers.map(fn => ((options, next) => {
Expand Down Expand Up @@ -160,7 +161,7 @@ const create = (defaults: NormalizedDefaults): Got => {
}

return create({
options: mergeOptions(...optionsArray),
options: mergeOptions(...optionsArray) as DefaultOptions,
handlers,
mutableDefaults: Boolean(mutableDefaults)
});
Expand Down
6 changes: 4 additions & 2 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const defaults: Defaults = {
'ENETUNREACH',
'EAI_AGAIN'
],
maxRetryAfter: Infinity,
maxRetryAfter: undefined,
calculateDelay: ({computedValue}) => computedValue
},
timeout: {},
Expand All @@ -59,7 +59,9 @@ const defaults: Defaults = {
resolveBodyOnly: false,
maxRedirects: 10,
prefixUrl: '',
methodRewriting: true
methodRewriting: true,
ignoreInvalidCookies: false,
context: {}
},
handlers: [defaultHandler],
mutableDefaults: false
Expand Down
76 changes: 45 additions & 31 deletions source/normalize-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const nonEnumerableProperties: NonEnumerableProperty[] = [

const isAgentByProtocol = (agent: Options['agent']): agent is AgentByProtocol => is.object(agent);

// TODO: `preNormalizeArguments` should merge `options` & `defaults`
export const preNormalizeArguments = (options: Options, defaults?: NormalizedOptions): NormalizedOptions => {
// `options.headers`
if (is.undefined(options.headers)) {
Expand All @@ -49,6 +50,12 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt
options.headers = lowercaseKeys(options.headers);
}

for (const [key, value] of Object.entries(options.headers)) {
if (is.null_(value)) {
throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
}
}

// `options.prefixUrl`
if (is.urlInstance(options.prefixUrl) || is.string(options.prefixUrl)) {
options.prefixUrl = options.prefixUrl.toString();
Expand Down Expand Up @@ -180,6 +187,11 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt
throw new TypeError('To get a Buffer, set `options.responseType` to `buffer` instead');
}

// `options.maxRedirects`
if (!Reflect.has(options, 'maxRedirects') && !(defaults && Reflect.has(defaults, 'maxRedirects'))) {
options.maxRedirects = 0;
}

return options as NormalizedOptions;
};

Expand All @@ -190,10 +202,6 @@ export const mergeOptions = (...sources: Options[]): NormalizedOptions => {
const properties: Partial<{[Key in NonEnumerableProperty]: any}> = {};

for (const source of sources) {
if (!source) {
continue;
}

merge(mergedOptions, preNormalizeArguments(merge({}, source), mergedOptions));

for (const name of nonEnumerableProperties) {
Expand Down Expand Up @@ -276,8 +284,6 @@ export const normalizeArguments = (url: URLOrOptions, options?: Options, default
if (is.undefined(value)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete normalizedOptions.headers[key];
} else if (is.null_(value)) {
throw new TypeError('Use `undefined` instead of `null` to delete HTTP headers');
}
}

Expand Down Expand Up @@ -305,41 +311,49 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro

// Serialize body
const {headers} = options;
const isForm = !is.undefined(options.form);
const isJSON = !is.undefined(options.json);
const isBody = !is.undefined(options.body);
if ((isBody || isForm || isJSON) && withoutBody.has(options.method)) {
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
}
const noContentType = is.undefined(headers['content-type']);

{
// TODO: these checks should be moved to `preNormalizeArguments`
const isForm = !is.undefined(options.form);
const isJSON = !is.undefined(options.json);
const isBody = !is.undefined(options.body);
if ((isBody || isForm || isJSON) && withoutBody.has(options.method)) {
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
}

if ([isBody, isForm, isJSON].filter(isTrue => isTrue).length > 1) {
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
}
if ([isBody, isForm, isJSON].filter(isTrue => isTrue).length > 1) {
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
}

if (isBody) {
if (is.object(options.body) && isFormData(options.body)) {
// Special case for https://github.com/form-data/form-data
if (!Reflect.has(headers, 'content-type')) {
// @ts-ignore We assign if it is undefined, so this IS correct
headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
}
} else if (!is.nodeStream(options.body) && !is.string(options.body) && !is.buffer(options.body)) {
if (
isBody &&
!is.nodeStream(options.body) &&
!is.string(options.body) &&
!is.buffer(options.body) &&
!(is.object(options.body) && isFormData(options.body))
) {
throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
}
} else if (isForm) {
if (!is.object(options.form)) {

if (isForm && !is.object(options.form)) {
throw new TypeError('The `form` option must be an Object');
}
}

if (!Reflect.has(headers, 'content-type')) {
// @ts-ignore We assign if it is undefined, so this IS correct
if (options.body) {
// Special case for https://github.com/form-data/form-data
if (is.object(options.body) && isFormData(options.body) && noContentType) {
headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
}
} else if (options.form) {
if (noContentType) {
headers['content-type'] = 'application/x-www-form-urlencoded';
}

options.body = (new URLSearchParams(options.form as Record<string, string>)).toString();
} else if (isJSON) {
if (!Reflect.has(headers, 'content-type')) {
// @ts-ignore We assign if it is undefined, so this IS correct
} else if (options.json) {
if (noContentType) {
headers['content-type'] = 'application/json';
}

Expand All @@ -361,7 +375,7 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro
// Content-Length header field when the request message does not contain
// a payload body and the method semantics do not anticipate such a
// body.
if (!Reflect.has(headers, 'content-length') && !Reflect.has(headers, 'transfer-encoding')) {
if (noContentType && is.undefined(headers['transfer-encoding'])) {
if (
(options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') &&
!is.undefined(uploadBodySize)
Expand Down
74 changes: 41 additions & 33 deletions source/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export interface DefaultRetryOptions {
statusCodes: number[];
errorCodes: string[];
calculateDelay: RetryFunction;
maxRetryAfter: number;
maxRetryAfter?: number;
}

export interface RetryOptions extends Partial<DefaultRetryOptions> {
Expand Down Expand Up @@ -132,32 +132,42 @@ interface PromiseCookieJar {
setCookie(rawCookie: string, url: string): Promise<unknown>;
}

export interface DefaultOptions {
method: Method;
retry: DefaultRetryOptions | number;
timeout: Delays | number;
headers: Headers;
hooks: Hooks;
decompress: boolean;
throwHttpErrors: boolean;
followRedirect: boolean;
isStream: boolean;
cache: CacheableRequest.StorageAdapter | string | false;
dnsCache: CacheableLookup | Map<string, string> | Keyv | false;
useElectronNet: boolean;
responseType: ResponseType;
resolveBodyOnly: boolean;
maxRedirects: number;
prefixUrl: URL | string;
}

// The library overrides agent/timeout in a non-standard way, so we have to override them
export interface Options extends Partial<Except<DefaultOptions, 'retry'>>, Merge<Except<https.RequestOptions, 'agent' | 'timeout'>, URLOptions> {
/* eslint-disable @typescript-eslint/indent */
export type DefaultOptions = Merge<
Required<
Except<
GotOptions,
// Overriden
'hooks' |
'retry' |
'timeout' |
'context' |

// Should not be present
'url' |
'body' |
'encoding' |
'cookieJar' |
'request' |
'agent' |
'form' |
'json' |
'lookup'
>
>,
{
hooks: Required<Hooks>;
retry: DefaultRetryOptions;
timeout: Delays;
context: {[key: string]: any};
}
>;
/* eslint-enable @typescript-eslint/indent */

export interface GotOptions {
url?: URL | string;
body?: string | Buffer | ReadableStream;
hostname?: string;
socketPath?: string;
hooks?: Partial<Hooks>;
hooks?: Hooks;
decompress?: boolean;
isStream?: boolean;
encoding?: BufferEncoding;
Expand All @@ -181,10 +191,13 @@ export interface Options extends Partial<Except<DefaultOptions, 'retry'>>, Merge
json?: {[key: string]: any};
context?: {[key: string]: any};
maxRedirects?: number;
lookup?: CacheableLookup['lookup'];
methodRewriting?: boolean;
}

export interface NormalizedOptions extends Except<DefaultOptions, 'dnsCache'>, Except<Options, keyof DefaultOptions> {
export type Options = Merge<https.RequestOptions, Merge<GotOptions, URLOptions>>;

export interface NormalizedOptions extends Options {
// Normalized Got options
headers: Headers;
hooks: Required<Hooks>;
Expand All @@ -197,6 +210,7 @@ export interface NormalizedOptions extends Except<DefaultOptions, 'dnsCache'>, E
url: URL;
cacheableRequest?: (options: string | URL | http.RequestOptions, callback?: (response: http.ServerResponse | ResponseLike) => void) => CacheableRequest.Emitter;
cookieJar?: PromiseCookieJar;
maxRedirects: number;

// UNIX socket support
path?: string;
Expand All @@ -208,16 +222,10 @@ export interface ExtendOptions extends Options {
}

export interface Defaults {
options: Merge<Options, {headers: Headers; hooks: Required<Hooks>}>;
options: DefaultOptions;
handlers: HandlerFunction[];
mutableDefaults: boolean;
}

export interface NormalizedDefaults {
options: Merge<Options, {headers: Headers; hooks: Required<Hooks>}>;
handlers: HandlerFunction[];
_rawHandlers?: HandlerFunction[];
mutableDefaults: boolean;
}

export type URLOrOptions = Options | string;
Expand Down
5 changes: 4 additions & 1 deletion test/arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ test('`url` should be utf-8 encoded', async t => {

test('throws if no arguments provided', async t => {
// @ts-ignore Error tests
await t.throwsAsync(got(), TypeError, 'Missing `url` argument');
await t.throwsAsync(got(), {
instanceOf: TypeError,
message: 'Missing `url` argument'
});
});

test('throws an error if the protocol is not specified', async t => {
Expand Down
18 changes: 18 additions & 0 deletions test/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,21 @@ test('throws on invalid `options.cookieJar.getCookieString`', async t => {
}
}), '`options.cookieJar.getCookieString` needs to be an async function with 1 argument');
});

test('cookies are cleared when redirecting to a different hostname (no cookieJar)', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.writeHead(302, {
location: 'https://httpbin.org/anything'
});
response.end();
});

const {headers} = await got('', {
headers: {
cookie: 'foo=bar',
'user-agent': 'custom'
}
}).json();
t.is(headers.Cookie, undefined);
t.is(headers['User-Agent'], 'custom');
});
4 changes: 2 additions & 2 deletions test/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,13 @@ test('extend with custom handlers', withServer, async (t, server, got) => {
test('extend with instances', t => {
const a = got.extend({prefixUrl: new URL('https://example.com/')});
const b = got.extend(a);
t.is(b.defaults.options.prefixUrl!.toString(), 'https://example.com/');
t.is(b.defaults.options.prefixUrl.toString(), 'https://example.com/');
});

test('extend with a chain', t => {
const a = got.extend({prefixUrl: 'https://example.com/'});
const b = got.extend(a, {headers: {foo: 'bar'}});
t.is(b.defaults.options.prefixUrl!.toString(), 'https://example.com/');
t.is(b.defaults.options.prefixUrl.toString(), 'https://example.com/');
t.is(b.defaults.options.headers.foo, 'bar');
});

Expand Down
5 changes: 4 additions & 1 deletion test/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ test('throws on null value headers', async t => {
headers: {
'user-agent': null
}
}), TypeError, 'Use `undefined` instead of `null` to delete HTTP headers');
}), {
instanceOf: TypeError,
message: 'Use `undefined` instead of `null` to delete the `user-agent` header'
});
});

test('removes undefined value headers', withServer, async (t, server, got) => {
Expand Down
Loading

0 comments on commit 85712d7

Please sign in to comment.