UNPKG

@eggjs/supertest

Version:

SuperAgent driven library for testing HTTP servers

237 lines (235 loc) 7.79 kB
import { AssertError } from "./error/AssertError.js"; import { STATUS_CODES } from "node:http"; import { inspect } from "node:util"; import { Server } from "node:tls"; import { deepStrictEqual } from "node:assert"; import { Request } from "superagent"; //#region src/test.ts var Test = class extends Request { app; _server; _asserts = []; /** * Initialize a new `Test` with the given `app`, * request `method` and `path`. */ constructor(app, method, path) { super(method.toUpperCase(), path); this.redirects(0); this.buffer(); this.app = app; this.url = typeof app === "string" ? app + path : this.serverAddress(app, path); } /** * Returns a URL, extracted from a server. * * @return {String} URL address * @private */ serverAddress(app, path) { if (!app.address()) this._server = app.listen(0); const port = app.address().port; return `${app instanceof Server || this._server instanceof Server ? "https" : "http"}://127.0.0.1:${port}${path}`; } /** * Expectations: * * ```js * .expect(200) * .expect(200, fn) * .expect(200, body) * .expect('Some body') * .expect('Some body', fn) * .expect(['json array body', { key: 'val' }]) * .expect('Content-Type', 'application/json') * .expect('Content-Type', 'application/json', fn) * .expect(fn) * .expect([200, 404]) * ``` * * @return {Test} The current Test instance for chaining. */ expect(a, b, c) { if (typeof a === "function") { this._asserts.push(wrapAssertFn(a)); return this; } if (typeof b === "function") this.end(b); if (typeof c === "function") this.end(c); if (typeof a === "number") { this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a))); if (typeof b !== "function" && arguments.length > 1) this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b))); return this; } if (Array.isArray(a) && a.length > 0 && a.every((val) => typeof val === "number")) { this._asserts.push(wrapAssertFn(this._assertStatusArray.bind(this, a))); return this; } if (typeof b === "string" || typeof b === "number" || b instanceof RegExp) { this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, { name: String(a), value: b }))); return this; } this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a))); return this; } /** * UnExpectations: * * .unexpectHeader('Content-Type') * .unexpectHeader('Content-Type', fn) */ unexpectHeader(name, fn) { if (typeof fn === "function") this.end(fn); if (typeof name === "string") this._asserts.push(this._unexpectHeader.bind(this, name)); return this; } /** * Expectations: * * .expectHeader('Content-Type') * .expectHeader('Content-Type', fn) */ expectHeader(name, fn) { if (typeof fn === "function") this.end(fn); if (typeof name === "string") this._asserts.push(this._expectHeader.bind(this, name)); return this; } _unexpectHeader(name, res) { const actual = res.headers[name.toLowerCase()]; if (actual) return new AssertError("unexpected \"" + name + "\" header field, got \"" + actual + "\"", name, actual); } _expectHeader(name, res) { const actual = res.headers[name.toLowerCase()]; if (!actual) return new AssertError("expected \"" + name + "\" header field", name, actual); } /** * Defer invoking superagent's `.end()` until * the server is listening. */ end(fn) { const server = this._server; super.end((err, res) => { const localAssert = () => { this.assert(err, res, fn); }; if (server && "_handle" in server && server._handle) return server.close(localAssert); localAssert(); }); return this; } /** * Perform assertions and invoke `fn(err, res)`. */ assert(resError, res, fn) { let errorObj; const sysErrors = { ECONNREFUSED: "Connection refused", ECONNRESET: "Connection reset by peer", EPIPE: "Broken pipe", ETIMEDOUT: "Operation timed out" }; if (!res && resError) if (resError instanceof Error && resError.syscall === "connect" && resError.code && sysErrors[resError.code]) errorObj = /* @__PURE__ */ new Error(resError.code + ": " + sysErrors[resError.code]); else errorObj = resError; for (let i = 0; i < this._asserts.length && !errorObj; i += 1) errorObj = this._assertFunction(this._asserts[i], res); if (!errorObj && resError instanceof Error && (!res || resError.status !== res.status)) errorObj = resError; if (!fn) { console.warn("[@eggjs/supertest] no callback function provided, fn: %s", typeof fn); return; } fn.call(this, errorObj || null, res); } /** * Perform assertions on a response body and return an Error upon failure. */ _assertBody(body, res) { const isRegexp = body instanceof RegExp; if (typeof body === "object" && !isRegexp) try { deepStrictEqual(body, res.body); } catch (err) { const a = inspect(body); const b = inspect(res.body); return new AssertError("expected " + a + " response body, got " + b, body, res.body, { cause: err }); } else if (body !== res.text) { const a = inspect(body); const b = inspect(res.text); if (isRegexp) { if (!body.test(res.text)) return new AssertError("expected body " + b + " to match " + body, body, res.body); } else return new AssertError("expected " + a + " response body, got " + b, body, res.body); } } /** * Perform assertions on a response header and return an Error upon failure. */ _assertHeader(header, res) { const field = header.name; const actual = res.header[field.toLowerCase()]; const fieldExpected = header.value; if (typeof actual === "undefined") return new AssertError("expected \"" + field + "\" header field", header, actual); if (Array.isArray(actual) && actual.toString() === fieldExpected || fieldExpected === actual) return; if (fieldExpected instanceof RegExp) { if (!fieldExpected.test(actual)) return new AssertError("expected \"" + field + "\" matching " + fieldExpected + ", got \"" + actual + "\"", header, actual); } else return new AssertError("expected \"" + field + "\" of \"" + fieldExpected + "\", got \"" + actual + "\"", header, actual); } /** * Perform assertions on the response status and return an Error upon failure. */ _assertStatus(status, res) { if (res.status !== status) { const a = STATUS_CODES[status]; const b = STATUS_CODES[res.status]; return new AssertError("expected " + status + " \"" + a + "\", got " + res.status + " \"" + b + "\"", status, res.status); } } /** * Perform assertions on the response status and return an Error upon failure. */ _assertStatusArray(statusArray, res) { if (!statusArray.includes(res.status)) { const b = STATUS_CODES[res.status]; const expectedList = statusArray.join(", "); return new AssertError("expected one of \"" + expectedList + "\", got " + res.status + " \"" + b + "\"", statusArray, res.status); } } /** * Performs an assertion by calling a function and return an Error upon failure. */ _assertFunction(fn, res) { let err; try { err = fn(res); } catch (e) { err = e; } if (err instanceof Error) return err; } }; /** * Wraps an assert function into another. * The wrapper function edit the stack trace of any assertion error, prepending a more useful stack to it. * * @param {Function} assertFn * @return {Function} wrapped assert function */ function wrapAssertFn(assertFn) { const savedStack = (/* @__PURE__ */ new Error()).stack.split("\n").slice(3); return (res) => { let badStack; let err; try { err = assertFn(res); } catch (e) { err = e; } if (err instanceof Error && err.stack) { badStack = err.stack.replace(err.message, "").split("\n").slice(1); err.stack = [err.toString()].concat(savedStack).concat("----").concat(badStack).join("\n"); } return err; }; } //#endregion export { Test };