UNPKG

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
"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;