Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

pactum

Package Overview
Dependencies
Maintainers
1
Versions
112
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pactum - npm Package Compare versions

Comparing version 1.0.18 to 1.0.19

2

package.json
{
"name": "pactum",
"version": "1.0.18",
"version": "1.0.19",
"description": "REST API endpoint testing tool with a mock server & compatible with pact.io for contract testing",

@@ -5,0 +5,0 @@ "main": "src/index.js",

const Interaction = require('../models/interaction');
const { PactumConfigurationError } = require('../helpers/errors');
const log = require('../helpers/logger');

@@ -67,2 +68,3 @@ const config = require('../config');

setDefaultPort(port) {
log.debug('Setting default port number for mock server', port);
if (typeof port !== 'number') {

@@ -82,2 +84,3 @@ throw new PactumConfigurationError(`Invalid default port number - ${port}`);

this.interactionIds.add(interactionObj.id);
log.debug('Added default mock interaction with id', interactionObj.id);
return interactionObj.id;

@@ -102,2 +105,3 @@ }

}
log.debug('Added default mock interactions with ids', ids);
return ids;

@@ -114,2 +118,3 @@ }

this.interactionIds.add(interactionObj.id);
log.debug('Added default pact interactions with id', interactionObj.id);
return interactionObj.id;

@@ -134,2 +139,3 @@ }

}
log.debug('Added default pact interactions with ids', ids);
return ids;

@@ -161,2 +167,3 @@ }

}
log.debug('Cleared default interactions with ids', ids);
}

@@ -163,0 +170,0 @@

const fs = require('fs');
const path = require('path');
const phin = require('phin');
const config = require('../config');
const helper = require('../helpers/helper');
const store = require('../helpers/store');

@@ -56,31 +56,3 @@ const { PactumConfigurationError } = require('../helpers/errors');

publish(options) {
const pactFilesOrDirs = options.pactFilesOrDirs;
const filePaths = new Set();
for (let i = 0; i < pactFilesOrDirs.length; i++) {
const pactFileOrDir = pactFilesOrDirs[i];
const stats = fs.lstatSync(pactFileOrDir);
if (stats.isDirectory()) {
const items = fs.readdirSync(pactFileOrDir);
for (let j = 0; j < items.length; j++) {
const item = items[j];
const itemPath = path.join(pactFileOrDir, item);
const childPathStats = fs.lstatSync(itemPath);
if (childPathStats.isDirectory()) {
const childItems = fs.readdirSync(itemPath);
for (let k = 0; k < childItems.length; k++) {
const childItem = childItems[k];
const childItemPath = path.join(itemPath, childItem);
const childItemStats = fs.lstatSync(childItemPath);
if (childItemStats.isFile()) {
filePaths.add(childItemPath);
}
}
} else {
filePaths.add(itemPath);
}
}
} else {
filePaths.add(pactFileOrDir);
}
}
const filePaths = helper.getLocalPactFiles(options.pactFilesOrDirs);
return _publish(filePaths, options);

@@ -103,17 +75,15 @@ }

for (const filePath of filePaths) {
const ext = path.extname(filePath);
if (ext === '.json') {
const pactFile = require(filePath);
const consumer = pactFile.consumer.name || config.pact.consumer || process.env.PACTUM_PACT_CONSUMER_NAME;
const provider = pactFile.provider.name;
consumers.add(consumer);
await phin({
url: `${pactBroker}/pacts/provider/${provider}/consumer/${consumer}/version/${consumerVersion}`,
method: 'PUT',
core: {
auth: `${pactBrokerUsername}:${pactBrokerPassword}`
},
data: pactFile
});
}
const rawData = fs.readFileSync(filePath);
const pactFile = JSON.parse(rawData);
const consumer = pactFile.consumer.name || config.pact.consumer || process.env.PACTUM_PACT_CONSUMER_NAME;
const provider = pactFile.provider.name;
consumers.add(consumer);
await phin({
url: `${pactBroker}/pacts/provider/${provider}/consumer/${consumer}/version/${consumerVersion}`,
method: 'PUT',
core: {
auth: `${pactBrokerUsername}:${pactBrokerPassword}`
},
data: pactFile
});
}

@@ -120,0 +90,0 @@ for (const consumer of consumers) {

const phin = require('phin');
const fs = require('fs');
const helper = require('../helpers/helper');
const Compare = require('../helpers/compare');
const log = require('../helpers/logger');
const { PactumConfigurationError } = require('../helpers/errors');
/**
* provider options
* @typedef {object} ProviderOptions
* @property {string} providerBaseUrl - running API provider host endpoint.
* @property {string} provider - name of the provider.
* @property {string} [providerVersion] - provider version, required to publish verification results to a broker
* @property {any} [stateHandlers] - provider state handlers. A map of 'string -> () => Promise', where each string is the state to setup, and the function is used to configure the state in the Provider.
* @property {any} [customProviderHeaders] - Header(s) to add to any requests to the provider service. eg { 'Authorization': 'Basic cGFjdDpwYWN0'}.
* @property {string[]} [pactFilesOrDirs] - array of local pact files or directories
* @property {string} [pactBrokerUrl] - URL of the Pact Broker to retrieve pacts from. Required if not using pactUrls.
* @property {string} [pactBrokerUsername] - username for Pact Broker basic authentication.
* @property {string} [pactBrokerPassword] - password for Pact Broker basic authentication.
* @property {string} [pactBrokerToken] - bearer token for Pact Broker authentication.
* @property {boolean} [publishVerificationResult] - publish verification result to Broker
* @property {string[]} [tags] - array of tags, used to filter pacts from the Broker.
*/
class Provider {
/**
* constructor
* @param {ProviderOptions} options - provider options
*/
constructor(options) {
if (!helper.isValidObject(options)) {
throw new PactumConfigurationError(`Invalid provider options provided - ${options}`);
}
this.pactBrokerUrl = options.pactBrokerUrl;
this.pactBrokerUsername = options.pactBrokerUsername;
this.pactBrokerPassword = options.pactBrokerPassword;
this.tags = options.tag || [];
this.pactBrokerToken = options.pactBrokerToken;
this.pactFilesOrDirs = options.pactFilesOrDirs;
// @property {string[]} [pactUrls] - array of HTTP-based URLs (e.g. from a broker). Required if not using a Broker.
this.tags = options.tags || [];
this.publishVerificationResult = options.publishVerificationResult;

@@ -17,27 +46,77 @@ this.stateHandlers = options.stateHandlers || {};

this.providerBaseUrl = options.providerBaseUrl;
this.providerVersion = options.providerVersion;
this.providerVersion = options.providerVersion;
this.customProviderHeaders = options.customProviderHeaders;
this.validateOptions();
this.testCount = 0;
this.testPassedCount = 0;
this.testFailedCount = 0;
this.testSkipped = 0;
}
validateOptions() {
if (!helper.isValidString(this.providerBaseUrl)) {
throw new PactumConfigurationError(`Invalid provider base url provided - ${this.providerBaseUrl}`);
}
if (!helper.isValidString(this.provider)) {
throw new PactumConfigurationError(`Invalid provider name provided - ${this.provider}`);
}
if (!this.pactBrokerUrl && !this.pactFilesOrDirs) {
throw new PactumConfigurationError(`Invalid pact-broker url (${this.pactBrokerUrl}) provided & invalid pact local files (${this.pactFilesOrDirs}) provided`);
}
if (this.customProviderHeaders && !helper.isValidObject(this.customProviderHeaders)) {
throw new PactumConfigurationError(`Invalid custom headers provided - ${this.customProviderHeaders}`);
}
if (this.stateHandlers) {
if (!helper.isValidObject(this.stateHandlers)) {
throw new PactumConfigurationError(`Invalid state handlers provided - ${this.stateHandlers}`);
}
for (const prop in this.stateHandlers) {
if (typeof this.stateHandlers[prop] !== 'function') {
throw new PactumConfigurationError(`Invalid state handlers function provided for - ${prop}`);
}
}
}
if (this.publishVerificationResult) {
if (!this.pactBrokerUrl) {
throw new PactumConfigurationError(`Invalid pact broker url provided - ${this.pactBrokerUrl}`);
}
if (!this.providerVersion) {
throw new PactumConfigurationError(`Invalid provider version provided - ${this.providerVersion}`);
}
}
if (this.pactFilesOrDirs) {
if (!Array.isArray(this.pactFilesOrDirs)) {
throw new PactumConfigurationError(`Invalid array of pact files or folders (${this.pactFilesOrDirs}) provided`);
}
if (this.pactFilesOrDirs.length === 0) {
throw new PactumConfigurationError(`Empty array of pact files or folders provided`);
}
}
}
async validate() {
log.info(`Provider Verification: `);
const providerPacts = await this._getLatestProviderPacts();
for (let i = 0; i < providerPacts.length; i++) {
const providerPact = providerPacts[i];
const versionString = providerPact.href.match(/\/version\/.*/g);
const consumerVersion = versionString[0].replace('/version/', '');
const consumerPactDetails = await this._getProviderConsumerPactDetails(providerPact.name, consumerVersion);
log.info();
log.info(` Consumer: ${providerPact.name} - ${consumerVersion}`);
const interactions = consumerPactDetails.interactions;
for (let j = 0; j < interactions.length; j++) {
const interaction = interactions[j];
const isValid = await this._validateInteraction(interaction);
log.info(` ${isValid.equal ? '√'.green : 'X'.red } Description: ${interaction.description}`);
if (isValid.message) {
log.warn(` ${isValid.message}`);
}
await this.validatePactsFromPactBroker();
await this.validatePactsFromLocal();
this.printSummary();
}
async validatePactsFromPactBroker() {
if (this.pactBrokerUrl) {
const providerPacts = await this._getLatestProviderPacts();
for (let i = 0; i < providerPacts.length; i++) {
const providerPact = providerPacts[i];
const versionString = providerPact.href.match(/\/version\/.*/g);
const consumerVersion = versionString[0].replace('/version/', '');
const consumerPactDetails = await this._getProviderConsumerPactDetails(providerPact.name, consumerVersion);
log.info();
log.info(` Consumer: ${providerPact.name} - ${consumerVersion}`);
const interactions = consumerPactDetails.interactions;
const success = await this.validateInteractions(interactions);
if (this.publishVerificationResult) {
const url = consumerPactDetails['_links']['pb:publish-verification-results']['href'];
const path = url.match(/\/pacts\/provider.*/g)[0];
await this._publishVerificationResults(path, isValid.equal);
await this._publishVerificationResults(path, success);
}

@@ -48,30 +127,69 @@ }

async validatePactsFromLocal() {
if (this.pactFilesOrDirs) {
const filePaths = helper.getLocalPactFiles(this.pactFilesOrDirs);
for (const filePath of filePaths) {
const rawData = fs.readFileSync(filePath);
const pactFile = JSON.parse(rawData);
const consumer = pactFile.consumer.name;
const provider = pactFile.provider.name;
if (this.provider === provider) {
log.info();
log.info(` Consumer: ${consumer}`);
const interactions = pactFile.interactions;
await this.validateInteractions(interactions);
} else {
log.warn(`Invalid provider ${provider} in ${filePath}`);
}
}
}
}
async validateInteractions(interactions) {
let success = true;
for (let j = 0; j < interactions.length; j++) {
this.testCount = this.testCount + 1;
const interaction = interactions[j];
const isValid = await this._validateInteraction(interaction);
if (isValid.equal) {
this.testPassedCount = this.testPassedCount + 1;
log.info(` ${'√'.green} ${interaction.description.gray}`);
} else {
success = false;
this.testFailedCount = this.testFailedCount + 1;
log.info(` ${'X'.red} ${interaction.description.gray}`);
log.error(` ${isValid.message.red}`);
}
}
return success;
}
async _getLatestProviderPacts() {
const response = await phin({
url: `${this.pactBrokerUrl}/pacts/provider/${this.provider}/latest`,
core: {
auth: `${this.pactBrokerUsername}:${this.pactBrokerPassword}`
},
method: 'GET'
});
const requestOptions = this._getPactBrokerRequestOptions();
requestOptions.url = `${this.pactBrokerUrl}/pacts/provider/${this.provider}/latest`;
requestOptions.method = 'GET';
log.debug('Fetching latest provider pacts', requestOptions);
const response = await phin(requestOptions);
if (response.statusCode === 200) {
const body = helper.getJson(response.body);
return body['_links']['pb:pacts'];
} else {
log.error(`Failed to fetch latest provider pacts. | Response: ${response.statusCode} - ${response.statusMessage}`);
return null;
}
return null;
}
async _getProviderConsumerPactDetails(consumer, consumerVersion) {
const response = await phin({
url: `${this.pactBrokerUrl}/pacts/provider/${this.provider}/consumer/${consumer}/version/${consumerVersion}`,
core: {
auth: `${this.pactBrokerUsername}:${this.pactBrokerPassword}`
},
method: 'GET'
});
const requestOptions = this._getPactBrokerRequestOptions();
requestOptions.url = `${this.pactBrokerUrl}/pacts/provider/${this.provider}/consumer/${consumer}/version/${consumerVersion}`;
requestOptions.method = 'GET';
log.debug('Fetching provider-consumer pacts', requestOptions);
const response = await phin(requestOptions);
if (response.statusCode === 200) {
const body = helper.getJson(response.body);
return body;
} else {
log.error(`Failed to fetch consumer pact details. | Response: ${response.statusCode} - ${response.statusMessage}`);
return null;
}
return null;
}

@@ -86,29 +204,132 @@

}
const actualResponse = await phin({
url: `${this.providerBaseUrl}${request.path}`,
method: request.method
});
const actualBody = helper.getJson(actualResponse.body);
const expectedBody = response.body;
let matchingRules = response.matchingRules;
if (!matchingRules) {
matchingRules = {};
const actualResponse = await phin(this._getInteractionRequestOptions(request));
return this._validateResponse(actualResponse, response);
}
async _publishVerificationResults(path, success) {
const requestOptions = this._getPactBrokerRequestOptions();
requestOptions.url = `${this.pactBrokerUrl}${path}`;
requestOptions.method = 'POST';
requestOptions.data = {
success,
providerApplicationVersion: this.providerVersion
};
log.debug('Publishing verification results.', requestOptions);
const response = await phin(requestOptions);
if (response.statusCode !== 200) {
log.error(`Failed to publish verification results. | Response: ${response.statusCode} - ${response.statusMessage}`);
}
const compare = new Compare();
return compare.jsonMatch(actualBody, expectedBody, matchingRules, '$.body');
}
_publishVerificationResults(path, success) {
return phin({
url: `${this.pactBrokerUrl}${path}`,
method: 'POST',
data: {
success,
providerApplicationVersion: this.providerVersion
_validateResponse(actual, expected) {
const isValidStatus = this._validateStatus(actual.statusCode, expected.status);
if (!isValidStatus.equal) {
return isValidStatus;
}
const isValidHeaders = this._validateHeaders(actual, expected);
if (!isValidHeaders.equal) {
return isValidHeaders;
}
return this._validateBody(actual, expected);
}
_validateStatus(actualStatus, expectedStatus) {
if (expectedStatus && actualStatus !== expectedStatus) {
return {
equal: false,
message: `HTTP status ${actualStatus} !== ${expectedStatus}`
};
} else {
return {
equal: true
};
}
}
_validateHeaders(actual, expected) {
if (expected.headers) {
let matchingRules = expected.matchingRules;
if (!matchingRules) {
matchingRules = {};
}
});
const compare = new Compare();
return compare.jsonMatch(actual.headers, expected.headers, matchingRules, '$.headers');
}
return {
equal: true
};
}
_validateBody(actual, expected) {
if (expected.body) {
const actualBody = helper.getJson(actual.body);
let matchingRules = expected.matchingRules;
if (!matchingRules) {
matchingRules = {};
}
const compare = new Compare();
return compare.jsonMatch(actualBody, expected.body, matchingRules, '$.body');
}
return {
equal: true
};
}
_getInteractionRequestOptions(request) {
const options = {
url: request.query ? `${this.providerBaseUrl}${request.path}?${request.query}` : `${this.providerBaseUrl}${request.path}`,
method: request.method,
headers: request.headers,
data: request.body
};
if (this.customProviderHeaders) {
if (!options.headers) {
options.headers = {};
}
for (const prop in this.customProviderHeaders) {
options.headers[prop] = this.customProviderHeaders[prop];
}
}
return options;
}
_getPactBrokerRequestOptions() {
const requestOptions = {};
if (this.pactBrokerUsername) {
requestOptions.core = {
auth: `${this.pactBrokerUsername}:${this.pactBrokerPassword}`
};
}
if (this.pactBrokerToken) {
requestOptions.headers = {
'authorization': `Basic ${this.pactBrokerToken}`
};
}
return requestOptions;
}
printSummary() {
log.info();
log.info(` ${this.testPassedCount} passing`.green);
if (this.testFailedCount > 0) {
log.info(` ${this.testFailedCount} failing`.red);
throw 'Provider Verification Failed';
}
}
}
module.exports = Provider;
const provider = {
/**
* validate provider
* @param {ProviderOptions} options - provider options
*/
validate(options) {
const providerObj = new Provider(options);
return providerObj.validate();
}
};
module.exports = provider;

@@ -0,1 +1,6 @@

const fs = require('fs');
const path = require('path');
const log = require('./logger');
const helper = {

@@ -132,2 +137,42 @@

return value !== null && typeof value === 'object' && !Array.isArray(value);
},
/**
* returns set of all pact files
* @param {string[]} pactFilesOrDirs - array of pact files & directories
*/
getLocalPactFiles(pactFilesOrDirs) {
const filePaths = new Set();
for (let i = 0; i < pactFilesOrDirs.length; i++) {
const pactFileOrDir = pactFilesOrDirs[i];
const stats = fs.lstatSync(pactFileOrDir);
if (stats.isDirectory()) {
const items = fs.readdirSync(pactFileOrDir);
for (let j = 0; j < items.length; j++) {
const item = items[j];
const itemPath = path.join(pactFileOrDir, item);
const childItemStats = fs.lstatSync(itemPath);
if (childItemStats.isFile()) {
const ext = path.extname(itemPath);
if (ext === '.json') {
filePaths.add(itemPath);
} else {
log.warn(`Invalid file type - ${ext} provided in pactFilesOrDirs: ${itemPath}`);
}
} else {
log.warn(`Invalid file provided in pactFilesOrDirs: ${itemPath}`);
}
}
} else if (stats.isFile()) {
const ext = path.extname(pactFileOrDir);
if (ext === '.json') {
filePaths.add(pactFileOrDir);
} else {
log.warn(`Invalid file type - ${ext} provided in pactFilesOrDirs: ${pactFileOrDir}`);
}
} else {
log.warn(`Invalid file provided in pactFilesOrDirs: ${pactFileOrDir}`);
}
}
return filePaths;
}

@@ -134,0 +179,0 @@

@@ -14,13 +14,13 @@ const colors = require('colors');

function getLevelValue(level) {
const lowerCaseLevel = level.toLowerCase();
const lowerCaseLevel = level.toUpperCase();
switch (lowerCaseLevel) {
case 'trace':
case 'TRACE':
return LEVEL_TRACE;
case 'debug':
case 'DEBUG':
return LEVEL_DEBUG;
case 'info':
case 'INFO':
return LEVEL_INFO;
case 'warn':
case 'WARN':
return LEVEL_WARN;
case 'error':
case 'ERROR':
return LEVEL_ERROR;

@@ -37,3 +37,3 @@ default:

// validate log level
this.level = process.env.PACTUM_LOG_LEVEL || 'info';
this.level = process.env.PACTUM_LOG_LEVEL || 'INFO';
this.levelValue = getLevelValue(this.level);

@@ -45,2 +45,11 @@ if (process.env.PACTUM_DISABLE_LOG_COLORS === 'true') {

/**
* sets log level
* @param {('TRACE'|'DEBUG'|'INFO'|'WARN'|'ERROR')} level - log level
*/
setLevel(level) {
this.level = level;
this.levelValue = getLevelValue(this.level);
}
trace(...msg) {

@@ -47,0 +56,0 @@ if (this.levelValue <= LEVEL_TRACE) {

@@ -8,3 +8,3 @@ const Spec = require('./models/spec');

const request = require('./exports/request');
const Provider = require('./exports/provider');
const provider = require('./exports/provider');

@@ -52,3 +52,3 @@ /**

request,
Provider,
provider,

@@ -55,0 +55,0 @@ /**

@@ -36,3 +36,3 @@ const assert = require('assert');

validateInteractions(interactions) {
for (let [id, interaction] of interactions) {
for (const [id, interaction] of interactions) {
assert.ok(interaction.exercised, `Interaction not Exercised: ${interaction.withRequest.method} - ${interaction.withRequest.path}`);

@@ -39,0 +39,0 @@ }

@@ -24,2 +24,3 @@ const polka = require('polka');

this.app = polka();
this.app.use(logger);
this.app.use(bodyParser);

@@ -43,2 +44,3 @@ registerPactumRemoteRoutes(this);

this.app.server.close(() => {
this.app = null;
log.info(`Mock server stopped on port ${config.mock.port}`);

@@ -69,3 +71,3 @@ resolve();

} else {
// error
log.warn('Unable to remove interaction. Interaction not found with id', id);
}

@@ -77,2 +79,3 @@

this.mockInteractions.delete(id);
log.trace('Removed mock interaction with id', id);
}

@@ -82,2 +85,3 @@

this.pactInteractions.delete(id);
log.trace('Removed pact interaction with id', id);
}

@@ -87,2 +91,3 @@

this.mockInteractions.clear();
log.trace('Cleared mock interactions');
}

@@ -92,7 +97,8 @@

this.pactInteractions.clear();
log.trace('Cleared pact interactions');
}
clearAllInteractions() {
this.mockInteractions.clear();
this.pactInteractions.clear();
this.clearMockInteractions();
this.clearPactInteractions();
}

@@ -132,4 +138,3 @@

if (!interactionExercised) {
log.warn('Interaction not found');
log.warn({
log.warn('Interaction not found', {
method: req.method,

@@ -192,3 +197,4 @@ path: req.path,

} else {
for (let [id, interaction] of interactions) {
for (const [id, interaction] of interactions) {
log.trace('Fetching remote interaction', id);
rawInteractions.push(interaction.rawInteraction);

@@ -229,2 +235,3 @@ }

req.body = helper.getJson(body);
log.trace('Request Body', req.body);
next();

@@ -234,2 +241,9 @@ });

function logger(req, res, next) {
log.trace('Request', req.method, req.path);
log.trace('Request Query', req.query);
log.trace('Request Headers', req.headers);
next();
}
class ExpressResponse {

@@ -236,0 +250,0 @@ constructor(res) {

@@ -527,6 +527,6 @@ const phin = require('phin');

}
for (let [id, interaction] of this.mockInteractions) {
for (const [id, interaction] of this.mockInteractions) {
this.server.addMockInteraction(id, interaction);
}
for (let [id, interaction] of this.pactInteractions) {
for (const [id, interaction] of this.pactInteractions) {
this.server.addPactInteraction(id, interaction);

@@ -550,6 +550,6 @@ }

this._response.responseTime = Date.now() - requestStartTime;
for (let [id, interaction] of this.mockInteractions) {
for (const [id, interaction] of this.mockInteractions) {
this.server.removeInteraction(id);
}
for (let [id, interaction] of this.pactInteractions) {
for (const [id, interaction] of this.pactInteractions) {
this.server.removeInteraction(id);

@@ -556,0 +556,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc