您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

pactjs-utils-provider-verifier.md 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. # Pact.js Utils Provider Verifier
  2. ## Principle
  3. 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.
  4. ## Rationale
  5. ### Problems with manual VerifierOptions
  6. - **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
  7. - **Environment variable logic**: Different env vars for local vs remote, CI vs local dev, breaking change vs normal flow
  8. - **Consumer version selector complexity**: Choosing between `mainBranch`, `deployedOrReleased`, `matchingBranch`, and `includeMainAndDeployed` requires understanding Pact Broker semantics
  9. - **Breaking change coordination**: When a provider intentionally breaks a contract, manual selector switching is error-prone
  10. - **Cross-execution protection**: `PACT_PAYLOAD_URL` webhook payloads need special handling to verify only the triggering pact
  11. ### Solutions
  12. - **`buildVerifierOptions`**: Single function that reads env vars, selects the right flow, and returns complete `VerifierOptions`
  13. - **`buildMessageVerifierOptions`**: Same as above for message/Kafka provider verification
  14. - **`handlePactBrokerUrlAndSelectors`**: Pure function for broker URL + selector resolution (used internally, also exported for advanced use)
  15. - **`getProviderVersionTags`**: Extracts CI branch/tag info from environment for provider version tagging
  16. ## Pattern Examples
  17. ### Example 1: HTTP Provider Verification (Remote Flow)
  18. ```typescript
  19. import { Verifier } from '@pact-foundation/pact';
  20. import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';
  21. import type { StateHandlers } from '@seontechnologies/pactjs-utils';
  22. const stateHandlers: StateHandlers = {
  23. 'movie with id 1 exists': {
  24. setup: async (params) => {
  25. await db.seed({ movies: [{ id: params?.id ?? 1, name: 'Inception' }] });
  26. },
  27. teardown: async () => {
  28. await db.clean('movies');
  29. },
  30. },
  31. 'no movies exist': async () => {
  32. await db.clean('movies');
  33. },
  34. };
  35. // buildVerifierOptions reads these env vars automatically:
  36. // - PACT_BROKER_BASE_URL (broker URL)
  37. // - PACT_BROKER_TOKEN (broker auth)
  38. // - PACT_PAYLOAD_URL (webhook trigger — cross-execution protection)
  39. // - PACT_BREAKING_CHANGE (if "true", uses includeMainAndDeployed selectors)
  40. // - GITHUB_SHA (provider version)
  41. // - CI (publish verification results if "true")
  42. const opts = buildVerifierOptions({
  43. provider: 'SampleMoviesAPI',
  44. port: '3001',
  45. includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
  46. stateHandlers,
  47. requestFilter: createRequestFilter({
  48. tokenGenerator: () => process.env.TEST_AUTH_TOKEN ?? 'test-token',
  49. }),
  50. });
  51. await new Verifier(opts).verifyProvider();
  52. ```
  53. **Key Points**:
  54. - Set `PACT_BROKER_BASE_URL` and `PACT_BROKER_TOKEN` as env vars — `buildVerifierOptions` reads them automatically
  55. - `port` is a string (e.g., `'3001'`) — the function builds `providerBaseUrl: http://localhost:${port}` internally
  56. - `includeMainAndDeployed` is **required** — set `true` for normal flow, `false` for breaking changes
  57. - State handlers support both simple functions and `{ setup, teardown }` objects
  58. - `params` in state handlers correspond to the `JsonMap` from consumer's `createProviderState`
  59. - Verification results are published by default (`publishVerificationResult` defaults to `true`)
  60. ### Example 2: Local Flow (Monorepo, No Broker)
  61. ```typescript
  62. import { Verifier } from '@pact-foundation/pact';
  63. import { buildVerifierOptions } from '@seontechnologies/pactjs-utils';
  64. // When PACT_BROKER_BASE_URL is NOT set, buildVerifierOptions
  65. // falls back to local pact file verification
  66. const opts = buildVerifierOptions({
  67. provider: 'SampleMoviesAPI',
  68. port: '3001',
  69. includeMainAndDeployed: true,
  70. // Specify local pact files directly — skips broker entirely
  71. pactUrls: ['./pacts/movie-web-SampleMoviesAPI.json'],
  72. stateHandlers: {
  73. 'movie exists': async (params) => {
  74. await db.seed({ movies: [{ id: params?.id }] });
  75. },
  76. },
  77. });
  78. await new Verifier(opts).verifyProvider();
  79. ```
  80. ### Example 3: Message Provider Verification (Kafka/Async)
  81. ```typescript
  82. import { Verifier } from '@pact-foundation/pact';
  83. import { buildMessageVerifierOptions } from '@seontechnologies/pactjs-utils';
  84. const opts = buildMessageVerifierOptions({
  85. provider: 'OrderEventsProducer',
  86. includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
  87. // Message handlers return the message content that the provider would produce
  88. messageProviders: {
  89. 'an order created event': async () => ({
  90. orderId: 'order-123',
  91. userId: 'user-456',
  92. items: [{ productId: 'prod-789', quantity: 2 }],
  93. createdAt: new Date().toISOString(),
  94. }),
  95. 'an order cancelled event': async () => ({
  96. orderId: 'order-123',
  97. reason: 'customer_request',
  98. cancelledAt: new Date().toISOString(),
  99. }),
  100. },
  101. stateHandlers: {
  102. 'order exists': async (params) => {
  103. await db.seed({ orders: [{ id: params?.orderId }] });
  104. },
  105. },
  106. });
  107. await new Verifier(opts).verifyProvider();
  108. ```
  109. **Key Points**:
  110. - `buildMessageVerifierOptions` adds `messageProviders` to the verifier config
  111. - Each message provider function returns the expected message payload
  112. - State handlers work the same as HTTP verification
  113. - Broker integration works identically (same env vars)
  114. ### Example 4: Breaking Change Coordination
  115. ```typescript
  116. // When a provider intentionally introduces a breaking change:
  117. //
  118. // 1. Set PACT_BREAKING_CHANGE=true in CI environment
  119. // 2. Your test reads the env var and passes includeMainAndDeployed: false
  120. // to buildVerifierOptions — this verifies ONLY against the matching
  121. // branch, skipping main/deployed consumers that would fail
  122. // 3. Coordinate with consumer team to update their pact on a matching branch
  123. // 4. Remove PACT_BREAKING_CHANGE flag after consumer updates
  124. // In CI environment (.github/workflows/provider-verify.yml):
  125. // env:
  126. // PACT_BREAKING_CHANGE: 'true'
  127. // Your provider test code reads the env var:
  128. const isBreakingChange = process.env.PACT_BREAKING_CHANGE === 'true';
  129. const opts = buildVerifierOptions({
  130. provider: 'SampleMoviesAPI',
  131. port: '3001',
  132. includeMainAndDeployed: !isBreakingChange, // false during breaking changes
  133. stateHandlers: {
  134. /* ... */
  135. },
  136. });
  137. // When includeMainAndDeployed is false (breaking change):
  138. // selectors = [{ matchingBranch: true }]
  139. // When includeMainAndDeployed is true (normal):
  140. // selectors = [{ matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true }]
  141. ```
  142. ### Example 5: handlePactBrokerUrlAndSelectors (Advanced)
  143. ```typescript
  144. import { handlePactBrokerUrlAndSelectors } from '@seontechnologies/pactjs-utils';
  145. import type { VerifierOptions } from '@pact-foundation/pact';
  146. // For advanced use cases — mutates the options object in-place (returns void)
  147. const options: VerifierOptions = {
  148. provider: 'SampleMoviesAPI',
  149. providerBaseUrl: 'http://localhost:3001',
  150. };
  151. handlePactBrokerUrlAndSelectors({
  152. pactPayloadUrl: process.env.PACT_PAYLOAD_URL,
  153. pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
  154. consumer: undefined, // or specific consumer name
  155. includeMainAndDeployed: true,
  156. options, // mutated in-place: sets pactBrokerUrl, consumerVersionSelectors, or pactUrls
  157. });
  158. // After call, options has been mutated with:
  159. // - options.pactBrokerUrl (from pactBrokerUrl param)
  160. // - options.consumerVersionSelectors (based on includeMainAndDeployed)
  161. // OR if pactPayloadUrl matches: options.pactUrls = [pactPayloadUrl]
  162. ```
  163. **Note**: `handlePactBrokerUrlAndSelectors` is called internally by `buildVerifierOptions`. You rarely need it directly — use it only for advanced custom verifier assembly.
  164. ### Example 6: getProviderVersionTags
  165. ```typescript
  166. import { getProviderVersionTags } from '@seontechnologies/pactjs-utils';
  167. // Extracts version tags from CI environment
  168. const tags = getProviderVersionTags();
  169. // In GitHub Actions on branch "feature/add-movies" (non-breaking):
  170. // tags = ['dev', 'feature/add-movies']
  171. //
  172. // In GitHub Actions on main branch (non-breaking):
  173. // tags = ['dev', 'main']
  174. //
  175. // In GitHub Actions with PACT_BREAKING_CHANGE=true:
  176. // tags = ['feature/add-movies'] (no 'dev' tag)
  177. //
  178. // Locally (no CI):
  179. // tags = ['local']
  180. ```
  181. ### Example 7: Provider Vitest Configuration (Required for Multi-File Verification)
  182. **Context**: The Pact Rust FFI that powers the JS `Verifier` holds process-wide state (native handles for messages, matchers, mocks). Vitest's default parallel file workers each spin up their own FFI instance and quickly corrupt that state — causing `MessagePact`/`Verifier` errors like `"Unable to get the MessageHandle"`, or non-deterministic verification passes/fails — as soon as you have more than one provider `.spec.ts` file.
  183. **Rule**: Provider verification suites **must** run in a single fork. Use Vitest's `forks` pool with `singleFork: true` in `vitest.config.contract.ts` (or equivalent).
  184. ```typescript
  185. // vitest.config.contract.ts — provider verification config
  186. import { defineConfig } from 'vitest/config';
  187. export default defineConfig({
  188. test: {
  189. environment: 'node',
  190. include: ['tests/contract/**/*.spec.ts'],
  191. testTimeout: 60000,
  192. // MANDATORY for multi-file provider verification.
  193. // The Pact Rust FFI backing the Verifier holds process-wide state; parallel workers corrupt it
  194. // and produce flaky verification results / "Unable to get the MessageHandle" errors.
  195. // This is especially important for message providers (Kafka/async) where verifier construction
  196. // allocates native handles per file — singleFork keeps them in one process so state is coherent.
  197. pool: 'forks',
  198. poolOptions: {
  199. forks: {
  200. singleFork: true,
  201. },
  202. },
  203. },
  204. });
  205. ```
  206. **Key Points**:
  207. - **Required for message providers** (`buildMessageVerifierOptions`) — the message-handle FFI state is almost guaranteed to corrupt under parallel workers.
  208. - **Required for HTTP providers with multiple contract test files** — even if each file works in isolation, running them together in parallel produces intermittent failures.
  209. - `pool: 'forks'` (rather than `threads`) + `singleFork: true` is the exact combo that keeps all verifier runs in a single child process with a single FFI instance.
  210. - Treat `pool: 'forks'` + `singleFork: true` as the required baseline for all provider suites, including single-file HTTP-only ones. A suite that works today with one file will flake the moment a second file is added, and removing the setting later introduces a regression window.
  211. - **The same `pool: 'forks'` + `singleFork: true` rule applies on the consumer side.** Consumer `vitest.config.pact.ts` sets it alongside `fileParallelism: false` — see `pact-consumer-framework-setup.md` Example 2. The rule is needed on either side wherever more than one pact test file exists per consumer+provider pair.
  212. - Use a dedicated `vitest.config.contract.ts` so unit tests still get full parallelism — only contract tests pay the serialization cost.
  213. - Related `package.json` entry:
  214. ```json
  215. {
  216. "scripts": {
  217. "test:pact:provider": "vitest run --config vitest.config.contract.ts"
  218. }
  219. }
  220. ```
  221. ## Environment Variables Reference
  222. | Variable | Required | Description | Default |
  223. | ---------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
  224. | `PACT_BROKER_BASE_URL` | For remote flow | Pact Broker / PactFlow URL | — |
  225. | `PACT_BROKER_TOKEN` | For remote flow | API token for broker authentication | — |
  226. | `GITHUB_SHA` | Recommended | Provider version for verification result publishing (auto-set by GitHub Actions) | `'unknown'` |
  227. | `GITHUB_BRANCH` | Recommended | Branch name for provider version branch and version tags (**not auto-set** — define as `${{ github.head_ref \|\| github.ref_name }}`) | `'main'` |
  228. | `PACT_PAYLOAD_URL` | Optional | Webhook payload URL — triggers verification of specific pact only | — |
  229. | `PACT_BREAKING_CHANGE` | Optional | Set to `"true"` to use breaking change selector strategy | `'false'` |
  230. | `CI` | Auto-detected | When `"true"`, enables verification result publishing | — |
  231. ## Key Points
  232. - **Flow auto-detection**: If `PACT_BROKER_BASE_URL` is set → remote flow; otherwise → local flow (requires `pactUrls`)
  233. - **`port` is a string**: Pass port number as string (e.g., `'3001'`); function builds `http://localhost:${port}` internally
  234. - **`includeMainAndDeployed` is required**: `true` = verify matchingBranch + mainBranch + deployedOrReleased; `false` = verify matchingBranch only (for breaking changes)
  235. - **Selector strategy**: Normal flow (`includeMainAndDeployed: true`) includes all selectors; breaking change flow (`false`) includes only `matchingBranch`
  236. - **Webhook support**: `PACT_PAYLOAD_URL` takes precedence — verifies only the specific pact that triggered the webhook
  237. - **State handler types**: Both `async (params) => void` and `{ setup: async (params) => void, teardown: async () => void }` are supported
  238. - **Version publishing**: Verification results are published by default (`publishVerificationResult` defaults to `true`)
  239. - **Provider Vitest config is MANDATORY for multi-file suites**: Set `pool: 'forks'` + `poolOptions.forks.singleFork: true` in `vitest.config.contract.ts`. Without this the Rust FFI corrupts under parallel workers (see Example 7).
  240. ## Related Fragments
  241. - `pactjs-utils-overview.md` — installation, decision tree, design philosophy
  242. - `pactjs-utils-consumer-helpers.md` — consumer-side state parameter creation, **one-interaction-per-`it()` rule**
  243. - `pactjs-utils-request-filter.md` — auth injection for provider verification
  244. - `pact-consumer-framework-setup.md` — consumer-side framework setup, Vitest `fileParallelism: false`, determinism gate
  245. - `pact-broker-webhooks.md` — PactFlow → GitHub webhook auth/staleness for webhook-triggered provider verification (`contract_requiring_verification_published`)
  246. - `contract-testing.md` — foundational patterns with raw Pact.js
  247. ## Anti-Patterns
  248. ### Wrong: Manual broker URL and selector assembly
  249. ```typescript
  250. // ❌ Manual environment variable handling
  251. const opts: VerifierOptions = {
  252. provider: 'my-api',
  253. providerBaseUrl: 'http://localhost:3001',
  254. pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
  255. pactBrokerToken: process.env.PACT_BROKER_TOKEN,
  256. publishVerificationResult: process.env.CI === 'true',
  257. providerVersion: process.env.GIT_SHA || process.env.GITHUB_SHA || 'dev',
  258. providerVersionBranch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME,
  259. consumerVersionSelectors:
  260. process.env.PACT_BREAKING_CHANGE === 'true'
  261. ? [{ matchingBranch: true }]
  262. : [{ matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true }],
  263. pactUrls: process.env.PACT_PAYLOAD_URL ? [process.env.PACT_PAYLOAD_URL] : undefined,
  264. stateHandlers: {
  265. /* ... */
  266. },
  267. requestFilter: (req, res, next) => {
  268. req.headers['authorization'] = `Bearer ${process.env.TEST_TOKEN}`;
  269. next();
  270. },
  271. };
  272. ```
  273. ### Right: Use buildVerifierOptions
  274. ```typescript
  275. // ✅ All env var logic handled internally
  276. const opts = buildVerifierOptions({
  277. provider: 'my-api',
  278. port: '3001',
  279. includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
  280. stateHandlers: {
  281. /* ... */
  282. },
  283. requestFilter: createRequestFilter({
  284. tokenGenerator: () => process.env.TEST_TOKEN ?? 'test-token',
  285. }),
  286. });
  287. ```
  288. ### Wrong: Hardcoding consumer version selectors
  289. ```typescript
  290. // ❌ Hardcoded selectors — breaks when flow changes
  291. consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }],
  292. ```
  293. ### Right: Let buildVerifierOptions choose selectors
  294. ```typescript
  295. // ✅ Selector strategy adapts to PACT_BREAKING_CHANGE env var
  296. const opts = buildVerifierOptions({
  297. /* ... */
  298. });
  299. // Selectors chosen automatically based on environment
  300. ```
  301. ### Wrong: Parallel Vitest workers for provider verification
  302. ```typescript
  303. // ❌ vitest.config.contract.ts — uses default parallel workers
  304. import { defineConfig } from 'vitest/config';
  305. export default defineConfig({
  306. test: {
  307. environment: 'node',
  308. include: ['tests/contract/**/*.spec.ts'],
  309. // NO pool/singleFork config — defaults to parallel file workers
  310. },
  311. });
  312. // Symptoms: "Unable to get the MessageHandle", non-deterministic verification pass/fail,
  313. // green locally on single-file run but red in CI with multiple files
  314. ```
  315. ### Right: Single fork for provider verification
  316. ```typescript
  317. // ✅ vitest.config.contract.ts — serializes provider verification files
  318. import { defineConfig } from 'vitest/config';
  319. export default defineConfig({
  320. test: {
  321. environment: 'node',
  322. include: ['tests/contract/**/*.spec.ts'],
  323. pool: 'forks',
  324. poolOptions: { forks: { singleFork: true } },
  325. },
  326. });
  327. ```
  328. _Source: @seontechnologies/pactjs-utils provider-verifier module, pact-js-example-provider CI workflows_