Skip to content

Commit

Permalink
feat: support HTTP + non-HTTP in ConsumerPact
Browse files Browse the repository at this point in the history
Prior to version 4 of the Pact specification,
contracts could not contain mixed interaction types
(messages or HTTP interactions). With V4, they now can.

It makes sense to support DSLs behind a unified interface, and having
two entrypoints makes this difficult.

Furthermore, the FFI now unifies the Interaction and Pact types to
support this motion.

See https://github.com/pact-foundation/pact-specification/tree/version-4
  • Loading branch information
mefellows committed Nov 8, 2022
1 parent bbcdf6d commit 56786f2
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 29 deletions.
150 changes: 124 additions & 26 deletions src/consumer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,7 @@ export const makeConsumerPact = (
return ffi.pactffiMockServerMatched(port);
},
mockServerMismatches: (port: number): MatchingResult[] => {
const results: MatchingResult[] = JSON.parse(
ffi.pactffiMockServerMismatches(port)
);
return results.map((result: MatchingResult) => ({
...result,
...('mismatches' in result
? {
mismatches: result.mismatches.map((m: string | Mismatch) =>
typeof m === 'string' ? JSON.parse(m) : m
),
}
: {}),
}));
return mockServerMismatches(ffi, port);
},
cleanupMockServer: (port: number): boolean => {
return wrapWithCheck<(port: number) => boolean>(
Expand All @@ -113,9 +101,116 @@ export const makeConsumerPact = (
},
writePactFile: (dir: string, merge = true) =>
writePact(ffi, pactPtr, dir, merge),
writePactFileForPluginServer: (port: number, dir: string, merge = true) =>
writePact(ffi, pactPtr, dir, merge, port),
addMetadata: (namespace: string, name: string, value: string): boolean => {
return ffi.pactffiWithPactMetadata(pactPtr, namespace, name, value);
},
newAsynchronousMessage: (description: string): AsynchronousMessage => {
const interactionPtr = ffi.pactffiNewAsyncMessage(pactPtr, description);

return asyncMessage(ffi, interactionPtr);
},
newSynchronousMessage: (description: string): SynchronousMessage => {
// TODO: will this automatically set the correct spec version?
const interactionPtr = ffi.pactffiNewSyncMessage(pactPtr, description);

return {
withPluginRequestInteractionContents: (
contentType: string,
contents: string
) => {
ffi.pactffiPluginInteractionContents(
interactionPtr,
INTERACTION_PART_REQUEST,
contentType,
contents
);
return true;
},
withPluginResponseInteractionContents: (
contentType: string,
contents: string
) => {
ffi.pactffiPluginInteractionContents(
interactionPtr,
INTERACTION_PART_RESPONSE,
contentType,
contents
);
return true;
},
withPluginRequestResponseInteractionContents: (
contentType: string,
contents: string
) => {
ffi.pactffiPluginInteractionContents(
interactionPtr,
INTERACTION_PART_REQUEST,
contentType,
contents
);
return true;
},
given: (state: string) => {
return ffi.pactffiGiven(interactionPtr, state);
},
givenWithParam: (state: string, name: string, value: string) => {
return ffi.pactffiGivenWithParam(interactionPtr, state, name, value);
},
withRequestContents: (body: string, contentType: string) => {
return ffi.pactffiWithBody(
interactionPtr,
INTERACTION_PART_REQUEST,
contentType,
body
);
},
withResponseContents: (body: string, contentType: string) => {
return ffi.pactffiWithBody(
interactionPtr,
INTERACTION_PART_RESPONSE,
contentType,
body
);
},
withRequestBinaryContents: (body: Buffer, contentType: string) => {
return ffi.pactffiWithBinaryFile(
interactionPtr,
INTERACTION_PART_REQUEST,
contentType,
body,
body.length
);
},
withResponseBinaryContents: (body: Buffer, contentType: string) => {
return ffi.pactffiWithBinaryFile(
interactionPtr,
INTERACTION_PART_RESPONSE,
contentType,
body,
body.length
);
},
withMetadata: (name: string, value: string) => {
return ffi.pactffiMessageWithMetadata(interactionPtr, name, value);
},
};
},
pactffiCreateMockServerForTransport(
address: string,
transport: string,
config: string,
port?: number
) {
return ffi.pactffiCreateMockServerForTransport(
pactPtr,
address,
port || 0,
transport,
config
);
},
newInteraction: (description: string): ConsumerInteraction => {
const interactionPtr = ffi.pactffiNewInteraction(pactPtr, description);

Expand Down Expand Up @@ -421,25 +516,28 @@ export const makeConsumerMessagePact = (
return ffi.pactffiMockServerMatched(port);
},
mockServerMismatches: (port: number): MatchingResult[] => {
const results: MatchingResult[] = JSON.parse(
ffi.pactffiMockServerMismatches(port)
);
return results.map((result: MatchingResult) => ({
...result,
...('mismatches' in result
? {
mismatches: result.mismatches.map((m: string | Mismatch) =>
typeof m === 'string' ? JSON.parse(m) : m
),
}
: {}),
}));
return mockServerMismatches(ffi, port);
},
};
};

export const makeConsumerAsyncMessagePact = makeConsumerMessagePact;

const mockServerMismatches = (ffi: Ffi, port: number) => {
const results: MatchingResult[] = JSON.parse(
ffi.pactffiMockServerMismatches(port)
);
return results.map((result: MatchingResult) => ({
...result,
...('mismatches' in result
? {
mismatches: result.mismatches.map((m: string | Mismatch) =>
typeof m === 'string' ? JSON.parse(m) : m
),
}
: {}),
}));
};
const writePact = (
ffi: Ffi,
pactPtr: FfiPactHandle,
Expand Down
22 changes: 22 additions & 0 deletions src/consumer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export type ConsumerInteraction = PluginInteraction & {

export type ConsumerPact = PluginPact & {
newInteraction: (description: string) => ConsumerInteraction;
newAsynchronousMessage: (description: string) => AsynchronousMessage;
newSynchronousMessage: (description: string) => SynchronousMessage;
pactffiCreateMockServerForTransport: (
address: string,
transport: string,
config: string,
port?: number
) => number;
createMockServer: (address: string, port?: number, tls?: boolean) => number;
mockServerMismatches: (port: number) => MatchingResult[];
cleanupMockServer: (port: number) => boolean;
Expand All @@ -180,6 +188,20 @@ export type ConsumerPact = PluginPact & {
* @param merge whether or not to merge the pact file contents (default true)
*/
writePactFile: (dir: string, merge?: boolean) => void;
/**
* This function writes the pact file, using the given plugin transport port.
* If you are using plugins in your test, you must use this method
*
* @param port The port that identifies the custom mock server
* @param dir The directory to write the pact file to
* @param merge whether or not to merge the pact file contents with previous test runs (default true)
* @returns
*/
writePactFileForPluginServer: (
port: number,
dir: string,
merge?: boolean
) => void;
/**
* Check if a mock server has matched all its requests.
*
Expand Down
10 changes: 7 additions & 3 deletions test/matt.consumer.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ describe.skip('MATT protocol test', () => {
const mattRequest = `{"request": {"body": "hello"}}`;
const mattResponse = `{"response":{"body":"world"}}`;

provider = makeConsumerPact('matt-consumer', 'matt-provider');
provider.addPlugin('matt', '0.0.1');
provider = makeConsumerPact(
'matt-consumer',
'matt-provider',
FfiSpecificationVersion.SPECIFICATION_VERSION_V4
);
provider.addPlugin('matt', '0.0.2');

const interaction = provider.newInteraction('');
interaction.uponReceiving('A request to communicate via MATT');
Expand Down Expand Up @@ -101,7 +105,7 @@ describe.skip('MATT protocol test', () => {

beforeEach(() => {
const mattMessage = `{"request": {"body": "hellotcp"}, "response":{"body":"tcpworld"}}`;
tcpProvider.addPlugin('matt', '0.0.1');
tcpProvider.addPlugin('matt', '0.0.2');

const message = tcpProvider.newSynchronousMessage('a MATT message');
message.withPluginRequestResponseInteractionContents(
Expand Down

0 comments on commit 56786f2

Please sign in to comment.