Security News
Input Validation Vulnerabilities Dominate MITRE's 2024 CWE Top 25 List
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Implementation of the consumer driven contract library Pact for Javascript.
From the Pact website:
The Pact family of frameworks provide support for Consumer Driven Contracts testing.
A Contract is a collection of agreements between a client (Consumer) and an API (Provider) that describes the interactions that can take place between them.
Consumer Driven Contracts is a pattern that drives the development of the Provider from its Consumers point of view.
Pact is a testing tool that guarantees those Contracts are satisfied.
Read Getting started with Pact for more information on how to get going.
NOTE: This project supersedes Pact Consumer JS DSL.
It's easy, simply run the below:
npm install --save-dev pact
To use the library on your tests, add the pact dependency:
let Pact = require('pact')
The Pact
interface provides the following high-level APIs, they are listed in the order in which they typically get called in the lifecycle of testing a consumer:
API | Options | Returns | Description |
---|---|---|---|
pact(options) | See Pact Node documentation for options | Object | Creates a Mock Server test double of your Provider API. If you need multiple Providers for a scenario, you can create as many as these as you need. |
setup() | n/a | Promise | Start the Mock Server |
addInteraction() | Object | Promise | Register an expectation on the Mock Server, which must be called by your test case(s). You can add multiple interactions per server. These will be validated and written to a pact if successful. |
verify() | n/a | Promise | Verifies that all and only the interactions specified occurred and that they match. You should call this function after any other assertions and once per test case. |
finalize() | n/a | Promise | Records the interactions registered to the Mock Server into the pact file and shuts it down. |
removeInteractions | n/a | Promise | In some cases you might want to clear out the expectations of the Mock Service, call this to clear out any expectations for the next test run. NOTE: verify() will implicitly call this. |
The first step is to create a test for your API Consumer. The example below uses Mocha, and demonstrates the basic approach:
Check out the examples
folder for examples with Karma Jasmine, Mocha and Jest. The example below is taken from the integration spec.
const path = require('path')
const chai = require('chai')
const pact = require('pact')
const chaiAsPromised = require('chai-as-promised')
const expect = chai.expect
const MOCK_SERVER_PORT = 2202
chai.use(chaiAsPromised);
describe('Pact', () => {
// (1) Create the Pact object to represent your provider
const provider = pact({
consumer: 'TodoApp',
provider: 'TodoService',
port: MOCK_SERVER_PORT,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'INFO',
spec: 2
})
// this is the response you expect from your Provider
const EXPECTED_BODY = [{
id: 1,
name: 'Project 1',
due: '2016-02-11T09:46:56.023Z',
tasks: [
{id: 1, name: 'Do the laundry', 'done': true},
{id: 2, name: 'Do the dishes', 'done': false},
{id: 3, name: 'Do the backyard', 'done': false},
{id: 4, name: 'Do nothing', 'done': false}
]
}]
context('when there are a list of projects', () => {
describe('and there is a valid user session', () => {
before((done) => {
// (2) Start the mock server
provider.setup()
// (3) add interactions to the Mock Server, as many as required
.then(() => {
provider.addInteraction({
// The 'state' field specifies a "Provider State"
state: 'i have a list of projects',
uponReceiving: 'a request for projects',
withRequest: {
method: 'GET',
path: '/projects',
headers: { 'Accept': 'application/json' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: EXPECTED_BODY
}
})
})
.then(() => done())
})
// (4) write your test(s)
it('should generate a list of TODOs for the main screen', () => {
const todoApp = new TodoApp();
todoApp.getProjects() // <- this method would make the remote http call
.then((projects) => {
expect(projects).to.be.a('array')
expect(projects).to.have.deep.property('projects[0].id', 1)
// (5) validate the interactions you've registered and expected occurred
// this will throw an error if it fails telling you what went wrong
expect(provider.verify()).to.not.throw()
})
})
// (6) write the pact file for this consumer-provider pair,
// and shutdown the associated mock server.
// You should do this only _once_ per Provider you are testing.
after(() => {
provider.finalize()
})
})
})
})
Pact tests tend to be quite long, due to the need to be specific about request/response payloads. Often times it is nicer to be able to split your tests across multiple files for manageability.
You have two options to achieve this feat:
Create a Pact test helper to orchestrate the setup and teardown of the mock service for multiple tests.
In larger test bases, this can significantly reduce test suite time and the amount of code you have to manage.
Set pactfileWriteMode
to update
in the pact()
constructor
This will allow you to have multiple independent tests for a given Consumer-Provider pair, without it clobbering previous interactions.
In larger test suites, you'll incur a slow down due to the time taken to start and stop the underlying mock servers.
See this PR for background.
NOTE: If using this approach, you must be careful to clear out existing pact files (e.g. rm ./pacts/*.json
) before you run tests to ensure you don't have left over requests that are no longer relevent.
Once you have created Pacts for your Consumer, you need to validate those Pacts against your Provider. The Verifier object provides the following API for you to do so:
API | Options | Returns | Description |
---|---|---|---|
verifyProvider() | n/a | Promise | Start the Mock Server |
const verifier = require('pact').Verifier;
let opts = {
providerBaseUrl: <String>, // Running API provider host endpoint. Required.
pactBrokerUrl: <String>, // URL of the Pact Broker to retrieve pacts from. Required if not using pactUrls.
provider: <String>, // Name of the Provider. Required.
tags: <Array>, // Array of tags, used to filter pacts from the Broker. Optional.
pactUrls: <Array>, // Array of local Pact file paths or HTTP-based URLs (e.g. from a broker). Required if not using a Broker.
providerStatesSetupUrl: <String>, // URL to send PUT requests to setup a given provider state. Optional, required only if you provide a 'state' in any consumer tests.
pactBrokerUsername: <String>, // Username for Pact Broker basic authentication. Optional
pactBrokerPassword: <String>, // Password for Pact Broker basic authentication. Optional
publishVerificationResult: <Boolean>, // Publish verification result to Broker. Optional
providerVersion: <Boolean>, // Provider version, required to publish verification result to Broker. Optional otherwise.
timeout: <Number> // The duration in ms we should wait to confirm verification process was successful. Defaults to 30000, Optional.
};
verifier.verifyProvider(opts).then(function () {
// do something
});
That's it! Read more about Verifying Pacts.
If you have any state
's in your consumer tests that you need to validate during verification, you will need
to configure your provider for Provider States. This means you must specify providerStatesSetupUrl
in the verifier
constructor and configure an extra (dynamic) API endpoint to setup provider state (--provider-states-setup-url
) for the given test state, which sets the active pact consumer and provider state accepting two parameters: consumer
and state
and returns an HTTP 200
eg. consumer=web&state=customer%20is%20logged%20in
.
See this Provider for a working example, or read more about Provider States.
If you're using a Pact Broker (e.g. a hosted one at pact.dius.com.au), you can publish your verification results so that consumers can query if they are safe to release.
It looks like this:
You need to specify the following when constructing the pact object:
publishVerificationResult: true,
providerVersion: "1.0.0",
provider: "Foo",
NOTE: You need to be already pulling pacts from the broker for this feature to work.
Sharing is caring - to simplify sharing Pacts between Consumers and Providers, checkout sharing pacts using the Pact Broker.
let pact = require('@pact-foundation/pact-node');
let opts = {
pactUrls: <Array>, // Array of local Pact files or directories containing pact files. Path must be absolute. Required.
pactBroker: <String>, // The base URL of the Pact Broker. eg. https://test.pact.dius.com.au. Required.
pactBrokerUsername: <String>, // Username for Pact Broker basic authentication. Optional
pactBrokerPassword: <String>, // Password for Pact Broker basic authentication. Optional
consumerVersion: <String> // A string containing a semver-style version e.g. 1.0.0. Required.
};
pact.publishPacts(opts)).then(function () {
// do something
});
Flexible matching makes your tests more expressive making your tests less brittle. Rather than use hard-coded values which must then be present on the Provider side, you can use regular expressions and type matches on objects and arrays to validate the structure of your APIs.
Read more about using regular expressions and type based matching before continuing.
NOTE: Make sure to start the mock service via the Pact
declaration with the option specification: 2
to get access to these features.
For simplicity, we alias the main matches to make our code more readable:
The underlying mock service is written in Ruby, so the regular expression must be in a Ruby format, not a Javascript format.
const { term } = pact.Matchers
provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for an animal',
withRequest: {
method: 'GET',
path: '/animals/1'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
id: 100,
name: "billy",
'gender': term({
matcher: 'F|M',
generate: 'F'
}),
}
}
})
const { somethingLike: like } = pact.Matchers
provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for an animal',
withRequest: {
method: 'GET',
path: '/animals/1'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
id: 1,
name: like('Billy'),
address: like({
street: '123 Smith St',
suburb: 'Smithsville',
postcode: 7777
})
}
}
})
Note that you can wrap a like
around a single value or an object. When wrapped around an object, all values and child object values will be matched according to types, unless overridden by something more specific like a term
.
Matching provides the ability to specify flexible length arrays. For example:
pact.Matchers.eachLike(obj, { min: 3 })
Where obj
can be any javascript object, value or Pact.Match. It takes optional argument ({ min: 3 }
) where min is greater than 0 and defaults to 1 if not provided.
Below is an example that uses all of the Pact Matchers.
const { somethingLike: like, term, eachLike } = pact.Matchers
const animalBodyExpectation = {
'id': 1,
'first_name': 'Billy',
'last_name': 'Goat',
'animal': 'goat',
'age': 21,
'gender': term({
matcher: 'F|M',
generate: 'M'
}),
'location': {
'description': 'Melbourne Zoo',
'country': 'Australia',
'post_code': 3000
},
'eligibility': {
'available': true,
'previously_married': false
},
'children': eachLike({'name': 'Sally', 'age': 2})
}
// Define animal list payload, reusing existing object matcher
// Note that using eachLike ensure that all values are matched by type
const animalListExpectation = eachLike(animalBodyExpectation, {
min: MIN_ANIMALS
})
provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for all animals',
withRequest: {
method: 'GET',
path: '/animals/available'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: animalListExpectation
}
})
Learn everything in Pact JS in 60 minutes: https://github.com/DiUS/pact-workshop-js
Pact requires a Node runtime to be able to start and stop Mock servers, write logs and other things.
However, when used within browser or non-Node based environments - such as with Karma or ng-test
To address this challenge, we have released a separate 'web' based module for this purpose - pact-web
.
Whilst it still provides a testing DSL, it cannot start and stop mock servers as per the pact
package, so you will need to coordinate this yourself prior to and after executing any tests.
To get started, install pact-web
and Pact Node:
npm install --save-dev pact-web @pact-foundation/pact-node
If you're not using Karma, you can start and stop the mock server using Pact Node or something like Grunt Pact.
We have create a plugin for Karma, which will automatically start and stop any Mock Server for your Pact tests.
Modify your karma.conf.js
file as per below to get started:
module.exports = function (config) {
config.set({
// in here we are simply telling to use Jasmine with Pact
frameworks: ['pact'],
// the Pact options will go here, you can start
// as many providers as you need
pact: [{
port: 1234,
consumer: "some-consumer",
provider: "some-provider",
dir: "pact/files/go/here",
log: "log/files/go/here"
}],
// ensure Pact and default karma plugins are loaded
plugins: [
'karma-*',
'@pact-foundation/karma-pact',
],
});
};
Check out the Examples for how to use the Karma interface.
The module name should be "Pact" - not "pact-js". An example config with a karma test might look like the following:
In client-spec.js
change the define
to:
define(['client', 'Pact'], function (example, Pact) {
In test-main.js
:
require.config({
baseUrl: '/base',
paths: {
'Pact': 'node_modules/pact-web/pact-web',
'client': 'js/client'
},
deps: allTestFiles,
callback: window.__karma__.start
})
See this Stack Overflow question for background, and this gist with a working example.
If you are having issues, a good place to start is setting logLevel: 'DEBUG'
when configuring the pact({...})
object.
Under the hood, Pact JS spins up a Ruby Mock Service. On some systems, this may take more than a few seconds to start. It is recommended to review your unit testing timeout to ensure it has sufficient time to start the server.
See here for more details.
Jest uses JSDOM under the hood which may cause issues with libraries making HTTP request.
You'll need to add the following snippet to your package.json
to ensure it uses
the proper Node environment:
"jest": {
"testEnvironment": "node"
}
Also, from Jest 20, you can add the environment to the top of the test file as a comment. This will allow your pact test to run along side the rest of your JSDOM env tests.
/**
* @jest-environment node
*/
See this issue for background, and the Jest example for a working example.
If your standard tricks don't get you anywhere, setting the logLevel to DEBUG
and increasing the timeout doesn't help and you don't know where else to look, it could be that the binaries we use to do much of the Pact magic aren't starting as expected.
Try starting the mock service manually and seeing if it comes up. When submitting a bug report, it would be worth running these commands before hand as it will greatly help us:
./node_modules/@pact-foundation/pact-standalone/platforms/<platform>/bin/pact-mock-service
...and also the verifier (it will whinge about missing params, but that means it works):
./node_modules/@pact-foundation/pact-standalone/platforms/darwin/bin/pact-provider-verifier
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)If you would like to implement Pact
in another language, please check out the Pact specification and have a chat to one of us on the pact-dev Google group.
The vision is to have a compatible Pact
implementation in all the commonly used languages, your help would be greatly appreciated!
FAQs
Pact for all things Javascript
The npm package pact receives a total of 1,262 weekly downloads. As such, pact popularity was classified as popular.
We found that pact demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.