Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/v4/consumer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { LogLevel } from '@pact-foundation/pact-core';
import axios from 'axios';
import { SpecificationVersion, MatchersV3 } from '../v3';
import { PactV4 } from '.';

chai.use(chaiAsPromised);

const { expect } = chai;

process.env.ENABLE_FEATURE_V4 = 'true';

describe('V4ConsumerPact', () => {
const pact = new PactV4({
consumer: 'mymessageconsumer',
provider: 'mymessageprovider',
spec: SpecificationVersion.SPECIFICATION_VERSION_V4,
logLevel: (process.env.LOG_LEVEL as LogLevel) || 'trace',
});

describe('httpInteraction', () => {
it('passes', async () => {
await pact
.addInteraction()
.uponReceiving('a request only checks the keys and ignores the values')
.withRequest('GET', '/')
.willRespondWith(200, (builder) => {
builder.jsonBody({
key1: MatchersV3.string("a string we don't care about"),
key2: 1,
});
})
.executeTest((mockserver) =>
axios
.request({
baseURL: mockserver.url,
method: 'GET',
url: '/',
})
.then((res) => {
expect(res.data.key1).to.equal("a string we don't care about");
expect(res.data.key2).to.equal(1);
})
);
});
});

describe('synchronousMessageInteraction', () => {
it('passes', async () => {
await pact
.addSynchronousInteraction('a synchronous message')
.given('a test')
.withRequest((builder) => {
builder.withJSONContent({
key1: MatchersV3.string('request string'),
key2: 1,
});
})
.withResponse((builder) => {
builder.withJSONContent({
key1: MatchersV3.string('response string'),
key2: 1,
});
})
.executeTest(async () => {});
});
});

describe('asynchronousMessageInteraction', () => {
it('passes', () => {
pact
.addAsynchronousInteraction('an asynchronous message')
.given('a test')
.expectsToReceive('an asynchronous message')
.withContents({
messageId: '1234',
messageBody: {
value: 'expected',
},
})
.executeTest(async (message) => {
expect(message).to.deep.equal({
messageId: '1234',
messageBody: {
value: 'expected',
},
});
});
});
});
});
23 changes: 21 additions & 2 deletions src/v4/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { UnconfiguredInteraction } from './http';
import { PactV4Options, V4UnconfiguredInteraction } from './http/types';
import { V4ConsumerPact } from './types';
import { version as pactPackageVersion } from '../../package.json';
import { V4UnconfiguredSynchronousMessage } from './message/types';
import { UnconfiguredSynchronousMessage } from './message';
import {
V4UnconfiguredSynchronousMessage,
V4UnconfiguredAsynchronousMessage,
} from './message/types';
import {
UnconfiguredSynchronousMessage,
UnconfiguredAsynchronousMessage,
} from './message';
import { SpecificationVersion } from '../v3';

export class PactV4 implements V4ConsumerPact {
Expand Down Expand Up @@ -53,4 +59,17 @@ export class PactV4 implements V4ConsumerPact {
}
);
}

addAsynchronousInteraction(
description: string
): V4UnconfiguredAsynchronousMessage {
return new UnconfiguredAsynchronousMessage(
this.pact,
this.pact.newAsynchronousMessage(description),
this.opts,
() => {
this.setup();
}
);
}
}
144 changes: 143 additions & 1 deletion src/v4/message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import {
V4SynchronousMessageWithResponseBuilder,
V4SynchronousMessageWithTransport,
V4UnconfiguredSynchronousMessage,
V4AsynchronousMessage,
V4AsynchronousMessageWithContents,
V4UnconfiguredAsynchronousMessage,
} from './types';
import {
AsynchronousMessage as PactCoreAsynchronousMessage,
SynchronousMessage as PactCoreSynchronousMessage,
ConsumerPact,
} from '@pact-foundation/pact-core';
Expand All @@ -28,7 +32,10 @@ import {
generateMockServerError,
} from '../../v3/display';
import logger from '../../common/logger';
import { isMatcher as isV3Matcher } from '../../v3/matchers';
import {
isMatcher as isV3Matcher,
matcherValueOrString,
} from '../../v3/matchers';

const defaultPactDir = './pacts';

Expand Down Expand Up @@ -375,6 +382,51 @@ const cleanup = (
cleanupFn();
};

type ReifiedMessage = {
contents: {
content: unknown | null;
contentType: string;
encoded: boolean;
};
description: string;
pending: boolean;
providerStates: Array<{ name: string; params?: JsonMap }>;
};

const executeAsyncMessageTest = async <T>(
pact: ConsumerPact,
interaction: PactCoreAsynchronousMessage,
opts: PactV4Options,
integrationTest: (content: unknown, contentType: string, encoded: boolean) => Promise<T>,
cleanupFn: () => void
): Promise<T | undefined> => {
let val: T | undefined;
let error: Error | undefined;

try {
const rawInteraction: ReifiedMessage = JSON.parse(
interaction.reifyMessage()
);
const { content, contentType, encoded } = rawInteraction.contents;
val = await integrationTest(content, contentType, encoded);
} catch (e) {
error = e;
console.error(`Error: ${e}`);
}

// Scenario: test threw an error, but Pact validation was OK (error in client or test)
if (error) {
cleanup(false, pact, opts, cleanupFn);

throw error;
}

// Scenario: Pact validation passed, test didn't throw - return the callback value
cleanup(true, pact, opts, cleanupFn);

return val;
};

const executeNonTransportTest = async <T>(
pact: ConsumerPact,
opts: PactV4Options,
Expand Down Expand Up @@ -402,3 +454,93 @@ const executeNonTransportTest = async <T>(

return val;
};

// ASYNCHRONOUS

export class UnconfiguredAsynchronousMessage
implements V4UnconfiguredAsynchronousMessage
{
constructor(
protected pact: ConsumerPact,
protected interaction: PactCoreAsynchronousMessage,
protected opts: PactV4Options,
protected cleanupFn: () => void
) {}

given(
state: string,
parameters?: JsonMap
): V4UnconfiguredAsynchronousMessage {
if (parameters) {
this.interaction.givenWithParams(state, JSON.stringify(parameters));
} else {
this.interaction.given(state);
}

return this;
}

usingPlugin(config: PluginConfig): V4UnconfiguredAsynchronousMessage {
this.pact.addPlugin(config.plugin, config.version);

return this;
}

expectsToReceive(description: string): V4AsynchronousMessage {
this.interaction.expectsToReceive(description);

return new AsynchronousMessage(
this.pact,
this.interaction,
this.opts,
this.cleanupFn
);
}
}

export class AsynchronousMessage implements V4AsynchronousMessage {
constructor(
protected pact: ConsumerPact,
protected interaction: PactCoreAsynchronousMessage,
protected opts: PactV4Options,
protected cleanupFn: () => void
) {}

withContents(contents: unknown): V4AsynchronousMessageWithContents {
this.interaction.withContents(
matcherValueOrString(contents),
'application/json'
);

return new AsynchronousMessageWithContents(
this.pact,
this.interaction,
this.opts,
this.cleanupFn
);
}
}

export class AsynchronousMessageWithContents
implements V4AsynchronousMessageWithContents
{
constructor(
protected pact: ConsumerPact,
protected interaction: PactCoreAsynchronousMessage,
protected opts: PactV4Options,
protected cleanupFn: () => void
) {}

executeTest<T>(
integrationTest: (m: unknown) => Promise<T>
): Promise<T | undefined> {
return executeAsyncMessageTest(
this.pact,
this.interaction,
this.opts,
integrationTest,
this.cleanupFn
);
}
}

18 changes: 18 additions & 0 deletions src/v4/message/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,21 @@ export interface V4SynchronousMessageWithResponse {
integrationTest: (m: SynchronousMessage) => Promise<T>
): Promise<T | undefined>;
}

// ASYNCHRONOUS

export interface V4UnconfiguredAsynchronousMessage {
given(state: string, parameters?: JsonMap): V4UnconfiguredAsynchronousMessage;
usingPlugin(config: PluginConfig): V4UnconfiguredAsynchronousMessage;
expectsToReceive(description: string): V4AsynchronousMessage;
}

export interface V4AsynchronousMessage {
withContents(contents: unknown): V4AsynchronousMessageWithContents;
}

export interface V4AsynchronousMessageWithContents {
executeTest<T>(
integrationTest: (m: unknown) => Promise<T>
): Promise<T | undefined>;
}
8 changes: 7 additions & 1 deletion src/v4/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { V4UnconfiguredInteraction } from './http/types';
import { V4UnconfiguredSynchronousMessage } from './message/types';
import {
V4UnconfiguredSynchronousMessage,
V4UnconfiguredAsynchronousMessage,
} from './message/types';

export interface V4ConsumerPact {
addInteraction(): V4UnconfiguredInteraction;
addSynchronousInteraction(
description: string
): V4UnconfiguredSynchronousMessage;
addAsynchronousInteraction(
description: string
): V4UnconfiguredAsynchronousMessage;
}