Initial commit
Some checks failed
test / Test completion check (push) Has been cancelled
test / test (20, macos-latest) (push) Has been cancelled
test / test (20, ubuntu-latest) (push) Has been cancelled
test / test (20, windows-latest) (push) Has been cancelled
test / test (22, macos-latest) (push) Has been cancelled
test / test (22, ubuntu-latest) (push) Has been cancelled
test / test (22, windows-latest) (push) Has been cancelled
test / test (24, macos-latest) (push) Has been cancelled
test / test (24, ubuntu-latest) (push) Has been cancelled
test / test (24, windows-latest) (push) Has been cancelled
Some checks failed
test / Test completion check (push) Has been cancelled
test / test (20, macos-latest) (push) Has been cancelled
test / test (20, ubuntu-latest) (push) Has been cancelled
test / test (20, windows-latest) (push) Has been cancelled
test / test (22, macos-latest) (push) Has been cancelled
test / test (22, ubuntu-latest) (push) Has been cancelled
test / test (22, windows-latest) (push) Has been cancelled
test / test (24, macos-latest) (push) Has been cancelled
test / test (24, ubuntu-latest) (push) Has been cancelled
test / test (24, windows-latest) (push) Has been cancelled
This commit is contained in:
commit
9e8b0de006
24 changed files with 6762 additions and 0 deletions
160
.bashrc
Normal file
160
.bashrc
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# ~/.bashrc: executed by bash(1) for non-login shells.
|
||||||
|
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
|
||||||
|
# for examples
|
||||||
|
clear
|
||||||
|
|
||||||
|
echo -e '
|
||||||
|
tttt iiii
|
||||||
|
ttt:::t i::::i
|
||||||
|
t:::::t iiii
|
||||||
|
t:::::t
|
||||||
|
ppppp ppppppppp aaaaaaaaaaaaa ccccccccccccccccttttttt:::::ttttttt iiiiiii ooooooooooo
|
||||||
|
p::::ppp:::::::::p a::::::::::::a cc:::::::::::::::ct:::::::::::::::::t i:::::i oo:::::::::::oo
|
||||||
|
p:::::::::::::::::p aaaaaaaaa:::::a c:::::::::::::::::ct:::::::::::::::::t i::::i o:::::::::::::::o
|
||||||
|
pp::::::ppppp::::::p a::::ac:::::::cccccc:::::ctttttt:::::::tttttt i::::i o:::::ooooo:::::o
|
||||||
|
p:::::p p:::::p aaaaaaa:::::ac::::::c ccccccc t:::::t i::::i o::::o o::::o
|
||||||
|
p:::::p p:::::p aa::::::::::::ac:::::c t:::::t i::::i o::::o o::::o
|
||||||
|
p:::::p p:::::p a::::aaaa::::::ac:::::c t:::::t i::::i o::::o o::::o
|
||||||
|
p:::::p p::::::pa::::a a:::::ac::::::c ccccccc t:::::t tttttt i::::i o::::o o::::o
|
||||||
|
p:::::ppppp:::::::pa::::a a:::::ac:::::::cccccc:::::c t::::::tttt:::::t i::::::io:::::ooooo:::::o
|
||||||
|
p::::::::::::::::p a:::::aaaa::::::a c:::::::::::::::::c tt::::::::::::::t ...... i::::::io:::::::::::::::o
|
||||||
|
p::::::::::::::pp a::::::::::aa:::a cc:::::::::::::::c tt:::::::::::tt .::::. i::::::i oo:::::::::::oo
|
||||||
|
p::::::pppppppp aaaaaaaaaa aaaa cccccccccccccccc ttttttttttt ...... iiiiiiii ooooooooooo
|
||||||
|
p:::::p
|
||||||
|
p:::::p
|
||||||
|
p:::::::p
|
||||||
|
p:::::::p
|
||||||
|
p:::::::p
|
||||||
|
ppppppppp
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "Welcome to the Pact 5 minute getting starting guide"
|
||||||
|
|
||||||
|
# If not running interactively, don't do anything
|
||||||
|
case $- in
|
||||||
|
*i*) ;;
|
||||||
|
*) return;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# don't put duplicate lines or lines starting with space in the history.
|
||||||
|
# See bash(1) for more options
|
||||||
|
HISTCONTROL=ignoreboth
|
||||||
|
|
||||||
|
# append to the history file, don't overwrite it
|
||||||
|
shopt -s histappend
|
||||||
|
|
||||||
|
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
|
||||||
|
HISTSIZE=1000
|
||||||
|
HISTFILESIZE=2000
|
||||||
|
|
||||||
|
# check the window size after each command and, if necessary,
|
||||||
|
# update the values of LINES and COLUMNS.
|
||||||
|
shopt -s checkwinsize
|
||||||
|
|
||||||
|
# If set, the pattern "**" used in a pathname expansion context will
|
||||||
|
# match all files and zero or more directories and subdirectories.
|
||||||
|
#shopt -s globstar
|
||||||
|
|
||||||
|
# make less more friendly for non-text input files, see lesspipe(1)
|
||||||
|
#[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
|
||||||
|
|
||||||
|
# set variable identifying the chroot you work in (used in the promptbelow)
|
||||||
|
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
|
||||||
|
debian_chroot=$(cat /etc/debian_chroot)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# set a fancy prompt (non-color, unless we know we "want" color)
|
||||||
|
case "$TERM" in
|
||||||
|
xterm-color|*-256color) color_prompt=yes;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# uncomment for a colored prompt, if the terminal has the capability;turned
|
||||||
|
# off by default to not distract the user: the focus in a terminal window
|
||||||
|
# should be on the output of commands, not on the prompt
|
||||||
|
#force_color_prompt=yes
|
||||||
|
|
||||||
|
if [ -n "$force_color_prompt" ]; then
|
||||||
|
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
|
||||||
|
# We have color support; assume it's compliant with Ecma-48
|
||||||
|
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
|
||||||
|
# a case would tend to support setf rather than setaf.)
|
||||||
|
color_prompt=yes
|
||||||
|
else
|
||||||
|
color_prompt=
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$color_prompt" = yes ]; then
|
||||||
|
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
|
||||||
|
else
|
||||||
|
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
|
||||||
|
fi
|
||||||
|
unset color_prompt force_color_prompt
|
||||||
|
|
||||||
|
# If this is an xterm set the title to user@host:dir
|
||||||
|
case "$TERM" in
|
||||||
|
xterm*|rxvt*)
|
||||||
|
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# enable color support of ls and also add handy aliases
|
||||||
|
if [ -x /usr/bin/dircolors ]; then
|
||||||
|
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
|
||||||
|
alias ls='ls --color=auto'
|
||||||
|
#alias dir='dir --color=auto'
|
||||||
|
#alias vdir='vdir --color=auto'
|
||||||
|
|
||||||
|
#alias grep='grep --color=auto'
|
||||||
|
#alias fgrep='fgrep --color=auto'
|
||||||
|
#alias egrep='egrep --color=auto'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# colored GCC warnings and errors
|
||||||
|
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
|
||||||
|
|
||||||
|
# some more ls aliases
|
||||||
|
#alias ll='ls -l'
|
||||||
|
#alias la='ls -A'
|
||||||
|
#alias l='ls -CF'
|
||||||
|
|
||||||
|
# Alias definitions.
|
||||||
|
# You may want to put all your additions into a separate file like
|
||||||
|
# ~/.bash_aliases, instead of adding them here directly.
|
||||||
|
# See /usr/share/doc/bash-doc/examples in the bash-doc package.
|
||||||
|
|
||||||
|
if [ -f ~/.bash_aliases ]; then
|
||||||
|
. ~/.bash_aliases
|
||||||
|
fi
|
||||||
|
|
||||||
|
# enable programmable completion features (you don't need to enable
|
||||||
|
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
|
||||||
|
# sources /etc/bash.bashrc).
|
||||||
|
if ! shopt -oq posix; then
|
||||||
|
if [ -f /usr/share/bash-completion/bash_completion ]; then
|
||||||
|
. /usr/share/bash-completion/bash_completion
|
||||||
|
elif [ -f /etc/bash_completion ]; then
|
||||||
|
. /etc/bash_completion
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
RED='\e[91m'
|
||||||
|
GREEN='\e[92m'
|
||||||
|
BROWN='\e[93m'
|
||||||
|
BLUE='\e[94m'
|
||||||
|
PURPLE='\e[95m'
|
||||||
|
CYAN='\e[96m'
|
||||||
|
GRAY='\e[97m'
|
||||||
|
|
||||||
|
export TERM=xterm-256color
|
||||||
|
|
||||||
|
export PATH="$PATH:$HOME"
|
||||||
|
|
||||||
|
PS1="\[${BLUE}\]welcome\[${GRAY}\]@\[${GREEN}\]pact.io\[${GRAY}\]:\[${BROWN}\]\w\[${GRAY}\]$ \[${GRAY}\]"
|
||||||
|
|
||||||
|
export GIT_COMMIT=$(git rev-parse HEAD)
|
||||||
|
export GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
echo "Welcome! try and run one of the scripts below!"
|
||||||
|
npm run intro
|
||||||
57
.github/workflows/test.yml
vendored
Normal file
57
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
complete:
|
||||||
|
name: Test completion check
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Failed
|
||||||
|
run: exit 1
|
||||||
|
if: >
|
||||||
|
contains(needs.*.result, 'failure')
|
||||||
|
|| contains(needs.*.result, 'cancelled')
|
||||||
|
|| contains(needs.*.result, 'skipped')
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
node-version: [20, 22, 24]
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- run: ./scripts/ci/test.sh
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
pacts
|
||||||
|
!pacts/GettingStartedOrderWeb-GettingStartedOrderApi.json
|
||||||
|
pact_broker
|
||||||
41
.replit
Normal file
41
.replit
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
run = "bash --rcfile .bashrc"
|
||||||
|
|
||||||
|
language = "python"
|
||||||
|
|
||||||
|
hidden = ["node_modules", ".github/**",'.git/**','.config/**','pact_broker/**']
|
||||||
|
|
||||||
|
[nix]
|
||||||
|
channel = "stable-22_05"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
VIRTUAL_ENV = "/home/runner/${REPL_SLUG}/venv"
|
||||||
|
PYTHONPATH = "${VIRTUAL_ENV}/lib/python3.8/site-packages"
|
||||||
|
REPLIT_POETRY_PYPI_REPOSITORY = "https://package-proxy.replit.com/pypi/"
|
||||||
|
MPLBACKEND = "TkAgg"
|
||||||
|
POETRY_CACHE_DIR = "${HOME}/${REPL_SLUG}/.cache/pypoetry"
|
||||||
|
XDG_CONFIG_HOME = "/home/runner/.config"
|
||||||
|
PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin:${VIRTUAL_ENV}/lib/python3.8/site-packages:${VIRTUAL_ENV}/bin"
|
||||||
|
npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global"
|
||||||
|
IS_REPL = "true"
|
||||||
|
|
||||||
|
[unitTest]
|
||||||
|
language = "nodejs"
|
||||||
|
|
||||||
|
[packager]
|
||||||
|
language = "nodejs"
|
||||||
|
|
||||||
|
[packager.features]
|
||||||
|
packageSearch = true
|
||||||
|
guessImports = true
|
||||||
|
enabledForHosting = false
|
||||||
|
|
||||||
|
[gitHubImport]
|
||||||
|
requiredFiles = [".replit", "replit.nix"]
|
||||||
|
|
||||||
|
[languages]
|
||||||
|
|
||||||
|
[languages.javascript]
|
||||||
|
pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
|
||||||
|
|
||||||
|
[languages.javascript.languageServer]
|
||||||
|
start = "typescript-language-server --stdio"
|
||||||
378
README.md
Normal file
378
README.md
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
# pact-5-minute-getting-started-guide
|
||||||
|
|
||||||
|
|
||||||
|
> From zero to running Pact tests in 5 mins
|
||||||
|
|
||||||
|
Provides runnable code to support the Pact 5 minute getting started guide on our Website [here](https://docs.pact.io/5-minute-getting-started-guide)
|
||||||
|
|
||||||
|
If this is useful to you, please :star: the repo and help show your support!
|
||||||
|
|
||||||
|
## Ways to run
|
||||||
|
|
||||||
|
> When you've picked an option, click the corresponding button
|
||||||
|
|
||||||
|
1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) this repository (and [clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) it to your local machine)
|
||||||
|
|
||||||
|
<a href="https://github.com/pact-foundation/pact-5-minute-getting-started-guide/fork">
|
||||||
|
<img src="https://img.shields.io/badge/fork%20this-repo-blue?logo=github">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
2. Create a [new repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template) from this template (and [clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) it to your local machine)
|
||||||
|
|
||||||
|
|
||||||
|
<a href="https://github.com/pact-foundation/pact-5-minute-getting-started-guide/generate">
|
||||||
|
<img src="https://img.shields.io/badge/use%20this-template-blue?logo=github">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
3. Run it in your browser by creating a new Repl from our template.
|
||||||
|
|
||||||
|
> You'll need to sign up, don't worry it is free, and you can run it from your phone, via the [repl.it mobile app](https://replit.com/mobile)
|
||||||
|
|
||||||
|
[](https://repl.it/github/pact-foundation/pact-5-minute-getting-started-guide)
|
||||||
|
|
||||||
|
4. Run it in your browser by running our [Repl](https://replit.com/@pact)
|
||||||
|
|
||||||
|
<a href="https://replit.com/@pact/pact-5-minute-getting-started-guide?outputonly=1&lite=true">
|
||||||
|
<img src="https://img.shields.io/badge/run%20in-browser-blue?logo=react">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
5. See it as part of our getting started section on our website [here](https://docs.pact.io/5-minute-getting-started-guide)
|
||||||
|
|
||||||
|
<a href="https://docs.pact.io/5-minute-getting-started-guide">
|
||||||
|
<img src="https://img.shields.io/badge/visit%20our-guide-blue?logo=react">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## An example scenario: Order API
|
||||||
|
|
||||||
|
Here we have an example describing Pact tests between a consumer \(Order Web\), and its provider \(the Order API\).
|
||||||
|
|
||||||
|
In the Consumer project, we're going to need:
|
||||||
|
|
||||||
|
* A model \(the `Order` class\) to represent the data returned from the Order API
|
||||||
|
* A client \(the `OrderApiClient`\) which will be responsible for making the HTTP calls to the Order API and returning an internal representation of an Order.
|
||||||
|
|
||||||
|
Note that to create a pact, you _do_ need to write the code that executes the HTTP requests to your service \(in your client class\), but you _don't_ need to write the full stack of consumer code \(eg. the UI\).
|
||||||
|
|
||||||
|
## Testing the Order Web \(consumer\) project
|
||||||
|
|
||||||
|
### Scope of a Consumer Pact Test
|
||||||
|
|
||||||
|
Ideally, the Pact tests should be "unit tests" for your client class, and they should just focus on ensuring that the request creation and response handling are correct. If you use pact for your UI tests, you'll end up with an explosion of redundant interactions that will make the verification process tedious. Remember that pact is for testing the contract used for communication, and not for testing particular UI behaviour or business logic.
|
||||||
|
|
||||||
|
Usually, your application will be broken down into a number of sub-components, depending on what type of application your consumer is \(e.g. a Web application or another API\). This is how you might visualise the coverage of a consumer Pact test:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here, a _Collaborator_ is a component whose job is to communicate with another system. In our case, this is the `OrderApiClient`communicating with the external `Order Api` system. This is what we want our consumer test to inspect.
|
||||||
|
|
||||||
|
### 1. Start with your model
|
||||||
|
|
||||||
|
Imagine a simple model class that looks something like this \(order.js\). The attributes for an Order live on a remote server, and will need to be retrieved by an HTTP call to the Order API.
|
||||||
|
|
||||||
|
<!--DOCUSAURUS_CODE_TABS-->
|
||||||
|
<!--order.js-->
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Order {
|
||||||
|
constructor(id, items) {
|
||||||
|
this.id = id;
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
total() {
|
||||||
|
return this.items.reduce((acc, v) => {
|
||||||
|
acc += v.quantity * v.value;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Order ${this.id}, Total: ${this.total()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Order,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--Sample Order-->
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "burger",
|
||||||
|
quantity: 2,
|
||||||
|
value: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "coke",
|
||||||
|
quantity: 2,
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||||
|
|
||||||
|
### 2. Create an Order API client
|
||||||
|
|
||||||
|
Here we have our external collaborator client. Its job is to both make the external request to the Order API and convert the response into the internal Order model as per above:
|
||||||
|
|
||||||
|
<!--DOCUSAURUS_CODE_TABS-->
|
||||||
|
<!--orderClient.js-->
|
||||||
|
|
||||||
|
```js
|
||||||
|
const request = require("superagent");
|
||||||
|
const { Order } = require("./order");
|
||||||
|
|
||||||
|
const fetchOrders = () => {
|
||||||
|
return request.get(`http://localhost:${process.env.API_PORT}/orders`).then(
|
||||||
|
(res) => {
|
||||||
|
return res.body.reduce((acc, o) => {
|
||||||
|
acc.push(new Order(o.id, o.items));
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.log(err)
|
||||||
|
throw new Error(`Error from response: ${err.body}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fetchOrders,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||||
|
|
||||||
|
### 3. Configure the mock Order API
|
||||||
|
|
||||||
|
The following code will create a mock service on `localhost:1234` which will respond to your application's queries over HTTP as if it were the real Order API. It also creates a mock provider object which you will use to set up your expectations.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Setting up our test framework
|
||||||
|
const chai = require("chai");
|
||||||
|
const expect = chai.expect;
|
||||||
|
const chaiAsPromised = require("chai-as-promised");
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
// We need Pact in order to use it in our test
|
||||||
|
const { provider } = require("../pact");
|
||||||
|
const { eachLike } = require("@pact-foundation/pact").MatchersV3;
|
||||||
|
|
||||||
|
// Importing our system under test (the orderClient) and our Order model
|
||||||
|
const { Order } = require("./order");
|
||||||
|
const { fetchOrders } = require("./orderClient");
|
||||||
|
|
||||||
|
// This is where we start writing our test
|
||||||
|
describe("Pact with Order API", () => {
|
||||||
|
describe("given there are orders", () => {
|
||||||
|
const itemProperties = {
|
||||||
|
name: "burger",
|
||||||
|
quantity: 2,
|
||||||
|
value: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderProperties = {
|
||||||
|
id: 1,
|
||||||
|
items: eachLike(itemProperties),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("when a call to the API is made", () => {
|
||||||
|
before(() => {
|
||||||
|
provider
|
||||||
|
.given("there are orders")
|
||||||
|
.uponReceiving("a request for orders")
|
||||||
|
.withRequest({
|
||||||
|
method: "GET",
|
||||||
|
path: "/orders",
|
||||||
|
})
|
||||||
|
.willRespondWith({
|
||||||
|
body: eachLike(orderProperties),
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will receive the list of current orders", () => {
|
||||||
|
return provider.executeTest((mockserver) => {
|
||||||
|
// The mock server is started on a randomly available port,
|
||||||
|
// so we set the API mock service port so HTTP clients
|
||||||
|
// can dynamically find the endpoint
|
||||||
|
process.env.API_PORT = mockserver.port;
|
||||||
|
return expect(fetchOrders()).to.eventually.have.deep.members([
|
||||||
|
new Order(orderProperties.id, [itemProperties]),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||||
|
|
||||||
|
Green!
|
||||||
|
|
||||||
|
Running the passing Order API spec will generate a pact file in the configured pact dir \(`./pacts` by default\). Logs will be output to the configured log dir \(`./log` by default\) that can be useful when diagnosing problems.
|
||||||
|
|
||||||
|
You now have a pact file that can be used to verify your expectations of the Order API provider project.
|
||||||
|
|
||||||
|
Now, in real life you would rinse and repeat for other likely status codes that may be returned. For example, consider how you want your client to respond to a:
|
||||||
|
|
||||||
|
* 404 \(return null, or raise an error?\)
|
||||||
|
* 400 \(how should validation errors be handled, what will the body look like when there is one?\)
|
||||||
|
* 500 \(specifying that the response body should contain an error message, and ensuring that your client logs that error message will make your life much easier when things go wrong. Note that it may be hard to force your provider to generate a 500 error on demand. You may need to collaborate with your provider team to create a known provider state that will artificially return a 500 error, or you may just wish to use a standard unit test without a pact to test this.\)
|
||||||
|
* 401/403 if there is authorisation.
|
||||||
|
|
||||||
|
#### **Run the consumer Tests!**
|
||||||
|
|
||||||
|
OK enough talk - let's run the consumer test. If you like, click around the project to see the files from above in context. The most interesting file is the consumer test in `./consumer/consumer.spec.js` .
|
||||||
|
|
||||||
|
## Sharing the contracts with the provider team
|
||||||
|
|
||||||
|
Now that you have created and run your consumer tests, producing a contract \(the pact file\) as an artefact, we need to share it with the team responsible for managing the Order API, so that they can confirm they meet all of the expectations set in it. There are multiple ways to [share pacts](getting_started/sharing_pacts.md), but the recommended approach is to use a [Pact Broker](pact_broker/) as it enables powerful automation workflows.
|
||||||
|
|
||||||
|
For this purpose, we are going to use a hosted Pact Broker from pactflow.io.
|
||||||
|
|
||||||
|
> The Pact Broker is an open source tool that requires you to deploy, administer and host it yourself. If you would prefer a plug-and-play option, we've created [PactFlow](https://pactflow.io/?utm_source=ossdocs&utm_campaign=five_minute_guide), a fully managed Pact Broker with additional features to simplify teams getting started and scaling with Pact.
|
||||||
|
>
|
||||||
|
> To get started for free, you can sign up to our Developer Plan [here](https://pactflow.io/pricing/?utm_source=ossdocs&utm_campaign=five_minute_guide_dev_plan).
|
||||||
|
|
||||||
|
<!-- <iframe style="padding-bottom:20px" frameborder="0" width="100%" height="500px" src="https://repl.it/@mefellows/docspactio-getting-started-publish?lite=true"></iframe> -->
|
||||||
|
|
||||||
|
You can see the published pact [here](https://test.pactflow.io/pacts/provider/GettingStartedOrderApi/consumer/GettingStartedOrderWeb/latest). Login to the PactFlow account using this [special link](https://test.pactflow.io/login?code=98f7810e-c7dc-493b-9c3d-7849952f1d9a&utm_medium=web&utm_source=5-minute-getting-started-guide).
|
||||||
|
|
||||||
|
After publishing the pact, we can now verify that the Provider meets these expectations.
|
||||||
|
|
||||||
|
## Testing the Order API \(provider\) project
|
||||||
|
|
||||||
|
### Scope of a Provider Pact Test
|
||||||
|
|
||||||
|
On the Provider side, Pact needs to replay all of the interactions \(usually HTTP requests\) against your service. There are a number of choices that can be made here, but usually these are the choices:
|
||||||
|
|
||||||
|
* Invoke just the controller layer \(in an MVC app, or the "Adapter" in our diagram\) and stub out layers beneath
|
||||||
|
* Choosing a real vs mocked out database
|
||||||
|
* Choosing to hit mock HTTP servers or mocks for external services
|
||||||
|
|
||||||
|
Generally speaking, we test the entire service and mock out external services such as downstream APIs \(which would need their own set of Pact tests\) and databases. This gives you some of the benefits of an integration test without the high costs of maintenance.
|
||||||
|
|
||||||
|
This is how you might visualise the coverage of a provider Pact test:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 1. Create the Order API
|
||||||
|
|
||||||
|
Below we have created a simple API using [Express JS](https://expressjs.com).
|
||||||
|
|
||||||
|
<!--DOCUSAURUS_CODE_TABS-->
|
||||||
|
<!-- orderApi.js -->
|
||||||
|
|
||||||
|
```js
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const bodyParser = require("body-parser");
|
||||||
|
const server = express();
|
||||||
|
|
||||||
|
server.use(cors());
|
||||||
|
server.use(bodyParser.json());
|
||||||
|
server.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
server.use((_, res, next) => {
|
||||||
|
res.header("Content-Type", "application/json; charset=utf-8");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// "In memory" data store
|
||||||
|
let dataStore = require("./data/orders.js");
|
||||||
|
|
||||||
|
server.get("/orders", (_, res) => {
|
||||||
|
res.json(dataStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
server,
|
||||||
|
dataStore,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||||
|
|
||||||
|
### 2. Run provider verification tests
|
||||||
|
|
||||||
|
We now need to perform the "provider verification" task, which involves the following:
|
||||||
|
|
||||||
|
1. Telling Pact where to find the contract files, and where the Order API will be running \(lines 3-13\)
|
||||||
|
2. Starting the API \(line 16-18\)
|
||||||
|
3. Running the Provider verification task \(line 22\)
|
||||||
|
|
||||||
|
<!--DOCUSAURUS_CODE_TABS-->
|
||||||
|
<!-- provider.spec.js -->
|
||||||
|
```js
|
||||||
|
// Verify that the provider meets all consumer expectations
|
||||||
|
const Verifier = require("@pact-foundation/pact").Verifier;
|
||||||
|
const chai = require("chai");
|
||||||
|
const chaiAsPromised = require("chai-as-promised");
|
||||||
|
const { server } = require("./provider.js");
|
||||||
|
const { providerName, pactFile } = require("../pact.js");
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
let port;
|
||||||
|
let opts;
|
||||||
|
let app;
|
||||||
|
|
||||||
|
// Verify that the provider meets all consumer expectations
|
||||||
|
describe("Pact Verification", () => {
|
||||||
|
before(async () => {
|
||||||
|
port = 3000;
|
||||||
|
opts = {
|
||||||
|
provider: providerName,
|
||||||
|
providerBaseUrl: `http://localhost:${port}`,
|
||||||
|
// pactUrls: [pactFile], // if you don't use a broker
|
||||||
|
pactBrokerUrl: "https://test.pactflow.io",
|
||||||
|
pactBrokerToken: "129cCdfCWhMzcC9pFwb4bw",
|
||||||
|
publishVerificationResult: false,
|
||||||
|
providerVersionBranch: process.env.GIT_BRANCH ?? "master",
|
||||||
|
providerVersion: process.env.GIT_COMMIT ?? "1.0." + process.env.HOSTNAME,
|
||||||
|
consumerVersionSelectors: [
|
||||||
|
{ mainBranch: true },
|
||||||
|
{ deployedOrReleased: true }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
app = server.listen(port, () => {
|
||||||
|
console.log(`Provider service listening on http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
if (app) {
|
||||||
|
app.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("should validate the expectations of Order Web", () => {
|
||||||
|
return new Verifier(opts)
|
||||||
|
.verifyProvider()
|
||||||
|
.then((output) => {
|
||||||
|
console.log("Pact Verification Complete!");
|
||||||
|
console.log(output);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Pact verification failed :(", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
<!--END_DOCUSAURUS_CODE_TABS-->
|
||||||
|
|
||||||
|
#### Run the Provider tests
|
||||||
|
|
||||||
|
Let's run the provider test. If you like, click around the project to see the files from above in context. The most interesting file is the provider test in `./provider/provider.spec.js` .
|
||||||
60
consumer/consumer.spec.js
Normal file
60
consumer/consumer.spec.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Setting up our test framework
|
||||||
|
const chai = require("chai");
|
||||||
|
const expect = chai.expect;
|
||||||
|
const chaiAsPromised = require("chai-as-promised");
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
// We need Pact in order to use it in our test
|
||||||
|
const { provider } = require("../pact");
|
||||||
|
const { eachLike } = require("@pact-foundation/pact").MatchersV3;
|
||||||
|
|
||||||
|
// Importing our system under test (the orderClient) and our Order model
|
||||||
|
const { Order } = require("./order");
|
||||||
|
const { fetchOrders } = require("./orderClient");
|
||||||
|
|
||||||
|
// This is where we start writing our test
|
||||||
|
describe("Pact with Order API", () => {
|
||||||
|
describe("given there are orders", () => {
|
||||||
|
const itemProperties = {
|
||||||
|
name: "burger",
|
||||||
|
quantity: 2,
|
||||||
|
value: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderProperties = {
|
||||||
|
id: 1,
|
||||||
|
items: eachLike(itemProperties),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("when a call to the API is made", () => {
|
||||||
|
before(() => {
|
||||||
|
provider
|
||||||
|
.given("there are orders")
|
||||||
|
.uponReceiving("a request for orders")
|
||||||
|
.withRequest({
|
||||||
|
method: "GET",
|
||||||
|
path: "/orders",
|
||||||
|
})
|
||||||
|
.willRespondWith({
|
||||||
|
body: eachLike(orderProperties),
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will receive the list of current orders", () => {
|
||||||
|
return provider.executeTest((mockserver) => {
|
||||||
|
// The mock server is started on a randomly available port,
|
||||||
|
// so we set the API mock service port so HTTP clients
|
||||||
|
// can dynamically find the endpoint
|
||||||
|
process.env.API_PORT = mockserver.port;
|
||||||
|
return expect(fetchOrders()).to.eventually.have.deep.members([
|
||||||
|
new Order(orderProperties.id, [itemProperties]),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
21
consumer/order.js
Normal file
21
consumer/order.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
class Order {
|
||||||
|
constructor(id, items) {
|
||||||
|
this.id = id;
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
total() {
|
||||||
|
return this.items.reduce((acc, v) => {
|
||||||
|
acc += v.quantity * v.value;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Order ${this.id}, Total: ${this.total()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Order,
|
||||||
|
};
|
||||||
23
consumer/orderClient.js
Normal file
23
consumer/orderClient.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const request = require("superagent");
|
||||||
|
const { Order } = require("./order");
|
||||||
|
|
||||||
|
const hostname = "127.0.0.1"
|
||||||
|
|
||||||
|
const fetchOrders = () => {
|
||||||
|
return request.get(`http://${hostname}:${process.env.API_PORT}/orders`).then(
|
||||||
|
(res) => {
|
||||||
|
return res.body.reduce((acc, o) => {
|
||||||
|
acc.push(new Order(o.id, o.items));
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.log(err)
|
||||||
|
throw new Error(`Error from response: ${err.body}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fetchOrders,
|
||||||
|
};
|
||||||
1
index.html
Normal file
1
index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<iframe frameborder="0" width="100%" height="500px" src="https://replit.com/@pact/pact-5-minute-getting-started-guide?outputonly=1&lite=true"></iframe>
|
||||||
5
index.js
Normal file
5
index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Let's use async/await
|
||||||
|
(async () => {
|
||||||
|
const { runTests } = require("./runTest.js");
|
||||||
|
await runTests("consumer");
|
||||||
|
})();
|
||||||
5628
package-lock.json
generated
Normal file
5628
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
package.json
Normal file
28
package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "1.10.0",
|
||||||
|
"body-parser": "^2.0.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^5.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"intro": "cat package.json | jq .scripts",
|
||||||
|
"preintro": "npm install",
|
||||||
|
"test:consumer": "node runConsumerTest.js",
|
||||||
|
"test:provider": "node runProviderTest.js",
|
||||||
|
"pact:publish": "./publish.sh",
|
||||||
|
"pact:show": "cat pacts/GettingStartedOrderWeb-GettingStartedOrderApi.json | jq .",
|
||||||
|
"test": "cross-env PACT_URL=$PWD/pacts/GettingStartedOrderWeb-GettingStartedOrderApi.json npm run test:consumer && npm run test:provider",
|
||||||
|
"test:broker": "cross-env PACT_BROKER_BASE_URL=https://pactbroker.$REPL_OWNER.repl.co npm run test:consumer && npm run pact:publish && npm run test:provider",
|
||||||
|
"get:broker": "echo 'fork a copy of https://replit.com/@pact/pactbroker and run npm test:broker when it has started'"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"@pact-foundation/pact": "15.0.1",
|
||||||
|
"mocha": "^11.0.0",
|
||||||
|
"superagent": "^10.0.0",
|
||||||
|
"chai": "^5.0.0",
|
||||||
|
"chai-as-promised": "^7.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
pact.js
Normal file
34
pact.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
const pact = require("@pact-foundation/pact");
|
||||||
|
const Pact = pact.PactV3;
|
||||||
|
const path = require("path");
|
||||||
|
const process = require("process");
|
||||||
|
const consumerName = "GettingStartedOrderWeb";
|
||||||
|
const providerName = "GettingStartedOrderApi";
|
||||||
|
const pactFile = path.resolve(`./pacts/${consumerName}-${providerName}.json`);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
pactFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
const provider = new Pact({
|
||||||
|
log: path.resolve(process.cwd(), "logs", "pact.log"),
|
||||||
|
dir: path.resolve(process.cwd(), "pacts"),
|
||||||
|
logLevel: "info",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
consumer: consumerName,
|
||||||
|
provider: providerName,
|
||||||
|
host: "127.0.0.1"
|
||||||
|
});
|
||||||
|
|
||||||
|
// used to kill any left over mock server instances in case of errors
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
pact.removeAllServers();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
provider,
|
||||||
|
pactFile,
|
||||||
|
consumerName,
|
||||||
|
providerName,
|
||||||
|
consumerVersion: "1.0.0",
|
||||||
|
};
|
||||||
75
pacts/GettingStartedOrderWeb-GettingStartedOrderApi.json
Normal file
75
pacts/GettingStartedOrderWeb-GettingStartedOrderApi.json
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"consumer": {
|
||||||
|
"name": "GettingStartedOrderWeb"
|
||||||
|
},
|
||||||
|
"interactions": [
|
||||||
|
{
|
||||||
|
"description": "a request for orders",
|
||||||
|
"providerStates": [
|
||||||
|
{
|
||||||
|
"name": "there are orders"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/orders"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "burger",
|
||||||
|
"quantity": 2,
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json; charset=utf-8"
|
||||||
|
},
|
||||||
|
"matchingRules": {
|
||||||
|
"body": {
|
||||||
|
"$": {
|
||||||
|
"combine": "AND",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"match": "type",
|
||||||
|
"min": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"$[*].items": {
|
||||||
|
"combine": "AND",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"match": "type",
|
||||||
|
"min": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"header": {}
|
||||||
|
},
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"pact-js": {
|
||||||
|
"version": "12.1.0"
|
||||||
|
},
|
||||||
|
"pactRust": {
|
||||||
|
"ffi": "0.4.7",
|
||||||
|
"models": "1.1.9"
|
||||||
|
},
|
||||||
|
"pactSpecification": {
|
||||||
|
"version": "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "GettingStartedOrderApi"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
provider/data/orders.js
Normal file
17
provider/data/orders.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "burger",
|
||||||
|
quantity: 2,
|
||||||
|
value: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "coke",
|
||||||
|
quantity: 2,
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
24
provider/provider.js
Normal file
24
provider/provider.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const bodyParser = require("body-parser");
|
||||||
|
const server = express();
|
||||||
|
|
||||||
|
server.use(cors());
|
||||||
|
server.use(bodyParser.json());
|
||||||
|
server.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
server.use((_, res, next) => {
|
||||||
|
res.header("Content-Type", "application/json; charset=utf-8");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// "In memory" data store
|
||||||
|
let dataStore = require("./data/orders.js");
|
||||||
|
|
||||||
|
server.get("/orders", (_, res) => {
|
||||||
|
res.json(dataStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
server,
|
||||||
|
dataStore,
|
||||||
|
};
|
||||||
121
provider/provider.spec.js
Normal file
121
provider/provider.spec.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
const Verifier = require("@pact-foundation/pact").Verifier;
|
||||||
|
const chai = require("chai");
|
||||||
|
const chaiAsPromised = require("chai-as-promised");
|
||||||
|
const { server } = require("./provider.js");
|
||||||
|
const { providerName, pactFile } = require("../pact.js");
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
let port;
|
||||||
|
let opts;
|
||||||
|
let app;
|
||||||
|
|
||||||
|
const hostname = "127.0.0.1"
|
||||||
|
|
||||||
|
// Verify that the provider meets all consumer expectations
|
||||||
|
describe("Pact Verification", () => {
|
||||||
|
before(async () => {
|
||||||
|
port = 3000;
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
// we need to know the providers name
|
||||||
|
provider: providerName,
|
||||||
|
// we need to where the provider will be running,
|
||||||
|
// we are starting it locally and defined the port above
|
||||||
|
providerBaseUrl: `http://${hostname}:${port}`,
|
||||||
|
// You can set the log level here, useful for debugging
|
||||||
|
logLevel: "info"
|
||||||
|
};
|
||||||
|
|
||||||
|
// The PACT_URL can either be a path to a local file
|
||||||
|
// or one from a Pact Broker
|
||||||
|
if (process.env.PACT_URL) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
pactUrls: [process.env.PACT_URL]
|
||||||
|
}
|
||||||
|
// as a convenience, we have provided a path to the example consumer/provider pact
|
||||||
|
// generated when running npm run test:consumer
|
||||||
|
} else if (!process.env.PACT_URL && !process.env.PACT_BROKER_BASE_URL) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
pactUrls: [pactFile]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a broker, then some more options are relevant
|
||||||
|
if (process.env.PACT_BROKER_BASE_URL) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
// we need to know where our broker is located
|
||||||
|
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
|
||||||
|
// we need specifics about the provider version we are verifying so we
|
||||||
|
// can identify it later
|
||||||
|
providerVersion: process.env.GIT_COMMIT,
|
||||||
|
providerVersionBranch: process.env.GIT_BRANCH,
|
||||||
|
// we only want to publish pacts if we are in CI
|
||||||
|
publishVerificationResult: !!process.env.CI ?? false,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// we need to setup our broker authentication options
|
||||||
|
// if setup
|
||||||
|
if (process.env.PACT_BROKER_USERNAME) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
pactBrokerUsername: process.env.PACT_BROKER_USERNAME,
|
||||||
|
pactBrokerPassword: process.env.PACT_BROKER_PASSWORD
|
||||||
|
}
|
||||||
|
} else if (process.env.PACT_BROKER_TOKEN) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a PACT_URL provided to use by the Pact broker
|
||||||
|
// we do not need to set these options.
|
||||||
|
// In regular provider builds, these options become relevant to select
|
||||||
|
// your pacts
|
||||||
|
if (!process.env.PACT_URL) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
// We can use consumer version selectors for fine grained control
|
||||||
|
// over the pacts we retrieve
|
||||||
|
consumerVersionSelectors: [
|
||||||
|
{ mainBranch: true },
|
||||||
|
{ deployedOrReleased: true }
|
||||||
|
],
|
||||||
|
// Dont allow pending pacts that haven't had a successful
|
||||||
|
// verification to block provider build
|
||||||
|
enablePending: true,
|
||||||
|
// Allow the provider to catch any in-flight work in progress
|
||||||
|
// pacts from the consumers
|
||||||
|
includeWipPactsSince: "2022-01-01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app = server.listen('3000', '127.0.0.1', () => {
|
||||||
|
console.log(`Provider service listening on http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
if (app) {
|
||||||
|
app.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("should validate the expectations of Order Web", () => {
|
||||||
|
console.log(opts)
|
||||||
|
return new Verifier(opts)
|
||||||
|
.verifyProvider()
|
||||||
|
.then((output) => {
|
||||||
|
console.log("Pact Verification Complete!");
|
||||||
|
console.log(output);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Pact verification failed :(", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
15
publish.sh
Executable file
15
publish.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
GIT_COMMIT=$(git rev-parse HEAD)
|
||||||
|
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
pact-broker publish pacts \
|
||||||
|
--branch $GIT_BRANCH \
|
||||||
|
--consumer-app-version $GIT_COMMIT \
|
||||||
|
--broker-base-url ${PACT_BROKER_BASE_URL} \
|
||||||
|
--broker-username ${PACT_BROKER_USERNAME} \
|
||||||
|
--broker-password ${PACT_BROKER_PASSWORD}
|
||||||
|
|
||||||
|
echo "Pact contract publishing complete!"
|
||||||
|
echo "Head over to ${PACT_BROKER_BASE_URL}/pacts/provider/GettingStartedOrderApi/consumer/GettingStartedOrderWeb/version/$GIT_COMMIT and login with"
|
||||||
|
echo "=> Username: ${PACT_BROKER_USERNAME}"
|
||||||
|
echo "=> Password: ${PACT_BROKER_PASSWORD}"
|
||||||
|
echo "to see your published contracts."
|
||||||
6
renovate.json
Normal file
6
renovate.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
replit.nix
Normal file
10
replit.nix
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{ pkgs }: {
|
||||||
|
deps = [
|
||||||
|
pkgs.python39Full
|
||||||
|
pkgs.nodejs-16_x
|
||||||
|
pkgs.nodePackages.typescript-language-server
|
||||||
|
pkgs.yarn
|
||||||
|
pkgs.replitPackages.jest
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
|
}
|
||||||
5
runConsumerTest.js
Normal file
5
runConsumerTest.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Let's use async/await
|
||||||
|
(async () => {
|
||||||
|
const { runTests } = require("./runTest.js");
|
||||||
|
await runTests("consumer");
|
||||||
|
})();
|
||||||
5
runProviderTest.js
Normal file
5
runProviderTest.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Let's use async/await
|
||||||
|
(async () => {
|
||||||
|
const { runTests } = require("./runTest.js");
|
||||||
|
await runTests("provider");
|
||||||
|
})();
|
||||||
33
runTest.js
Normal file
33
runTest.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
const { pactFile } = require("./pact.js");
|
||||||
|
|
||||||
|
// Let's use async/await
|
||||||
|
module.exports = {
|
||||||
|
runTests: async (testType) => {
|
||||||
|
const Mocha = require("mocha");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
// Instantiate a Mocha instance.
|
||||||
|
const mocha = new Mocha();
|
||||||
|
const testDir = `${__dirname}/${testType}`;
|
||||||
|
// Add spec files
|
||||||
|
fs.readdirSync(testDir)
|
||||||
|
.filter((file) => file.endsWith("spec.js"))
|
||||||
|
.forEach((file) => mocha.addFile(path.join(testDir, file)));
|
||||||
|
|
||||||
|
mocha.timeout(45000);
|
||||||
|
mocha.run((failures) => {
|
||||||
|
if (failures) {
|
||||||
|
console.log(
|
||||||
|
`${testType} test failed :(\nOpen the log file in ./logs to see what happened`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`${testType} test passed! Open the pact file in ${pactFile}`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
11
scripts/ci/test.sh
Executable file
11
scripts/ci/test.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash -eu
|
||||||
|
set -eu # This needs to be here for windows bash, which doesn't read the #! line above
|
||||||
|
|
||||||
|
|
||||||
|
node --version
|
||||||
|
npm run intro
|
||||||
|
npm run test:consumer
|
||||||
|
npm run pact:show
|
||||||
|
npm run test:provider
|
||||||
|
npm test
|
||||||
|
npm run get:broker
|
||||||
Loading…
Add table
Reference in a new issue