Contract Test Generator — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Contract Test Generator (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Contract testing bridges the gap between unit tests and full integration tests by verifying that services can communicate correctly without requiring all services to be running simultaneously. Consumer-driven contract testing, pioneered by the Pact framework, inverts the traditional approach: consumers define what they expect from providers, and providers verify they can satisfy those expectations. This skill guides AI coding agents through generating robust contract tests that catch integration breaking changes before they reach production.
project-root/
├── consumer/
│ ├── src/
│ │ ├── api-client.ts
│ │ └── types.ts
│ ├── tests/
│ │ └── contract/
│ │ ├── user-service.consumer.pact.ts
│ │ ├── order-service.consumer.pact.ts
│ │ └── helpers/
│ │ ├── pact-setup.ts
│ │ └── matchers.ts
│ ├── pacts/ # Generated pact files (JSON)
│ │ └── consumer-user_service.json
│ └── pact-config.ts
├── provider/
│ ├── src/
│ │ └── controllers/
│ ├── tests/
│ │ └── contract/
│ │ ├── provider-verification.pact.ts
│ │ └── state-handlers/
│ │ ├── user-states.ts
│ │ └── order-states.ts
│ └── pact-config.ts
├── pact-broker/
│ └── docker-compose.yml
└── ci/
├── publish-pacts.sh
├── verify-provider.sh
└── can-i-deploy.shConsumer tests define the expectations a consumer has of a provider API. The Pact mock server simulates the provider during consumer tests.
// consumer/tests/contract/user-service.consumer.pact.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import { resolve } from 'path';
import { UserApiClient } from '../../src/api-client';
const { like, eachLike, regex, integer, string, timestamp } = MatchersV3;
const provider = new PactV4({
consumer: 'frontend-app',
provider: 'user-service',
dir: resolve(__dirname, '../../pacts'),
logLevel: 'warn',
});
describe('User Service Contract', () => {
describe('GET /api/users/:id', () => {
it('returns a user when one exists', async () => {
await provider
.addInteraction()
.given('a user with ID 42 exists')
.uponReceiving('a request for user 42')
.withRequest('GET', '/api/users/42', (builder) => {
builder.headers({
Accept: 'application/json',
Authorization: regex(/^Bearer\s[\w-]+\.[\w-]+\.[\w-]+$/, 'Bearer eyJ.test.token'),
});
})
.willRespondWith(200, (builder) => {
builder.headers({ 'Content-Type': 'application/json' });
builder.jsonBody({
id: integer(42),
name: string('Jane Doe'),
email: regex(/^[\w.]+@[\w.]+\.\w+$/, '[email protected]'),
role: regex(/^(admin|user|moderator)$/, 'user'),
createdAt: timestamp("yyyy-MM-dd'T'HH:mm:ss.SSSX", '2024-01-15T10:30:00.000Z'),
preferences: like({
theme: string('dark'),
notifications: like(true),
}),
});
})
.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
const user = await client.getUser(42);
expect(user.id).toBe(42);
expect(user.name).toBeDefined();
expect(user.email).toContain('@');
});
});
it('returns 404 when user does not exist', async () => {
await provider
.addInteraction()
.given('no user with ID 999 exists')
.uponReceiving('a request for non-existent user 999')
.withRequest('GET', '/api/users/999', (builder) => {
builder.headers({
Accept: 'application/json',
Authorization: regex(/^Bearer\s[\w-]+\.[\w-]+\.[\w-]+$/, 'Bearer eyJ.test.token'),
});
})
.willRespondWith(404, (builder) => {
builder.headers({ 'Content-Type': 'application/json' });
builder.jsonBody({
error: string('Not Found'),
message: string('User with ID 999 not found'),
});
})
.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
await expect(client.getUser(999)).rejects.toThrow('User not found');
});
});
});
describe('POST /api/users', () => {
it('creates a new user', async () => {
await provider
.addInteraction()
.given('the user creation endpoint is available')
.uponReceiving('a request to create a user')
.withRequest('POST', '/api/users', (builder) => {
builder.headers({
'Content-Type': 'application/json',
Authorization: regex(/^Bearer\s.+$/, 'Bearer eyJ.admin.token'),
});
builder.jsonBody({
name: string('John Smith'),
email: regex(/^[\w.]+@[\w.]+\.\w+$/, '[email protected]'),
role: regex(/^(admin|user|moderator)$/, 'user'),
});
})
.willRespondWith(201, (builder) => {
builder.headers({ 'Content-Type': 'application/json' });
builder.jsonBody({
id: integer(1),
name: string('John Smith'),
email: string('[email protected]'),
role: string('user'),
createdAt: timestamp("yyyy-MM-dd'T'HH:mm:ss.SSSX", '2024-06-01T12:00:00.000Z'),
});
})
.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
const created = await client.createUser({
name: 'John Smith',
email: '[email protected]',
role: 'user',
});
expect(created.id).toBeDefined();
expect(created.name).toBe('John Smith');
});
});
});
});// consumer/src/test/java/com/example/contract/UserServiceConsumerPactTest.java
package com.example.contract;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.core.model.V4Pact;
import au.com.dius.pact.core.model.annotations.Pact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "user-service", port = "8080")
public class UserServiceConsumerPactTest {
@Pact(consumer = "order-service")
public V4Pact getUserPact(PactDslWithProvider builder) {
return builder
.given("a user with ID 42 exists")
.uponReceiving("a request for user 42")
.path("/api/users/42")
.method("GET")
.headers("Accept", "application/json")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new PactDslJsonBody()
.integerType("id", 42)
.stringType("name", "Jane Doe")
.stringMatcher("email", "^[\\w.]+@[\\w.]+\\.\\w+$", "[email protected]")
.stringType("role", "user")
.object("address")
.stringType("city", "Portland")
.stringType("state", "OR")
.closeObject()
)
.toPact(V4Pact.class);
}
@Test
@PactTestFor(pactMethod = "getUserPact")
void testGetUser(MockServer mockServer) {
UserApiClient client = new UserApiClient(mockServer.getUrl());
User user = client.getUser(42);
assertThat(user.getId()).isEqualTo(42);
assertThat(user.getName()).isNotBlank();
assertThat(user.getEmail()).contains("@");
}
}// provider/tests/contract/provider-verification.pact.ts
import { Verifier } from '@pact-foundation/pact';
import { resolve } from 'path';
import { app } from '../../src/app';
import { Server } from 'http';
let server: Server;
const PORT = 8081;
beforeAll(async () => {
server = app.listen(PORT);
});
afterAll(async () => {
server.close();
});
describe('Provider Verification', () => {
it('validates the expectations of all consumers', async () => {
const verifier = new Verifier({
providerBaseUrl: `http://localhost:${PORT}`,
provider: 'user-service',
// Option A: Verify from Pact Broker
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_COMMIT_SHA,
providerVersionBranch: process.env.GIT_BRANCH,
// Enable pending pacts (new contracts won't break provider builds)
enablePending: true,
includeWipPactsSince: '2024-01-01',
// State handlers set up test data for each provider state
stateHandlers: {
'a user with ID 42 exists': async () => {
await seedDatabase({
users: [{ id: 42, name: 'Jane Doe', email: '[email protected]', role: 'user' }],
});
},
'no user with ID 999 exists': async () => {
await clearDatabase('users');
},
'the user creation endpoint is available': async () => {
await clearDatabase('users');
await resetSequences('users');
},
},
// Request filter to add auth headers the provider requires
requestFilter: (req, _res, next) => {
req.headers['Authorization'] = `Bearer ${generateTestToken()}`;
next();
},
});
await verifier.verifyProvider();
});
});@Provider("user-service")
@PactBroker(
url = "${PACT_BROKER_BASE_URL}",
authentication = @PactBrokerAuth(token = "${PACT_BROKER_TOKEN}")
)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserServiceProviderPactTest {
@LocalServerPort
private int port;
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@State("a user with ID 42 exists")
void userExists() {
userRepository.save(new User(42L, "Jane Doe", "[email protected]", "user"));
}
@State("no user with ID 999 exists")
void userDoesNotExist() {
userRepository.deleteAll();
}
}# pact-broker/docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: pact_broker
POSTGRES_USER: pact_broker
POSTGRES_PASSWORD: pact_broker_password
volumes:
- pact-db:/var/lib/postgresql/data
pact-broker:
image: pactfoundation/pact-broker:latest
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_URL: postgres://pact_broker:pact_broker_password@postgres/pact_broker
PACT_BROKER_BASIC_AUTH_USERNAME: admin
PACT_BROKER_BASIC_AUTH_PASSWORD: admin
PACT_BROKER_ALLOW_PUBLIC_READ: "true"
PACT_BROKER_WEBHOOK_SCHEME_WHITELIST: https
PACT_BROKER_CHECK_FOR_POTENTIAL_DUPLICATE_PACTICIPANT_NAMES: "true"
depends_on:
- postgres
volumes:
pact-db:// scripts/publish-pacts.ts
import { Publisher } from '@pact-foundation/pact';
import { resolve } from 'path';
import { execSync } from 'child_process';
const gitCommitSha = execSync('git rev-parse HEAD').toString().trim();
const gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
async function publishPacts() {
const publisher = new Publisher({
pactBroker: process.env.PACT_BROKER_BASE_URL || 'http://localhost:9292',
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
pactFilesOrDirs: [resolve(__dirname, '../pacts')],
consumerVersion: gitCommitSha,
branch: gitBranch,
tags: [gitBranch],
buildUrl: process.env.CI_BUILD_URL,
});
await publisher.publishPacts();
console.log('Pacts published successfully');
}
publishPacts().catch((err) => {
console.error('Failed to publish pacts:', err);
process.exit(1);
});The can-i-deploy tool queries the Pact Broker to determine whether it is safe to deploy a particular version of an application.
#!/bin/bash
# ci/can-i-deploy.sh
PACTICIPANT=$1
VERSION=$(git rev-parse HEAD)
ENVIRONMENT=${2:-production}
echo "Checking if $PACTICIPANT version $VERSION can be deployed to $ENVIRONMENT..."
pact-broker can-i-deploy \
--pacticipant "$PACTICIPANT" \
--version "$VERSION" \
--to-environment "$ENVIRONMENT" \
--broker-base-url "$PACT_BROKER_BASE_URL" \
--broker-token "$PACT_BROKER_TOKEN" \
--retry-while-unknown 30 \
--retry-interval 10
if [ $? -eq 0 ]; then
echo "Safe to deploy. Recording deployment..."
pact-broker record-deployment \
--pacticipant "$PACTICIPANT" \
--version "$VERSION" \
--environment "$ENVIRONMENT" \
--broker-base-url "$PACT_BROKER_BASE_URL" \
--broker-token "$PACT_BROKER_TOKEN"
else
echo "BLOCKED: Cannot deploy $PACTICIPANT to $ENVIRONMENT"
exit 1
fi# .github/workflows/contract-tests.yml
name: Contract Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test:contract:consumer
- name: Publish pacts
if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request'
run: npx ts-node scripts/publish-pacts.ts
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
provider-verification:
runs-on: ubuntu-latest
needs: consumer-tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test:contract:provider
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GIT_COMMIT_SHA: ${{ github.sha }}
GIT_BRANCH: ${{ github.ref_name }}
can-i-deploy:
runs-on: ubuntu-latest
needs: [consumer-tests, provider-verification]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: |
docker run --rm \
pactfoundation/pact-cli:latest \
pact-broker can-i-deploy \
--pacticipant frontend-app \
--version ${{ github.sha }} \
--to-environment production \
--broker-base-url ${{ secrets.PACT_BROKER_BASE_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}Configure Pact Broker webhooks to trigger provider verification whenever a consumer publishes a new pact.
// scripts/setup-webhooks.ts
import axios from 'axios';
const BROKER_URL = process.env.PACT_BROKER_BASE_URL;
const BROKER_TOKEN = process.env.PACT_BROKER_TOKEN;
async function createWebhook() {
await axios.post(
`${BROKER_URL}/webhooks`,
{
description: 'Trigger provider verification on new pact',
events: [
{ name: 'contract_content_changed' },
{ name: 'contract_requiring_verification_published' },
],
request: {
method: 'POST',
url: 'https://api.github.com/repos/OWNER/REPO/dispatches',
headers: {
'Content-Type': 'application/json',
Accept: 'application/vnd.github.v3+json',
Authorization: 'Bearer ${user.githubToken}',
},
body: {
event_type: 'pact_changed',
client_payload: {
pact_url: '${pactbroker.pactUrl}',
consumer: '${pactbroker.consumerName}',
provider: '${pactbroker.providerName}',
consumer_version: '${pactbroker.consumerVersionNumber}',
},
},
},
},
{
headers: {
Authorization: `Bearer ${BROKER_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
}Pending pacts prevent new consumers from breaking existing provider builds. WIP (Work in Progress) pacts allow verification of contracts from feature branches.
// provider/pact-config.ts
export const providerVerificationConfig = {
// Pending pacts: new contracts won't fail the provider build
// Once a pact is successfully verified, it transitions out of pending
enablePending: true,
// WIP pacts: include pacts from consumer feature branches
// Only pacts published after this date are considered
includeWipPactsSince: '2024-01-01',
// Consumer version selectors determine which pacts to verify
consumerVersionSelectors: [
{ mainBranch: true }, // Pacts from consumers' main branch
{ deployedOrReleased: true }, // Pacts from currently deployed consumers
{ matchingBranch: true }, // Pacts from same-named feature branch
{ branch: 'develop' }, // Always verify develop branch pacts
],
};Bi-directional contract testing allows providers to publish their own OpenAPI specification rather than running consumer pact tests directly. The Pact Broker compares the consumer pact with the provider specification.
// provider/scripts/publish-provider-contract.ts
import { execSync } from 'child_process';
const gitSha = execSync('git rev-parse HEAD').toString().trim();
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
// Publish the provider's OpenAPI spec as its contract
execSync(`
pactflow publish-provider-contract \
./openapi/user-service.yaml \
--provider user-service \
--provider-app-version ${gitSha} \
--branch ${branch} \
--content-type application/yaml \
--verification-exit-code 0 \
--verification-results ./test-results/provider-tests.json \
--verification-results-content-type application/json \
--verifier pactflow-self-verification \
--broker-base-url ${process.env.PACT_BROKER_BASE_URL} \
--broker-token ${process.env.PACT_BROKER_TOKEN}
`);// consumer/tests/contract/graphql.consumer.pact.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, string, integer } = MatchersV3;
const provider = new PactV4({
consumer: 'graphql-client',
provider: 'graphql-gateway',
});
describe('GraphQL Contract', () => {
it('fetches user with orders via GraphQL', async () => {
await provider
.addInteraction()
.given('user 42 has orders')
.uponReceiving('a GraphQL query for user with orders')
.withRequest('POST', '/graphql', (builder) => {
builder.headers({ 'Content-Type': 'application/json' });
builder.jsonBody({
query: string(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
orders {
id
total
status
}
}
}
`),
variables: like({ id: '42' }),
});
})
.willRespondWith(200, (builder) => {
builder.jsonBody({
data: {
user: {
id: string('42'),
name: string('Jane Doe'),
orders: eachLike({
id: string('order-1'),
total: like(99.99),
status: string('SHIPPED'),
}),
},
},
});
})
.executeTest(async (mockServer) => {
const client = new GraphQLClient(mockServer.url + '/graphql');
const result = await client.query(GET_USER_WITH_ORDERS, { id: '42' });
expect(result.data.user.orders.length).toBeGreaterThan(0);
});
});
});For asynchronous event-driven systems, message pact testing validates the structure of messages published to queues or topics.
// consumer/tests/contract/order-events.consumer.pact.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like, string, integer, timestamp, regex } = MatchersV3;
const provider = new PactV4({
consumer: 'notification-service',
provider: 'order-service',
});
describe('Order Event Contract', () => {
it('handles order.completed events', async () => {
await provider
.addInteraction()
.given('an order has been completed')
.expectsToReceive('an order.completed event')
.withContentV4((builder) => {
builder.jsonBody({
eventType: string('order.completed'),
timestamp: timestamp("yyyy-MM-dd'T'HH:mm:ss.SSSX", '2024-06-01T12:00:00.000Z'),
payload: {
orderId: string('ord-12345'),
customerId: integer(42),
total: like(149.99),
currency: regex(/^[A-Z]{3}$/, 'USD'),
items: [
{
productId: string('prod-001'),
quantity: integer(2),
price: like(74.99),
},
],
shippingAddress: {
street: string('123 Main St'),
city: string('Portland'),
state: regex(/^[A-Z]{2}$/, 'OR'),
zip: regex(/^\d{5}(-\d{4})?$/, '97201'),
},
},
});
})
.executeTest(async (message) => {
const handler = new OrderCompletedHandler();
const result = await handler.handle(JSON.parse(message.contents.toString()));
expect(result.notificationSent).toBe(true);
expect(result.recipientId).toBe(42);
});
});
});
// provider/tests/contract/message-provider-verification.ts
import { MessageProviderPact } from '@pact-foundation/pact';
describe('Message Provider Verification', () => {
it('verifies order event messages', async () => {
const messageProvider = new MessageProviderPact({
provider: 'order-service',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_COMMIT_SHA,
messageProviders: {
'an order.completed event': async () => {
// Return the actual message your service would produce
return {
eventType: 'order.completed',
timestamp: new Date().toISOString(),
payload: {
orderId: 'ord-12345',
customerId: 42,
total: 149.99,
currency: 'USD',
items: [{ productId: 'prod-001', quantity: 2, price: 74.99 }],
shippingAddress: {
street: '123 Main St',
city: 'Portland',
state: 'OR',
zip: '97201',
},
},
};
},
},
stateHandlers: {
'an order has been completed': async () => {
// Set up state as needed
},
},
});
await messageProvider.verify();
});
});// consumer/pact-config.ts
import { LogLevel } from '@pact-foundation/pact';
import { resolve } from 'path';
export const pactConfig = {
// Pact file output directory
dir: resolve(__dirname, 'pacts'),
// Logging level: trace, debug, info, warn, error
logLevel: (process.env.PACT_LOG_LEVEL as LogLevel) || 'warn',
// Pact specification version
spec: 4,
// Timeout for pact mock server (milliseconds)
timeout: 30000,
};// provider/pact-config.ts
export const providerConfig = {
providerBaseUrl: process.env.PROVIDER_BASE_URL || 'http://localhost:3000',
provider: 'user-service',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_COMMIT_SHA || 'local',
providerVersionBranch: process.env.GIT_BRANCH || 'local',
enablePending: true,
includeWipPactsSince: '2024-01-01',
consumerVersionSelectors: [
{ mainBranch: true },
{ deployedOrReleased: true },
{ matchingBranch: true },
],
// Timeout for provider state setup (milliseconds)
stateHandlerTimeout: 10000,
// Request timeout for each interaction verification
requestTimeout: 15000,
};{
"scripts": {
"test:contract:consumer": "jest --testPathPattern=consumer.pact",
"test:contract:provider": "jest --testPathPattern=provider-verification",
"pact:publish": "ts-node scripts/publish-pacts.ts",
"pact:can-i-deploy": "bash ci/can-i-deploy.sh",
"pact:broker": "docker compose -f pact-broker/docker-compose.yml up -d"
}
}like(), regex(), and eachLike() validate structure and type rather than specific values, making contracts resilient to data changes.given() state should describe a precondition clearly (e.g., "a user with ID 42 exists") and the state handler should set up only what is needed.id and name fields from a 20-field response, the contract should only assert on those two fields.mainBranch and deployedOrReleased; add matchingBranch for feature branch coordination between teams.publishVerificationResult: true only when running in CI to avoid polluting the broker with local verification results.like(42) is correct; using the literal 42 is fragile.--retry-while-unknown flag handles asynchronous verification gracefully.logLevel: 'debug' in the Pact configuration or use the environment variable PACT_LOG_LEVEL=debug to see detailed request/response matching output.mockServer.url. Always construct the API client with the dynamic mock server URL.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.