@eggjs/supertest
Version:
SuperAgent driven library for testing HTTP servers
237 lines (235 loc) • 7.79 kB
JavaScript
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 };