import type { ListenerLike, ServerLike } from "./types.ts";import { assertEquals, STATUS_TEXT } from "../deps.ts";import { superagent } from "./superagent.ts";import { close } from "./close.ts";import { isListener, isServer, isStdNativeServer, isString } from "./utils.ts";import { exposeSham } from "./xhrSham.js";
type ExpectChecker = (res: IResponse) => any;
type CallbackHandler = (err: any, res: IResponse) => void;
type Serializer = (obj: any) => string;
type Parser = (str: string) => any;
type MultipartValueSingle = | Blob | Uint8Array | Deno.Reader | string | boolean | number;
type MultipartValue = MultipartValueSingle | MultipartValueSingle[];
type HeaderValue = string | string[];type Header = { [key: string]: HeaderValue };
interface HTTPError extends Error { status: number; text: string; method: string; path: string;}
interface XMLHttpRequest {}
export interface IResponse { accepted: boolean; badRequest: boolean; body: any; charset: string; clientError: boolean; error: false | HTTPError; files: any; forbidden: boolean; get(header: string): HeaderValue; header: Header; headers: Header; info: boolean; links: object; noContent: boolean; notAcceptable: boolean; notFound: boolean; ok: boolean; redirect: boolean; serverError: boolean; status: number; statusCode: number; statusType: number; statusText: string; text: string; type: string; unauthorized: boolean; xhr: XMLHttpRequest; redirects: string[];}
export interface IRequest extends Promise<IResponse> { new (method: string, url: string): IRequest;
agent(agent?: any): this;
cookies: string; method: string; url: string;
abort(): void; accept(type: string): this; attach( field: string, file: MultipartValueSingle, options?: string | { filename?: string; contentType?: string }, ): this; auth(user: string, pass: string, options?: { type: "basic" | "auto" }): this; auth(token: string, options: { type: "bearer" }): this; buffer(val?: boolean): this; ca(cert: any | any[]): this; cert(cert: any | any[]): this; clearTimeout(): this; disableTLSCerts(): this; end(callback?: CallbackHandler): void; field(name: string, val: MultipartValue): this; field(fields: { [fieldName: string]: MultipartValue }): this; get(field: string): string; http2(enable?: boolean): this; key(cert: any | any[]): this; ok(callback: (res: IResponse) => boolean): this; on(name: "error", handler: (err: any) => void): this; on(name: "progress", handler: (event: ProgressEvent) => void): this; on(name: "response", handler: (response: IResponse) => void): this; on(name: string, handler: (event: any) => void): this; parse(parser: Parser): this; part(): this; pfx( cert: any | any[] | { pfx: string | any; passphrase: string; }, ): this; pipe(stream: any, options?: object): any; query(val: object | string): this; redirects(n: number): this; responseType(type: string): this; retry(count?: number, callback?: CallbackHandler): this; send(data?: string | object): this; serialize(serializer: Serializer): this; set(field: object): this; set(field: string, val: string): this; set(field: "Cookie", val: string[]): this; timeout(ms: number | { deadline?: number; response?: number }): this; trustLocalhost(enabled?: boolean): this; type(val: string): this; unset(field: string): this; use(fn: Plugin): this; withCredentials(): this; write(data: string | any, encoding?: string): boolean; maxResponseSize(size: number): this;}
type Plugin = (req: IRequest) => void;
const SHAM_SYMBOL = Symbol("SHAM_SYMBOL");exposeSham(SHAM_SYMBOL);
async function completeXhrPromises() { for ( const promise of Object.values( (window as any)[SHAM_SYMBOL].promises, ) ) { if (promise) { try { await promise; } catch (_) { } } }}
const SuperRequest: IRequest = (superagent as any).Request;
export class Test extends SuperRequest { #asserts!: any[]; #redirects: number; #redirectList: string[]; #server!: ServerLike;
public app: string | ListenerLike | ServerLike; public url: string;
constructor( app: string | ListenerLike | ServerLike, method: string, path: string, host?: string, secure: boolean = false, ) { super(method.toUpperCase(), path); this.redirects(0); this.#redirects = 0; this.#redirectList = [];
this.app = app; this.#asserts = [];
if (isString(app)) { this.url = `${app}${path}`; } else { if (isStdNativeServer(app)) { const listenAndServePromise = app.listenAndServe().catch((err) => close(app, app, err) );
this.#server = { async close() { try { app.close(); await listenAndServePromise; } catch { } }, addrs: app.addrs, async listenAndServe() {}, }; } else if (isServer(app)) { this.#server = app as ServerLike; } else if (isListener(app)) { secure = false; this.#server = (app as ListenerLike).listen(":0"); } else { throw new Error( "superdeno is unable to identify or create a valid test server", ); }
this.url = this.#serverAddress(path, host, secure); } }
#serverAddress = ( path: string, host?: string, secure?: boolean, ) => { const address = ("addrs" in this.#server ? this.#server.addrs[0] : this.#server.listener.addr) as Deno.NetAddr; const port = address.port; const protocol = secure ? "https" : "http";
return `${protocol}://${(host || "127.0.0.1")}:${port}${path}`; };
expect(callback: ExpectChecker): this; expect(status: number, callback?: CallbackHandler): this; expect(status: number, body: any, callback?: CallbackHandler): this; expect(body: string | RegExp | Object, callback?: CallbackHandler): this; expect( field: string, value: string | RegExp | number, callback?: CallbackHandler, ): this; expect(a: any, b?: any, c?: any): this { if (typeof a === "function") { this.#asserts.push(a); return this; } if (typeof b === "function") this.end(b); if (typeof c === "function") this.end(c);
if (typeof a === "number") { this.#asserts.push(this.#assertStatus.bind(this, a)); if (typeof b !== "function" && arguments.length > 1) { this.#asserts.push(this.#assertBody.bind(this, b)); } return this; }
if (typeof b === "string" || typeof b === "number" || b instanceof RegExp) { this.#asserts.push( this.#assertHeader.bind(this, { name: "" + a, value: b }), ); return this; }
this.#asserts.push(this.#assertBody.bind(this, a));
return this; }
#redirect = (res: IResponse, callback?: CallbackHandler): this => { const url = res.headers.location as string;
if (!url) { close(this.#server, this.app, undefined, async () => { await completeXhrPromises(); callback?.(new Error("No location header for redirect"), res); });
return this; }
const parsedUrl = new URL(url, this.url); const changesOrigin = parsedUrl.host !== new URL(this.url).host;
let headers = (this as any)._header;
if (res.statusCode === 301 || res.statusCode === 302) { headers = cleanHeader(headers, changesOrigin);
this.method = this.method === "HEAD" ? "HEAD" : "GET";
(this as any)._data = null; }
if (res.statusCode === 303) { headers = cleanHeader(headers, changesOrigin);
this.method = "GET";
(this as any)._data = null; }
delete headers.host;
delete (this as any)._formData;
initHeaders(this);
(this as any)._endCalled = false; this.url = parsedUrl.href; (this as any).qs = {}; (this as any)._query = []; this.set(headers); (this as any).emit("redirect", res); this.#redirectList.push(this.url);
this.end(callback);
return this; };
end(callback?: CallbackHandler): this { const self = this; const end = SuperRequest.prototype.end;
end.call( self, function (err: any, res: any) { const redirect = isRedirect(res?.statusCode); const max: number = (self as any)._maxRedirects;
if (redirect && self.#redirects++ !== max) { return self.#redirect(res, callback); }
return close(self.#server, self.app, undefined, async () => { await completeXhrPromises();
await new Promise((resolve) => setTimeout(resolve, 20));
self.#assert(err, res, callback); }); }, );
return this; }
#assert = (resError: HTTPError, res: IResponse, fn?: Function): void => { let error;
if (!res && resError) { error = resError; }
for (let i = 0; i < this.#asserts.length && !error; i += 1) { error = this.#assertFunction(this.#asserts[i], res); }
if ( !error && resError instanceof Error && (!res || (resError as any).status !== res.status) ) { error = resError; }
if (fn) fn.call(this, error || null, res); };
#assertBody = function (body: any, res: IResponse): Error | void { const isRegExp = body instanceof RegExp;
if (typeof body === "object" && !isRegExp) { try { assertEquals(body, res.body); } catch (_) { const a = Deno.inspect(body); const b = Deno.inspect(res.body);
return error( `expected ${a} response body, got ${b}`, body, res.body, ); } } else if (body !== res.text) { const a = Deno.inspect(body); const b = Deno.inspect(res.text);
if (isRegExp) { if (!body.test(res.text)) { return error( `expected body ${b} to match ${body}`, body, res.body, ); } } else { return error( `expected ${a} response body, got ${b}`, body, res.body, ); } } };
#assertHeader = ( header: { name: string; value: string | number | RegExp }, res: IResponse, ): Error | void => { const field = header.name; const actual = res.headers[field.toLowerCase()]; const fieldExpected = header.value;
if (typeof actual === "undefined") { return new Error(`expected "${field}" header field`); }
if ( (Array.isArray(actual) && actual.toString() === fieldExpected) || fieldExpected === actual ) { return; }
if (fieldExpected instanceof RegExp) { if (!fieldExpected.test(actual as string)) { return new Error( `expected "${field}" matching ${fieldExpected}, got "${actual}"`, ); } } else { return new Error( `expected "${field}" of "${fieldExpected}", got "${actual}"`, ); } };
#assertStatus = (status: number, res: IResponse): Error | void => { if (res.status !== status) { const a = STATUS_TEXT.get(status); const b = STATUS_TEXT.get(res.status);
return new Error(`expected ${status} "${a}", got ${res.status} "${b}"`); } };
#assertFunction = (fn: Function, res: IResponse): Error | void => { let err;
try { err = fn(res); } catch (e) { err = e; }
if (err instanceof Error) return err; };}
function error(msg: string, expected: any, actual: any): Error { const err = new Error(msg);
(err as any).expected = expected; (err as any).actual = actual; (err as any).showDiff = true;
return err;}
function isRedirect(code = 0) { return [301, 302, 303, 305, 307, 308].includes(code);}
function cleanHeader(header: Header, changesOrigin: boolean) { delete header["content-type"]; delete header["content-length"]; delete header["transfer-encoding"]; delete header.host;
if (changesOrigin) { delete header.authorization; delete header.cookie; }
return header;}
function initHeaders(req: any) { req._header = {}; req.header = {};}