# Pact - Contract Testing

Comprehensive guide to consumer-driven contract testing with Pact for microservices and APIs.

## Overview

**Container**: `pactfoundation/pact-broker:latest` (Broker)
**Container**: `pactfoundation/pact-cli:latest` (CLI)
**Category**: Contract Testing
**Port**: 9292 (Broker)
**URL**: http://localhost:9292

Pact is a contract testing tool that ensures service compatibility by capturing consumer expectations and verifying that providers meet them.

## Quick Start

```bash
# Access Pact Broker
open http://localhost:9292

# Default credentials
# Username: admin
# Password: admin

# Publish pact file
docker exec pact-cli pact-broker publish /pacts \
  --broker-base-url=http://pact-broker:9292 \
  --consumer-app-version=1.0.0
```

## Core Concepts

| Concept | Description |
|---------|-------------|
| **Consumer** | Service that makes API requests |
| **Provider** | Service that responds to requests |
| **Pact** | Contract file capturing interactions |
| **Interaction** | Single request/response pair |
| **Verification** | Provider testing against contracts |
| **Broker** | Central hub for sharing contracts |

## Consumer Tests (JavaScript)

### Basic Consumer Test

```javascript
// consumer.pact.test.js
const { Pact } = require('@pact-foundation/pact');
const { UserService } = require('./user-service');

describe('User API Consumer', () => {
  const provider = new Pact({
    consumer: 'UserWeb',
    provider: 'UserAPI',
    port: 1234,
    log: './logs/pact.log',
    dir: './pacts',
  });

  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());
  afterEach(() => provider.verify());

  describe('get user by id', () => {
    beforeAll(() => {
      return provider.addInteraction({
        state: 'a user exists with id 1',
        uponReceiving: 'a request for user 1',
        withRequest: {
          method: 'GET',
          path: '/api/users/1',
          headers: {
            Accept: 'application/json',
          },
        },
        willRespondWith: {
          status: 200,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            id: 1,
            name: 'John Doe',
            email: 'john@example.com',
          },
        },
      });
    });

    it('returns the user', async () => {
      const user = await UserService.getUser(1);
      expect(user.id).toBe(1);
      expect(user.name).toBe('John Doe');
    });
  });
});
```

### Using Matchers

```javascript
const { Matchers } = require('@pact-foundation/pact');
const { like, eachLike, term, iso8601DateTimeWithMillis } = Matchers;

provider.addInteraction({
  state: 'users exist',
  uponReceiving: 'a request for all users',
  withRequest: {
    method: 'GET',
    path: '/api/users',
    query: {
      page: term({ matcher: '\\d+', generate: '1' }),
    },
  },
  willRespondWith: {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: {
      data: eachLike({
        id: like(1),
        name: like('John Doe'),
        email: term({
          matcher: '[\\w.]+@[\\w.]+',
          generate: 'john@example.com',
        }),
        createdAt: iso8601DateTimeWithMillis(),
      }),
      meta: {
        total: like(100),
        page: like(1),
        perPage: like(20),
      },
    },
  },
});
```

## Provider Verification (JavaScript)

```javascript
// provider.pact.test.js
const { Verifier } = require('@pact-foundation/pact');
const { app } = require('./app');

describe('Provider Verification', () => {
  let server;

  beforeAll((done) => {
    server = app.listen(3000, done);
  });

  afterAll((done) => {
    server.close(done);
  });

  it('validates the expectations of the consumer', () => {
    return new Verifier({
      provider: 'UserAPI',
      providerBaseUrl: 'http://localhost:3000',
      pactBrokerUrl: 'http://localhost:9292',
      pactBrokerUsername: 'admin',
      pactBrokerPassword: 'admin',
      publishVerificationResult: true,
      providerVersion: '1.0.0',
      stateHandlers: {
        'a user exists with id 1': async () => {
          // Set up state: create user with id 1
          await database.createUser({ id: 1, name: 'John Doe' });
        },
        'no users exist': async () => {
          // Set up state: clear all users
          await database.clearUsers();
        },
      },
    }).verifyProvider();
  });
});
```

## Consumer Tests (Python)

```python
# test_consumer.py
import pytest
from pact import Consumer, Provider
from user_service import UserService

pact = Consumer('UserWeb').has_pact_with(
    Provider('UserAPI'),
    host_name='localhost',
    port=1234,
    pact_dir='./pacts'
)

def test_get_user():
    expected = {
        'id': 1,
        'name': 'John Doe',
        'email': 'john@example.com'
    }

    (pact
     .given('a user exists with id 1')
     .upon_receiving('a request for user 1')
     .with_request('GET', '/api/users/1')
     .will_respond_with(200, body=expected))

    with pact:
        user = UserService('http://localhost:1234').get_user(1)
        assert user['id'] == 1
        assert user['name'] == 'John Doe'
```

## Provider Verification (Python)

```python
# test_provider.py
import pytest
from pact import Verifier

def test_provider_against_pacts():
    verifier = Verifier(
        provider='UserAPI',
        provider_base_url='http://localhost:3000'
    )

    output, _ = verifier.verify_pacts(
        './pacts/userweb-userapi.json',
        enable_pending=False,
        publish_verification_results=True,
        provider_version='1.0.0'
    )

    assert output == 0
```

## Pact Broker CLI

```bash
# Publish pacts
docker exec pact-cli pact-broker publish /pacts \
  --broker-base-url=http://pact-broker:9292 \
  --consumer-app-version=1.0.0 \
  --branch=main \
  --tag=production

# Can I Deploy check
docker exec pact-cli pact-broker can-i-deploy \
  --pacticipant UserWeb \
  --version 1.0.0 \
  --to-environment production \
  --broker-base-url=http://pact-broker:9292

# Record deployment
docker exec pact-cli pact-broker record-deployment \
  --pacticipant UserWeb \
  --version 1.0.0 \
  --environment production \
  --broker-base-url=http://pact-broker:9292

# Create webhook
docker exec pact-cli pact-broker create-webhook \
  'https://ci.example.com/trigger' \
  --broker-base-url=http://pact-broker:9292 \
  --contract-content-changed \
  --provider UserAPI

# List latest pact versions
docker exec pact-cli pact-broker list-latest-pact-versions \
  --broker-base-url=http://pact-broker:9292

# Delete pacticipant
docker exec pact-cli pact-broker delete-branch \
  --pacticipant UserWeb \
  --branch feature-x \
  --broker-base-url=http://pact-broker:9292
```

## Pact Broker API

```bash
# Get all pacts for provider
curl http://localhost:9292/pacts/provider/UserAPI/latest

# Get specific pact
curl http://localhost:9292/pacts/provider/UserAPI/consumer/UserWeb/latest

# Get matrix (compatibility)
curl "http://localhost:9292/matrix?q[][pacticipant]=UserWeb&q[][version]=1.0.0&q[][pacticipant]=UserAPI&q[][version]=2.0.0"

# Trigger provider verification
curl -X POST http://localhost:9292/pacts/provider/UserAPI/consumer/UserWeb/pact-version/abc123/verification-results \
  -H "Content-Type: application/json" \
  -d '{"success": true, "providerApplicationVersion": "2.0.0"}'
```

## Can I Deploy

The "Can I Deploy" check prevents incompatible deployments:

```bash
# Check if consumer can deploy
docker exec pact-cli pact-broker can-i-deploy \
  --pacticipant UserWeb \
  --version 1.0.0 \
  --to production

# Check specific provider version
docker exec pact-cli pact-broker can-i-deploy \
  --pacticipant UserWeb \
  --version 1.0.0 \
  --pacticipant UserAPI \
  --version 2.0.0

# Dry run (don't fail)
docker exec pact-cli pact-broker can-i-deploy \
  --pacticipant UserWeb \
  --version 1.0.0 \
  --to production \
  --dry-run
```

## Provider States

Provider states set up test data before verification:

```javascript
// Provider state handlers
const stateHandlers = {
  'no users exist': async () => {
    await db.query('DELETE FROM users');
    return { description: 'Users table emptied' };
  },
  'a user exists with id 1': async () => {
    await db.query(
      'INSERT INTO users (id, name, email) VALUES (1, "John Doe", "john@example.com")'
    );
    return { userId: 1 };
  },
  'a user exists': async (params) => {
    await db.query(
      'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
      [params.id, params.name, params.email]
    );
    return { userId: params.id };
  },
};
```

## CI/CD Integration

### GitHub Actions

```yaml
name: Contract Testing
on: [push]

jobs:
  consumer-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install dependencies
        run: npm install

      - name: Run consumer tests
        run: npm run test:pact

      - name: Publish pacts
        run: |
          docker run --rm -v ${PWD}/pacts:/pacts \
            pactfoundation/pact-cli:latest \
            publish /pacts \
            --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
            --broker-token=${{ secrets.PACT_BROKER_TOKEN }} \
            --consumer-app-version=${{ github.sha }} \
            --branch=${{ github.ref_name }}

  provider-verification:
    runs-on: ubuntu-latest
    needs: consumer-tests
    steps:
      - uses: actions/checkout@v3

      - name: Start provider
        run: docker-compose up -d provider

      - name: Verify pacts
        run: |
          npm run test:pact:verify -- \
            --provider-version=${{ github.sha }} \
            --publish-verification-results

      - name: Can I Deploy
        run: |
          docker run --rm \
            pactfoundation/pact-cli:latest \
            can-i-deploy \
            --pacticipant UserAPI \
            --version ${{ github.sha }} \
            --to production \
            --broker-base-url=${{ secrets.PACT_BROKER_URL }}
```

## Webhooks

Configure webhooks for automatic verification:

```bash
# Webhook on contract change
docker exec pact-cli pact-broker create-webhook \
  'https://api.github.com/repos/org/provider/dispatches' \
  --broker-base-url=http://pact-broker:9292 \
  --request-method POST \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/vnd.github.v3+json' \
  --header 'Authorization: token ${GITHUB_TOKEN}' \
  --data '{"event_type": "pact_changed", "client_payload": {"pact_url": "${pactbroker.pactUrl}"}}' \
  --contract-content-changed \
  --provider UserAPI

# Webhook on verification published
docker exec pact-cli pact-broker create-webhook \
  'https://hooks.slack.com/services/xxx' \
  --broker-base-url=http://pact-broker:9292 \
  --request-method POST \
  --header 'Content-Type: application/json' \
  --data '{"text": "Pact verification ${pactbroker.verificationResultSuccess} for ${pactbroker.consumerName} -> ${pactbroker.providerName}"}' \
  --provider-verification-published
```

## Best Practices

### 1. Consumer-First Development
- Write consumer tests first
- Define expected behavior upfront
- Provider implements to contract

### 2. Minimal Contracts
- Only test consumer needs
- Don't verify entire API
- Focus on actual usage

### 3. Provider States
- Keep states simple
- Use meaningful names
- Clean up after tests

### 4. Versioning Strategy
- Use git SHA for versions
- Tag with branches
- Track environments

### 5. Can I Deploy
- Always check before deploy
- Integrate in CI/CD
- Block incompatible releases

## Integration with Stack

- Validates API contracts alongside Newman tests
- Complements RESTler API fuzzing
- Works with WireMock for complex mocks
- Findings can be tracked in DefectDojo
- Results visible in Allure reports

## Troubleshooting

### Common Issues

**Issue**: Pact verification fails
```bash
# Check provider state setup
# Verify mock data matches expectations
# Check for network issues
```

**Issue**: Publishing fails
```bash
# Verify broker URL and credentials
# Check pact file exists and is valid JSON
# Ensure version is unique
```

**Issue**: Can I Deploy fails
```bash
# Check if contracts are published
# Verify provider has verified
# Check environment tags
```
