Skip to content
This repository was archived by the owner on Jul 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/requestSender/requestSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Omit<Paths[Path], 'parameters'>, undefined>,
>(app: express.Application, options: PathRequestOptions<Paths, Path, Method>): RequestReturn<Paths[Path][Method]> {
>(
app: express.Application,
options: PathRequestOptions<Paths, Path, Method>,
internalOptions: RequestSenderOptions = {}
): RequestReturn<Paths[Path][Method]> {
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);
Expand Down Expand Up @@ -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<RequestSender<Paths, Operations>>} A promise that resolves to a RequestSender object.
*
* @example
Expand All @@ -125,25 +130,27 @@ export { RequestSender };
*/
export async function createRequestSender<Paths extends PathsTemplate = never, Operations extends OperationsTemplate = never>(
openapiFilePath: Operations extends never ? never : string,
app: express.Application
app: express.Application,
options: RequestSenderOptions = {}
): Promise<RequestSender<Paths, Operations>> {
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: <Path extends keyof Paths, Method extends keyof OmitProperties<Omit<Paths[Path], 'parameters'>, undefined>>(
options: PathRequestOptions<Paths, Path, Method>
) => 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<T['operations'][keyof T['operations']]>) =>
sendRequest(app, { path, method: method as 'get', ...options });
sendRequest(app, { path, method: method as 'get', ...options }, baseOptions);
}

return returnObj as RequestSender<Paths, Operations>;
Expand Down
13 changes: 13 additions & 0 deletions src/requestSender/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
}
Expand Down
3 changes: 3 additions & 0 deletions tests/openapi3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ info:
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: /
- url: /api
paths:
/simple-request:
get:
Expand Down
28 changes: 28 additions & 0 deletions tests/supertest/requestSender.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<paths, operations>('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) => {
Expand Down Expand Up @@ -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<paths, operations>('tests/openapi3.yaml', expressApp, {
baseUrl: '/api',
});

const res = await requestSender.sendRequest({ method: 'get', path: '/simple-request' });
expect(res).toHaveProperty('body', { message: 'Hello, World!' });
});
});
});
Loading