@pact-foundation/pact
Version:
Pact for all things Javascript
233 lines • 10.5 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.PactV3 = void 0;
const pact_core_1 = require("@pact-foundation/pact-core");
const ramda_1 = require("ramda");
const fs = require("node:fs");
const package_json_1 = require("../../package.json");
const logger_1 = __importDefault(require("../common/logger"));
const matchingRules_1 = require("../common/matchingRules");
const display_1 = require("./display");
const ffi_1 = require("./ffi");
const matchers_1 = require("./matchers");
const types_1 = require("./types");
const readBinaryData = (file) => {
try {
const body = fs.readFileSync(file);
return body;
}
catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
throw new Error(`unable to read file for binary request: ${error.message}`);
}
};
class PactV3 {
opts;
states = [];
pact;
interaction;
constructor(opts) {
this.opts = opts;
this.setup();
}
// JSON object interface for V3, to aid with migration from the previous major version
addInteraction(interaction) {
if (interaction.uponReceiving === '') {
throw new Error("must provide a valid interaction description via 'uponReceiving'");
}
(interaction.states || []).forEach((s) => {
this.given(s.description, s.parameters);
});
this.uponReceiving(interaction.uponReceiving);
this.withRequest(interaction.withRequest);
this.willRespondWith(interaction.willRespondWith);
return this;
}
// TODO: this currently must be called before other methods, else it won't work
given(providerState, parameters) {
if (parameters) {
const json = JSON.stringify(parameters);
// undefined arguments not supported (invalid JSON)
if (json === undefined) {
throw new Error(`Invalid provider state parameter received. Parameters must not be undefined. Received: ${parameters}`);
}
// Check nested objects
const jsonParsed = JSON.parse(json);
if (!(0, ramda_1.equals)(parameters, jsonParsed)) {
throw new Error(`Invalid provider state parameter received. Parameters must not contain undefined values. Received: ${parameters}`);
}
}
this.states.push({ description: providerState, parameters });
return this;
}
uponReceiving(description) {
this.interaction = this.pact.newInteraction(description);
this.states.forEach((s) => {
if (s.parameters) {
this.interaction.givenWithParams(s.description, JSON.stringify(s.parameters));
}
else {
this.interaction.given(s.description);
}
});
return this;
}
withRequest(req) {
if (req.body) {
this.interaction.withRequestBody((0, matchers_1.matcherValueOrString)(req.body), req.contentType ||
(0, ffi_1.contentTypeFromHeaders)(req.headers, 'application/json'));
}
(0, ffi_1.setRequestDetails)(this.interaction, req);
return this;
}
withRequestBinaryFile(req, contentType, file) {
const body = readBinaryData(file);
this.interaction.withRequestBinaryBody(body, contentType);
(0, ffi_1.setRequestDetails)(this.interaction, req);
return this;
}
/**
* Applies matching rules to the consumer request.
* Matching rules allow you to define flexible matching criteria for request attributes
* beyond exact equality (e.g., regex patterns, type matching, number ranges).
*
* @param req - The request configuration (method, path, headers, etc.)
* @param rules - The matching rules.
* @returns The PactV3 instance for method chaining
*/
withRequestMatchingRules(req, rules) {
(0, matchingRules_1.validateRules)(rules);
const ffiRules = (0, matchingRules_1.convertRulesToFFI)(rules);
this.interaction.withRequestMatchingRules(JSON.stringify(ffiRules));
(0, ffi_1.setRequestDetails)(this.interaction, req);
return this;
}
/**
* Applies matching rules to the provider response.
* Matching rules allow you to define flexible matching criteria for response attributes
* beyond exact equality (e.g., regex patterns, type matching, number ranges).
*
* @param req - The request configuration (method, path, headers, etc.)
* @param rules - The matching rules.
* @returns The PactV3 instance for method chaining
*/
withResponseMatchingRules(req, rules) {
(0, matchingRules_1.validateRules)(rules);
const ffiRules = (0, matchingRules_1.convertRulesToFFI)(rules);
this.interaction.withResponseMatchingRules(JSON.stringify(ffiRules));
(0, ffi_1.setRequestDetails)(this.interaction, req);
return this;
}
/**
* Sets up the expected consumer request with multipart file upload data.
* This is useful for testing APIs that accept multipart/form-data uploads.
*
* @param req - The request configuration (method, path, headers, etc.)
* @param contentType - The content type of the multipart body (e.g., 'multipart/form-data')
* @param file - Path to the file containing the multipart body content
* @param mimePartName - The name of the mime part in the multipart body
* @param boundary - Optional boundary string for the multipart content. If not provided, will be passed as undefined.
* @returns The PactV3 instance for method chaining
*/
withRequestMultipartFileUpload(req, contentType, file, mimePartName, boundary) {
this.interaction.withRequestMultipartBody(contentType, file, mimePartName, boundary);
(0, ffi_1.setRequestDetails)(this.interaction, req);
return this;
}
willRespondWith(res) {
(0, ffi_1.setResponseDetails)(this.interaction, res);
if (res.body) {
this.interaction.withResponseBody((0, matchers_1.matcherValueOrString)(res.body), res.contentType ||
(0, ffi_1.contentTypeFromHeaders)(res.headers, 'application/json'));
}
this.states = [];
return this;
}
withResponseBinaryFile(res, contentType, file) {
const body = readBinaryData(file);
this.interaction.withResponseBinaryBody(body, contentType);
(0, ffi_1.setResponseDetails)(this.interaction, res);
return this;
}
/**
* Sets up the expected provider response with multipart file upload data.
* This is useful for testing APIs that respond with multipart/form-data content.
*
* @param res - The response configuration (status, headers, etc.)
* @param contentType - The content type of the multipart body (e.g., 'multipart/form-data')
* @param file - Path to the file containing the multipart body content
* @param mimePartName - The name of the mime part in the multipart body
* @param boundary - Optional boundary string for the multipart content. If not provided, will be passed as undefined.
* @returns The PactV3 instance for method chaining
*/
withResponseMultipartFileUpload(res, contentType, file, mimePartName, boundary) {
this.interaction.withResponseMultipartBody(contentType, file, mimePartName, boundary);
(0, ffi_1.setResponseDetails)(this.interaction, res);
return this;
}
async executeTest(testFn) {
const scheme = this.opts.tls ? 'https' : 'http';
const host = this.opts.host || '127.0.0.1';
const cors = this.opts.cors ?? true;
const config = JSON.stringify({ corsPreflight: cors });
const port = this.pact.pactffiCreateMockServerForTransport(host, scheme, config, this.opts.port);
if (port <= 0) {
throw new Error(`Failed to start mock server: received error code ${port}`);
}
const server = { port, url: `${scheme}://${host}:${port}`, id: 'unknown' };
let val;
let error;
try {
val = await testFn(server);
}
catch (e) {
error = e instanceof Error ? e : new Error(String(e));
}
const matchingResults = this.pact.mockServerMismatches(port);
const errors = (0, display_1.filterMissingFeatureFlag)(matchingResults);
const success = this.pact.mockServerMatchedSuccessfully(port);
// Scenario: Pact validation failed
if (!success && errors.length > 0) {
let errorMessage = 'Test failed for the following reasons:';
errorMessage += `\n\n ${(0, display_1.generateMockServerError)(matchingResults, '\t')}`;
this.cleanup(false, server);
// If the tests throws an error, we need to rethrow the error, but print out
// any additional mock server errors to help the user understand what happened
// (The proximate cause here is often the HTTP 500 from the mock server,
// where the HTTP client then throws)
if (error) {
logger_1.default.error(errorMessage);
throw error;
}
// Test didn't throw, so we need to ensure the test fails
return Promise.reject(new Error(errorMessage));
}
// Scenario: test threw an error, but Pact validation was OK (error in client or test)
if (error) {
this.cleanup(false, server);
throw error;
}
// Scenario: Pact validation passed, test didn't throw - return the callback value
this.cleanup(true, server);
return val;
}
cleanup(success, server) {
if (success) {
this.pact.writePactFile(this.opts.dir || './pacts');
}
this.pact.cleanupMockServer(server.port);
this.setup();
}
// reset the internal state
// (this.pact cannot be re-used between tests)
setup() {
this.states = [];
this.pact = (0, pact_core_1.makeConsumerPact)(this.opts.consumer, this.opts.provider, this.opts.spec ?? types_1.SpecificationVersion.SPECIFICATION_VERSION_V3, this.opts.logLevel ?? 'info');
this.pact.addMetadata('pact-js', 'version', package_json_1.version);
}
}
exports.PactV3 = PactV3;
//# sourceMappingURL=pact.js.map