Files
rust_browser/_bmad/tea/testarch/knowledge/pactjs-utils-overview.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

9.3 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, 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
  • 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)
  • 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
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)
  • Type exports: Library exports StateHandlers, RequestFilter, JsonMap, ConsumerVersionSelector types
  • pactjs-utils-consumer-helpers.md — detailed createProviderState and toJsonMap 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