Files
rust_browser/_bmad/tea/testarch/knowledge/pactjs-utils-provider-verifier.md
Zachary D. Rowitsch 931f17b70e
All checks were successful
ci / fast (linux) (push) Successful in 6m46s
Add BMAD framework, planning artifacts, and architecture decision document
Install BMAD workflow framework with agent commands and templates.
Create product brief, PRD, project context, and architecture decision
document covering networking/persistence strategy, JS engine evolution
path, threading model, web_api scaling, system integration, and
tab/process model. Add generated project documentation (architecture
overview, component inventory, development guide, source tree analysis).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:13:06 -04:00

14 KiB

Pact.js Utils Provider Verifier

Principle

Use buildVerifierOptions, buildMessageVerifierOptions, handlePactBrokerUrlAndSelectors, and getProviderVersionTags from @seontechnologies/pactjs-utils to assemble complete provider verification configuration in a single call. These utilities handle local/remote flow detection, broker URL resolution, consumer version selector strategy, and CI-aware version tagging. The caller controls breaking change behavior via the required includeMainAndDeployed parameter.

Rationale

Problems with manual VerifierOptions

  • 30+ lines of scattered config: Assembling VerifierOptions manually requires broker URL, token, selectors, state handlers, request filters, version info, publish flags — all in one object
  • Environment variable logic: Different env vars for local vs remote, CI vs local dev, breaking change vs normal flow
  • Consumer version selector complexity: Choosing between mainBranch, deployedOrReleased, matchingBranch, and includeMainAndDeployed requires understanding Pact Broker semantics
  • Breaking change coordination: When a provider intentionally breaks a contract, manual selector switching is error-prone
  • Cross-execution protection: PACT_PAYLOAD_URL webhook payloads need special handling to verify only the triggering pact

Solutions

  • buildVerifierOptions: Single function that reads env vars, selects the right flow, and returns complete VerifierOptions
  • buildMessageVerifierOptions: Same as above for message/Kafka provider verification
  • handlePactBrokerUrlAndSelectors: Pure function for broker URL + selector resolution (used internally, also exported for advanced use)
  • getProviderVersionTags: Extracts CI branch/tag info from environment for provider version tagging

Pattern Examples

Example 1: HTTP Provider Verification (Remote Flow)

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

const stateHandlers: StateHandlers = {
  'movie with id 1 exists': {
    setup: async (params) => {
      await db.seed({ movies: [{ id: params?.id ?? 1, name: 'Inception' }] });
    },
    teardown: async () => {
      await db.clean('movies');
    },
  },
  'no movies exist': async () => {
    await db.clean('movies');
  },
};

// buildVerifierOptions reads these env vars automatically:
// - PACT_BROKER_BASE_URL (broker URL)
// - PACT_BROKER_TOKEN (broker auth)
// - PACT_PAYLOAD_URL (webhook trigger — cross-execution protection)
// - PACT_BREAKING_CHANGE (if "true", uses includeMainAndDeployed selectors)
// - GITHUB_SHA (provider version)
// - CI (publish verification results if "true")

const opts = buildVerifierOptions({
  provider: 'SampleMoviesAPI',
  port: '3001',
  includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
  stateHandlers,
  requestFilter: createRequestFilter({
    tokenGenerator: () => process.env.TEST_AUTH_TOKEN ?? 'test-token',
  }),
});

await new Verifier(opts).verifyProvider();

Key Points:

  • Set PACT_BROKER_BASE_URL and PACT_BROKER_TOKEN as env vars — buildVerifierOptions reads them automatically
  • port is a string (e.g., '3001') — the function builds providerBaseUrl: http://localhost:${port} internally
  • includeMainAndDeployed is required — set true for normal flow, false for breaking changes
  • State handlers support both simple functions and { setup, teardown } objects
  • params in state handlers correspond to the JsonMap from consumer's createProviderState
  • Verification results are published by default (publishVerificationResult defaults to true)

Example 2: Local Flow (Monorepo, No Broker)

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

// When PACT_BROKER_BASE_URL is NOT set, buildVerifierOptions
// falls back to local pact file verification
const opts = buildVerifierOptions({
  provider: 'SampleMoviesAPI',
  port: '3001',
  includeMainAndDeployed: true,
  // Specify local pact files directly — skips broker entirely
  pactUrls: ['./pacts/movie-web-SampleMoviesAPI.json'],
  stateHandlers: {
    'movie exists': async (params) => {
      await db.seed({ movies: [{ id: params?.id }] });
    },
  },
});

await new Verifier(opts).verifyProvider();

Example 3: Message Provider Verification (Kafka/Async)

import { Verifier } from '@pact-foundation/pact';
import { buildMessageVerifierOptions } from '@seontechnologies/pactjs-utils';

const opts = buildMessageVerifierOptions({
  provider: 'OrderEventsProducer',
  includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
  // Message handlers return the message content that the provider would produce
  messageProviders: {
    'an order created event': async () => ({
      orderId: 'order-123',
      userId: 'user-456',
      items: [{ productId: 'prod-789', quantity: 2 }],
      createdAt: new Date().toISOString(),
    }),
    'an order cancelled event': async () => ({
      orderId: 'order-123',
      reason: 'customer_request',
      cancelledAt: new Date().toISOString(),
    }),
  },
  stateHandlers: {
    'order exists': async (params) => {
      await db.seed({ orders: [{ id: params?.orderId }] });
    },
  },
});

await new Verifier(opts).verifyProvider();

Key Points:

  • buildMessageVerifierOptions adds messageProviders to the verifier config
  • Each message provider function returns the expected message payload
  • State handlers work the same as HTTP verification
  • Broker integration works identically (same env vars)

Example 4: Breaking Change Coordination

// When a provider intentionally introduces a breaking change:
//
// 1. Set PACT_BREAKING_CHANGE=true in CI environment
// 2. Your test reads the env var and passes includeMainAndDeployed: false
//    to buildVerifierOptions — this verifies ONLY against the matching
//    branch, skipping main/deployed consumers that would fail
// 3. Coordinate with consumer team to update their pact on a matching branch
// 4. Remove PACT_BREAKING_CHANGE flag after consumer updates

// In CI environment (.github/workflows/provider-verify.yml):
// env:
//   PACT_BREAKING_CHANGE: 'true'

// Your provider test code reads the env var:
const isBreakingChange = process.env.PACT_BREAKING_CHANGE === 'true';

const opts = buildVerifierOptions({
  provider: 'SampleMoviesAPI',
  port: '3001',
  includeMainAndDeployed: !isBreakingChange, // false during breaking changes
  stateHandlers: {
    /* ... */
  },
});
// When includeMainAndDeployed is false (breaking change):
//   selectors = [{ matchingBranch: true }]
// When includeMainAndDeployed is true (normal):
//   selectors = [{ matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true }]

Example 5: handlePactBrokerUrlAndSelectors (Advanced)

import { handlePactBrokerUrlAndSelectors } from '@seontechnologies/pactjs-utils';
import type { VerifierOptions } from '@pact-foundation/pact';

// For advanced use cases — mutates the options object in-place (returns void)
const options: VerifierOptions = {
  provider: 'SampleMoviesAPI',
  providerBaseUrl: 'http://localhost:3001',
};

handlePactBrokerUrlAndSelectors({
  pactPayloadUrl: process.env.PACT_PAYLOAD_URL,
  pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
  consumer: undefined, // or specific consumer name
  includeMainAndDeployed: true,
  options, // mutated in-place: sets pactBrokerUrl, consumerVersionSelectors, or pactUrls
});

// After call, options has been mutated with:
// - options.pactBrokerUrl (from pactBrokerUrl param)
// - options.consumerVersionSelectors (based on includeMainAndDeployed)
// OR if pactPayloadUrl matches: options.pactUrls = [pactPayloadUrl]

Note: handlePactBrokerUrlAndSelectors is called internally by buildVerifierOptions. You rarely need it directly — use it only for advanced custom verifier assembly.

Example 6: getProviderVersionTags

import { getProviderVersionTags } from '@seontechnologies/pactjs-utils';

// Extracts version tags from CI environment
const tags = getProviderVersionTags();

// In GitHub Actions on branch "feature/add-movies" (non-breaking):
//   tags = ['dev', 'feature/add-movies']
//
// In GitHub Actions on main branch (non-breaking):
//   tags = ['dev', 'main']
//
// In GitHub Actions with PACT_BREAKING_CHANGE=true:
//   tags = ['feature/add-movies']  (no 'dev' tag)
//
// Locally (no CI):
//   tags = ['local']

Environment Variables Reference

Variable Required Description Default
PACT_BROKER_BASE_URL For remote flow Pact Broker / PactFlow URL
PACT_BROKER_TOKEN For remote flow API token for broker authentication
GITHUB_SHA Recommended Provider version for verification result publishing (auto-set by GitHub Actions) 'unknown'
GITHUB_BRANCH Recommended Branch name for provider version branch and version tags (not auto-set — define as ${{ github.head_ref || github.ref_name }}) 'main'
PACT_PAYLOAD_URL Optional Webhook payload URL — triggers verification of specific pact only
PACT_BREAKING_CHANGE Optional Set to "true" to use breaking change selector strategy 'false'
CI Auto-detected When "true", enables verification result publishing

Key Points

  • Flow auto-detection: If PACT_BROKER_BASE_URL is set → remote flow; otherwise → local flow (requires pactUrls)
  • port is a string: Pass port number as string (e.g., '3001'); function builds http://localhost:${port} internally
  • includeMainAndDeployed is required: true = verify matchingBranch + mainBranch + deployedOrReleased; false = verify matchingBranch only (for breaking changes)
  • Selector strategy: Normal flow (includeMainAndDeployed: true) includes all selectors; breaking change flow (false) includes only matchingBranch
  • Webhook support: PACT_PAYLOAD_URL takes precedence — verifies only the specific pact that triggered the webhook
  • State handler types: Both async (params) => void and { setup: async (params) => void, teardown: async () => void } are supported
  • Version publishing: Verification results are published by default (publishVerificationResult defaults to true)
  • pactjs-utils-overview.md — installation, decision tree, design philosophy
  • pactjs-utils-consumer-helpers.md — consumer-side state parameter creation
  • pactjs-utils-request-filter.md — auth injection for provider verification
  • contract-testing.md — foundational patterns with raw Pact.js

Anti-Patterns

Wrong: Manual broker URL and selector assembly

// ❌ Manual environment variable handling
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 || process.env.GITHUB_SHA || 'dev',
  providerVersionBranch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME,
  consumerVersionSelectors:
    process.env.PACT_BREAKING_CHANGE === 'true'
      ? [{ matchingBranch: true }]
      : [{ matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true }],
  pactUrls: process.env.PACT_PAYLOAD_URL ? [process.env.PACT_PAYLOAD_URL] : undefined,
  stateHandlers: {
    /* ... */
  },
  requestFilter: (req, res, next) => {
    req.headers['authorization'] = `Bearer ${process.env.TEST_TOKEN}`;
    next();
  },
};

Right: Use buildVerifierOptions

// ✅ All env var logic handled internally
const opts = buildVerifierOptions({
  provider: 'my-api',
  port: '3001',
  includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
  stateHandlers: {
    /* ... */
  },
  requestFilter: createRequestFilter({
    tokenGenerator: () => process.env.TEST_TOKEN ?? 'test-token',
  }),
});

Wrong: Hardcoding consumer version selectors

// ❌ Hardcoded selectors — breaks when flow changes
consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }],

Right: Let buildVerifierOptions choose selectors

// ✅ Selector strategy adapts to PACT_BREAKING_CHANGE env var
const opts = buildVerifierOptions({
  /* ... */
});
// Selectors chosen automatically based on environment

Source: @seontechnologies/pactjs-utils provider-verifier module, pact-js-example-provider CI workflows