apiveritas
Version:
Lightweight CLI tool for consumer-driven API contract testing via JSON schema and payload comparisons.
125 lines (102 loc) • 4.58 kB
text/typescript
/**
* @file ApiCaller.ts
* @author Mario Galea
* @description
* Responsible for executing a list of API requests defined in a Test Suite.
* This class handles HTTP request logic using Axios, saves the responses to disk, and logs execution details.
*/
import axios from 'axios';
import { IApiRequest } from '../../interfaces/IApiRequest';
import { ResponseSaver } from '../utils/ResponseSaver';
import { Logger } from '../utils/Logger';
import chalk from 'chalk';
type AxiosRequestConfig = Parameters<typeof axios>[0];
/**
* Executes a list of API requests, logs outcomes, and saves payloads for contract testing.
*/
export class ApiCaller {
private requests: IApiRequest[];
private logger: Logger;
/**
* Constructs a new ApiCaller instance.
*
* @param requests - An array of API requests to be executed.
* @param logger - A logger instance for structured logging. Defaults to a new Logger instance.
* @param baseUrl - The base URL to prepend to all request paths unless overridden per request.
*/
constructor(requests: IApiRequest[], logger: Logger = new Logger(), private baseUrl: string = '') {
this.requests = requests;
this.logger = logger;
this.baseUrl = baseUrl;
}
/**
* Iterates over the provided list of API requests, executes them using Axios, and saves each response to disk.
*
* - Logs all actions using the provided Logger.
* - Automatically validates HTTP status codes if `expectedStatus` is defined in the request.
* - Responses are saved to the payloads directory using the ResponseSaver utility.
* - Supports per-request `baseUrl` override (useful for mock server mode).
*
* @returns A Promise that resolves when all API calls have completed (successfully or with handled failure).
*
* @throws If a request is missing its required `testSuite` field, an error is thrown immediately.
* If a request is missing its `url`, an error is logged and the request is skipped.
*/
public async callAll(): Promise<void> {
this.logger.info(`Base URL: ${this.baseUrl}`);
const saver = new ResponseSaver();
this.logger.info(chalk.white.bold.underline('Test Run:\n'));
for (const req of this.requests) {
const method = (req.method ?? 'GET').toUpperCase();
const safeName = req.name.replace(/\s+/g, '_');
const testSuite = req.testSuite;
if (!testSuite) {
throw new Error(`Missing testSuite name in request: ${req.name}`);
}
if (!req.url) {
this.logger.error(`Missing URL in request: ${req.name}`);
continue;
}
// Allow per-request base URL override
const effectiveBaseUrl = req.baseUrl ?? this.baseUrl;
const fullUrl = effectiveBaseUrl + req.url;
console.log(fullUrl);
this.logger.info(`Calling URL: ${fullUrl} (method: ${method})`);
try {
const axiosConfig: AxiosRequestConfig = {
url: fullUrl,
method,
auth: req.auth,
data: req.body ?? undefined,
validateStatus: () => true, // allow all HTTP responses
};
this.logger.debug(`Calling [${req.name}] with config:`);
this.logger.debug(JSON.stringify(axiosConfig, null, 2));
const response = await axios(axiosConfig);
if (req.expectedStatus && response.status !== req.expectedStatus) {
this.logger.error(`[${req.name}] returned status ${response.status}, expected ${req.expectedStatus}`);
} else {
this.logger.info(`[${req.name}] returned expected status ${response.status}`);
}
this.logger.debug(`Response from [${req.name}]`);
saver.saveResponse(testSuite, safeName, response.data);
this.logger.info(
`Saved response: payloads/${saver.getTimestampFolderName()}/${testSuite}/${safeName}.json`
);
} catch (error: unknown) {
if (error instanceof Error) {
this.logger.error(chalk.red(`❌ Failed to call [${req.name}]: ${error.message}`));
// Cast error to any to access response (Axios error)
const anyErr = error as any;
if (anyErr.response) {
this.logger.error(chalk.red(`-> HTTP Status: ${anyErr.response.status}`));
this.logger.error(chalk.red(`-> Response body: ${JSON.stringify(anyErr.response.data)}`));
}
} else {
this.logger.error(chalk.red(`❌ Failed to call [${req.name}]: Unknown error`));
}
}
console.log('');
}
}
}