UNPKG

@pact-foundation/pact

Version:
233 lines 10.5 kB
"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