autotel-pact 5.0.0 → 7.0.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/package.json +3 -4
- package/src/attrs.test.ts +0 -35
- package/src/attrs.ts +0 -53
- package/src/audit.test.ts +0 -189
- package/src/audit.ts +0 -251
- package/src/auto-wrap.test.ts +0 -149
- package/src/auto-wrap.ts +0 -283
- package/src/broker.test.ts +0 -175
- package/src/broker.ts +0 -118
- package/src/cli.test.ts +0 -148
- package/src/cli.ts +0 -287
- package/src/index.ts +0 -94
- package/src/labels.ts +0 -25
- package/src/ledger-normalize.test.ts +0 -141
- package/src/ledger-normalize.ts +0 -82
- package/src/ledger.test.ts +0 -92
- package/src/ledger.ts +0 -156
- package/src/pact-file.test.ts +0 -124
- package/src/pact-file.ts +0 -65
- package/src/processor.test.ts +0 -90
- package/src/processor.ts +0 -191
- package/src/tag.test.ts +0 -72
- package/src/tag.ts +0 -21
- package/src/types.ts +0 -169
- package/src/wrapper-http.test.ts +0 -133
- package/src/wrapper-http.ts +0 -194
- package/src/wrapper-provider.test.ts +0 -132
- package/src/wrapper-provider.ts +0 -163
- package/src/wrapper.test.ts +0 -176
- package/src/wrapper.ts +0 -221
package/src/auto-wrap.test.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { installAutoWrap } from './auto-wrap.js';
|
|
6
|
-
import { readLedger } from './ledger.js';
|
|
7
|
-
|
|
8
|
-
// We can't reuse the patched-once prototypes between tests cleanly —
|
|
9
|
-
// monkey-patching is by design a one-shot operation. So each test defines
|
|
10
|
-
// its own fresh fake classes, then installs the auto-wrap against them.
|
|
11
|
-
|
|
12
|
-
function makeFakeMessagePact() {
|
|
13
|
-
return class FakeMessageConsumerPact {
|
|
14
|
-
config = { consumer: 'A', provider: 'B' };
|
|
15
|
-
async verify(handler: (m: unknown) => Promise<unknown>): Promise<unknown> {
|
|
16
|
-
return handler({
|
|
17
|
-
contents: { x: 1 },
|
|
18
|
-
description: 'an evt',
|
|
19
|
-
providerStates: [{ name: 'state-x' }],
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function makeFakePactV3() {
|
|
26
|
-
return class FakePactV3 {
|
|
27
|
-
opts = { consumer: 'Web', provider: 'Catalog' };
|
|
28
|
-
added: Array<{ uponReceiving: string; states?: Array<{ description: string }> }> = [];
|
|
29
|
-
addInteraction(interaction: {
|
|
30
|
-
uponReceiving: string;
|
|
31
|
-
states?: Array<{ description: string }>;
|
|
32
|
-
}) {
|
|
33
|
-
this.added.push(interaction);
|
|
34
|
-
return this;
|
|
35
|
-
}
|
|
36
|
-
async executeTest<T>(
|
|
37
|
-
fn: (s: { url: string; port: number }) => Promise<T>,
|
|
38
|
-
): Promise<T | undefined> {
|
|
39
|
-
return fn({ url: 'http://localhost', port: 0 });
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let workDir: string;
|
|
45
|
-
let originalCwd: string;
|
|
46
|
-
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
originalCwd = process.cwd();
|
|
49
|
-
workDir = mkdtempSync(path.join(tmpdir(), 'autotel-pact-auto-'));
|
|
50
|
-
process.chdir(workDir);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
afterEach(() => {
|
|
54
|
-
process.chdir(originalCwd);
|
|
55
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
56
|
-
delete process.env.AUTOTEL_PACT_RUN_ID;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('auto-wrap', () => {
|
|
60
|
-
it('patches MessageConsumerPact.prototype.verify to emit a ledger entry', async () => {
|
|
61
|
-
process.env.AUTOTEL_PACT_RUN_ID = 'r-auto-msg';
|
|
62
|
-
const Pact = makeFakeMessagePact();
|
|
63
|
-
installAutoWrap({
|
|
64
|
-
MessageConsumerPact: Pact as unknown as { prototype: Record<string | symbol, unknown> },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const pact = new Pact();
|
|
68
|
-
await pact.verify(async () => 'handled');
|
|
69
|
-
|
|
70
|
-
const entries = readLedger({ runId: 'r-auto-msg' });
|
|
71
|
-
expect(entries).toHaveLength(1);
|
|
72
|
-
expect(entries[0]).toMatchObject({
|
|
73
|
-
consumer: 'A',
|
|
74
|
-
provider: 'B',
|
|
75
|
-
interaction: 'an evt',
|
|
76
|
-
states: ['state-x'],
|
|
77
|
-
kind: 'message',
|
|
78
|
-
outcome: 'passed',
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('patches PactV3 so addInteraction + executeTest emits ledger entries', async () => {
|
|
83
|
-
process.env.AUTOTEL_PACT_RUN_ID = 'r-auto-http';
|
|
84
|
-
const Pact = makeFakePactV3();
|
|
85
|
-
installAutoWrap({
|
|
86
|
-
PactV3: Pact as unknown as { prototype: Record<string | symbol, unknown> },
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const pact = new Pact();
|
|
90
|
-
pact.addInteraction({
|
|
91
|
-
uponReceiving: 'get widgets',
|
|
92
|
-
states: [{ description: 'widgets exist' }],
|
|
93
|
-
});
|
|
94
|
-
await pact.executeTest(async () => {});
|
|
95
|
-
|
|
96
|
-
const entries = readLedger({ runId: 'r-auto-http' });
|
|
97
|
-
expect(entries).toHaveLength(1);
|
|
98
|
-
expect(entries[0]).toMatchObject({
|
|
99
|
-
consumer: 'Web',
|
|
100
|
-
provider: 'Catalog',
|
|
101
|
-
interaction: 'get widgets',
|
|
102
|
-
states: ['widgets exist'],
|
|
103
|
-
kind: 'http',
|
|
104
|
-
outcome: 'passed',
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('records failed outcome when the test body throws', async () => {
|
|
109
|
-
process.env.AUTOTEL_PACT_RUN_ID = 'r-auto-fail';
|
|
110
|
-
const Pact = makeFakePactV3();
|
|
111
|
-
installAutoWrap({
|
|
112
|
-
PactV3: Pact as unknown as { prototype: Record<string | symbol, unknown> },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const pact = new Pact();
|
|
116
|
-
pact.addInteraction({ uponReceiving: 'broken' });
|
|
117
|
-
|
|
118
|
-
await expect(
|
|
119
|
-
pact.executeTest(async () => {
|
|
120
|
-
throw new Error('assertion failed');
|
|
121
|
-
}),
|
|
122
|
-
).rejects.toThrow('assertion failed');
|
|
123
|
-
|
|
124
|
-
const entries = readLedger({ runId: 'r-auto-fail' });
|
|
125
|
-
expect(entries[0]).toMatchObject({ outcome: 'failed', error: 'assertion failed' });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('is idempotent — installing twice on the same prototype does not double-wrap', async () => {
|
|
129
|
-
process.env.AUTOTEL_PACT_RUN_ID = 'r-auto-idem';
|
|
130
|
-
const Pact = makeFakeMessagePact();
|
|
131
|
-
installAutoWrap({
|
|
132
|
-
MessageConsumerPact: Pact as unknown as { prototype: Record<string | symbol, unknown> },
|
|
133
|
-
});
|
|
134
|
-
installAutoWrap({
|
|
135
|
-
MessageConsumerPact: Pact as unknown as { prototype: Record<string | symbol, unknown> },
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const pact = new Pact();
|
|
139
|
-
await pact.verify(async () => {});
|
|
140
|
-
|
|
141
|
-
const entries = readLedger({ runId: 'r-auto-idem' });
|
|
142
|
-
expect(entries).toHaveLength(1); // not 2
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('skips patching when no recognised ctors are present in the module', () => {
|
|
146
|
-
// Empty module is a successful no-op — nothing crashes, nothing patched.
|
|
147
|
-
expect(installAutoWrap({})).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
});
|
package/src/auto-wrap.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-wrap entry for vitest / jest setup files.
|
|
3
|
-
*
|
|
4
|
-
* Importing this module monkey-patches `@pact-foundation/pact`'s
|
|
5
|
-
* `MessageConsumerPact.prototype.verify` and `PactV3.prototype.executeTest`
|
|
6
|
-
* so every contract test in the project records a ledger entry without
|
|
7
|
-
* users having to wrap each call by hand.
|
|
8
|
-
*
|
|
9
|
-
* Usage (vitest):
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* // vitest.config.ts
|
|
13
|
-
* import { defineConfig } from 'vitest/config';
|
|
14
|
-
* export default defineConfig({
|
|
15
|
-
* test: { setupFiles: ['autotel-pact/auto-wrap'] },
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* Usage (jest):
|
|
20
|
-
*
|
|
21
|
-
* ```js
|
|
22
|
-
* // jest.config.js
|
|
23
|
-
* module.exports = { setupFilesAfterEach: ['autotel-pact/auto-wrap'] };
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* Configuration is via environment variables — there's no API surface,
|
|
27
|
-
* because setup files don't get parameters:
|
|
28
|
-
*
|
|
29
|
-
* - `AUTOTEL_PACT_RUN_ID` — tag every ledger entry with this run id
|
|
30
|
-
* - `AUTOTEL_PACT_LEDGER_DIR` — override the default `.autotel-pact/` dir
|
|
31
|
-
*
|
|
32
|
-
* Idempotent: importing twice is a no-op.
|
|
33
|
-
*
|
|
34
|
-
* Safe when Pact-JS isn't installed: logs a single warning and exits
|
|
35
|
-
* cleanly. The same setup file can stay in place for a repo whose
|
|
36
|
-
* non-contract test packages don't have Pact as a dependency.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import { span as autotelSpan, getActiveSpan } from 'autotel';
|
|
40
|
-
import { createRequire } from 'node:module';
|
|
41
|
-
import { buildPactAttributes, outcomeAttribute } from './attrs.js';
|
|
42
|
-
import { appendLedgerEntry } from './ledger.js';
|
|
43
|
-
import { LEDGER_ENTRY_SPEC, type InteractionLedgerEntry, type PactInteractionMeta } from './types.js';
|
|
44
|
-
import type { HttpInteraction } from './wrapper-http.js';
|
|
45
|
-
import type { ReifiedMessage } from './wrapper.js';
|
|
46
|
-
|
|
47
|
-
const INSTALLED = Symbol.for('autotel-pact:auto-wrap-installed');
|
|
48
|
-
|
|
49
|
-
interface PactJsModule {
|
|
50
|
-
MessageConsumerPact?: { prototype: Record<string | symbol, unknown> };
|
|
51
|
-
PactV3?: { prototype: Record<string | symbol, unknown> };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Install the auto-wrap. Called at module load when this file is imported.
|
|
56
|
-
* Exposed for tests + explicit-invocation users.
|
|
57
|
-
*
|
|
58
|
-
* @param pactModule Override Pact-JS resolution. When omitted, requires
|
|
59
|
-
* `@pact-foundation/pact` from disk. Tests can pass synthetic classes.
|
|
60
|
-
*
|
|
61
|
-
* Returns `true` if Pact-JS was found and patched, `false` otherwise.
|
|
62
|
-
*/
|
|
63
|
-
export function installAutoWrap(pactModule?: PactJsModule): boolean {
|
|
64
|
-
const pactJs = pactModule ?? loadPactJs();
|
|
65
|
-
if (!pactJs) return false;
|
|
66
|
-
|
|
67
|
-
patchMessagePact(pactJs);
|
|
68
|
-
patchHttpPact(pactJs);
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function loadPactJs(): PactJsModule | undefined {
|
|
73
|
-
try {
|
|
74
|
-
// Use createRequire so we work from both ESM and CJS without
|
|
75
|
-
// pulling Pact-JS into the dynamic import graph.
|
|
76
|
-
const require = createRequire(import.meta.url);
|
|
77
|
-
return require('@pact-foundation/pact') as PactJsModule;
|
|
78
|
-
} catch {
|
|
79
|
-
process.stderr.write(
|
|
80
|
-
'autotel-pact/auto-wrap: @pact-foundation/pact not installed — skipping.\n',
|
|
81
|
-
);
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function patchMessagePact(mod: PactJsModule): void {
|
|
87
|
-
const ctor = mod.MessageConsumerPact;
|
|
88
|
-
if (!ctor) return;
|
|
89
|
-
const proto = ctor.prototype;
|
|
90
|
-
if (proto[INSTALLED]) return;
|
|
91
|
-
|
|
92
|
-
const originalVerify = proto.verify as (
|
|
93
|
-
handler: (m: ReifiedMessage) => Promise<unknown>,
|
|
94
|
-
) => Promise<unknown>;
|
|
95
|
-
|
|
96
|
-
proto.verify = async function patchedVerify(
|
|
97
|
-
this: { config: { consumer: string; provider: string } },
|
|
98
|
-
handler: (m: ReifiedMessage) => Promise<unknown>,
|
|
99
|
-
): Promise<unknown> {
|
|
100
|
-
const start = process.hrtime.bigint();
|
|
101
|
-
let captured: ReifiedMessage | undefined;
|
|
102
|
-
const consumer = this.config?.consumer ?? '<unknown>';
|
|
103
|
-
const provider = this.config?.provider ?? '<unknown>';
|
|
104
|
-
|
|
105
|
-
return autotelSpan('pact.interaction', async (span) => {
|
|
106
|
-
span.setAttributes({
|
|
107
|
-
'pact.consumer': consumer,
|
|
108
|
-
'pact.provider': provider,
|
|
109
|
-
'pact.kind': 'message',
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const result = await originalVerify.call(this, async (reified) => {
|
|
114
|
-
captured = reified;
|
|
115
|
-
const meta: PactInteractionMeta = {
|
|
116
|
-
consumer,
|
|
117
|
-
provider,
|
|
118
|
-
description: reified.description ?? '<unknown>',
|
|
119
|
-
states: (reified.providerStates ?? []).map((s) => s.name),
|
|
120
|
-
kind: 'message',
|
|
121
|
-
};
|
|
122
|
-
span.setAttributes(buildPactAttributes(meta));
|
|
123
|
-
return handler(reified);
|
|
124
|
-
});
|
|
125
|
-
span.setAttributes(outcomeAttribute('passed'));
|
|
126
|
-
writeAutoLedgerEntry({
|
|
127
|
-
consumer,
|
|
128
|
-
provider,
|
|
129
|
-
interaction: captured?.description ?? '<unknown>',
|
|
130
|
-
states: (captured?.providerStates ?? []).map((s) => s.name),
|
|
131
|
-
kind: 'message',
|
|
132
|
-
outcome: 'passed',
|
|
133
|
-
start,
|
|
134
|
-
});
|
|
135
|
-
return result;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
span.setAttributes(outcomeAttribute('failed'));
|
|
138
|
-
writeAutoLedgerEntry({
|
|
139
|
-
consumer,
|
|
140
|
-
provider,
|
|
141
|
-
interaction: captured?.description ?? '<unknown>',
|
|
142
|
-
states: (captured?.providerStates ?? []).map((s) => s.name),
|
|
143
|
-
kind: 'message',
|
|
144
|
-
outcome: 'failed',
|
|
145
|
-
start,
|
|
146
|
-
error: error instanceof Error ? error.message : String(error),
|
|
147
|
-
});
|
|
148
|
-
throw error;
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
proto[INSTALLED] = true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function patchHttpPact(mod: PactJsModule): void {
|
|
157
|
-
const ctor = mod.PactV3;
|
|
158
|
-
if (!ctor) return;
|
|
159
|
-
const proto = ctor.prototype;
|
|
160
|
-
if (proto[INSTALLED]) return;
|
|
161
|
-
|
|
162
|
-
// Track interactions added to each PactV3 instance. Cleared after
|
|
163
|
-
// executeTest emits ledger entries for them, so the same instance can
|
|
164
|
-
// be reused across multiple test cases.
|
|
165
|
-
const tracked = new WeakMap<object, HttpInteraction[]>();
|
|
166
|
-
|
|
167
|
-
const originalAdd = proto.addInteraction as (i: HttpInteraction) => unknown;
|
|
168
|
-
proto.addInteraction = function patchedAddInteraction(this: object, interaction: HttpInteraction) {
|
|
169
|
-
const list = tracked.get(this) ?? [];
|
|
170
|
-
list.push(interaction);
|
|
171
|
-
tracked.set(this, list);
|
|
172
|
-
return originalAdd.call(this, interaction);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const originalExecute = proto.executeTest as <T>(
|
|
176
|
-
fn: (server: { url: string; port: number }) => Promise<T>,
|
|
177
|
-
) => Promise<T | undefined>;
|
|
178
|
-
|
|
179
|
-
proto.executeTest = async function patchedExecuteTest<T>(
|
|
180
|
-
this: { opts?: { consumer?: string; provider?: string } },
|
|
181
|
-
fn: (server: { url: string; port: number }) => Promise<T>,
|
|
182
|
-
): Promise<T | undefined> {
|
|
183
|
-
const start = process.hrtime.bigint();
|
|
184
|
-
const consumer = this.opts?.consumer ?? '<unknown>';
|
|
185
|
-
const provider = this.opts?.provider ?? '<unknown>';
|
|
186
|
-
const interactions = tracked.get(this) ?? [];
|
|
187
|
-
tracked.set(this, []);
|
|
188
|
-
|
|
189
|
-
return autotelSpan('pact.interaction', async (span) => {
|
|
190
|
-
span.setAttributes({
|
|
191
|
-
'pact.consumer': consumer,
|
|
192
|
-
'pact.provider': provider,
|
|
193
|
-
'pact.kind': 'http',
|
|
194
|
-
});
|
|
195
|
-
// If multiple interactions were added before executeTest, the span
|
|
196
|
-
// attributes describe the first; ledger entries are still written
|
|
197
|
-
// for each. Most real tests use one interaction per executeTest.
|
|
198
|
-
const first = interactions[0];
|
|
199
|
-
if (first) {
|
|
200
|
-
span.setAttributes(
|
|
201
|
-
buildPactAttributes({
|
|
202
|
-
consumer,
|
|
203
|
-
provider,
|
|
204
|
-
description: first.uponReceiving,
|
|
205
|
-
states: (first.states ?? []).map((s) => s.description),
|
|
206
|
-
kind: 'http',
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const result = (await originalExecute.call(this, fn)) as T | undefined;
|
|
213
|
-
span.setAttributes(outcomeAttribute('passed'));
|
|
214
|
-
for (const i of interactions) {
|
|
215
|
-
writeAutoLedgerEntry({
|
|
216
|
-
consumer,
|
|
217
|
-
provider,
|
|
218
|
-
interaction: i.uponReceiving,
|
|
219
|
-
states: (i.states ?? []).map((s) => s.description),
|
|
220
|
-
kind: 'http',
|
|
221
|
-
outcome: 'passed',
|
|
222
|
-
start,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
226
|
-
} catch (error_) {
|
|
227
|
-
span.setAttributes(outcomeAttribute('failed'));
|
|
228
|
-
const error = error_ instanceof Error ? error_.message : String(error_);
|
|
229
|
-
for (const i of interactions) {
|
|
230
|
-
writeAutoLedgerEntry({
|
|
231
|
-
consumer,
|
|
232
|
-
provider,
|
|
233
|
-
interaction: i.uponReceiving,
|
|
234
|
-
states: (i.states ?? []).map((s) => s.description),
|
|
235
|
-
kind: 'http',
|
|
236
|
-
outcome: 'failed',
|
|
237
|
-
start,
|
|
238
|
-
error,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
throw error_;
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
proto[INSTALLED] = true;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function writeAutoLedgerEntry(args: {
|
|
250
|
-
consumer: string;
|
|
251
|
-
provider: string;
|
|
252
|
-
interaction: string;
|
|
253
|
-
states: string[];
|
|
254
|
-
kind: 'message' | 'http';
|
|
255
|
-
outcome: 'passed' | 'failed';
|
|
256
|
-
start: bigint;
|
|
257
|
-
error?: string;
|
|
258
|
-
}): void {
|
|
259
|
-
const ctx = getActiveSpan()?.spanContext();
|
|
260
|
-
const entry: InteractionLedgerEntry = {
|
|
261
|
-
type: 'interaction',
|
|
262
|
-
spec: LEDGER_ENTRY_SPEC,
|
|
263
|
-
consumer: args.consumer,
|
|
264
|
-
provider: args.provider,
|
|
265
|
-
interaction: args.interaction,
|
|
266
|
-
states: args.states,
|
|
267
|
-
kind: args.kind,
|
|
268
|
-
source: 'test',
|
|
269
|
-
role: 'consumer',
|
|
270
|
-
outcome: args.outcome,
|
|
271
|
-
duration_ms: Number(process.hrtime.bigint() - args.start) / 1e6,
|
|
272
|
-
observed_at: new Date().toISOString(),
|
|
273
|
-
trace_id: ctx?.traceId,
|
|
274
|
-
span_id: ctx?.spanId,
|
|
275
|
-
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
276
|
-
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA,
|
|
277
|
-
error: args.error,
|
|
278
|
-
};
|
|
279
|
-
appendLedgerEntry(entry);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Install on import — this file is meant to be loaded once as a setup file.
|
|
283
|
-
installAutoWrap();
|
package/src/broker.test.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
brokerConfigFromEnv,
|
|
4
|
-
fetchBrokerVerifications,
|
|
5
|
-
parseBrokerVerificationResult,
|
|
6
|
-
} from './broker.js';
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
vi.unstubAllGlobals();
|
|
10
|
-
vi.unstubAllEnvs();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('parseBrokerVerificationResult', () => {
|
|
14
|
-
it('parses success and verifiedAt', () => {
|
|
15
|
-
const r = parseBrokerVerificationResult('A', 'B', {
|
|
16
|
-
success: true,
|
|
17
|
-
verifiedAt: '2026-01-01T00:00:00Z',
|
|
18
|
-
});
|
|
19
|
-
expect(r).toEqual({
|
|
20
|
-
consumer: 'A',
|
|
21
|
-
provider: 'B',
|
|
22
|
-
success: true,
|
|
23
|
-
verifiedAt: '2026-01-01T00:00:00Z',
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('falls back to verified_at (snake_case) and createdAt', () => {
|
|
28
|
-
expect(
|
|
29
|
-
parseBrokerVerificationResult('A', 'B', { success: true, verified_at: '2026-01-02T00:00:00Z' }),
|
|
30
|
-
).toMatchObject({ verifiedAt: '2026-01-02T00:00:00Z' });
|
|
31
|
-
expect(
|
|
32
|
-
parseBrokerVerificationResult('A', 'B', { success: true, createdAt: '2026-01-03T00:00:00Z' }),
|
|
33
|
-
).toMatchObject({ verifiedAt: '2026-01-03T00:00:00Z' });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('infers success from result: "success" when success field missing', () => {
|
|
37
|
-
expect(
|
|
38
|
-
parseBrokerVerificationResult('A', 'B', { result: 'success' }),
|
|
39
|
-
).toMatchObject({ success: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('returns null for non-object input', () => {
|
|
43
|
-
expect(parseBrokerVerificationResult('A', 'B', null)).toBeNull();
|
|
44
|
-
expect(parseBrokerVerificationResult('A', 'B', 'string')).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('fetchBrokerVerifications', () => {
|
|
49
|
-
it('fetches latest verification per pair with bearer auth', async () => {
|
|
50
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
51
|
-
ok: true,
|
|
52
|
-
json: async () => ({ success: true, verifiedAt: '2026-06-01T00:00:00Z' }),
|
|
53
|
-
});
|
|
54
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
55
|
-
|
|
56
|
-
const results = await fetchBrokerVerifications(
|
|
57
|
-
{ baseUrl: 'https://broker.example', token: 'tok' },
|
|
58
|
-
[{ consumer: 'A', provider: 'B' }],
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
expect(results[0]).toMatchObject({ consumer: 'A', provider: 'B', success: true });
|
|
62
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
63
|
-
'https://broker.example/pacts/provider/B/consumer/A/latest/verification-results',
|
|
64
|
-
expect.objectContaining({
|
|
65
|
-
headers: expect.objectContaining({ Authorization: 'Bearer tok' }),
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('uses basic auth when username + password supplied', async () => {
|
|
71
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
72
|
-
ok: true,
|
|
73
|
-
json: async () => ({ success: true }),
|
|
74
|
-
});
|
|
75
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
76
|
-
|
|
77
|
-
await fetchBrokerVerifications(
|
|
78
|
-
{ baseUrl: 'https://b.example', username: 'u', password: 'p' },
|
|
79
|
-
[{ consumer: 'A', provider: 'B' }],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const expectedAuth = `Basic ${Buffer.from('u:p').toString('base64')}`;
|
|
83
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
84
|
-
expect.any(String),
|
|
85
|
-
expect.objectContaining({
|
|
86
|
-
headers: expect.objectContaining({ Authorization: expectedAuth }),
|
|
87
|
-
}),
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('records error and success:false on non-2xx response', async () => {
|
|
92
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
93
|
-
ok: false,
|
|
94
|
-
status: 404,
|
|
95
|
-
statusText: 'Not Found',
|
|
96
|
-
json: async () => ({}),
|
|
97
|
-
});
|
|
98
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
99
|
-
|
|
100
|
-
const [result] = await fetchBrokerVerifications(
|
|
101
|
-
{ baseUrl: 'https://b.example' },
|
|
102
|
-
[{ consumer: 'A', provider: 'B' }],
|
|
103
|
-
);
|
|
104
|
-
expect(result).toMatchObject({
|
|
105
|
-
consumer: 'A',
|
|
106
|
-
provider: 'B',
|
|
107
|
-
success: false,
|
|
108
|
-
error: 'HTTP 404 Not Found',
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('records error and success:false on network failure', async () => {
|
|
113
|
-
const fetchMock = vi.fn().mockRejectedValue(new Error('ENOTFOUND broker.example'));
|
|
114
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
115
|
-
|
|
116
|
-
const [result] = await fetchBrokerVerifications(
|
|
117
|
-
{ baseUrl: 'https://b.example' },
|
|
118
|
-
[{ consumer: 'A', provider: 'B' }],
|
|
119
|
-
);
|
|
120
|
-
expect(result).toMatchObject({
|
|
121
|
-
consumer: 'A',
|
|
122
|
-
provider: 'B',
|
|
123
|
-
success: false,
|
|
124
|
-
error: 'ENOTFOUND broker.example',
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('handles multiple pairs and trims trailing slash on base url', async () => {
|
|
129
|
-
const calls: string[] = [];
|
|
130
|
-
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
131
|
-
calls.push(url);
|
|
132
|
-
return Promise.resolve({
|
|
133
|
-
ok: true,
|
|
134
|
-
json: async () => ({ success: true }),
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
138
|
-
|
|
139
|
-
const results = await fetchBrokerVerifications(
|
|
140
|
-
{ baseUrl: 'https://b.example/' },
|
|
141
|
-
[
|
|
142
|
-
{ consumer: 'A', provider: 'B' },
|
|
143
|
-
{ consumer: 'C', provider: 'D' },
|
|
144
|
-
],
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
expect(results).toHaveLength(2);
|
|
148
|
-
expect(calls[0]).toBe(
|
|
149
|
-
'https://b.example/pacts/provider/B/consumer/A/latest/verification-results',
|
|
150
|
-
);
|
|
151
|
-
expect(calls[1]).toBe(
|
|
152
|
-
'https://b.example/pacts/provider/D/consumer/C/latest/verification-results',
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('brokerConfigFromEnv', () => {
|
|
158
|
-
it('returns undefined when PACT_BROKER_BASE_URL is unset', () => {
|
|
159
|
-
vi.stubEnv('PACT_BROKER_BASE_URL', '');
|
|
160
|
-
expect(brokerConfigFromEnv()).toBeUndefined();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('reads baseUrl, token, and basic auth from env', () => {
|
|
164
|
-
vi.stubEnv('PACT_BROKER_BASE_URL', 'https://b.example');
|
|
165
|
-
vi.stubEnv('PACT_BROKER_TOKEN', 'tok');
|
|
166
|
-
vi.stubEnv('PACT_BROKER_USERNAME', 'u');
|
|
167
|
-
vi.stubEnv('PACT_BROKER_PASSWORD', 'p');
|
|
168
|
-
expect(brokerConfigFromEnv()).toEqual({
|
|
169
|
-
baseUrl: 'https://b.example',
|
|
170
|
-
token: 'tok',
|
|
171
|
-
username: 'u',
|
|
172
|
-
password: 'p',
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
});
|