Skip to content

Commit 7f87896

Browse files
committed
feat: support V4 Pact interface (beta)
Creates a unified ConsumerPact interface for consumer tests and provider verification. It also supports plugins. This means you can create messages and HTTP interactions in the same test through the same entrypoint, and conversely verify interactions for multiple interaction types, including custom transports. NOTE: this feature is currently in beta and is only usable behind a feature toggle. It can be enabled by setting the environment variable ENABLE_FEATURE_V4 to any value.
1 parent 688a124 commit 7f87896

25 files changed

+2899
-4273
lines changed

.eslintrc.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
"import/prefer-default-export": "off",
3232
"no-underscore-dangle": ["error", { "allow": ["__pactMessageMetadata"] }],
3333
"class-methods-use-this": "off",
34-
"no-use-before-define": "off"
34+
"no-use-before-define": "off",
35+
"no-empty-function": [
36+
"error",
37+
{ "allow": ["constructors"] }
38+
]
3539
},
3640
"overrides": [
3741
{

.github/workflows/build-and-test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ on:
88
env:
99
GIT_COMMIT: ${{ github.sha }}
1010
GIT_REF: ${{ github.ref }}
11+
ENABLE_FEATURE_V4: true
12+
LOG_LEVEL: info
1113

1214
jobs:
1315
build-and-test-ubuntu:
@@ -52,7 +54,7 @@ jobs:
5254
env:
5355
NODE_VERSION: ${{ matrix.node-version }}
5456

55-
# Failures not reproducible locally on an M1 Mac
57+
# Failures not reproducible locally on an M1 Mac
5658
# build-and-test-macos:
5759
# runs-on: macos-latest
5860
# strategy:

package-lock.json

Lines changed: 1242 additions & 4204 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"postdist": "npm test",
1818
"predist": "npm run clean && npm run format:check && npm run lint",
1919
"release": "standard-version",
20-
"test": "nyc --check-coverage --reporter=html --reporter=text-summary mocha"
20+
"test": "nyc --check-coverage --reporter=html --reporter=text-summary mocha -t 120000"
2121
},
2222
"repository": {
2323
"type": "git",
@@ -94,7 +94,7 @@
9494
]
9595
},
9696
"dependencies": {
97-
"@pact-foundation/pact-core": "^13.7.8",
97+
"@pact-foundation/pact-core": "^13.12.0",
9898
"@types/bluebird": "^3.5.20",
9999
"@types/express": "^4.17.11",
100100
"axios": "^0.27.2",

src/dsl/matchers.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
2+
/* eslint-disable @typescript-eslint/no-unused-vars,no-unused-vars */
23
import { expect } from 'chai';
34
import {
45
boolean,
@@ -61,7 +62,6 @@ describe('Matcher', () => {
6162
},
6263
};
6364

64-
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
6565
const a: AnyTemplate = like(template);
6666
});
6767
});
@@ -77,13 +77,11 @@ describe('Matcher', () => {
7777
},
7878
};
7979

80-
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
8180
const a: AnyTemplate = like(template);
8281
});
8382
});
8483

8584
it('compiles nested likes', () => {
86-
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
8785
const a: AnyTemplate = like({
8886
someArray: ['one', 'two'],
8987
someNumber: like(1),
@@ -200,6 +198,7 @@ describe('Matcher', () => {
200198
describe('when an invalid value is provided', () => {
201199
it('throws an Error', () => {
202200
expect(createTheValue(undefined)).to.throw(Error);
201+
// eslint-disable-next-line no-empty-function
203202
expect(createTheValue(() => {})).to.throw(Error);
204203
});
205204
});

src/dsl/verifier/proxy/messages.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import logger from '../../../common/logger';
2+
import {
3+
MessageDescriptor,
4+
MessageFromProviderWithMetadata,
5+
MessageProvider,
6+
} from '../../message';
7+
import express from 'express';
8+
import bodyParser from 'body-parser';
9+
import { encode as encodeBase64 } from 'js-base64';
10+
import { ProxyOptions } from './types';
11+
12+
// Find a provider message handler, and invoke it
13+
export const findMessageHandler = (
14+
message: MessageDescriptor,
15+
config: ProxyOptions
16+
): Promise<MessageProvider> => {
17+
const handler = config.messageProviders
18+
? config.messageProviders[message.description]
19+
: undefined;
20+
21+
if (!handler) {
22+
logger.error(`no handler found for message ${message.description}`);
23+
24+
return Promise.reject(
25+
new Error(
26+
`No handler found for message "${message.description}".
27+
Check your "handlers" configuration`
28+
)
29+
);
30+
}
31+
32+
return Promise.resolve(handler);
33+
};
34+
35+
// Get the Express app that will run on the HTTP Proxy
36+
export const setupMessageProxyApplication = (
37+
config: ProxyOptions
38+
): express.Express => {
39+
const app = express();
40+
41+
app.use(bodyParser.json());
42+
app.use(bodyParser.urlencoded({ extended: true }));
43+
app.use((_, res, next) => {
44+
// TODO: this seems to override the metadata for content-type
45+
res.header('Content-Type', 'application/json; charset=utf-8');
46+
next();
47+
});
48+
49+
// Proxy server will respond to Verifier process
50+
app.all('/*', createProxyMessageHandler(config));
51+
52+
return app;
53+
};
54+
55+
// Get the API handler for the verification CLI process to invoke on POST /*
56+
export const createProxyMessageHandler = (
57+
config: ProxyOptions
58+
): ((req: express.Request, res: express.Response) => void) => {
59+
return (req, res) => {
60+
const message: MessageDescriptor = req.body;
61+
62+
// Invoke the handler, and return the JSON response body
63+
// wrapped in a Message
64+
findMessageHandler(message, config)
65+
.then((handler) => handler(message))
66+
.then((messageFromHandler) => {
67+
if (hasMetadata(messageFromHandler)) {
68+
const metadata = encodeBase64(
69+
JSON.stringify(messageFromHandler.__pactMessageMetadata)
70+
);
71+
res.header('Pact-Message-Metadata', metadata);
72+
res.header('PACT_MESSAGE_METADATA', metadata);
73+
74+
return res.json(messageFromHandler.message);
75+
}
76+
return res.json(messageFromHandler);
77+
})
78+
.catch((e) => res.status(500).send(e));
79+
};
80+
};
81+
82+
// // Get the Proxy we'll pass to the CLI for verification
83+
// export const setupProxyServer = (
84+
// app: (request: http.IncomingMessage, response: http.ServerResponse) => void
85+
// ): http.Server => http.createServer(app).listen();
86+
87+
const hasMetadata = (
88+
o: unknown | MessageFromProviderWithMetadata
89+
): o is MessageFromProviderWithMetadata =>
90+
Boolean((o as MessageFromProviderWithMetadata).__pactMessageMetadata);
91+
92+
export const providerWithMetadata =
93+
(
94+
provider: MessageProvider,
95+
metadata: Record<string, string>
96+
): MessageProvider =>
97+
(descriptor: MessageDescriptor) =>
98+
Promise.resolve(provider(descriptor)).then((message) =>
99+
hasMetadata(message)
100+
? {
101+
__pactMessageMetadata: {
102+
...message.__pactMessageMetadata,
103+
...metadata,
104+
},
105+
message,
106+
}
107+
: { __pactMessageMetadata: metadata, message }
108+
);

src/dsl/verifier/proxy/proxy.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createProxyStateHandler } from './stateHandler/stateHandler';
99
import { registerAfterHook, registerBeforeHook } from './hooks';
1010
import { createRequestTracer, createResponseTracer } from './tracer';
1111
import { parseBody } from './parseBody';
12+
import { createProxyMessageHandler } from './messages';
1213

1314
// Listens for the server start event
1415
export const waitForServerReady = (server: http.Server): Promise<http.Server> =>
@@ -22,7 +23,8 @@ export const waitForServerReady = (server: http.Server): Promise<http.Server> =>
2223
// Get the Proxy we'll pass to the CLI for verification
2324
export const createProxy = (
2425
config: ProxyOptions,
25-
stateSetupPath: string
26+
stateSetupPath: string,
27+
messageTransportPath: string
2628
): http.Server => {
2729
const app = express();
2830
const proxy = new HttpProxy();
@@ -59,18 +61,32 @@ export const createProxy = (
5961
// Setup provider state handler
6062
app.post(stateSetupPath, createProxyStateHandler(config));
6163

64+
// Register message handler and transport
65+
// TODO: ensure proxy does not interfere with this
66+
app.post(messageTransportPath, createProxyMessageHandler(config));
67+
6268
// Proxy server will respond to Verifier process
6369
app.all('/*', (req, res) => {
6470
logger.debug(`Proxying ${req.method}: ${req.path}`);
6571

6672
proxy.web(req, res, {
6773
changeOrigin: config.changeOrigin === true,
6874
secure: config.validateSSL === true,
69-
target: config.providerBaseUrl,
75+
target: config.providerBaseUrl || defaultBaseURL(),
7076
});
7177
});
7278

7379
proxy.on('proxyReq', (proxyReq, req) => parseBody(proxyReq, req));
7480

75-
return http.createServer(app).listen();
81+
// TODO: node is now using ipv6 as a default. This should be customised
82+
return http
83+
.createServer(app)
84+
.listen(undefined, config.proxyHost || '127.0.0.1');
85+
};
86+
87+
// A base URL is always needed for the proxy, even
88+
// if there are no targets to proxy (e.g. in the case
89+
// of message pact
90+
const defaultBaseURL = () => {
91+
return 'http://127.0.0.1/';
7692
};

src/dsl/verifier/proxy/stateHandler/stateHandler.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('#createProxyStateHandler', () => {
1818
status: (status: number) => {
1919
res = status;
2020
return {
21+
// eslint-disable-next-line no-empty-function
2122
send: () => {},
2223
};
2324
},

src/dsl/verifier/proxy/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as express from 'express';
22
import { LogLevel } from '../../options';
33
import { JsonMap, AnyJson } from '../../../common/jsonTypes';
4+
import { MessageProviders } from 'dsl/message';
45

56
export type Hook = () => Promise<unknown>;
67

@@ -46,10 +47,11 @@ export interface ProxyOptions {
4647
logLevel?: LogLevel;
4748
requestFilter?: express.RequestHandler;
4849
stateHandlers?: StateHandlers;
50+
messageProviders?: MessageProviders;
4951
beforeEach?: Hook;
5052
afterEach?: Hook;
5153
validateSSL?: boolean;
5254
changeOrigin?: boolean;
53-
providerBaseUrl: string;
55+
providerBaseUrl?: string;
5456
proxyHost?: string;
5557
}

src/dsl/verifier/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
import { VerifierOptions as PactCoreVerifierOptions } from '@pact-foundation/pact-core';
2+
import { MessageProviderOptions } from 'dsl/options';
23

34
import { ProxyOptions } from './proxy/types';
45

5-
export type VerifierOptions = PactCoreVerifierOptions & ProxyOptions;
6+
type ExcludedPactNodeVerifierKeys = Exclude<
7+
keyof PactCoreVerifierOptions,
8+
'providerBaseUrl'
9+
>;
10+
11+
export type PactNodeVerificationExcludedOptions = Pick<
12+
PactCoreVerifierOptions,
13+
ExcludedPactNodeVerifierKeys
14+
>;
15+
16+
export type VerifierOptions = PactNodeVerificationExcludedOptions &
17+
ProxyOptions &
18+
Partial<MessageProviderOptions>;

0 commit comments

Comments
 (0)