From 8f301a5e8d2eff2cdf2179f02e0c5d8c581c2a45 Mon Sep 17 00:00:00 2001 From: CptSchnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:45:10 +0300 Subject: [PATCH] feat: added baseUrl option --- src/requestSender/requestSender.ts | 19 ++++++++++++------ src/requestSender/types.ts | 13 +++++++++++++ tests/openapi3.yaml | 3 +++ tests/supertest/requestSender.spec.ts | 28 +++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/requestSender/requestSender.ts b/src/requestSender/requestSender.ts index 912232c..c310908 100644 --- a/src/requestSender/requestSender.ts +++ b/src/requestSender/requestSender.ts @@ -8,15 +8,19 @@ import oasNormalize from 'oas-normalize'; import type { OmitProperties } from 'ts-essentials'; import type { OpenAPIV3 } from 'openapi-types'; import { PathsTemplate, Methods, OperationsTemplate } from '../common/types'; -import type { PathRequestOptions, RequestOptions, OperationsNames, RequestSender, RequestReturn } from './types'; +import type { PathRequestOptions, RequestOptions, OperationsNames, RequestSender, RequestReturn, RequestSenderOptions } from './types'; function sendRequest< Paths extends PathsTemplate, Path extends keyof Paths, Method extends keyof OmitProperties, undefined>, ->(app: express.Application, options: PathRequestOptions): RequestReturn { +>( + app: express.Application, + options: PathRequestOptions, + internalOptions: RequestSenderOptions = {} +): RequestReturn { const method = options.method as Methods; - let actualPath = options.path as string; + let actualPath = (internalOptions.baseUrl ?? '') + (options.path as string); if ('pathParams' in options && options.pathParams !== undefined) { actualPath = Object.entries(options.pathParams).reduce((acc, [key, value]) => acc.replace(`{${key}}`, value as string), actualPath); @@ -100,6 +104,7 @@ export { RequestSender }; * @template Operations - The type representing the operations defined in the OpenAPI specification. * @param {string} openapiFilePath - The file path to the OpenAPI specification file. * @param {express.Application} app - The Express application instance. + * @param {RequestSenderOptions} [options] - Optional configuration options for the request sender. * @returns {Promise>} A promise that resolves to a RequestSender object. * * @example @@ -125,25 +130,27 @@ export { RequestSender }; */ export async function createRequestSender( openapiFilePath: Operations extends never ? never : string, - app: express.Application + app: express.Application, + options: RequestSenderOptions = {} ): Promise> { const fileContent = readFileSync(openapiFilePath, 'utf-8'); const normalized = new oasNormalize(fileContent); const derefed = await normalized.deref(); const operationsPathAndMethod = getOperationsPathAndMethod(derefed); + const baseOptions = options; const returnObj = { // eslint-disable-next-line @typescript-eslint/promise-function-async, @typescript-eslint/explicit-function-return-type sendRequest: , undefined>>( options: PathRequestOptions - ) => sendRequest(app, options), + ) => sendRequest(app, options, baseOptions), }; for (const [operation, { path, method }] of Object.entries(operationsPathAndMethod)) { // @ts-expect-error as we iterate over all the operations, the operationId is always defined // eslint-disable-next-line @typescript-eslint/explicit-function-return-type returnObj[operation] = async (options: RequestOptions) => - sendRequest(app, { path, method: method as 'get', ...options }); + sendRequest(app, { path, method: method as 'get', ...options }, baseOptions); } return returnObj as RequestSender; diff --git a/src/requestSender/types.ts b/src/requestSender/types.ts index 3d8b1c7..ba8fe4f 100644 --- a/src/requestSender/types.ts +++ b/src/requestSender/types.ts @@ -4,6 +4,19 @@ import type { OmitProperties, OptionalKeys, Prettify, RequiredKeys } from 'ts-es import type * as supertest from 'supertest'; import type { AddIfNotNever, OperationsTemplate, PathsTemplate, PickWritable } from '../common/types'; +/** + * Configuration options for the request sender. + * + * @interface RequestSenderOptions + */ +export interface RequestSenderOptions { + /** + * Base URL to prepend to all request paths. + * @type {string} + */ + baseUrl?: string; +} + interface Headers { headers?: Record; } diff --git a/tests/openapi3.yaml b/tests/openapi3.yaml index d1d9aaf..972ea85 100644 --- a/tests/openapi3.yaml +++ b/tests/openapi3.yaml @@ -6,6 +6,9 @@ info: license: name: MIT url: https://opensource.org/licenses/MIT +servers: + - url: / + - url: /api paths: /simple-request: get: diff --git a/tests/supertest/requestSender.spec.ts b/tests/supertest/requestSender.spec.ts index 9197503..93bc6f7 100644 --- a/tests/supertest/requestSender.spec.ts +++ b/tests/supertest/requestSender.spec.ts @@ -32,6 +32,19 @@ describe('requestSender', () => { expectTypeOf(requestSender.simpleRequest).parameter(1).toBeUndefined(); }); + it('should work with a custom base URL', async () => { + expect.assertions(2); + expressApp.get('/api/simple-request', (req, res) => { + expect(req.query).toEqual({}); + res.json({ message: 'Hello, World!' }); + }); + const customRequestSender = await createRequestSender('tests/openapi3.yaml', expressApp, { + baseUrl: '/api', + }); + const res = await customRequestSender.simpleRequest(); + expect(res).toHaveProperty('body', { message: 'Hello, World!' }); + }); + it('should allow to add headers to the request even when none are required', async () => { expect.assertions(1); expressApp.get('/simple-request', (req, res) => { @@ -237,5 +250,20 @@ describe('requestSender', () => { it('should only suggest the paths present in the openapi definition', () => { expectTypeOf(requestSender.sendRequest).parameter(0).toMatchTypeOf<{ path: keyof paths }>(); }); + + it('should support baseUrl in the options', async () => { + expect.assertions(2); + expressApp.get('/api/simple-request', (req, res) => { + expect(req.query).toEqual({}); + res.json({ message: 'Hello, World!' }); + }); + + const requestSender = await createRequestSender('tests/openapi3.yaml', expressApp, { + baseUrl: '/api', + }); + + const res = await requestSender.sendRequest({ method: 'get', path: '/simple-request' }); + expect(res).toHaveProperty('body', { message: 'Hello, World!' }); + }); }); });