All checks were successful
ci / fast (linux) (push) Successful in 6m46s
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>
9.3 KiB
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
JsonMaptype — manually casting every value is error-prone and verbose - Verifier configuration sprawl:
VerifierOptionsrequires 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 automaticallytoJsonMap: Explicit type coercion (null→"null", Date→ISO string, nested objects flattened)buildVerifierOptions: Single function assembles complete VerifierOptions from minimal inputs — handles local/remote/BDCT flowsbuildMessageVerifierOptions: Same as above but for message/Kafka provider verificationhandlePactBrokerUrlAndSelectors: Resolves broker URL and consumer version selectors from env vars with breaking change awarenessgetProviderVersionTags: CI-aware version tagging (extracts branch/tag from GitHub Actions, GitLab CI, etc.)createRequestFilter: Pluggable token generator pattern — prevents double-Bearer bugs by contractnoOpRequestFilter: 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
- One-call setup: Each utility does one thing completely — no multi-step assembly required
- Environment-aware: Utilities read env vars for CI/CD integration without manual wiring
- Type-safe: Full TypeScript types for all inputs and outputs, exported for consumer use
- Fail-safe defaults: Sensible defaults that work locally; env vars override for CI
- 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/pactmust be installed separately - Local flow: No broker needed — set
pactUrlsin verifier options pointing to local pact files - Remote flow: Set
PACT_BROKER_BASE_URLandPACT_BROKER_TOKENenv vars - Breaking changes: Set
includeMainAndDeployed: falsewhen coordinating breaking changes (verifies only matchingBranch) - Type exports: Library exports
StateHandlers,RequestFilter,JsonMap,ConsumerVersionSelectortypes
Related Fragments
pactjs-utils-consumer-helpers.md— detailed createProviderState and toJsonMap usagepactjs-utils-provider-verifier.md— detailed buildVerifierOptions and broker configurationpactjs-utils-request-filter.md— detailed createRequestFilter and auth patternscontract-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