fluentrest-ts
Version:
A lightweight, fluent TypeScript API testing library inspired by Java's RestAssured. Built on top of Axios, JSONPath, and Joi for powerful request handling and response validation.
289 lines (288 loc) • 12 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestBuilder = void 0;
const https_proxy_agent_1 = require("https-proxy-agent");
const form_data_1 = __importDefault(require("form-data"));
const fs_1 = __importDefault(require("fs"));
const request_executor_1 = require("./request-executor");
const config_1 = require("./config");
const qs_1 = __importDefault(require("qs"));
/**
* A fluent builder for configuring HTTP requests.
* Provides `given*` and `when*` methods to define and trigger requests.
*/
class RequestBuilder {
constructor(overrides = {}) {
this.config = {};
this.logToFile = false;
this.logLevel = "info";
const defaults = (0, config_1.getMergedDefaults)(overrides);
this.config.baseURL = defaults.baseUrl;
this.config.timeout = defaults.timeout;
this.logLevel = defaults.logLevel;
this.logToFile = false;
this.applyProxy(defaults.proxy);
}
/**
* Update the constructor to always call a utility method that handles the logic for proxy setup.
*/
applyProxy(proxy) {
if (!proxy)
return;
if (typeof proxy === "string") {
const agent = new https_proxy_agent_1.HttpsProxyAgent(proxy);
// Inspect base URL or proxy protocol to determine correct assignment
const isHttpsProxy = proxy.startsWith("https://");
this.proxyAgent = agent;
this.proxyOverride = undefined;
// PROTOCOL-AWARE assignment
this.config.httpAgent = isHttpsProxy ? undefined : agent;
this.config.httpsAgent = isHttpsProxy ? agent : undefined;
}
else if (proxy.host && proxy.port) {
// Classic Axios proxy config
this.proxyOverride = proxy;
this.proxyAgent = undefined;
// Clear both agents when classic config is used
delete this.config.httpAgent;
delete this.config.httpsAgent;
this.config.proxy = this.proxyOverride;
}
}
/** Sets the base URL for the request. */
setBaseUrl(url) {
this.config.baseURL = url;
return this;
}
/** Sets the timeout duration in milliseconds. */
setTimeout(ms) {
this.config.timeout = ms;
return this;
}
/** Overrides the log level for this request. */
setLogLevel(level) {
this.logLevel = level;
return this;
}
/**
* Sets a proxy for this request.
* Accepts either an Axios proxy config object or a proxy URL (for HTTPS tunneling).
*
* @param proxy Axios proxy config object or proxy URL string
*/
setProxy(proxy) {
if (typeof proxy === "string" && !/^https?:\/\//.test(proxy)) {
throw new Error(`Invalid proxy URL: "${proxy}". Must start with http:// or https://`);
}
if (typeof proxy !== "string" && (!proxy.host || !proxy.port)) {
throw new Error(`Invalid Axios proxy config. Must include 'host' and 'port'.`);
}
this.applyProxy(proxy);
return this;
}
/** Removes all proxy config (agent or classic Axios-style) for this request. */
clearProxy() {
this.proxyOverride = undefined;
this.proxyAgent = undefined;
delete this.config.proxy;
delete this.config.httpAgent;
delete this.config.httpsAgent;
return this;
}
/** Enables or disables file-based logging. */
enableFileLogging(enable) {
this.logToFile = enable;
return this;
}
/** Adds a request header. */
givenHeader(key, value) {
this.config.headers = { ...this.config.headers, [key]: value };
return this;
}
/** Adds a query string parameter. */
givenQueryParam(key, value) {
this.config.params = { ...this.config.params, [key]: value };
return this;
}
/**
* Adds a JSON request body.
* Adds a JSON string body
* Adds multipart form-data from file paths or strings.
* Automatically attaches files if they exist on disk.
*/
givenBody(body, contentType = "application/json") {
switch (contentType) {
case "application/json":
this.config.data = typeof body === "string" ? body : JSON.stringify(body);
break;
case "application/x-www-form-urlencoded":
this.config.data = typeof body === "string" ? body : qs_1.default.stringify(body);
break;
case "multipart/form-data":
const form = new form_data_1.default();
for (const key in body) {
const value = body[key];
const isFile = fs_1.default.existsSync(value);
form.append(key, isFile ? fs_1.default.createReadStream(value) : value);
}
this.config.data = form;
this.config.headers = {
...this.config.headers,
...form.getHeaders(),
};
break;
default:
this.config.data = body;
}
if (contentType !== "multipart/form-data") {
this.config.headers = {
...this.config.headers,
"Content-Type": contentType,
};
}
return this;
}
/** Prints the current request config as a snapshot for debugging. */
debug() {
console.dir(this.getSnapshot(), { depth: null, colors: true });
return this;
}
/** Returns a snapshot of the request config (used for debugging or logging). */
getSnapshot() {
return {
method: this.config.method,
url: this.config.url,
headers: this.config.headers,
params: this.config.params,
data: this.config.data,
timeout: this.config.timeout,
baseURL: this.config.baseURL,
};
}
/**
* Sends a GET request to the specified endpoint using the current request configuration.
* Returns a ResponseValidator which allows chaining expectations.
*
* @example
* const response = await fluentRest().whenGet("/users/1");
*/
async whenGet(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("get", endpoint, overrides);
}
/**
* Sends a POST request to the specified endpoint with the current configuration.
* Use `givenBody()` before this to attach a payload.
*
* @example
* const response = await fluentRest().givenBody({ name: "John" }).whenPost("/users");
*/
async whenPost(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("post", endpoint, overrides);
}
/**
* Sends a PUT request to the specified endpoint using the current request configuration.
* Typically used for full updates to a resource.
*/
async whenPut(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("put", endpoint, overrides);
}
/**
* Sends a DELETE request to the specified endpoint.
* Useful for resource cleanup or deletion tests.
*/
async whenDelete(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("delete", endpoint, overrides);
}
/**
* Sends a PATCH request to the specified endpoint using the current request configuration.
* Typically used for partial updates to a resource.
*/
async whenPatch(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("patch", endpoint, overrides);
}
/**
* Sends a HEAD request to the given endpoint using the current builder configuration.
* @param endpoint - The API endpoint path to send the request to.
* @returns A ResponseValidator for performing post-response assertions.
*/
async whenHead(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("head", endpoint, overrides);
}
/**
* Sends an OPTIONS request to the given endpoint using the current builder configuration.
* @param endpoint - The API endpoint path to send the request to.
* @returns A ResponseValidator for performing post-response assertions.
*/
async whenOptions(endpoint) {
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
return new request_executor_1.RequestExecutor(this.config, this.logLevel, this.logToFile).send("options", endpoint, overrides);
}
/**
* Returns the underlying Axios request configuration used in the builder including proxy settings.
* Useful for debugging or inspection.
* @returns The AxiosRequestConfig used in the current request builder.
*/
getConfig() {
return {
...this.config,
...(this.proxyOverride ? { proxy: this.proxyOverride } : {}),
...(this.proxyAgent ? { httpAgent: this.proxyAgent, httpsAgent: this.proxyAgent } : {}),
};
}
/**
* Returns the log level configured for this builder instance.
* Useful to determine logging behavior in tests.
* @returns The active LogLevel (e.g., 'debug', 'info', 'none').
*/
getLogLevel() {
return this.logLevel;
}
/**
* Indicates whether logging to file is enabled for this builder instance.
* @returns True if logs should be persisted to file, false otherwise.
*/
shouldLogToFile() {
return this.logToFile;
}
/**
* Executes a request and runs expectations in one step.
* Useful for compact tests when you want to send a request and immediately assert the response.
* Optionally accepts request overrides like headers, body, and query params.
*/
async sendAndExpect(method, endpoint, expect, configOverrides) {
// Clone the current builder for immutability
let builder = this;
// Apply override headers
if (configOverrides?.headers) {
for (const [key, value] of Object.entries(configOverrides.headers)) {
builder = builder.givenHeader(key, value);
}
}
// Apply override body
if (configOverrides?.body) {
builder = builder.givenBody(configOverrides.body);
}
// Apply override query parameters
if (configOverrides?.params) {
for (const [key, value] of Object.entries(configOverrides.params)) {
builder = builder.givenQueryParam(key, value); // assumes this method exists
}
}
// Create a new executor and perform the request
const executor = new request_executor_1.RequestExecutor(builder.getConfig(), builder.getLogLevel(), builder.shouldLogToFile());
const overrides = this.proxyOverride ? { proxy: this.proxyOverride } : undefined;
const responseValidator = await executor.send(method, endpoint, overrides);
// Pass response to assertion function
expect(responseValidator);
}
}
exports.RequestBuilder = RequestBuilder;