Files
calctext/_bmad/tea/testarch/knowledge/pactjs-utils-overview.md
2026-03-16 19:54:53 -04:00

10 KiB

Pact.js Utils Overview

Principle

Use production-ready utilities from @seontechnologies/pactjs-utils to eliminate boilerplate in consumer-driven contract testing. The library wraps @pact-foundation/pact with type-safe helpers for provider state creation, PactV4 JSON interaction builders, verifier configuration, and request filter injection — working equally well for HTTP and message (async/Kafka) contracts.

Rationale

Problems with raw @pact-foundation/pact

  • JsonMap casting: Provider state parameters require JsonMap type — manually casting every value is error-prone and verbose
  • Repeated builder lambdas: PactV4 interactions often repeat inline callbacks with builder.query(...), builder.headers(...), and builder.jsonBody(...)
  • Verifier configuration sprawl: VerifierOptions requires 30+ lines of scattered configuration (broker URL, selectors, state handlers, request filters, version tags)
  • Environment variable juggling: Different env vars for local vs remote flows, breaking change coordination, payload URL matching
  • Express middleware types: Request filter requires Express types that aren't re-exported from Pact
  • Bearer prefix bugs: Easy to double-prefix tokens as Bearer Bearer ... in request filters
  • CI version tagging: Manual logic to extract branch/tag info from CI environment

Solutions from pactjs-utils

  • createProviderState: One-call tuple builder for .given() — handles all JsonMap conversion automatically
  • toJsonMap: Explicit type coercion (null→"null", Date→ISO string, nested objects flattened)
  • setJsonContent: Curried callback helper for PactV4 .withRequest(...) / .willRespondWith(...) builders (query/headers/body)
  • setJsonBody: Body-only shorthand alias of setJsonContent({ body })
  • buildVerifierOptions: Single function assembles complete VerifierOptions from minimal inputs — handles local/remote/BDCT flows
  • buildMessageVerifierOptions: Same as above but for message/Kafka provider verification
  • handlePactBrokerUrlAndSelectors: Resolves broker URL and consumer version selectors from env vars with breaking change awareness
  • getProviderVersionTags: CI-aware version tagging (extracts branch/tag from GitHub Actions, GitLab CI, etc.)
  • createRequestFilter: Pluggable token generator pattern — prevents double-Bearer bugs by contract
  • noOpRequestFilter: Pass-through for providers that don't require auth injection

Installation

npm install -D @seontechnologies/pactjs-utils

# Peer dependency
npm install -D @pact-foundation/pact

Requirements: @pact-foundation/pact >= 16.2.0, Node.js >= 18

Available Utilities

Category Function Description Use Case
Consumer Helpers createProviderState Builds [stateName, JsonMap] tuple from typed input Consumer tests: .given(...createProviderState(input))
Consumer Helpers toJsonMap Converts any object to Pact-compatible JsonMap Explicit type coercion for provider state params
Consumer Helpers setJsonContent Curried request/response JSON callback helper PactV4 .withRequest(...) and .willRespondWith(...) builders
Consumer Helpers setJsonBody Body-only alias of setJsonContent Body-only .willRespondWith(...) responses
Provider Verifier buildVerifierOptions Assembles complete HTTP VerifierOptions Provider verification: new Verifier(buildVerifierOptions(...))
Provider Verifier buildMessageVerifierOptions Assembles message VerifierOptions Kafka/async provider verification
Provider Verifier handlePactBrokerUrlAndSelectors Resolves broker URL + selectors from env vars Env-aware broker configuration
Provider Verifier getProviderVersionTags CI-aware version tag extraction Provider version tagging in CI
Request Filter createRequestFilter Express middleware with pluggable token generator Auth injection for provider verification
Request Filter noOpRequestFilter Pass-through filter (no-op) Providers without auth requirements

Decision Tree: Which Flow?

Is this a monorepo (consumer + provider in same repo)?
├── YES → Local Flow
│   - Consumer generates pact files to ./pacts/
│   - Provider reads pact files from ./pacts/ (no broker needed)
│   - Use buildVerifierOptions with pactUrls option
│
└── NO → Do you have a Pact Broker / PactFlow?
    ├── YES → Remote (CDCT) Flow
    │   - Consumer publishes pacts to broker
    │   - Provider verifies from broker
    │   - Use buildVerifierOptions with broker config
    │   - Set PACT_BROKER_BASE_URL + PACT_BROKER_TOKEN
    │
    └── Do you have an OpenAPI spec?
        ├── YES → BDCT Flow (PactFlow only)
        │   - Provider publishes OpenAPI spec to PactFlow
        │   - PactFlow cross-validates consumer pacts against spec
        │   - No provider verification test needed
        │
        └── NO → Start with Local Flow, migrate to Remote later

Design Philosophy

  1. One-call setup: Each utility does one thing completely — no multi-step assembly required
  2. Environment-aware: Utilities read env vars for CI/CD integration without manual wiring
  3. Type-safe: Full TypeScript types for all inputs and outputs, exported for consumer use
  4. Fail-safe defaults: Sensible defaults that work locally; env vars override for CI
  5. Composable: Utilities work independently — use only what you need

Pattern Examples

Example 1: Minimal Consumer Test

import { PactV3 } from '@pact-foundation/pact';
import { createProviderState } from '@seontechnologies/pactjs-utils';

const provider = new PactV3({
  consumer: 'my-frontend',
  provider: 'my-api',
  dir: './pacts',
});

it('should get user by id', async () => {
  await provider
    .given(...createProviderState({ name: 'user exists', params: { id: 1 } }))
    .uponReceiving('a request for user 1')
    .withRequest({ method: 'GET', path: '/users/1' })
    .willRespondWith({ status: 200, body: { id: 1, name: 'John' } })
    .executeTest(async (mockServer) => {
      const res = await fetch(`${mockServer.url}/users/1`);
      expect(res.status).toBe(200);
    });
});

Example 2: Minimal Provider Verification

import { Verifier } from '@pact-foundation/pact';
import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';

const opts = buildVerifierOptions({
  provider: 'my-api',
  port: '3001',
  includeMainAndDeployed: true,
  stateHandlers: {
    'user exists': async (params) => {
      await db.seed({ users: [{ id: params?.id }] });
    },
  },
  requestFilter: createRequestFilter({
    tokenGenerator: () => 'test-token-123',
  }),
});

await new Verifier(opts).verifyProvider();

Key Points

  • Import path: Always use @seontechnologies/pactjs-utils (no subpath exports)
  • Peer dependency: @pact-foundation/pact must be installed separately
  • Local flow: No broker needed — set pactUrls in verifier options pointing to local pact files
  • Remote flow: Set PACT_BROKER_BASE_URL and PACT_BROKER_TOKEN env vars
  • Breaking changes: Set includeMainAndDeployed: false when coordinating breaking changes (verifies only matchingBranch)
  • Builder helpers: Use setJsonContent when you need query/headers/body together; use setJsonBody for body-only callbacks
  • Type exports: Library exports StateHandlers, RequestFilter, JsonMap, JsonContentInput, ConsumerVersionSelector types
  • pactjs-utils-consumer-helpers.md — detailed createProviderState, toJsonMap, setJsonContent, and setJsonBody usage
  • pactjs-utils-provider-verifier.md — detailed buildVerifierOptions and broker configuration
  • pactjs-utils-request-filter.md — detailed createRequestFilter and auth patterns
  • contract-testing.md — foundational contract testing patterns (raw Pact.js approach)
  • test-levels-framework.md — where contract tests fit in the testing pyramid

Anti-Patterns

Wrong: Manual VerifierOptions assembly when pactjs-utils is available

// ❌ Don't assemble VerifierOptions manually
const opts: VerifierOptions = {
  provider: 'my-api',
  providerBaseUrl: 'http://localhost:3001',
  pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
  pactBrokerToken: process.env.PACT_BROKER_TOKEN,
  publishVerificationResult: process.env.CI === 'true',
  providerVersion: process.env.GIT_SHA || 'dev',
  consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }],
  stateHandlers: {
    /* ... */
  },
  requestFilter: (req, res, next) => {
    /* ... */
  },
  // ... 20 more lines
};

Right: Use buildVerifierOptions

// ✅ Single call handles all configuration
const opts = buildVerifierOptions({
  provider: 'my-api',
  port: '3001',
  includeMainAndDeployed: true,
  stateHandlers: {
    /* ... */
  },
  requestFilter: createRequestFilter({ tokenGenerator: () => 'token' }),
});

Wrong: Importing raw Pact types for JsonMap conversion

// ❌ Manual JsonMap casting
import type { JsonMap } from '@pact-foundation/pact';

provider.given('user exists', { id: 1 as unknown as JsonMap['id'] });

Right: Use createProviderState

// ✅ Automatic type conversion
import { createProviderState } from '@seontechnologies/pactjs-utils';

provider.given(...createProviderState({ name: 'user exists', params: { id: 1 } }));

Source: @seontechnologies/pactjs-utils library, pactjs-utils README, pact-js-example-provider workflows