Skip to content

Commit

Permalink
feat(messages): add Message Pact support
Browse files Browse the repository at this point in the history
- Adds support for invoking the pact-message binary from the
latest standalone package
- Upgrades standalone to 1.33.1
  • Loading branch information
mefellows committed Mar 27, 2018
1 parent b2439f0 commit 980a3f5
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 0 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ An idiomatic Node interface for the [Pact](http://pact.io) mock service (Consume
- [Pact Broker Publishing](#pact-broker-publishing)
- [Stub Servers](#stub-servers)
- [Create Stub Server](#create-stub-server)
- [Message Pacts](#message-pacts)
- [Create Message Pacts](#create-message-pacts)
- [Example](#example)
- [Example CLI invocation:](#example-cli-invocation)
- [Contributing](#contributing)
- [Testing](#testing)
- [Questions?](#questions)
Expand Down Expand Up @@ -263,6 +267,53 @@ var server = pact.createStub({
| cors | false | boolean | Allow CORS OPTION requests to be accepted, defaults to 'false'|


### Message Pacts
#### Create Message Pacts

```js
var pact = require('@pact-foundation/pact-node');
var server = pact.createMessage({
...
});
```
**Options**:

|Parameter | Required? | Type | Description |
|---------------------|-----------|--------------|-------------|
| `dir` | true | string | Directory to write the pact contracts relative to the current working directory, defaults to none |
| `consumer` | true | string | The name of the consumer to be written to the pact contracts, defaults to none |
| `provider` | false | string | The name of the provider to be written to the pact contracts, defaults to none |
| `pactFileWriteMode` | false | `"overwrite" | "update" | "merge"` | Control how the pact file is created. Defaults to "overwrite" |

##### Example

```js
const messageFactory = messageFactory({
consumer: "consumer",
provider: "provider",
dir: dirname(`${__filename}/pacts`),
content: `{
"description": "a test mesage",
"content": {
"name": "Mary"
}
}`
});

messageFactory.createMessage()
```

##### Example CLI invocation:

```sh
node ./bin/pact-cli.js message --pact-file-write-mode update --consumer foo --provider bar -d /tmp/pacts -c '{
"description": "a test mesage",
"content": {
"name": "Mary"
}
}'
```

## Contributing

To develop this project, simply install the dependencies and run `npm run watch` to for continual development, linting and testing when a source file changes.
Expand Down
8 changes: 8 additions & 0 deletions bin/pact-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ cli
.option("--provider <providerName>", "Specify provider name for written Pact files.")
.option("--monkeypatch <file>", "Absolute path to a Ruby file that will monkeypatch the underlying Pact mock.")
.action((args: any, options: any) => pact.createServer(options).start());
cli
.command("message", "Creates or updates a message pact file")
.option("-c, --content <c>", "JSON content (message representation) to add to the contract file.")
.option("-w, --pact-file-write-mode <m>", "Controls how pact files are written to disk. One of 'overwrite', 'update', 'merge'", /^overwrite|update|merge$/)
.option("-d, --dir <directory>", "Directory to which the pacts will be written.")
.option("--consumer <consumerName>", "Specify consumer name for written Pact files.")
.option("--provider <providerName>", "Specify provider name for written Pact files.")
.action((args: any, options: any) => pact.createMessage(options));

cli
.command("stub", "Creates an API stub from pact files")
Expand Down
73 changes: 73 additions & 0 deletions src/message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import chai = require("chai");
import chaiAsPromised = require("chai-as-promised");
import messageFactory from "./message";
import { dirname } from "path";
const path = require("path");
const expect = chai.expect;
chai.use(chaiAsPromised);

describe("Message Spec", () => {
const currentDir = (process && process.mainModule) ? path.dirname(process.mainModule.filename) : "";
const validJSON = `{ "description": "a test mesage", "content": { "name": "Mary" } }`;

context("when not given any message content", () => {
it("should throw an Error", () => {
expect(() => messageFactory({
consumer: "a-consumer",
dir: currentDir
})).to.throw(Error);
});
});

context("when not given a consumer", () => {
it("should throw an Error", () => {
expect(() => messageFactory({
provider: "a-provider",
dir: currentDir,
content: validJSON
})).to.throw(Error);
});
});
context("when not given a provider", () => {
it("should throw an Error", () => {
expect(() => messageFactory({
consumer: "a-provider",
dir: currentDir,
content: validJSON
})).to.throw(Error);
});
});

context("when not given a pact dir", () => {
it("should throw an Error", () => {
expect(() => messageFactory({
consumer: "a-consumer",
content: validJSON
})).to.throw(Error);
});
});

context("when given an invalid JSON document", () => {
it("should throw an Error", () => {
expect(() => messageFactory({
consumer: "some-consumer",
provider: "a-provider",
dir: currentDir,
content: `{ "unparseable" }`
})).to.throw(Error);
});
});

context("when given the correct arguments", () => {
it("should return a message object", () => {
const message = messageFactory({
consumer: "some-consumer",
provider: "a-provider",
dir: currentDir,
content: validJSON
});
expect(message).to.be.a("object");
expect(message).to.respondTo("createMessage");
});
});
});
93 changes: 93 additions & 0 deletions src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import fs = require("fs");
import q = require("q");
import logger from "./logger";
import pactUtil, { DEFAULT_ARG, SpawnArguments } from "./pact-util";
import pactStandalone from "./pact-standalone";
import path = require("path");
const mkdirp = require("mkdirp");
const checkTypes = require("check-types");

export class Message {
public readonly options: MessageOptions;
private readonly __argMapping = {
"content": DEFAULT_ARG,
"pactFileWriteMode": DEFAULT_ARG,
"dir": "--pact_dir",
"consumer": "--consumer",
"provider": "--provider",
};

constructor(options: MessageOptions) {
options = options || {};
options.pactFileWriteMode = options.pactFileWriteMode || "update";

checkTypes.assert.nonEmptyString(options.consumer, "Must provide the consumer name");
checkTypes.assert.nonEmptyString(options.provider, "Must provide the provider name");
checkTypes.assert.nonEmptyString(options.content, "Must provide message content");
checkTypes.assert.nonEmptyString(options.dir, "Must provide pact output dir");

if (options.dir) {
try {
fs.statSync(path.normalize(options.dir)).isDirectory();
} catch (e) {
mkdirp.sync(path.normalize(options.dir));
}
}

if (options.content) {
try {
JSON.parse(options.content);
} catch (e) {
throw new Error("Unable to parse message content to JSON, invalid json supplied");
}
}

if (options.consumer) {
checkTypes.assert.string(options.consumer);
}

if (options.provider) {
checkTypes.assert.string(options.provider);
}

checkTypes.assert.includes(["overwrite", "update", "merge"], options.pactFileWriteMode);

if ((options.pactBrokerUsername && !options.pactBrokerPassword) || (options.pactBrokerPassword && !options.pactBrokerUsername)) {
throw new Error("Must provide both Pact Broker username and password. None needed if authentication on Broker is disabled.");
}

this.options = options;
}

public createMessage(): q.Promise<any> {
logger.info(`Creating message pact`);
const deferred = q.defer<any>();
const instance = pactUtil.spawnBinary(`${pactStandalone.messagePath}`, this.options, this.__argMapping);
const output: any[] = [];
instance.stdout.on("data", (l) => output.push(l));
instance.stderr.on("data", (l) => output.push(l));
instance.once("close", (code) => {
const o = output.join("\n");
logger.info(o);

if (code === 0) {
return deferred.resolve(o);
} else {
return deferred.reject(o);
}

});

return deferred.promise;
}
}

export default (options: MessageOptions) => new Message(options);

export interface MessageOptions extends SpawnArguments {
content?: string;
dir?: string;
consumer?: string;
provider?: string;
pactFileWriteMode?: "overwrite" | "update" | "merge";
}
5 changes: 5 additions & 0 deletions src/pact-standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface PactStandalone {
stubFullPath: string;
verifierPath: string;
verifierFullPath: string;
messagePath: string;
messageFullPath: string;
}

export function getPlatformFolderName(platform:string, arch:string) {
Expand All @@ -24,6 +26,7 @@ export const standalone = (platform?: string, arch?: string): PactStandalone =>
arch = arch || process.arch;
const binName = (name: string) => `${name}${platform === "win32" ? ".bat" : ""}`;
const mock = binName("pact-mock-service");
const message = binName("pact-message");
const verify = binName("pact-provider-verifier");
const broker = binName("pact-broker");
const stub = binName("pact-stub-service");
Expand All @@ -33,6 +36,8 @@ export const standalone = (platform?: string, arch?: string): PactStandalone =>
cwd: cwd,
brokerPath: path.join(basePath, broker),
brokerFullPath: path.resolve(cwd, basePath, broker).trim(),
messagePath: path.join(basePath, message),
messageFullPath: path.resolve(cwd, basePath, message).trim(),
mockServicePath: path.join(basePath, mock),
mockServiceFullPath: path.resolve(cwd, basePath, mock).trim(),
stubPath: path.join(basePath, stub),
Expand Down
7 changes: 7 additions & 0 deletions src/pact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as q from "q";
import serverFactory, {Server, ServerOptions} from "./server";
import stubFactory, {Stub, StubOptions} from "./stub";
import verifierFactory, {VerifierOptions} from "./verifier";
import messageFactory, {MessageOptions} from "./message";
import publisherFactory, {PublisherOptions} from "./publisher";
import logger, {LogLevels} from "./logger";
import {AbstractService} from "./service";
Expand Down Expand Up @@ -107,6 +108,12 @@ export class Pact {
return verifierFactory(options).verify();
}

// Run the Message Pact creation process
public createMessage(options: MessageOptions): q.Promise<string> {
logger.info("Creating Message");
return messageFactory(options).createMessage();
}

// Publish Pacts to a Pact Broker
public publishPacts(options: PublisherOptions): q.Promise<any[]> {
logger.info("Publishing Pacts to Broker");
Expand Down
43 changes: 43 additions & 0 deletions test/message.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import chai = require("chai");
import chaiAsPromised = require("chai-as-promised");
import fs = require("fs");
import messageFactory from "../src/message";
import path = require("path");
import logger from "../src/logger";
chai.use(chaiAsPromised);

const rimraf = require("rimraf");
const expect = chai.expect;

describe("Message Integration Spec", () => {
const pactDir = (process && process.mainModule) ? `${path.dirname(process.mainModule.filename)}/pacts` : "/tmp/pacts";
const contractFile = `${pactDir}/consumer-provider.json`;

const validJSON = `{ "description": "a test mesage", "content": { "name": "Mary" } }`;

context("when given a successful contract", () => {
before(() => {
try {
if (fs.statSync(contractFile)) {
rimraf(pactDir, () => logger.debug("removed existing pacts"));
}
} catch (e) { }
});

it("should return a successful promise", (done) => {
const message = messageFactory({
consumer: "consumer",
provider: "provider",
dir: `${pactDir}`,
content: validJSON
});

const promise = message.createMessage();

promise.then(() => {
expect(fs.statSync(contractFile).isFile()).to.eql(true);
done();
});
});
});
});

0 comments on commit 980a3f5

Please sign in to comment.