@testgorilla/tgo-pact 0.0.0 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,384 @@
1
+ # @testgorilla/tgo-pact
2
+
3
+ Pact consumer test helpers for TestGorilla's Angular apps. It replaces the helpers each app kept its own copy of: `pact-test-bed.ts` and `api-contract.ts` in tgo-frontend, `core/testing/pact-test-bed.ts` in tgo-talent-frontend, `talent-backend.pact.mocks.ts` in tgo-talent-assessment-frontend and `src/pact/admin-tool-pact.ts` in tgo-admin-tool.
4
+
5
+ - `createPact()` creates a V4 pact with the shared defaults.
6
+ - `configurePactTestBed()` configures TestBed with HttpClient pointed at the Pact mock server.
7
+ - `datetime`, `date`, `uuid`, `url`, `digits` and `shapeOf` build matchers.
8
+ - `verifyServiceContracts()` drives service methods from a table of contracts, on top of `verifyInteraction()`.
9
+ - `createPactJestConfig()`, from `@testgorilla/tgo-pact/jest`, turns the app's Jest config into the pact run's config.
10
+
11
+ App-specific code stays in the app: provider state names and params, fixed ids, tokens the provider states create, stubs of app services, and tgo-frontend's OpenAPI-driven `ApiDocumentContract` and `verifyClientContracts`.
12
+
13
+ Node 22.12 or later. Peer dependencies: `@angular/core` and `@angular/common` `>=21.0.0 <23.0.0`, `@pact-foundation/pact` `^17.0.0`, `jest` `^30.0.0`, `rxjs` `^7.8.0`.
14
+
15
+ ## Installation
16
+
17
+ The package is published to npmjs under the `@testgorilla` scope. The apps' `.npmrc` already maps the scope:
18
+
19
+ ```ini
20
+ @testgorilla:registry=https://registry.npmjs.org/
21
+ ```
22
+
23
+ ```bash
24
+ npm install --save-dev @testgorilla/tgo-pact
25
+ ```
26
+
27
+ To try an unreleased build, pack it and install the tarball:
28
+
29
+ ```bash
30
+ # in tgo-fe-libraries
31
+ npx nx build tgo-pact
32
+ cd dist/packages/tgo-pact && npm pack --pack-destination /tmp
33
+
34
+ # in the app
35
+ npm install --save-dev /tmp/testgorilla-tgo-pact-<version>.tgz
36
+ ```
37
+
38
+ ## Jest config
39
+
40
+ `createPactJestConfig(base?, overrides?)` takes the app's Jest config and returns one for the pact run. It keeps the preset, transforms, module mapping and setup files, and sets:
41
+
42
+ | Setting | Value | Why |
43
+ | ------------------------ | ------------------------------------------------ | --------------------------------------------------------------------- |
44
+ | `testMatch` | `['**/*.pact.spec.ts']` | Only pact specs |
45
+ | `maxWorkers` | `1` | Runs in band: specs of one consumer-provider pair write the same file |
46
+ | `testTimeout` | `30000` | Every test starts a mock server |
47
+ | `testPathIgnorePatterns` | the base's, minus those matching `.pact.spec.ts` | Unit configs usually ignore `*.pact.spec.ts` |
48
+ | `displayName` | the base's, else `pact` | |
49
+ | `collectCoverage` | `false` | |
50
+ | `testRegex`, `projects` | removed | They would override `testMatch` or run the unit projects |
51
+
52
+ `overrides` is a Jest config applied last, over the base and these defaults, e.g. `testMatch`, `displayName`, `globalSetup` or `reporters`.
53
+
54
+ ```js
55
+ // jest.pact.config.js
56
+ const { createPactJestConfig } = require('@testgorilla/tgo-pact/jest');
57
+ const baseConfig = require('./jest.unit.config.js');
58
+
59
+ module.exports = createPactJestConfig(baseConfig, {
60
+ reporters: [
61
+ 'default',
62
+ ['jest-junit', { outputDirectory: './pact-test-results', outputName: 'results.xml' }],
63
+ ],
64
+ });
65
+ ```
66
+
67
+ ```ts
68
+ // jest/pact.config.ts
69
+ import { createPactJestConfig } from '@testgorilla/tgo-pact/jest';
70
+
71
+ import { baseJestConfig } from './base.config.ts';
72
+
73
+ export default createPactJestConfig(baseJestConfig, {
74
+ testMatch: ['<rootDir>/src/**/*.pact.spec.ts'],
75
+ globalSetup: '<rootDir>/jest/pact.global-setup.ts',
76
+ });
77
+ ```
78
+
79
+ A TypeScript Jest config can share the unit config's base by exporting the unit project from `jest.config.ts` and importing it. The unit project's `displayName` then carries over, so override it: `createPactJestConfig(unitProject, { displayName: 'pact' })`.
80
+
81
+ The secondary entry point has no Angular imports, so loading it from a Jest config stays cheap. It ships as ESM; Node 22.12 and later can `require()` it from a CommonJS config.
82
+
83
+ ## createPact
84
+
85
+ ```ts
86
+ function createPact(consumer: string, provider: string, options?: CreatePactOptions): PactV4;
87
+ type CreatePactOptions = Partial<Omit<PactV4Options, 'consumer' | 'provider'>>;
88
+ ```
89
+
90
+ Defaults: V4 specification, `logLevel: 'warn'` and `cors: true`, because jsdom's XMLHttpRequest enforces CORS and the mock server runs on another origin. The pact file is written to `options.dir`, else `$PACT_DIR`, else `<cwd>/pacts`; `dir: undefined` counts as not given. Every other `PactV4` option, such as `port`, passes through.
91
+
92
+ Use the app's repository name as the consumer: `tgo-frontend`, `tgo-talent-frontend`, `tgo-talent-assessment-frontend` and `tgo-admin-tool`. The broker and the providers' verification jobs know the pacts by these names, so keep them exact. Create one pact per spec file: each one gets its own mock server.
93
+
94
+ ```ts
95
+ const pact = createPact('tgo-talent-frontend', 'tgo-talent-backend');
96
+ ```
97
+
98
+ ## configurePactTestBed
99
+
100
+ ```ts
101
+ function configurePactTestBed(
102
+ mockServerUrl: string,
103
+ providers?: Provider[],
104
+ options?: PactTestBedOptions
105
+ ): void;
106
+
107
+ interface PactTestBedOptions {
108
+ token?: string | null; // provided as PACT_AUTH_TOKEN; default null
109
+ authHeaders?: Record<string, string>; // added to every request that does not set them
110
+ interceptors?: HttpInterceptorFn[]; // functional interceptors, after the DI ones
111
+ interceptorClasses?: Type<HttpInterceptor>[]; // registered as HTTP_INTERCEPTORS
112
+ httpClient?: EnvironmentProviders; // replaces the default provideHttpClient(...)
113
+ }
114
+ ```
115
+
116
+ Call it inside `executeTest`, once the mock server URL is known. It provides:
117
+
118
+ - `provideHttpClient(withInterceptorsFromDi(), withInterceptors([...]))`, or `options.httpClient`. `httpClient` with `authHeaders` or `interceptors` throws: add them to the app's providers instead. `interceptorClasses` are registered as `HTTP_INTERCEPTORS`, which HttpClient only reads with `withInterceptorsFromDi()`, so custom `httpClient` providers need it.
119
+ - `HttpBackend` as `HttpXhrBackend`. jsdom has no `fetch`, and Angular 22 makes HttpClient use it by default.
120
+ - `PACT_MOCK_SERVER_URL`, the mock server URL without a trailing slash.
121
+ - `PACT_AUTH_TOKEN`, the token the app's auth stub hands to its real interceptor.
122
+ - `providers`, last, so they can override any of the above.
123
+
124
+ The package knows no app services. Stub them in `providers` and read the URL and token from the injection tokens with `useFactory`, so the app's real interceptors build the headers the provider sees. `authorizationHeader(token, scheme = 'Token')` returns `{ Authorization: '<scheme> <token>' }` for the expected request.
125
+
126
+ ```ts
127
+ // core/testing/pact-test-bed.ts in tgo-talent-frontend
128
+ export const PACT_API_TOKEN = 'pact-test-token-not-a-secret-talent-fe';
129
+ export const PACT_AUTH_HEADER = authorizationHeader(PACT_API_TOKEN);
130
+
131
+ export function configureTalentPactTestBed(
132
+ mockServerUrl: string,
133
+ providers: Provider[] = [],
134
+ token = PACT_API_TOKEN
135
+ ) {
136
+ configurePactTestBed(
137
+ mockServerUrl,
138
+ [
139
+ {
140
+ provide: AppConfigService,
141
+ useFactory: () => {
142
+ const apiUrl = `${inject(PACT_MOCK_SERVER_URL)}/api`;
143
+ return { config: { API_URL: apiUrl, CUSTOMER_APP_SERVICE_URL: apiUrl } };
144
+ },
145
+ },
146
+ {
147
+ provide: AuthenticationStore,
148
+ useFactory: () => ({ token: signal(inject(PACT_AUTH_TOKEN)), clearState: jest.fn() }),
149
+ },
150
+ { provide: Router, useValue: { navigate: jest.fn() } },
151
+ ...providers,
152
+ ],
153
+ { token, interceptorClasses: [TokenInterceptorService] }
154
+ );
155
+ }
156
+ ```
157
+
158
+ How each app wires its base URL and auth:
159
+
160
+ | App | Base URL from `PACT_MOCK_SERVER_URL` | Auth |
161
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | tgo-frontend | `APIConfigService`, `APIConfigServiceOptions` and `AppConfigService` (`apiUrl`, `cr2_endpoint`, `llm_api_url`) as is | `httpClient: provideAppHttpClient()`, `interceptorClasses: [AuthInterceptor]`, `AuthGeneralService` stub reading the tokens |
163
+ | tgo-talent-frontend | `AppConfigService` `API_URL` and `CUSTOMER_APP_SERVICE_URL` as `<url>/api` | `interceptorClasses: [TokenInterceptorService]`, `AuthenticationStore` stub with `token: signal(inject(PACT_AUTH_TOKEN))` and `candidateProfileUUId`, plus `FeatureFlagsStore` and `SnackbarService` stubs |
164
+ | tgo-talent-assessment-frontend | `AppConfigService` `API_URL` as `<url>/api/` | `interceptors: [apiInterceptor, cacheBusterInterceptor]`, `GlobalStateStore` stub holding the invitation uuid |
165
+ | tgo-admin-tool | `APIConfigService` `options.apiUrl` as is | `interceptorClasses: [AuthInterceptor]`, `AuthService` stub from `authServiceStub()` |
166
+
167
+ A test that needs a second token, such as tgo-frontend's test taker or survey token or tgo-admin-tool's impersonation token, keeps it in the app's stub and passes it through the app's own wrapper.
168
+
169
+ ## Matchers
170
+
171
+ ```ts
172
+ const datetime: (example?: string) => V3RegexMatcher; // ISO-8601 datetime, as DRF renders it
173
+ const date: (example?: string) => V3RegexMatcher; // YYYY-MM-DD
174
+ const uuid: (example?: string) => V3RegexMatcher; // lowercase canonical UUID
175
+ const url: (example?: string) => V3RegexMatcher; // absolute http(s) URL
176
+ const digits: (example: number | string) => V3RegexMatcher; // a string of digits
177
+ function shapeOf(example: unknown, minItems?: number): unknown;
178
+ ```
179
+
180
+ The patterns are exported as `ISO_DATETIME_PATTERN`, `ISO_DATE_PATTERN`, `UUID_PATTERN`, `URL_PATTERN` and `DIGITS_PATTERN`.
181
+
182
+ `shapeOf` turns an example of the app's model into type matchers:
183
+
184
+ - Whole numbers match integers only, so `{ price: 10 }` rejects `10.5`. Other numbers match any number. To accept decimals where the example is whole, pass the matcher: `{ price: MatchersV3.decimal(10) }` or `MatchersV3.number(10)`. This stays as it is, because changing it would change existing pacts.
185
+ - ISO datetimes match `datetime`. Other strings and booleans match by type.
186
+ - A top-level list needs `minItems` items (default 1). A nested list may be empty, except under a `results` key.
187
+ - Every list in the example needs an item, which describes the items' shape. An empty one throws, naming its path, e.g. `$.tags`.
188
+ - Matchers pass through unchanged.
189
+
190
+ Leave nullable fields out of the example: the app copes with null, so the contract should not pin their type. A nested list the app indexes into needs an explicit `eachLike`, because `shapeOf` lets nested lists be empty.
191
+
192
+ ```ts
193
+ willRespondWith(200, builder =>
194
+ builder.jsonBody(shapeOf({ results: [{ id: 1, created: '2026-10-01T09:30:00Z' }] }))
195
+ );
196
+ ```
197
+
198
+ ## Provider states
199
+
200
+ ```ts
201
+ interface ProviderState {
202
+ name: string;
203
+ params?: JsonMap;
204
+ }
205
+ const providerState: (name: string, params?: JsonMap) => ProviderState;
206
+ function givenStates(pact: PactV4, states: readonly ProviderState[]): V4UnconfiguredInteraction;
207
+ ```
208
+
209
+ `givenStates` starts an interaction with every state applied in order. It is useful when the auth state and the data states are kept as a list. Each app keeps its own state catalogue.
210
+
211
+ ```ts
212
+ await givenStates(pact, [
213
+ providerState(CANDIDATE_AUTHENTICATED, { profileUuid }),
214
+ providerState('the candidate has a badge'),
215
+ ])
216
+ .uponReceiving("a request for the candidate's badges")
217
+ .withRequest('GET', BADGES_URL, builder => builder.headers(PACT_AUTH_HEADER))
218
+ .willRespondWith(200, builder =>
219
+ builder.jsonBody(shapeOf({ results: [{ badge_type: 'ai_fluency' }] }))
220
+ )
221
+ .executeTest(async mockServer => {
222
+ configureTalentPactTestBed(mockServer.url, [BadgesDataService]);
223
+ // call the service and assert
224
+ });
225
+ ```
226
+
227
+ ## verifyServiceContracts
228
+
229
+ The table-driven contracts from tgo-frontend, with the app-specific parts moved to the suite:
230
+
231
+ ```ts
232
+ function verifyServiceContracts<S, O = undefined>(
233
+ suite: ContractSuite<O>,
234
+ service: Type<S>,
235
+ contracts: ServiceContract<S, O>[]
236
+ ): void;
237
+ function verifyServiceContract<S, O = undefined>(
238
+ suite: ContractSuite<O>,
239
+ service: Type<S>,
240
+ contract: ServiceContract<S, O>
241
+ ): Promise<void>;
242
+
243
+ interface ContractSuite<O = undefined> {
244
+ pact: PactV4;
245
+ given?: ProviderState[]; // before each contract's own states
246
+ headers?: TemplateHeaders | ((options: O | undefined) => TemplateHeaders); // expected on every request
247
+ configureTestBed?: (mockServerUrl: string, providers: Provider[], options: O | undefined) => void; // default: configurePactTestBed(mockServerUrl, providers)
248
+ responseFor?: (operationId: string) => ContractResponse; // resolves response.operationId
249
+ }
250
+
251
+ interface ServiceContract<S, O = undefined> {
252
+ description: string; // test name: `should <description>`
253
+ given?: ProviderState[];
254
+ replaceGiven?: boolean; // apply only `given`, without the suite's states
255
+ request: {
256
+ method: string;
257
+ path: string;
258
+ query?: TemplateQuery;
259
+ headers?: TemplateHeaders;
260
+ body?: unknown;
261
+ };
262
+ replaceHeaders?: boolean; // expect only `request.headers`, without the suite's headers
263
+ call: (service: S) => Observable<unknown>;
264
+ response: { status?: number; body?: unknown; operationId?: string };
265
+ expect?: (result: unknown, body: unknown) => void | Promise<void>; // replaces the body check
266
+ options?: O; // per-contract context for headers and configureTestBed
267
+ }
268
+ ```
269
+
270
+ `verifyServiceContracts` declares one `it` per contract. Each one registers the interaction, calls `contract.call` on the service from TestBed and checks the result:
271
+
272
+ - With a 2xx status and a body, the result must equal the reified body.
273
+ - With a 2xx status and no body, it must be `null` or `''`.
274
+ - With a status of 400 or more, the call must fail with an `HttpErrorResponse` of that status. Its `error` must equal the reified body, or be `null` or `''` without one.
275
+
276
+ `response.operationId` takes the operation's documented 2xx response from the suite's `responseFor`; `status` may replace its 2xx status. A contract with both `operationId` and `body`, or with `operationId` and a status of 400 or more, throws: give an error response's body explicitly.
277
+
278
+ `replaceGiven` and `replaceHeaders` fit calls the suite's defaults don't describe into the table, such as an anonymous call that gets a 401:
279
+
280
+ ```ts
281
+ {
282
+ description: 'reject an anonymous request for the jobs',
283
+ replaceGiven: true,
284
+ replaceHeaders: true,
285
+ request: { method: 'GET', path: '/api/jobs/' },
286
+ call: service => service.jobs(),
287
+ response: { status: 401, body: shapeOf({ detail: 'Authentication credentials were not provided.' }) },
288
+ options: { anonymous: true },
289
+ }
290
+ ```
291
+
292
+ `expect` replaces the body check, for a method that hands back only part of the response or reshapes it. It gets what the call resolved to, or the `HttpErrorResponse` of a failing call, and the reified response body. A failing call's status is still checked.
293
+
294
+ ```ts
295
+ {
296
+ description: 'list the recent talent searches',
297
+ request: { method: 'GET', path: '/api/sourcing/searches/', query: { limit: '5', offset: '0' } },
298
+ call: service => service.getRecentSearches(),
299
+ response: { body: shapeOf({ count: 1, results: [{ id: 3101 }] }) },
300
+ expect: (results, body) => expect(results).toEqual((body as { results: unknown[] }).results),
301
+ }
302
+ ```
303
+
304
+ `O` replaces tgo-frontend's `asTestTaker` and `surveyToken` flags. `responseFor` replaces the built-in OpenAPI lookup: the app resolves the operation from its own document.
305
+
306
+ ```ts
307
+ interface Session {
308
+ asTestTaker?: boolean;
309
+ }
310
+
311
+ export const tgoBackendSuite = (): ContractSuite<Session> => ({
312
+ pact: createPact('tgo-frontend', 'tgo-backend'),
313
+ given: [providerState(CUSTOMER_ADMIN_STATE, { userId: ID.user, customerId: ID.customer })],
314
+ headers: session => (session?.asTestTaker ? TEST_TAKER_AUTH_HEADER : AUTH_HEADER),
315
+ configureTestBed: (url, providers, session) =>
316
+ configureFrontendPactTestBed(url, providers, session?.asTestTaker ? TEST_TAKER_TOKEN : null),
317
+ responseFor: operationId => {
318
+ const operation = tgoApi.operation(operationId);
319
+ return { status: operation.status, body: operation.schema && tgoApi.matcher(operation.schema) };
320
+ },
321
+ });
322
+
323
+ verifyServiceContracts(tgoBackendSuite(), PromptsDaoService, [
324
+ {
325
+ description: 'extract the signals of a sourcing prompt',
326
+ given: [cannedPrompts],
327
+ request: {
328
+ method: 'POST',
329
+ path: '/api/sourcing/prompts/lightweight-query-extractor/',
330
+ body: { query: 'react' },
331
+ },
332
+ call: service => service.extractPromptSignals({ query: ' React ' }),
333
+ response: { body: shapeOf({ job_role: true, extraction_failed: false }) },
334
+ },
335
+ {
336
+ description: 'answer as the test taker',
337
+ request: { method: 'GET', path: '/api/test-taker/answers/1/' },
338
+ call: service => service.answer(1),
339
+ response: { operationId: 'answers_read' },
340
+ options: { asTestTaker: true },
341
+ },
342
+ ]);
343
+ ```
344
+
345
+ ## verifyInteraction
346
+
347
+ ```ts
348
+ function verifyInteraction(interaction: InteractionContract): Promise<void>;
349
+
350
+ interface InteractionContract {
351
+ pact: PactV4;
352
+ description: string; // the interaction description
353
+ states?: readonly ProviderState[];
354
+ request: ContractRequest; // with every header the request must carry
355
+ response: ContractResponse; // { status?: number; body?: unknown }
356
+ run: (mockServerUrl: string) => Observable<unknown> | Promise<unknown>;
357
+ expect?: (result: unknown, body: unknown) => void | Promise<void>;
358
+ }
359
+ ```
360
+
361
+ The runner under `verifyServiceContract`, for contract tables that call something other than a service from TestBed, such as tgo-frontend's generated API clients. It registers the interaction, calls `run` with the mock server URL and checks the outcome the same way: the reified body, `null` or `''` for an empty response, and an `HttpErrorResponse` of the status for a 4xx/5xx one. `run` configures TestBed and makes the call.
362
+
363
+ ```ts
364
+ await verifyInteraction({
365
+ pact: suite.pact,
366
+ description: contract.description,
367
+ states: [...suite.given, ...contract.given],
368
+ request: { method: operation.method, path, query, headers: { ...AUTH_HEADER, ...headers }, body },
369
+ response: { status, body },
370
+ run: mockServerUrl => {
371
+ configureFrontendPactTestBed(mockServerUrl, [client]);
372
+ return TestBed.inject(client)[contract.operationId](params);
373
+ },
374
+ });
375
+ ```
376
+
377
+ ## Migration checklist
378
+
379
+ 1. Install the package and replace the app's pact Jest config with `createPactJestConfig`. Keep the `test:pact` script; the pact run no longer needs `--runInBand`, but leaving it in is harmless.
380
+ 2. Replace the app's `new PactV4({...})` with `createPact('<app repo name>', '<provider>')`. Pass `port` and other options through `options`.
381
+ 3. Rewrite the app's TestBed helper on top of `configurePactTestBed`. Keep its signature if many specs call it, and rename it if it was itself called `configurePactTestBed` (tgo-admin-tool's became `configureAdminToolPactTestBed`), so it doesn't shadow the package export. Point the app's config stub at `PACT_MOCK_SERVER_URL` and the auth stub at `PACT_AUTH_TOKEN`.
382
+ 4. Replace the local `datetime`, `date`, `uuid`, `url`, `digits` and `shapeOf` with the package's. The patterns are the same.
383
+ 5. In tgo-frontend, move `verifyServiceContracts` callers to the package's version through a `ContractSuite` like the one above. `ApiDocumentContract` and `verifyClientContracts` stay in the app, with `verifyClientContracts` built on `verifyInteraction`.
384
+ 6. Run the pact specs and compare the generated pact files with the old ones. Only intended differences should appear, for example nested lists `shapeOf` now lets be empty.
@@ -0,0 +1,31 @@
1
+ const PACT_TEST_MATCH = ["**/*.pact.spec.ts"];
2
+ const PACT_TEST_TIMEOUT = 3e4;
3
+ const PACT_DISPLAY_NAME = "pact";
4
+ const DEFAULT_TEST_PATH_IGNORE_PATTERNS = ["/node_modules/"];
5
+ const PACT_SPEC_EXAMPLE_PATH = "/src/app/example.pact.spec.ts";
6
+ const PACT_JEST_DEFAULTS = {
7
+ testMatch: PACT_TEST_MATCH,
8
+ testTimeout: PACT_TEST_TIMEOUT,
9
+ maxWorkers: 1,
10
+ collectCoverage: false
11
+ };
12
+ function createPactJestConfig(base = {}, overrides = {}) {
13
+ const config = {
14
+ ...base,
15
+ ...PACT_JEST_DEFAULTS,
16
+ displayName: base.displayName ?? "pact",
17
+ testPathIgnorePatterns: keepingPactSpecs(base.testPathIgnorePatterns ?? DEFAULT_TEST_PATH_IGNORE_PATTERNS)
18
+ };
19
+ delete config.testRegex;
20
+ delete config.projects;
21
+ return {
22
+ ...config,
23
+ ...overrides
24
+ };
25
+ }
26
+ function keepingPactSpecs(patterns) {
27
+ return patterns.filter((pattern) => !new RegExp(pattern.replace("<rootDir>", "")).test(PACT_SPEC_EXAMPLE_PATH));
28
+ }
29
+ export { DEFAULT_TEST_PATH_IGNORE_PATTERNS, PACT_DISPLAY_NAME, PACT_JEST_DEFAULTS, PACT_SPEC_EXAMPLE_PATH, PACT_TEST_MATCH, PACT_TEST_TIMEOUT, createPactJestConfig };
30
+
31
+ //# sourceMappingURL=testgorilla-tgo-pact-jest.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testgorilla-tgo-pact-jest.mjs","names":[],"sources":["../../../../packages/tgo-pact/jest/src/pact-jest-config.constants.ts","../../../../packages/tgo-pact/jest/src/pact-jest-config.ts"],"sourcesContent":["import type { Config } from 'jest';\n\nexport const PACT_TEST_MATCH = ['**/*.pact.spec.ts'];\nexport const PACT_TEST_TIMEOUT = 30000;\nexport const PACT_DISPLAY_NAME = 'pact';\n\n/** Used when the base config sets no `testPathIgnorePatterns`; Jest's own default. */\nexport const DEFAULT_TEST_PATH_IGNORE_PATTERNS = ['/node_modules/'];\n\n/** A pact spec's path, for finding the base config's ignore patterns that would skip pact specs. */\nexport const PACT_SPEC_EXAMPLE_PATH = '/src/app/example.pact.spec.ts';\n\nexport const PACT_JEST_DEFAULTS: Readonly<Config> = {\n testMatch: PACT_TEST_MATCH,\n testTimeout: PACT_TEST_TIMEOUT,\n // Specs of one consumer-provider pair write the same pact file, and concurrent writers corrupt it.\n // Jest runs in band with a single worker.\n maxWorkers: 1,\n collectCoverage: false,\n};\n","import type { Config } from 'jest';\nimport {\n DEFAULT_TEST_PATH_IGNORE_PATTERNS,\n PACT_DISPLAY_NAME,\n PACT_JEST_DEFAULTS,\n PACT_SPEC_EXAMPLE_PATH,\n} from './pact-jest-config.constants';\n\n/**\n * Turns the app's unit Jest config into one for Pact consumer specs: `*.pact.spec.ts` only, in band, a 30s timeout\n * and no coverage. Everything else (preset, transforms, module mapping, setup files, the other ignore patterns) comes\n * from `base`, minus `testRegex` and `projects` and the ignore patterns that would skip pact specs. `displayName`\n * defaults to `pact` when `base` has none. `overrides` apply last, over both.\n */\nexport function createPactJestConfig(base: Config = {}, overrides: Config = {}): Config {\n const config: Config = {\n ...base,\n ...PACT_JEST_DEFAULTS,\n displayName: base.displayName ?? PACT_DISPLAY_NAME,\n testPathIgnorePatterns: keepingPactSpecs(\n base.testPathIgnorePatterns ?? DEFAULT_TEST_PATH_IGNORE_PATTERNS\n ),\n };\n delete config.testRegex;\n delete config.projects;\n\n return { ...config, ...overrides };\n}\n\nfunction keepingPactSpecs(patterns: string[]): string[] {\n return patterns.filter(\n pattern => !new RegExp(pattern.replace('<rootDir>', '')).test(PACT_SPEC_EXAMPLE_PATH)\n );\n}\n"],"mappings":"AAEA,MAAa,kBAAkB,CAAC,mBAAmB;AACnD,MAAa,oBAAoB;AACjC,MAAa,oBAAoB;AAGjC,MAAa,oCAAoC,CAAC,gBAAgB;AAGlE,MAAa,yBAAyB;AAEtC,MAAa,qBAAuC;CAClD,WAAW;CACX,aAAa;CAGb,YAAY;CACZ,iBAAiB;AAClB;ACLD,SAAgB,qBAAqB,OAAe,CAAA,GAAI,YAAoB,CAAA,GAAE;CAC5E,MAAM,SAAiB;EACrB,GAAG;EACH,GAAG;EACH,aAAa,KAAK,eAAA;EAClB,wBAAwB,iBACtB,KAAK,0BAA0B,iCAAiC;CAEnE;CACD,OAAO,OAAO;CACd,OAAO,OAAO;CAEd,OAAO;EAAE,GAAG;EAAQ,GAAG;CAAS;AAClC;AAEA,SAAS,iBAAiB,UAAkB;CAC1C,OAAO,SAAS,QACd,YAAW,CAAC,IAAI,OAAO,QAAQ,QAAQ,aAAa,EAAE,CAAC,CAAC,CAAC,KAAK,sBAAsB,CAAC;AAEzF"}
@@ -0,0 +1,172 @@
1
+ import { HTTP_INTERCEPTORS, HttpBackend, HttpErrorResponse, HttpXhrBackend, provideHttpClient, withInterceptors, withInterceptorsFromDi } from "@angular/common/http";
2
+ import { MatchersV3, PactV4, SpecificationVersion } from "@pact-foundation/pact";
3
+ import { firstValueFrom, isObservable } from "rxjs";
4
+ import { TestBed } from "@angular/core/testing";
5
+ import { InjectionToken, inject } from "@angular/core";
6
+ import { join } from "node:path";
7
+ const providerState = (name, params) => ({
8
+ name,
9
+ params
10
+ });
11
+ function givenStates(pact, states) {
12
+ return states.reduce((interaction, state) => interaction.given(state.name, state.params), pact.addInteraction());
13
+ }
14
+ const JSON_CONTENT_TYPE = { "Content-Type": "application/json" };
15
+ async function verifyInteraction(interaction) {
16
+ const { request, response, run } = interaction;
17
+ const status = response.status ?? 200;
18
+ const body = response.body === void 0 ? void 0 : MatchersV3.reify(response.body);
19
+ const headers = request.headers ?? {};
20
+ await givenStates(interaction.pact, interaction.states ?? []).uponReceiving(interaction.description).withRequest(request.method, request.path, (builder) => {
21
+ if (Object.keys(headers).length) builder.headers(headers);
22
+ if (request.query) builder.query(request.query);
23
+ if (request.body !== void 0) builder.jsonBody(request.body);
24
+ }).willRespondWith(status, (builder) => {
25
+ if (response.body !== void 0) builder.headers(JSON_CONTENT_TYPE).jsonBody(response.body);
26
+ }).executeTest(async (mockServer) => {
27
+ const outcome = run(mockServer.url);
28
+ const result = isObservable(outcome) ? firstValueFrom(outcome) : Promise.resolve(outcome);
29
+ const check = interaction.expect ?? expectBody;
30
+ if (status >= 400) {
31
+ await expect(result).rejects.toBeInstanceOf(HttpErrorResponse);
32
+ const error = await result.catch((failure) => failure);
33
+ expect(error.status).toBe(status);
34
+ await check(interaction.expect ? error : error.error, body);
35
+ } else await check(await result, body);
36
+ });
37
+ }
38
+ const expectBody = (result, body) => {
39
+ if (body === void 0) expect([null, ""]).toContain(result);
40
+ else expect(result).toEqual(body);
41
+ };
42
+ const PACT_MOCK_SERVER_URL = new InjectionToken("PACT_MOCK_SERVER_URL");
43
+ const PACT_AUTH_TOKEN = new InjectionToken("PACT_AUTH_TOKEN");
44
+ const PACT_AUTH_HEADERS = new InjectionToken("PACT_AUTH_HEADERS");
45
+ const DEFAULT_AUTH_SCHEME = "Token";
46
+ const authorizationHeader = (token, scheme = DEFAULT_AUTH_SCHEME) => ({ Authorization: `${scheme} ${token}` });
47
+ const pactAuthHeadersInterceptor = (request, next) => {
48
+ const headers = Object.entries(inject(PACT_AUTH_HEADERS)).filter(([name]) => !request.headers.has(name));
49
+ return next(headers.length ? request.clone({ setHeaders: Object.fromEntries(headers) }) : request);
50
+ };
51
+ function configurePactTestBed(mockServerUrl, providers = [], options = {}) {
52
+ const { token = null, authHeaders = {}, interceptors = [], interceptorClasses = [], httpClient } = options;
53
+ const authInterceptors = Object.keys(authHeaders).length ? [pactAuthHeadersInterceptor] : [];
54
+ if (httpClient && (authInterceptors.length || interceptors.length)) throw new Error("configurePactTestBed applies authHeaders and interceptors only to its default HttpClient providers; add them to the httpClient providers instead");
55
+ TestBed.configureTestingModule({ providers: [
56
+ httpClient ?? provideHttpClient(withInterceptorsFromDi(), withInterceptors([...authInterceptors, ...interceptors])),
57
+ {
58
+ provide: HttpBackend,
59
+ useExisting: HttpXhrBackend
60
+ },
61
+ ...interceptorClasses.map((useClass) => ({
62
+ provide: HTTP_INTERCEPTORS,
63
+ useClass,
64
+ multi: true
65
+ })),
66
+ {
67
+ provide: PACT_MOCK_SERVER_URL,
68
+ useValue: mockServerUrl.replace(/\/+$/, "")
69
+ },
70
+ {
71
+ provide: PACT_AUTH_TOKEN,
72
+ useValue: token
73
+ },
74
+ {
75
+ provide: PACT_AUTH_HEADERS,
76
+ useValue: authHeaders
77
+ },
78
+ ...providers
79
+ ] });
80
+ }
81
+ function resolveResponse(suite, response) {
82
+ const { operationId, ...given } = response;
83
+ if (!operationId) return given;
84
+ if ("body" in given) throw new Error(`Contract response ${operationId} has a body: give either operationId or body`);
85
+ if ((given.status ?? 0) >= 400) throw new Error(`Contract response ${operationId} has status ${given.status}: operationId resolves the documented 2xx body, so give the error body instead`);
86
+ if (!suite.responseFor) throw new Error(`Contract response ${operationId} needs the suite's responseFor`);
87
+ const resolved = suite.responseFor(operationId);
88
+ return {
89
+ ...resolved,
90
+ status: given.status ?? resolved.status
91
+ };
92
+ }
93
+ async function verifyServiceContract(suite, service, contract) {
94
+ const { request, options } = contract;
95
+ const response = resolveResponse(suite, contract.response);
96
+ const suiteHeaders = typeof suite.headers === "function" ? suite.headers(options) : suite.headers;
97
+ const configureTestBed = suite.configureTestBed ?? ((mockServerUrl, providers) => configurePactTestBed(mockServerUrl, providers));
98
+ await verifyInteraction({
99
+ pact: suite.pact,
100
+ description: contract.description,
101
+ states: [...contract.replaceGiven ? [] : suite.given ?? [], ...contract.given ?? []],
102
+ request: {
103
+ ...request,
104
+ headers: {
105
+ ...contract.replaceHeaders ? {} : suiteHeaders,
106
+ ...request.headers
107
+ }
108
+ },
109
+ response,
110
+ expect: contract.expect,
111
+ run: (mockServerUrl) => {
112
+ configureTestBed(mockServerUrl, [service], options);
113
+ return contract.call(TestBed.inject(service));
114
+ }
115
+ });
116
+ }
117
+ function verifyServiceContracts(suite, service, contracts) {
118
+ for (const contract of contracts) it(`should ${contract.description}`, () => verifyServiceContract(suite, service, contract));
119
+ }
120
+ const ISO_DATETIME_PATTERN = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$";
121
+ const ISO_DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
122
+ const UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
123
+ const URL_PATTERN = "^https?://\\S+$";
124
+ const DIGITS_PATTERN = "^\\d+$";
125
+ const EXAMPLE_DATETIME = "2026-10-01T09:30:00Z";
126
+ const EXAMPLE_DATE = "2026-10-01";
127
+ const EXAMPLE_UUID = "2f1e7f9a-3c5b-4d7e-9a1b-6c8d0e2f4a6b";
128
+ const EXAMPLE_URL = "https://example.com/pact";
129
+ const LISTED_KEYS = ["results"];
130
+ const ROOT_PATH = "$";
131
+ const { regex } = MatchersV3;
132
+ const datetime = (example = EXAMPLE_DATETIME) => regex(ISO_DATETIME_PATTERN, example);
133
+ const date = (example = EXAMPLE_DATE) => regex(ISO_DATE_PATTERN, example);
134
+ const uuid = (example = EXAMPLE_UUID) => regex(UUID_PATTERN, example);
135
+ const url = (example = EXAMPLE_URL) => regex(URL_PATTERN, example);
136
+ const digits = (example) => regex(DIGITS_PATTERN, String(example));
137
+ function shapeOf(example, minItems = 1) {
138
+ return shapeAt(example, minItems, "$");
139
+ }
140
+ function shapeAt(example, minItems, path) {
141
+ if (Array.isArray(example)) {
142
+ if (!example.length) throw new Error(`shapeOf cannot describe the items of the empty list at ${path}: give it an example item, or pass an explicit matcher for the list`);
143
+ const item = shapeAt(example[0], 0, `${path}[0]`);
144
+ return minItems ? MatchersV3.eachLike(item, minItems) : MatchersV3.atLeastLike(item, 0, 1);
145
+ }
146
+ if (example !== null && typeof example === "object") {
147
+ if (MatchersV3.isMatcher(example)) return example;
148
+ return Object.fromEntries(Object.entries(example).map(([key, value]) => [key, shapeAt(value, LISTED_KEYS.includes(key) ? 1 : 0, `${path}.${key}`)]));
149
+ }
150
+ if (typeof example === "number") return Number.isInteger(example) ? MatchersV3.integer(example) : MatchersV3.number(example);
151
+ if (typeof example === "string" && (/* @__PURE__ */ new RegExp("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$")).test(example)) return datetime(example);
152
+ return MatchersV3.like(example);
153
+ }
154
+ const PACT_DIR_ENV = "PACT_DIR";
155
+ const DEFAULT_PACT_DIR_NAME = "pacts";
156
+ const DEFAULT_PACT_OPTIONS = {
157
+ spec: SpecificationVersion.SPECIFICATION_VERSION_V4,
158
+ logLevel: "warn",
159
+ cors: true
160
+ };
161
+ function createPact(consumer, provider, options = {}) {
162
+ return new PactV4({
163
+ ...DEFAULT_PACT_OPTIONS,
164
+ ...options,
165
+ dir: options.dir ?? (process.env["PACT_DIR"] || join(process.cwd(), "pacts")),
166
+ consumer,
167
+ provider
168
+ });
169
+ }
170
+ export { DEFAULT_AUTH_SCHEME, DEFAULT_PACT_DIR_NAME, DEFAULT_PACT_OPTIONS, DIGITS_PATTERN, EXAMPLE_DATE, EXAMPLE_DATETIME, EXAMPLE_URL, EXAMPLE_UUID, ISO_DATETIME_PATTERN, ISO_DATE_PATTERN, LISTED_KEYS, PACT_AUTH_HEADERS, PACT_AUTH_TOKEN, PACT_DIR_ENV, PACT_MOCK_SERVER_URL, ROOT_PATH, URL_PATTERN, UUID_PATTERN, authorizationHeader, configurePactTestBed, createPact, date, datetime, digits, givenStates, pactAuthHeadersInterceptor, providerState, shapeOf, url, uuid, verifyInteraction, verifyServiceContract, verifyServiceContracts };
171
+
172
+ //# sourceMappingURL=testgorilla-tgo-pact.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testgorilla-tgo-pact.mjs","names":[],"sources":["../../../../packages/tgo-pact/src/lib/pact/provider-states.ts","../../../../packages/tgo-pact/src/lib/contracts/contracts.constants.ts","../../../../packages/tgo-pact/src/lib/contracts/verify-interaction.ts","../../../../packages/tgo-pact/src/lib/test-bed/pact-test-bed.constants.ts","../../../../packages/tgo-pact/src/lib/test-bed/configure-pact-test-bed.ts","../../../../packages/tgo-pact/src/lib/contracts/verify-service-contracts.ts","../../../../packages/tgo-pact/src/lib/matchers/matchers.constants.ts","../../../../packages/tgo-pact/src/lib/matchers/matchers.ts","../../../../packages/tgo-pact/src/lib/pact/pact.constants.ts","../../../../packages/tgo-pact/src/lib/pact/create-pact.ts"],"sourcesContent":["import type { PactV4 } from '@pact-foundation/pact';\nimport type { PactInteraction, ProviderState, ProviderStateParams } from './provider-states.model';\n\nexport const providerState = (name: string, params?: ProviderStateParams): ProviderState => ({\n name,\n params,\n});\n\n/** Starts an interaction with every state applied in order. */\nexport function givenStates(pact: PactV4, states: readonly ProviderState[]): PactInteraction {\n return states.reduce<PactInteraction>(\n (interaction, state) => interaction.given(state.name, state.params),\n pact.addInteraction()\n );\n}\n","export const JSON_CONTENT_TYPE = { 'Content-Type': 'application/json' };\nexport const MIN_ERROR_STATUS = 400;\n","import { HttpErrorResponse } from '@angular/common/http';\nimport { MatchersV3 } from '@pact-foundation/pact';\nimport { firstValueFrom, isObservable } from 'rxjs';\nimport { givenStates } from '../pact/provider-states';\nimport { JSON_CONTENT_TYPE, MIN_ERROR_STATUS } from './contracts.constants';\nimport type { ContractExpectation, InteractionContract } from './contracts.model';\n\n/**\n * The core of the contract runners: registers the interaction, runs the call against the mock server and checks\n * its outcome. A 2xx call must resolve to the reified response body, or to null/'' for an empty one. A 4xx/5xx call\n * must fail with an `HttpErrorResponse` of that status, whose `error` equals the reified body, or is null/'' for an\n * empty one. `interaction.expect` replaces the body check.\n */\nexport async function verifyInteraction(interaction: InteractionContract): Promise<void> {\n const { request, response, run } = interaction;\n const status = response.status ?? 200;\n const body = response.body === undefined ? undefined : MatchersV3.reify(response.body);\n const headers = request.headers ?? {};\n\n await givenStates(interaction.pact, interaction.states ?? [])\n .uponReceiving(interaction.description)\n .withRequest(request.method, request.path, builder => {\n if (Object.keys(headers).length) {\n builder.headers(headers);\n }\n if (request.query) {\n builder.query(request.query);\n }\n if (request.body !== undefined) {\n builder.jsonBody(request.body);\n }\n })\n .willRespondWith(status, builder => {\n if (response.body !== undefined) {\n builder.headers(JSON_CONTENT_TYPE).jsonBody(response.body);\n }\n })\n .executeTest(async mockServer => {\n const outcome = run(mockServer.url);\n const result = isObservable(outcome) ? firstValueFrom(outcome) : Promise.resolve(outcome);\n const check = interaction.expect ?? expectBody;\n\n if (status >= MIN_ERROR_STATUS) {\n await expect(result).rejects.toBeInstanceOf(HttpErrorResponse);\n const error = (await result.catch((failure: unknown) => failure)) as HttpErrorResponse;\n expect(error.status).toBe(status);\n await check(interaction.expect ? error : error.error, body);\n } else {\n await check(await result, body);\n }\n });\n}\n\nconst expectBody: ContractExpectation = (result, body) => {\n if (body === undefined) {\n // An empty body reaches JSON clients as null and text clients as ''.\n expect([null, '']).toContain(result);\n } else {\n expect(result).toEqual(body);\n }\n};\n","import { InjectionToken } from '@angular/core';\n\n/** Base URL of the Pact mock server, without a trailing slash. Build the app's API base URL from it. */\nexport const PACT_MOCK_SERVER_URL = new InjectionToken<string>('PACT_MOCK_SERVER_URL');\n\n/** Token the provider state creates, or null for anonymous calls. */\nexport const PACT_AUTH_TOKEN = new InjectionToken<string | null>('PACT_AUTH_TOKEN');\n\nexport const PACT_AUTH_HEADERS = new InjectionToken<Record<string, string>>('PACT_AUTH_HEADERS');\n\nexport const DEFAULT_AUTH_SCHEME = 'Token';\n","import {\n HTTP_INTERCEPTORS,\n HttpBackend,\n HttpInterceptorFn,\n HttpXhrBackend,\n provideHttpClient,\n withInterceptors,\n withInterceptorsFromDi,\n} from '@angular/common/http';\nimport { inject, Provider } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\nimport {\n DEFAULT_AUTH_SCHEME,\n PACT_AUTH_HEADERS,\n PACT_AUTH_TOKEN,\n PACT_MOCK_SERVER_URL,\n} from './pact-test-bed.constants';\nimport type { PactTestBedOptions } from './pact-test-bed.model';\n\n/** `{ Authorization: '<scheme> <token>' }`, the header DRF token auth expects. */\nexport const authorizationHeader = (token: string, scheme: string = DEFAULT_AUTH_SCHEME) => ({\n Authorization: `${scheme} ${token}`,\n});\n\n/** Adds `PactTestBedOptions.authHeaders` to every request that does not set them itself. */\nexport const pactAuthHeadersInterceptor: HttpInterceptorFn = (request, next) => {\n const headers = Object.entries(inject(PACT_AUTH_HEADERS)).filter(\n ([name]) => !request.headers.has(name)\n );\n return next(\n headers.length ? request.clone({ setHeaders: Object.fromEntries(headers) }) : request\n );\n};\n\n/**\n * Configures TestBed with HttpClient for a Pact interaction: the mock server URL and auth token are provided as\n * `PACT_MOCK_SERVER_URL` and `PACT_AUTH_TOKEN`, for the app's config and auth stubs in `providers` to read.\n * Requests go through `HttpXhrBackend`, which jsdom supports. Call it inside `executeTest`, once the mock server URL\n * is known. With `options.httpClient`, `authHeaders` and `interceptors` throw, and `interceptorClasses` apply only if\n * those providers include `withInterceptorsFromDi()`.\n */\nexport function configurePactTestBed(\n mockServerUrl: string,\n providers: Provider[] = [],\n options: PactTestBedOptions = {}\n): void {\n const {\n token = null,\n authHeaders = {},\n interceptors = [],\n interceptorClasses = [],\n httpClient,\n } = options;\n const authInterceptors = Object.keys(authHeaders).length ? [pactAuthHeadersInterceptor] : [];\n if (httpClient && (authInterceptors.length || interceptors.length)) {\n throw new Error(\n 'configurePactTestBed applies authHeaders and interceptors only to its default HttpClient providers; add them to the httpClient providers instead'\n );\n }\n\n TestBed.configureTestingModule({\n providers: [\n httpClient ??\n provideHttpClient(\n withInterceptorsFromDi(),\n withInterceptors([...authInterceptors, ...interceptors])\n ),\n // jsdom has no fetch, and Angular 22 defaults HttpClient to it. withXhr() does not exist in Angular 21.\n { provide: HttpBackend, useExisting: HttpXhrBackend },\n ...interceptorClasses.map(useClass => ({\n provide: HTTP_INTERCEPTORS,\n useClass,\n multi: true,\n })),\n { provide: PACT_MOCK_SERVER_URL, useValue: mockServerUrl.replace(/\\/+$/, '') },\n { provide: PACT_AUTH_TOKEN, useValue: token },\n { provide: PACT_AUTH_HEADERS, useValue: authHeaders },\n ...providers,\n ],\n });\n}\n","import type { Provider, Type } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\nimport { configurePactTestBed } from '../test-bed/configure-pact-test-bed';\nimport { MIN_ERROR_STATUS } from './contracts.constants';\nimport type { ContractResponse, ContractSuite, ServiceContract } from './contracts.model';\nimport { verifyInteraction } from './verify-interaction';\n\nfunction resolveResponse<O>(\n suite: ContractSuite<O>,\n response: ServiceContract<unknown, O>['response']\n): ContractResponse {\n const { operationId, ...given } = response;\n if (!operationId) {\n return given;\n }\n if ('body' in given) {\n throw new Error(`Contract response ${operationId} has a body: give either operationId or body`);\n }\n if ((given.status ?? 0) >= MIN_ERROR_STATUS) {\n throw new Error(\n `Contract response ${operationId} has status ${given.status}: operationId resolves the documented 2xx body, so give the error body instead`\n );\n }\n if (!suite.responseFor) {\n throw new Error(`Contract response ${operationId} needs the suite's responseFor`);\n }\n const resolved = suite.responseFor(operationId);\n return { ...resolved, status: given.status ?? resolved.status };\n}\n\n/**\n * Runs one contract through `verifyInteraction`: the suite's states and headers, then the contract's, unless it\n * replaces them. The call is made on the service from TestBed, configured by the suite.\n */\nexport async function verifyServiceContract<S, O = undefined>(\n suite: ContractSuite<O>,\n service: Type<S>,\n contract: ServiceContract<S, O>\n): Promise<void> {\n const { request, options } = contract;\n const response = resolveResponse(suite, contract.response);\n const suiteHeaders = typeof suite.headers === 'function' ? suite.headers(options) : suite.headers;\n const configureTestBed =\n suite.configureTestBed ??\n ((mockServerUrl: string, providers: Provider[]) =>\n configurePactTestBed(mockServerUrl, providers));\n\n await verifyInteraction({\n pact: suite.pact,\n description: contract.description,\n states: [...(contract.replaceGiven ? [] : (suite.given ?? [])), ...(contract.given ?? [])],\n request: {\n ...request,\n headers: { ...(contract.replaceHeaders ? {} : suiteHeaders), ...request.headers },\n },\n response,\n expect: contract.expect,\n run: mockServerUrl => {\n configureTestBed(mockServerUrl, [service], options);\n return contract.call(TestBed.inject(service));\n },\n });\n}\n\n/** Declares one `it` per contract, driving a service method against the suite's mock server. */\nexport function verifyServiceContracts<S, O = undefined>(\n suite: ContractSuite<O>,\n service: Type<S>,\n contracts: ServiceContract<S, O>[]\n): void {\n for (const contract of contracts) {\n it(`should ${contract.description}`, () => verifyServiceContract(suite, service, contract));\n }\n}\n","export const ISO_DATETIME_PATTERN =\n '^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d+)?(Z|[+-]\\\\d{2}:\\\\d{2})$';\nexport const ISO_DATE_PATTERN = '^\\\\d{4}-\\\\d{2}-\\\\d{2}$';\nexport const UUID_PATTERN = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';\nexport const URL_PATTERN = '^https?://\\\\S+$';\nexport const DIGITS_PATTERN = '^\\\\d+$';\n\nexport const EXAMPLE_DATETIME = '2026-10-01T09:30:00Z';\nexport const EXAMPLE_DATE = '2026-10-01';\nexport const EXAMPLE_UUID = '2f1e7f9a-3c5b-4d7e-9a1b-6c8d0e2f4a6b';\nexport const EXAMPLE_URL = 'https://example.com/pact';\n\n/** Keys whose lists `shapeOf` requires at least one item in, at any depth: paginated `results`. */\nexport const LISTED_KEYS: readonly string[] = ['results'];\n\n/** How `shapeOf` names the example itself in errors, e.g. `$.results[0].tags`. */\nexport const ROOT_PATH = '$';\n","import { MatchersV3 } from '@pact-foundation/pact';\nimport {\n DIGITS_PATTERN,\n EXAMPLE_DATE,\n EXAMPLE_DATETIME,\n EXAMPLE_URL,\n EXAMPLE_UUID,\n ISO_DATE_PATTERN,\n ISO_DATETIME_PATTERN,\n LISTED_KEYS,\n ROOT_PATH,\n URL_PATTERN,\n UUID_PATTERN,\n} from './matchers.constants';\n\nconst { regex } = MatchersV3;\n\n/** ISO-8601 datetime, as DRF renders datetimes. */\nexport const datetime = (example: string = EXAMPLE_DATETIME) =>\n regex(ISO_DATETIME_PATTERN, example);\n\n/** ISO-8601 date. */\nexport const date = (example: string = EXAMPLE_DATE) => regex(ISO_DATE_PATTERN, example);\n\n/** Lowercase canonical UUID. */\nexport const uuid = (example: string = EXAMPLE_UUID) => regex(UUID_PATTERN, example);\n\n/** Absolute http(s) URL, e.g. presigned S3 URLs. */\nexport const url = (example: string = EXAMPLE_URL) => regex(URL_PATTERN, example);\n\n/** A string of digits, e.g. an id sent as a string. */\nexport const digits = (example: number | string) => regex(DIGITS_PATTERN, String(example));\n\n/**\n * Turns an example the app's model describes into type matchers. Leave nullable fields out: the app copes with null,\n * so the contract should not pin their type. Top-level lists need `minItems` items (default 1); nested lists may be\n * empty, except `results`. Every list needs an item in the example to describe the items' shape, so an empty one\n * throws. ISO datetimes match the datetime pattern, and matchers pass through unchanged.\n *\n * A whole-number example becomes an `integer` matcher, so `{ price: 10 }` rejects `10.5`. Other numbers match any\n * number. To accept decimals for a field whose example is whole, pass the matcher yourself:\n * `{ price: MatchersV3.decimal(10) }` or `MatchersV3.number(10)`.\n */\nexport function shapeOf(example: unknown, minItems = 1): unknown {\n return shapeAt(example, minItems, ROOT_PATH);\n}\n\nfunction shapeAt(example: unknown, minItems: number, path: string): unknown {\n if (Array.isArray(example)) {\n if (!example.length) {\n throw new Error(\n `shapeOf cannot describe the items of the empty list at ${path}: give it an example item, or pass an explicit matcher for the list`\n );\n }\n const item = shapeAt(example[0], 0, `${path}[0]`);\n return minItems ? MatchersV3.eachLike(item, minItems) : MatchersV3.atLeastLike(item, 0, 1);\n }\n if (example !== null && typeof example === 'object') {\n if (MatchersV3.isMatcher(example)) {\n return example;\n }\n return Object.fromEntries(\n Object.entries(example).map(([key, value]) => [\n key,\n shapeAt(value, LISTED_KEYS.includes(key) ? 1 : 0, `${path}.${key}`),\n ])\n );\n }\n if (typeof example === 'number') {\n return Number.isInteger(example) ? MatchersV3.integer(example) : MatchersV3.number(example);\n }\n if (typeof example === 'string' && new RegExp(ISO_DATETIME_PATTERN).test(example)) {\n return datetime(example);\n }\n return MatchersV3.like(example);\n}\n","import { SpecificationVersion } from '@pact-foundation/pact';\nimport type { CreatePactOptions } from './pact.model';\n\nexport const PACT_DIR_ENV = 'PACT_DIR';\nexport const DEFAULT_PACT_DIR_NAME = 'pacts';\n\nexport const DEFAULT_PACT_OPTIONS = {\n spec: SpecificationVersion.SPECIFICATION_VERSION_V4,\n logLevel: 'warn',\n // jsdom's XMLHttpRequest enforces CORS, and the mock server runs on another origin.\n cors: true,\n} as const satisfies CreatePactOptions;\n","import { join } from 'node:path';\nimport { PactV4 } from '@pact-foundation/pact';\nimport { DEFAULT_PACT_DIR_NAME, DEFAULT_PACT_OPTIONS, PACT_DIR_ENV } from './pact.constants';\nimport type { CreatePactOptions } from './pact.model';\n\n/**\n * Creates a V4 pact between `consumer` and `provider`. The pact file goes to `options.dir`, else `$PACT_DIR`,\n * else `<cwd>/pacts`. Specs of one consumer-provider pair merge into the same file, so run them in band.\n */\nexport function createPact(\n consumer: string,\n provider: string,\n options: CreatePactOptions = {}\n): PactV4 {\n return new PactV4({\n ...DEFAULT_PACT_OPTIONS,\n ...options,\n dir: options.dir ?? (process.env[PACT_DIR_ENV] || join(process.cwd(), DEFAULT_PACT_DIR_NAME)),\n consumer,\n provider,\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,5 +1,41 @@
1
1
  {
2
2
  "name": "@testgorilla/tgo-pact",
3
- "version": "0.0.0",
4
- "description": "Placeholder; the real package is published from TestGorilla-BV/tgo-fe-libraries CI."
3
+ "version": "1.0.0-rc.0",
4
+ "peerDependencies": {
5
+ "@angular/common": ">=21.0.0 <23.0.0",
6
+ "@angular/core": ">=21.0.0 <23.0.0",
7
+ "@pact-foundation/pact": "^17.0.0",
8
+ "jest": "^30.0.0",
9
+ "rxjs": "^7.8.0"
10
+ },
11
+ "engines": {
12
+ "node": ">=22.12"
13
+ },
14
+ "sideEffects": false,
15
+ "license": "PROPRIETARY",
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "displayName": "Pact",
21
+ "description": "Pact consumer test helpers for Angular apps: V4 pacts, a TestBed wired to the mock server, matchers and a Jest config",
22
+ "module": "fesm2022/testgorilla-tgo-pact.mjs",
23
+ "typings": "types/testgorilla-tgo-pact.d.ts",
24
+ "exports": {
25
+ "./package.json": {
26
+ "default": "./package.json"
27
+ },
28
+ ".": {
29
+ "types": "./types/testgorilla-tgo-pact.d.ts",
30
+ "default": "./fesm2022/testgorilla-tgo-pact.mjs"
31
+ },
32
+ "./jest": {
33
+ "types": "./types/testgorilla-tgo-pact-jest.d.ts",
34
+ "default": "./fesm2022/testgorilla-tgo-pact-jest.mjs"
35
+ }
36
+ },
37
+ "type": "module",
38
+ "dependencies": {
39
+ "tslib": "^2.3.0"
40
+ }
5
41
  }
@@ -0,0 +1,16 @@
1
+ import { Config } from "jest";
2
+ /**
3
+ * Turns the app's unit Jest config into one for Pact consumer specs: `*.pact.spec.ts` only, in band, a 30s timeout
4
+ * and no coverage. Everything else (preset, transforms, module mapping, setup files, the other ignore patterns) comes
5
+ * from `base`, minus `testRegex` and `projects` and the ignore patterns that would skip pact specs. `displayName`
6
+ * defaults to `pact` when `base` has none. `overrides` apply last, over both.
7
+ */
8
+ export declare function createPactJestConfig(base?: Config, overrides?: Config): Config;
9
+ export declare const PACT_TEST_MATCH: string[];
10
+ export declare const PACT_TEST_TIMEOUT = 30000;
11
+ export declare const PACT_DISPLAY_NAME = "pact";
12
+ /** Used when the base config sets no `testPathIgnorePatterns`; Jest's own default. */
13
+ export declare const DEFAULT_TEST_PATH_IGNORE_PATTERNS: string[];
14
+ /** A pact spec's path, for finding the base config's ignore patterns that would skip pact specs. */
15
+ export declare const PACT_SPEC_EXAMPLE_PATH = "/src/app/example.pact.spec.ts";
16
+ export declare const PACT_JEST_DEFAULTS: Readonly<Config>;
@@ -0,0 +1,182 @@
1
+ import { EnvironmentProviders, InjectionToken, Provider, Type } from "@angular/core";
2
+ import { MatchersV3, PactV4, SpecificationVersion, TemplateHeaders, TemplateQuery } from "@pact-foundation/pact";
3
+ import { Observable } from "rxjs";
4
+ import { HttpInterceptor, HttpInterceptorFn } from "@angular/common/http";
5
+ export type PactInteraction = ReturnType<PactV4['addInteraction']>;
6
+ export type ProviderStateParams = Parameters<PactInteraction['given']>[1];
7
+ export interface ProviderState {
8
+ name: string;
9
+ params?: ProviderStateParams;
10
+ }
11
+ export interface ContractResponse {
12
+ status?: number;
13
+ /** A body or matchers. Leave out for an empty response, which the call must resolve to null or ''. */
14
+ body?: unknown;
15
+ }
16
+ export interface ContractRequest {
17
+ method: string;
18
+ path: string;
19
+ query?: TemplateQuery;
20
+ /** In a `ServiceContract`, merged over the suite's headers. */
21
+ headers?: TemplateHeaders;
22
+ body?: unknown;
23
+ }
24
+ /**
25
+ * Replaces the default check of the call's outcome against the response body. `result` is what a 2xx call resolved
26
+ * to, or the `HttpErrorResponse` of a 4xx/5xx one, whose status is still checked. `body` is the reified response
27
+ * body, undefined for an empty response.
28
+ */
29
+ export type ContractExpectation = (result: unknown, body: unknown) => void | Promise<void>;
30
+ /** One interaction, and how to make the app send its request: the input of `verifyInteraction`. */
31
+ export interface InteractionContract {
32
+ pact: PactV4;
33
+ /** The interaction description. */
34
+ description: string;
35
+ states?: readonly ProviderState[];
36
+ /** The expected request, with every header it must carry. */
37
+ request: ContractRequest;
38
+ response: ContractResponse;
39
+ /** Configures TestBed for the mock server and makes the call, returning its result. */
40
+ run: (mockServerUrl: string) => Observable<unknown> | Promise<unknown>;
41
+ expect?: ContractExpectation;
42
+ }
43
+ /**
44
+ * One interaction of a service method. `O` is whatever per-contract context the suite needs, e.g.
45
+ * `{ asTestTaker: boolean }`, passed to the suite's `headers` and `configureTestBed`.
46
+ */
47
+ export interface ServiceContract<S, O = undefined> {
48
+ /** Becomes the interaction description and the test name, `should <description>`. */
49
+ description: string;
50
+ /** States after the suite's own. */
51
+ given?: ProviderState[];
52
+ /** Applies `given` without the suite's states, e.g. for an anonymous call. */
53
+ replaceGiven?: boolean;
54
+ request: ContractRequest;
55
+ /** Expects `request.headers` without the suite's headers, e.g. for an anonymous call. */
56
+ replaceHeaders?: boolean;
57
+ call: (service: S) => Observable<unknown>;
58
+ /**
59
+ * A response, or the id of an operation the suite's `responseFor` resolves to its documented 2xx response. Not
60
+ * both: an `operationId` takes no `body`, nor a 4xx/5xx `status`.
61
+ */
62
+ response: ContractResponse & {
63
+ operationId?: string;
64
+ };
65
+ /** Replaces the default check, e.g. for a method that hands back only part of the body. */
66
+ expect?: ContractExpectation;
67
+ options?: O;
68
+ }
69
+ export interface ContractSuite<O = undefined> {
70
+ /** One pact per spec file: each gets its own mock server. */
71
+ pact: PactV4;
72
+ /** States every interaction needs, applied before the contract's own. */
73
+ given?: ProviderState[];
74
+ /** Headers every request carries, e.g. `authorizationHeader(token)`. */
75
+ headers?: TemplateHeaders | ((options: O | undefined) => TemplateHeaders);
76
+ /** Defaults to `configurePactTestBed(mockServerUrl, providers)`. */
77
+ configureTestBed?: (mockServerUrl: string, providers: Provider[], options: O | undefined) => void;
78
+ /** Resolves `response.operationId`, e.g. from an OpenAPI document. */
79
+ responseFor?: (operationId: string) => ContractResponse;
80
+ }
81
+ /**
82
+ * The core of the contract runners: registers the interaction, runs the call against the mock server and checks
83
+ * its outcome. A 2xx call must resolve to the reified response body, or to null/'' for an empty one. A 4xx/5xx call
84
+ * must fail with an `HttpErrorResponse` of that status, whose `error` equals the reified body, or is null/'' for an
85
+ * empty one. `interaction.expect` replaces the body check.
86
+ */
87
+ export declare function verifyInteraction(interaction: InteractionContract): Promise<void>;
88
+ /**
89
+ * Runs one contract through `verifyInteraction`: the suite's states and headers, then the contract's, unless it
90
+ * replaces them. The call is made on the service from TestBed, configured by the suite.
91
+ */
92
+ export declare function verifyServiceContract<S, O = undefined>(suite: ContractSuite<O>, service: Type<S>, contract: ServiceContract<S, O>): Promise<void>;
93
+ /** Declares one `it` per contract, driving a service method against the suite's mock server. */
94
+ export declare function verifyServiceContracts<S, O = undefined>(suite: ContractSuite<O>, service: Type<S>, contracts: ServiceContract<S, O>[]): void;
95
+ /** ISO-8601 datetime, as DRF renders datetimes. */
96
+ export declare const datetime: (example?: string) => MatchersV3.V3RegexMatcher;
97
+ /** ISO-8601 date. */
98
+ export declare const date: (example?: string) => MatchersV3.V3RegexMatcher;
99
+ /** Lowercase canonical UUID. */
100
+ export declare const uuid: (example?: string) => MatchersV3.V3RegexMatcher;
101
+ /** Absolute http(s) URL, e.g. presigned S3 URLs. */
102
+ export declare const url: (example?: string) => MatchersV3.V3RegexMatcher;
103
+ /** A string of digits, e.g. an id sent as a string. */
104
+ export declare const digits: (example: number | string) => MatchersV3.V3RegexMatcher;
105
+ /**
106
+ * Turns an example the app's model describes into type matchers. Leave nullable fields out: the app copes with null,
107
+ * so the contract should not pin their type. Top-level lists need `minItems` items (default 1); nested lists may be
108
+ * empty, except `results`. Every list needs an item in the example to describe the items' shape, so an empty one
109
+ * throws. ISO datetimes match the datetime pattern, and matchers pass through unchanged.
110
+ *
111
+ * A whole-number example becomes an `integer` matcher, so `{ price: 10 }` rejects `10.5`. Other numbers match any
112
+ * number. To accept decimals for a field whose example is whole, pass the matcher yourself:
113
+ * `{ price: MatchersV3.decimal(10) }` or `MatchersV3.number(10)`.
114
+ */
115
+ export declare function shapeOf(example: unknown, minItems?: number): unknown;
116
+ export declare const ISO_DATETIME_PATTERN = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$";
117
+ export declare const ISO_DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
118
+ export declare const UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
119
+ export declare const URL_PATTERN = "^https?://\\S+$";
120
+ export declare const DIGITS_PATTERN = "^\\d+$";
121
+ export declare const EXAMPLE_DATETIME = "2026-10-01T09:30:00Z";
122
+ export declare const EXAMPLE_DATE = "2026-10-01";
123
+ export declare const EXAMPLE_UUID = "2f1e7f9a-3c5b-4d7e-9a1b-6c8d0e2f4a6b";
124
+ export declare const EXAMPLE_URL = "https://example.com/pact";
125
+ /** Keys whose lists `shapeOf` requires at least one item in, at any depth: paginated `results`. */
126
+ export declare const LISTED_KEYS: readonly string[];
127
+ /** How `shapeOf` names the example itself in errors, e.g. `$.results[0].tags`. */
128
+ export declare const ROOT_PATH = "$";
129
+ export type PactV4Options = ConstructorParameters<typeof PactV4>[0];
130
+ /** Everything `PactV4` accepts except the consumer and provider, which `createPact` takes as arguments. */
131
+ export type CreatePactOptions = Partial<Omit<PactV4Options, 'consumer' | 'provider'>>;
132
+ /**
133
+ * Creates a V4 pact between `consumer` and `provider`. The pact file goes to `options.dir`, else `$PACT_DIR`,
134
+ * else `<cwd>/pacts`. Specs of one consumer-provider pair merge into the same file, so run them in band.
135
+ */
136
+ export declare function createPact(consumer: string, provider: string, options?: CreatePactOptions): PactV4;
137
+ export declare const PACT_DIR_ENV = "PACT_DIR";
138
+ export declare const DEFAULT_PACT_DIR_NAME = "pacts";
139
+ export declare const DEFAULT_PACT_OPTIONS: {
140
+ readonly spec: SpecificationVersion.SPECIFICATION_VERSION_V4;
141
+ readonly logLevel: "warn";
142
+ readonly cors: true;
143
+ };
144
+ export declare const providerState: (name: string, params?: ProviderStateParams) => ProviderState;
145
+ /** Starts an interaction with every state applied in order. */
146
+ export declare function givenStates(pact: PactV4, states: readonly ProviderState[]): PactInteraction;
147
+ export interface PactTestBedOptions {
148
+ /** Provided as `PACT_AUTH_TOKEN`, for the app's auth stubs to hand to its real interceptor. Defaults to null. */
149
+ token?: string | null;
150
+ /** Headers added to every request, for apps whose own interceptor is not under test. */
151
+ authHeaders?: Record<string, string>;
152
+ /** Functional interceptors, registered after the DI ones. */
153
+ interceptors?: HttpInterceptorFn[];
154
+ /** Class interceptors, registered as `HTTP_INTERCEPTORS`. */
155
+ interceptorClasses?: Type<HttpInterceptor>[];
156
+ /**
157
+ * Replaces the default `provideHttpClient(withInterceptorsFromDi(), withInterceptors([...]))`, e.g. with the
158
+ * app's own HttpClient providers. Combining it with `authHeaders` or `interceptors` throws, and `interceptorClasses`
159
+ * need `withInterceptorsFromDi()` in these providers.
160
+ */
161
+ httpClient?: EnvironmentProviders;
162
+ }
163
+ /** `{ Authorization: '<scheme> <token>' }`, the header DRF token auth expects. */
164
+ export declare const authorizationHeader: (token: string, scheme?: string) => {
165
+ Authorization: string;
166
+ };
167
+ /** Adds `PactTestBedOptions.authHeaders` to every request that does not set them itself. */
168
+ export declare const pactAuthHeadersInterceptor: HttpInterceptorFn;
169
+ /**
170
+ * Configures TestBed with HttpClient for a Pact interaction: the mock server URL and auth token are provided as
171
+ * `PACT_MOCK_SERVER_URL` and `PACT_AUTH_TOKEN`, for the app's config and auth stubs in `providers` to read.
172
+ * Requests go through `HttpXhrBackend`, which jsdom supports. Call it inside `executeTest`, once the mock server URL
173
+ * is known. With `options.httpClient`, `authHeaders` and `interceptors` throw, and `interceptorClasses` apply only if
174
+ * those providers include `withInterceptorsFromDi()`.
175
+ */
176
+ export declare function configurePactTestBed(mockServerUrl: string, providers?: Provider[], options?: PactTestBedOptions): void;
177
+ /** Base URL of the Pact mock server, without a trailing slash. Build the app's API base URL from it. */
178
+ export declare const PACT_MOCK_SERVER_URL: InjectionToken<string>;
179
+ /** Token the provider state creates, or null for anonymous calls. */
180
+ export declare const PACT_AUTH_TOKEN: InjectionToken<string | null>;
181
+ export declare const PACT_AUTH_HEADERS: InjectionToken<Record<string, string>>;
182
+ export declare const DEFAULT_AUTH_SCHEME = "Token";