Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
217 lines
10 KiB
Markdown
217 lines
10 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
## Related Fragments
|
|
|
|
- `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
|
|
|
|
```typescript
|
|
// ❌ 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
|
|
|
|
```typescript
|
|
// ✅ 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
|
|
|
|
```typescript
|
|
// ❌ Manual JsonMap casting
|
|
import type { JsonMap } from '@pact-foundation/pact';
|
|
|
|
provider.given('user exists', { id: 1 as unknown as JsonMap['id'] });
|
|
```
|
|
|
|
### Right: Use createProviderState
|
|
|
|
```typescript
|
|
// ✅ 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_
|