@japa/api-client
Version:
Browser and API testing client for Japa. Built on top of Playwright
1,294 lines (1,286 loc) • 30.2 kB
JavaScript
// src/client.ts
import Macroable3 from "@poppinss/macroable";
// src/request.ts
import cookie from "cookie";
import Hooks from "@poppinss/hooks";
import Macroable2 from "@poppinss/macroable";
import superagent from "superagent";
// src/response.ts
import Macroable from "@poppinss/macroable";
import setCookieParser from "set-cookie-parser";
// src/utils.ts
import { inspect } from "node:util";
import { parse } from "qs";
var INSPECT_OPTIONS = { colors: true, depth: 2, showHidden: false };
function stackToError(errorStack) {
if (typeof errorStack === "string" && /^\s*at .*(\S+:\d+|\(native\))/m.test(errorStack)) {
const customError = new Error(errorStack.split("\n")[0]);
customError.stack = errorStack;
return customError;
}
return errorStack;
}
function dumpResponseError(response) {
if (response.status() >= 500 && response.hasError()) {
console.log(`"error" => ${inspect(stackToError(response.text()))}`);
return;
}
}
function dumpRequestCookies(request) {
console.log(`"cookies" => ${inspect(request.cookiesJar, INSPECT_OPTIONS)}`);
}
function dumpResponseCookies(response) {
console.log(`"cookies" => ${inspect(response.cookies(), INSPECT_OPTIONS)}`);
}
function dumpRequestHeaders(request) {
console.log(`"headers" => ${inspect(request.request["header"], INSPECT_OPTIONS)}`);
}
function dumpResponseHeaders(response) {
console.log(`"headers" => ${inspect(response.headers(), INSPECT_OPTIONS)}`);
}
function dumpRequestBody(request) {
const data = request.request["_data"];
if (data) {
console.log(`"body" => ${inspect(data, INSPECT_OPTIONS)}`);
}
}
function dumpResponseBody(response) {
if (response.status() >= 500) {
return;
}
if (response.hasBody()) {
console.log(`"body" => ${inspect(response.body(), INSPECT_OPTIONS)}`);
} else if (response.text()) {
console.log(`"text" => ${inspect(response.text(), INSPECT_OPTIONS)}`);
}
if (response.hasFiles()) {
const files = Object.keys(response.files()).reduce(
(result, fileName) => {
result[fileName] = response.files()[fileName].toJSON();
return result;
},
{}
);
console.log(`"files" => ${inspect(files, INSPECT_OPTIONS)}`);
}
}
function dumpRequest(request) {
console.log(
`"request" => ${inspect(
{
method: request.request.method,
endpoint: request.config.endpoint
},
INSPECT_OPTIONS
)}`
);
if ("qsRaw" in request.request && Array.isArray(request.request.qsRaw)) {
console.log(`"qs" => ${inspect(parse(request.request.qsRaw.join("&")), INSPECT_OPTIONS)}`);
}
}
function dumpResponse(response) {
console.log(
`"response" => ${inspect(
{
status: response.status()
},
INSPECT_OPTIONS
)}`
);
}
// src/response.ts
var ApiResponse = class extends Macroable {
constructor(request, response, config, assert) {
super();
this.request = request;
this.response = response;
this.config = config;
this.assert = assert;
this.cookiesJar = this.#parseCookies();
this.#processCookies();
}
#valuesDumped = /* @__PURE__ */ new Set();
/**
* Parsed cookies
*/
cookiesJar;
/**
* Parse response header to collect cookies
*/
#parseCookies() {
const cookieHeader = this.header("set-cookie");
if (!cookieHeader) {
return {};
}
return setCookieParser.parse(cookieHeader, { map: true });
}
/**
* Process cookies using the serializer
*/
#processCookies() {
const cookiesSerializer = this.config.serializers?.cookie;
const processMethod = cookiesSerializer?.process;
if (!processMethod) {
return;
}
Object.keys(this.cookiesJar).forEach((key) => {
const cookie2 = this.cookiesJar[key];
const processedValue = processMethod(cookie2.name, cookie2.value, this);
if (processedValue !== void 0) {
cookie2.value = processedValue;
}
});
}
/**
* Ensure assert plugin is installed and configured
*/
#ensureHasAssert() {
if (!this.assert) {
throw new Error(
"Response assertions are not available. Make sure to install the @japa/assert plugin"
);
}
}
/**
* Ensure OpenAPI assertions package is installed and
* configured
*/
#ensureHasOpenAPIAssertions() {
this.#ensureHasAssert();
if ("isValidApiResponse" in this.assert === false) {
throw new Error(
"OpenAPI assertions are not available. Make sure to install the @japa/openapi-assertions plugin"
);
}
}
/**
* Response content-type charset. Undefined if no charset
* is mentioned.
*/
charset() {
return this.response.charset;
}
/**
* Parsed files from the multipart response.
*/
files() {
return this.response.files;
}
/**
* Returns an object of links by parsing the "Link" header.
*
* @example
* Link: <https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preload"
* response.links()
* // {
* // preconnect: 'https://one.example.com',
// preload: 'https://two.example.com',
* // }
*/
links() {
return this.response.links;
}
/**
* Response status type
*/
statusType() {
return this.response.statusType;
}
/**
* Request raw parsed text
*/
text() {
return this.response.text;
}
/**
* Response body
*/
body() {
return this.response.body;
}
/**
* Read value for a given response header
*/
header(key) {
key = key.toLowerCase();
return this.response.headers[key];
}
/**
* Get all response headers
*/
headers() {
return this.response.headers;
}
/**
* Get response status
*/
status() {
return this.response.status;
}
/**
* Get response content-type
*/
type() {
return this.response.type;
}
/**
* Get redirects URLs the request has followed before
* getting the response
*/
redirects() {
return this.response.redirects;
}
/**
* Find if the response has parsed body. The check is performed
* by inspecting the response content-type and returns true
* when content-type is either one of the following.
*
* - application/json
* - application/x-www-form-urlencoded
* - multipart/form-data
*
* Or when the response body is a buffer.
*/
hasBody() {
return this.type() === "application/json" || this.type() === "application/x-www-form-urlencoded" || this.type() === "multipart/form-data" || Buffer.isBuffer(this.response.body);
}
/**
* Find if the response body has files
*/
hasFiles() {
return this.files() && Object.keys(this.files()).length > 0;
}
/**
* Find if response is an error
*/
hasError() {
return this.error() ? true : false;
}
/**
* Find if response is an fatal error. Response with >=500
* status code are concerned as fatal errors
*/
hasFatalError() {
return this.status() >= 500;
}
/**
* Find if the request client failed to make the request
*/
hasClientError() {
return this.response.clientError;
}
/**
* Find if the server responded with an error
*/
hasServerError() {
return this.response.serverError;
}
/**
* Access to response error
*/
error() {
return this.response.error;
}
/**
* Get cookie by name
*/
cookie(name) {
return this.cookiesJar[name];
}
/**
* Parsed response cookies
*/
cookies() {
return this.cookiesJar;
}
/**
* Dump request headers
*/
dumpHeaders() {
if (this.#valuesDumped.has("headers")) {
return this;
}
this.#valuesDumped.add("headers");
dumpResponseHeaders(this);
return this;
}
/**
* Dump request cookies
*/
dumpCookies() {
if (this.#valuesDumped.has("cookies")) {
return this;
}
this.#valuesDumped.add("cookies");
dumpResponseCookies(this);
return this;
}
/**
* Dump request body
*/
dumpBody() {
if (this.#valuesDumped.has("body")) {
return this;
}
this.#valuesDumped.add("body");
dumpResponseBody(this);
return this;
}
/**
* Dump request body
*/
dumpError() {
if (this.#valuesDumped.has("error")) {
return this;
}
this.#valuesDumped.add("error");
dumpResponseError(this);
return this;
}
/**
* Dump request
*/
dump() {
if (this.#valuesDumped.has("response")) {
return this;
}
this.#valuesDumped.add("response");
dumpResponse(this);
this.dumpCookies();
this.dumpHeaders();
this.dumpBody();
this.dumpError();
return this;
}
/**
* Assert response status to match the expected status
*/
assertStatus(expectedStatus) {
this.#ensureHasAssert();
this.assert.equal(this.status(), expectedStatus);
}
/**
* Assert response body to match the expected body
*/
assertBody(expectedBody) {
this.#ensureHasAssert();
this.assert.deepEqual(this.body(), expectedBody);
}
/**
* Assert response body to match the subset from the
* expected body
*/
assertBodyContains(expectedBody) {
this.#ensureHasAssert();
this.assert.containsSubset(this.body(), expectedBody);
}
/**
* Assert response body not to match the subset from the
* expected body
*/
assertBodyNotContains(expectedBody) {
this.#ensureHasAssert();
this.assert.notContainsSubset(this.body(), expectedBody);
}
/**
* Assert response to contain a given cookie and optionally
* has the expected value
*/
assertCookie(name, value) {
this.#ensureHasAssert();
this.assert.property(this.cookies(), name);
if (value !== void 0) {
this.assert.deepEqual(this.cookie(name).value, value);
}
}
/**
* Assert response to not contain a given cookie
*/
assertCookieMissing(name) {
this.#ensureHasAssert();
this.assert.notProperty(this.cookies(), name);
}
/**
* Assert response to contain a given header and optionally
* has the expected value
*/
assertHeader(name, value) {
name = name.toLowerCase();
this.#ensureHasAssert();
this.assert.property(this.headers(), name);
if (value !== void 0) {
this.assert.deepEqual(this.header(name), value);
}
}
/**
* Assert response to not contain a given header
*/
assertHeaderMissing(name) {
name = name.toLowerCase();
this.#ensureHasAssert();
this.assert.notProperty(this.headers(), name);
}
/**
* Assert response text to include the expected value
*/
assertTextIncludes(expectedSubset) {
this.#ensureHasAssert();
this.assert.include(this.text(), expectedSubset);
}
/**
* Assert response body is valid as per the API spec.
*/
assertAgainstApiSpec() {
this.#ensureHasOpenAPIAssertions();
this.assert.isValidApiResponse(this.response);
}
/**
* Assert there is a matching redirect
*/
assertRedirectsTo(pathname) {
this.#ensureHasAssert();
const redirects = this.redirects().map((url) => new URL(url).pathname);
this.assert.evaluate(
redirects.find((one) => one === pathname),
`Expected #{exp} to be one of #{act}`,
{
expected: [pathname],
actual: redirects,
operator: "includes"
}
);
}
/**
* Assert that response has an ok (200) status
*/
assertOk() {
this.assertStatus(200);
}
/**
* Assert that response has a created (201) status
*/
assertCreated() {
this.assertStatus(201);
}
/**
* Assert that response has an accepted (202) status
*/
assertAccepted() {
this.assertStatus(202);
}
/**
* Assert that response has a no content (204) status
*/
assertNoContent() {
this.assertStatus(204);
}
/**
* Assert that response has a moved permanently (301) status
*/
assertMovedPermanently() {
this.assertStatus(301);
}
/**
* Assert that response has a found (302) status
*/
assertFound() {
this.assertStatus(302);
}
/**
* Assert that response has a bad request (400) status
*/
assertBadRequest() {
this.assertStatus(400);
}
/**
* Assert that response has an unauthorized (401) status
*/
assertUnauthorized() {
this.assertStatus(401);
}
/**
* Assert that response has a payment required (402) status
*/
assertPaymentRequired() {
this.assertStatus(402);
}
/**
* Assert that response has a forbidden (403) status
*/
assertForbidden() {
this.assertStatus(403);
}
/**
* Assert that response has a not found (404) status
*/
assertNotFound() {
this.assertStatus(404);
}
/**
* Assert that response has a method not allowed (405) status
*/
assertMethodNotAllowed() {
this.assertStatus(405);
}
/**
* Assert that response has a not acceptable (406) status
*/
assertNotAcceptable() {
this.assertStatus(406);
}
/**
* Assert that response has a request timeout (408) status
*/
assertRequestTimeout() {
this.assertStatus(408);
}
/**
* Assert that response has a conflict (409) status
*/
assertConflict() {
this.assertStatus(409);
}
/**
* Assert that response has a gone (410) status
*/
assertGone() {
this.assertStatus(410);
}
/**
* Assert that response has a length required (411) status
*/
assertLengthRequired() {
this.assertStatus(411);
}
/**
* Assert that response has a precondition failed (412) status
*/
assertPreconditionFailed() {
this.assertStatus(412);
}
/**
* Assert that response has a payload too large (413) status
*/
assertPayloadTooLarge() {
this.assertStatus(413);
}
/**
* Assert that response has a URI too long (414) status
*/
assertURITooLong() {
this.assertStatus(414);
}
/**
* Assert that response has an unsupported media type (415) status
*/
assertUnsupportedMediaType() {
this.assertStatus(415);
}
/**
* Assert that response has a range not satisfiable (416) status
*/
assertRangeNotSatisfiable() {
this.assertStatus(416);
}
/**
* Assert that response has an im a teapot (418) status
*/
assertImATeapot() {
this.assertStatus(418);
}
/**
* Assert that response has an unprocessable entity (422) status
*/
assertUnprocessableEntity() {
this.assertStatus(422);
}
/**
* Assert that response has a locked (423) status
*/
assertLocked() {
this.assertStatus(423);
}
/**
* Assert that response has a too many requests (429) status
*/
assertTooManyRequests() {
this.assertStatus(429);
}
};
// src/request.ts
var DUMP_CALLS = {
request: dumpRequest,
body: dumpRequestBody,
cookies: dumpRequestCookies,
headers: dumpRequestHeaders
};
var ApiRequest = class _ApiRequest extends Macroable2 {
constructor(config, assert) {
super();
this.config = config;
this.#assert = assert;
this.request = this.#createRequest();
this.config.hooks?.setup.forEach((handler) => this.setup(handler));
this.config.hooks?.teardown.forEach((handler) => this.teardown(handler));
}
/**
* The serializer to use for serializing request query params
*/
static qsSerializer = (value) => value;
/**
* Register/remove custom superagent parser, Parsers are used
* to parse the incoming response
*/
static addParser = (contentType, parser) => {
superagent.parse[contentType] = parser;
};
static removeParser = (contentType) => {
delete superagent.parse[contentType];
};
/**
* Register/remove custom superagent serializers. Serializers are used
* to serialize the request body
*/
static addSerializer = (contentType, serializer) => {
superagent.serialize[contentType] = serializer;
};
static removeSerializer = (contentType) => {
delete superagent.serialize[contentType];
};
/**
* Specify the serializer for query strings. Serializers are used to convert
* request querystring values to a string
*/
static setQsSerializer = (serializer) => {
_ApiRequest.qsSerializer = serializer;
};
static removeQsSerializer = () => {
_ApiRequest.qsSerializer = (value) => value;
};
/**
* Reference to registered hooks
*/
hooks = new Hooks();
#setupRunner;
#teardownRunner;
/**
* Reference to Assert module
*/
#assert;
/**
* Dump calls
*/
#valuesToDump = /* @__PURE__ */ new Set();
/**
* The underlying super agent request
*/
request;
/**
* Cookies to be sent with the request
*/
cookiesJar = {};
/**
* Set cookies header
*/
#setCookiesHeader() {
const prepareMethod = this.config.serializers?.cookie?.prepare;
const cookies = Object.keys(this.cookiesJar).map((key) => {
let { name, value } = this.cookiesJar[key];
if (prepareMethod) {
value = prepareMethod(name, value, this);
}
return cookie.serialize(name, value);
});
if (!cookies.length) {
return;
}
this.header("Cookie", cookies);
}
/**
* Instantiate hooks runner
*/
#instantiateHooksRunners() {
this.#setupRunner = this.hooks.runner("setup");
this.#teardownRunner = this.hooks.runner("teardown");
}
/**
* Run setup hooks
*/
async #runSetupHooks() {
try {
await this.#setupRunner.run(this);
} catch (error) {
await this.#setupRunner.cleanup(error, this);
throw error;
}
}
/**
* Run teardown hooks
*/
async #runTeardownHooks(response) {
try {
await this.#teardownRunner.run(response);
} catch (error) {
await this.#teardownRunner.cleanup(error, response);
throw error;
}
await this.#teardownRunner.cleanup(null, response);
}
/**
* Send HTTP request to the server. Errors except the client errors
* are tured into a response object.
*/
async #sendRequest() {
let response;
try {
this.#setCookiesHeader();
this.#dumpValues();
response = await this.request.buffer(true);
} catch (error) {
this.request.abort();
if (!error.response) {
await this.#setupRunner.cleanup(error, this);
throw error;
}
response = error.response;
}
await this.#setupRunner.cleanup(null, this);
return new ApiResponse(this, response, this.config, this.#assert);
}
/**
* Invoke calls calls
*/
#dumpValues() {
if (!this.#valuesToDump.size) {
return;
}
try {
this.#valuesToDump.forEach((key) => {
DUMP_CALLS[key](this);
});
} catch (error) {
console.log(error);
}
}
/**
* Is endpoint a fully qualified URL or not
*/
#isUrl(url) {
return url.startsWith("http://") || url.startsWith("https://");
}
/**
* Prepend baseUrl to the endpoint
*/
#prependBaseUrl(url) {
if (!this.config.baseUrl) {
return url;
}
return `${this.config.baseUrl}/${url.replace(/^\//, "")}`;
}
/**
* Creates the request instance for the given HTTP method
*/
#createRequest() {
let url = this.config.endpoint;
if (!this.#isUrl(url)) {
url = this.#prependBaseUrl(url);
}
return superagent(this.config.method, url);
}
/**
* Register a setup hook. Setup hooks are called before
* making the request
*/
setup(handler) {
this.hooks.add("setup", handler);
return this;
}
/**
* Register a teardown hook. Teardown hooks are called after
* making the request
*/
teardown(handler) {
this.hooks.add("teardown", handler);
return this;
}
/**
* Set cookie as a key-value pair to be sent to the server
*/
cookie(key, value) {
this.cookiesJar[key] = { name: key, value };
return this;
}
/**
* Set cookies as an object to be sent to the server
*/
cookies(cookies) {
Object.keys(cookies).forEach((key) => this.cookie(key, cookies[key]));
return this;
}
/**
* Define request header as a key-value pair.
*
* @example
* request.header('x-foo', 'bar')
* request.header('x-foo', ['bar', 'baz'])
*/
header(key, value) {
this.headers({ [key]: value });
return this;
}
/**
* Define request headers as an object.
*
* @example
* request.headers({ 'x-foo': 'bar' })
* request.headers({ 'x-foo': ['bar', 'baz'] })
*/
headers(headers) {
this.request.set(headers);
return this;
}
/**
* Define the field value for a multipart request.
*
* @note: This method makes a multipart request. See [[this.form]] to
* make HTML style form submissions.
*
* @example
* request.field('name', 'virk')
* request.field('age', 22)
*/
field(name, value) {
this.request.field(name, value);
return this;
}
/**
* Define fields as an object for a multipart request
*
* @note: This method makes a multipart request. See [[this.form]] to
* make HTML style form submissions.
*
* @example
* request.fields({'name': 'virk', age: 22})
*/
fields(values) {
this.request.field(values);
return this;
}
/**
* Upload file for a multipart request. Either you can pass path to a
* file, a readable stream, or a buffer
*
* @example
* request.file('avatar', 'absolute/path/to/file')
* request.file('avatar', createReadStream('./path/to/file'))
*/
file(name, value, options) {
this.request.attach(name, value, options);
return this;
}
/**
* Set form values. Calling this method will set the content type
* to "application/x-www-form-urlencoded".
*
* @example
* request.form({
* email: 'virk@adonisjs.com',
* password: 'secret'
* })
*/
form(values) {
this.type("form");
this.request.send(values);
return this;
}
/**
* Set JSON body for the request. Calling this method will set
* the content type to "application/json".
*
* @example
* request.json({
* email: 'virk@adonisjs.com',
* password: 'secret'
* })
*/
json(values) {
this.type("json");
this.request.send(values);
return this;
}
qs(key, value) {
if (!value) {
this.request.query(typeof key === "string" ? key : _ApiRequest.qsSerializer(key));
} else {
this.request.query(_ApiRequest.qsSerializer({ [key]: value }));
}
return this;
}
/**
* Set timeout for the request.
*
* @example
* request.timeout(5000)
* request.timeout({ response: 5000, deadline: 60000 })
*/
timeout(ms) {
this.request.timeout(ms);
return this;
}
/**
* Set content-type for the request
*
* @example
* request.type('json')
*/
type(value) {
this.request.type(value);
return this;
}
/**
* Set "accept" header in the request
*
* @example
* request.accept('json')
*/
accept(type) {
this.request.accept(type);
return this;
}
/**
* Follow redirects from the response
*
* @example
* request.redirects(3)
*/
redirects(count) {
this.request.redirects(count);
return this;
}
/**
* Set basic auth header from user and password
*
* @example
* request.basicAuth('foo@bar.com', 'secret')
*/
basicAuth(user, password) {
this.request.auth(user, password, { type: "basic" });
return this;
}
/**
* Pass auth bearer token as authorization header.
*
* @example
* request.apiToken('tokenValue')
*/
bearerToken(token) {
this.request.auth(token, { type: "bearer" });
return this;
}
/**
* Set the ca certificates to trust
*/
ca(certificate) {
this.request.ca(certificate);
return this;
}
/**
* Set the client certificates
*/
cert(certificate) {
this.request.cert(certificate);
return this;
}
/**
* Set the client private key(s)
*/
privateKey(key) {
this.request.key(key);
return this;
}
/**
* Set the client PFX or PKCS12 encoded private key and certificate chain
*/
pfx(key) {
this.request.pfx(key);
return this;
}
/**
* Does not reject expired or invalid TLS certs. Sets internally rejectUnauthorized=true
*/
disableTLSCerts() {
this.request.disableTLSCerts();
return this;
}
/**
* Trust broken HTTPs connections on localhost
*/
trustLocalhost(trust = true) {
this.request.trustLocalhost(trust);
return this;
}
/**
* Dump request headers
*/
dumpHeaders() {
this.#valuesToDump.add("headers");
return this;
}
/**
* Dump request cookies
*/
dumpCookies() {
this.#valuesToDump.add("cookies");
return this;
}
/**
* Dump request body
*/
dumpBody() {
this.#valuesToDump.add("body");
return this;
}
/**
* Dump request
*/
dump() {
this.#valuesToDump.add("request");
this.dumpCookies();
this.dumpHeaders();
this.dumpBody();
return this;
}
/**
* Retry a failing request. Along with the count, you can also define
* a callback to decide how long the request should be retried.
*
* The max count is applied regardless of whether callback is defined
* or not
*
* The following response codes are considered failing.
* - 408
* - 413
* - 429
* - 500
* - 502
* - 503
* - 504
* - 521
* - 522
* - 524
*
* The following error codes are considered failing.
* - 'ETIMEDOUT'
* - 'ECONNRESET'
* - 'EADDRINUSE'
* - 'ECONNREFUSED'
* - 'EPIPE'
* - 'ENOTFOUND'
* - 'ENETUNREACH'
* - 'EAI_AGAIN'
*/
retry(count, retryUntilCallback) {
if (retryUntilCallback) {
this.request.retry(count, (error, response) => {
return retryUntilCallback(error, new ApiResponse(this, response, this.config, this.#assert));
});
return this;
}
this.request.retry(count);
return this;
}
/**
* Make the API request
*/
async send() {
this.#instantiateHooksRunners();
await this.#runSetupHooks();
const response = await this.#sendRequest();
await this.#runTeardownHooks(response);
return response;
}
/**
* Implementation of `then` for the promise API
*/
then(resolve, reject) {
return this.send().then(resolve, reject);
}
/**
* Implementation of `catch` for the promise API
*/
catch(reject) {
return this.send().catch(reject);
}
/**
* Implementation of `finally` for the promise API
*/
finally(fullfilled) {
return this.send().finally(fullfilled);
}
/**
* Required when Promises are extended
*/
get [Symbol.toStringTag]() {
return this.constructor.name;
}
};
// src/client.ts
var ApiClient = class extends Macroable3 {
/**
* Invoked when a new instance of request is created
*/
static #onRequestHandlers = [];
/**
* Hooks handlers to pass onto the request
*/
static #hooksHandlers = {
setup: [],
teardown: []
};
static #customCookiesSerializer;
#baseUrl;
#assert;
constructor(baseUrl, assert) {
super();
this.#baseUrl = baseUrl;
this.#assert = assert;
}
/**
* Remove all globally registered setup hooks
*/
static clearSetupHooks() {
this.#hooksHandlers.setup = [];
return this;
}
/**
* Remove all globally registered teardown hooks
*/
static clearTeardownHooks() {
this.#hooksHandlers.teardown = [];
return this;
}
/**
* Clear on request handlers registered using "onRequest"
* method
*/
static clearRequestHandlers() {
this.#onRequestHandlers = [];
return this;
}
/**
* Register a handler to be invoked everytime a new request
* instance is created
*/
static onRequest(handler) {
this.#onRequestHandlers.push(handler);
return this;
}
/**
* Register setup hooks. Setup hooks are called before the request
*/
static setup(handler) {
this.#hooksHandlers.setup.push(handler);
return this;
}
/**
* Register teardown hooks. Teardown hooks are called before the request
*/
static teardown(handler) {
this.#hooksHandlers.teardown.push(handler);
return this;
}
/**
* Register a custom cookies serializer
*/
static cookiesSerializer(serailizer) {
this.#customCookiesSerializer = serailizer;
return this;
}
/**
* Create an instance of the request
*/
request(endpoint, method) {
const hooks = this.constructor.#hooksHandlers;
const requestHandlers = this.constructor.#onRequestHandlers;
const cookiesSerializer = this.constructor.#customCookiesSerializer;
let baseUrl = this.#baseUrl;
const envHost = process.env.HOST;
const envPort = process.env.PORT;
if (!baseUrl && envHost && envPort) {
baseUrl = `http://${envHost}:${envPort}`;
}
const request = new ApiRequest(
{
baseUrl,
method,
endpoint,
hooks,
serializers: { cookie: cookiesSerializer }
},
this.#assert
);
requestHandlers.forEach((handler) => handler(request));
return request;
}
/**
* Create an instance of the request for GET method
*/
get(endpoint) {
return this.request(endpoint, "GET");
}
/**
* Create an instance of the request for POST method
*/
post(endpoint) {
return this.request(endpoint, "POST");
}
/**
* Create an instance of the request for PUT method
*/
put(endpoint) {
return this.request(endpoint, "PUT");
}
/**
* Create an instance of the request for PATCH method
*/
patch(endpoint) {
return this.request(endpoint, "PATCH");
}
/**
* Create an instance of the request for DELETE method
*/
delete(endpoint) {
return this.request(endpoint, "DELETE");
}
/**
* Create an instance of the request for HEAD method
*/
head(endpoint) {
return this.request(endpoint, "HEAD");
}
/**
* Create an instance of the request for OPTIONS method
*/
options(endpoint) {
return this.request(endpoint, "OPTIONS");
}
};
// index.ts
import { TestContext } from "@japa/runner/core";
function apiClient(options) {
return function() {
TestContext.getter(
"client",
function() {
return new ApiClient(typeof options === "string" ? options : options?.baseURL, this.assert);
},
true
);
};
}
export {
ApiClient,
ApiRequest,
ApiResponse,
apiClient
};