Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16.x, 18.x, 20.x]
node-version: [20.x, 22.x, 24.x]
steps:

- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node.js 18.x
- name: Setup Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
cache: 'yarn'

- run: corepack enable
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:
uses: cycjimmy/semantic-release-action@v4
with:
dry_run: false
semantic_version: 18
semantic_version: 20
branches: |
[
'master',
Expand Down
159 changes: 152 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
- [About](#about)
- [Introduction](#introduction)
- [Consumer](#consumer)
- [V2 setup](#v2-setup)
- [V3 setup](#v3-setup)
- [V4 setup](#v4-setup)
- [V2 test](#v2-test)
- [V3 test](#v3-test)
- [V4 test](#v4-test)
- [Provider](#provider)
- [License](#license)
- [Acknowledgements](#acknowledgements)
Expand Down Expand Up @@ -73,17 +79,62 @@ The obvious advantage of this package is that Pact can be used in combination wi
In order to use the `Consumer` module, you need to follow a few simple steps, let's go over it!

First, create a file called `pact.module.ts` in your `test` folder (or wherever you put your tests), and simply
load the `PactConsumerModule` like below:
load the `PactConsumerModule`, `PactV2ConsumerModule`, or `PactV3ConsumerModule` like below:

**test/pact/pact.module.ts**

#### V2 setup

```typescript
import { Module } from '@nestjs/common';
import { PactV2ConsumerModule } from 'nestjs-pact';
import { PactV2ConsumerConfigOptionsService } from './pact-consumer-config-options.service';
import { AppModule } from '../../src/app.module';

@Module({
imports: [
PactV2ConsumerModule.registerAsync({
imports: [AppModule],
useClass: PactV2ConsumerConfigOptionsService,
}),
],
})
export class PactModule {}
```

#### V3 setup

```typescript
import { DynamicModule, Module } from '@nestjs/common';
import { PactV3ConsumerModule } from 'nestjs-pact';
import { PactV3ConsumerConfigOptionsService } from './pact-consumer-config-options.service';
import { AppModule } from '../../src/app.module';

@Module({
imports: [
PactV3ConsumerModule.registerAsync({
imports: [AppModule],
useClass: PactV3ConsumerConfigOptionsService,
}) as DynamicModule,
],
})
export class PactModule {}
```

#### V4 setup

```typescript
import { DynamicModule, Module } from '@nestjs/common';
import { PactConsumerModule } from 'nestjs-pact';
import { PactConsumerConfigOptionsService } from './pact-consumer-config-options.service';
import { AppModule } from '../../src/app.module';

@Module({
imports: [
PactConsumerModule.register({ ... }),
PactConsumerModule.registerAsync({
imports: [AppModule],
useClass: PactConsumerConfigOptionsService,
}) as DynamicModule,
],
})
export class PactModule {}
Expand All @@ -93,22 +144,24 @@ Yay, now let's create the test file! let's call it `my-test.spec.ts`

**test/pact/my-test.spec.ts**

#### V2 test

```typescript
import { Pact } from '@pact-foundation/pact';
import { PactV2 } from '@pact-foundation/pact';
import { Test } from '@nestjs/testing';
import { PactFactory } from 'nestjs-pact';
import { PactV2Factory } from 'nestjs-pact';
import { PactModule } from '@test/pact/pact.module';

describe('Pact', () => {
let pactFactory: PactFactory;
let provider: Pact;
let pactFactory: PactV2Factory;
let provider: PactV2;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [SomeOtherModule, AndAnotherModuleYouNeed, PactModule],
}).compile();

pactFactory = moduleRef.get(PactFactory);
pactFactory = moduleRef.get(PactV2Factory);

provider = pactFactory.createContractBetween({
consumer: 'Consumer Service Name',
Expand All @@ -134,6 +187,98 @@ describe('Pact', () => {
});
```

#### V3 test

```typescript
import { PactV3 } from '@pact-foundation/pact';
import { Test } from '@nestjs/testing';
import { PactV3Factory } from 'nestjs-pact';
import { PactModule } from '@test/pact/pact.module';

describe('Pact', () => {
let pactFactory: PactV3Factory;
let provider: PactV3;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [SomeOtherModule, AndAnotherModuleYouNeed, PactModule],
}).compile();

pactFactory = moduleRef.get(PactV3Factory);

provider = pactFactory.createContractBetween({
consumer: 'Consumer Service Name',
provider: 'Provider Service Name',
});

});

describe('when something happens', () => {
describe('and another thing happens too', () => {
beforeAll(() => provider.addInteraction({ ... }));

it('returns a 401 unauthorized', () => {
return provider.executeTest(async (mockServer) => {
// mockServer.url is the full url + port of the pact mock server
process.env.API_HOST = mockServer.url;
return expect( ... );
});

});
});
});
});
```

#### V4 test

```typescript
import { Pact } from '@pact-foundation/pact';
import { Test } from '@nestjs/testing';
import { PactFactory } from 'nestjs-pact';
import { PactModule } from '@test/pact/pact.module';

describe('Pact', () => {
let pactFactory: PactFactory;
let provider: Pact;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [SomeOtherModule, AndAnotherModuleYouNeed, PactModule],
}).compile();

pactFactory = moduleRef.get(PactFactory);

provider = pactFactory.createContractBetween({
consumer: 'Consumer Service Name',
provider: 'Provider Service Name',
});

});

describe('when something happens', () => {
it('returns the animal', async () => {
await provider
.addInteraction()
.given('Has an animal with ID 1')
.uponReceiving('a request for an animal with ID 1')
.withRequest('GET', regex('/animals/[0-9]+','/animals/1'), (builder) => {
builder.headers({ Authorization: 'Bearer token' });
})
.willRespondWith(HttpStatus.OK, (builder) => {
builder.headers({ 'Content-Type': 'application/json; charset=utf-8' });
builder.jsonBody(animalBodyExpectation);
})
.executeTest(async (mockServer) => {
process.env.API_HOST = mockServer.url;
const suggestedMates = await animalsService.getAnimalById(11);
expect(suggestedMates).toHaveProperty('id', 1);
});
});
});
});
```

Now let's look at how we can publish the pacts created from the test file to a Pact broker!

**test/pact/publish-pacts.ts**
Expand Down
17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nestjs-pact",
"version": "2.3.5",
"version": "3.0.0",
"license": "MIT",
"description": "Injectable Pact.js Consumer/Producer for NestJS",
"keywords": [
Expand All @@ -24,6 +24,9 @@
"bugs": {
"url": "https://github.com/omermorad/nestjs-pact/issues"
},
"engines": {
"node": ">=20"
},
"readme": "https://github.com/omermorad/nestjs-pact/README.md",
"scripts": {
"prebuild": "./node_modules/.bin/rimraf dist",
Expand All @@ -44,11 +47,11 @@
"devDependencies": {
"@commitlint/cli": "^17.3.0",
"@commitlint/config-conventional": "^17.3.0",
"@nestjs/common": "^9.2.1",
"@nestjs/core": "^9.2.1",
"@nestjs/testing": "^9.2.1",
"@pact-foundation/pact": "^15.0.1",
"@pact-foundation/pact-cli": "^16.0.7",
"@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6",
"@nestjs/testing": "11.1.6",
"@pact-foundation/pact": "^16.0.0",
"@pact-foundation/pact-cli": "^16.1.4",
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/git": "^10.0.1",
"@types/get-port": "^4.2.0",
Expand Down Expand Up @@ -80,7 +83,7 @@
"peerDependencies": {
"@nestjs/common": "7.x || 8.x || 9.x || 10.x || 11.x",
"@nestjs/core": "7.x || 8.x || 9.x || 10.x || 11.x",
"@pact-foundation/pact": "10.x || 11.x || 12.x || 13.x || 14.x || 15.x",
"@pact-foundation/pact": "16.x",
"@pact-foundation/pact-cli": "15.x || 16.x"
},
"dependencies": {
Expand Down
2 changes: 2 additions & 0 deletions src/common/pact-module-providers.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export enum PactModuleProviders {
PublicationOptions = 'PUBLICATION_OPTIONS',
PactPublisher = 'PACT_PUBLISHER',
Pact = 'PACT_INSTANCE',
PactV2 = 'PACT_V2_INSTANCE',
PactV3 = 'PACT_V3_INSTANCE',
}
36 changes: 35 additions & 1 deletion src/interfaces/pact-consumer-module-options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';

import { PactOptions } from '@pact-foundation/pact';
import { PactOptions, PactV2Options, PactV3Options } from '@pact-foundation/pact';
import { PublisherOptions } from '@pact-foundation/pact-cli';

export type PactConsumerOptions = Omit<PactOptions, 'consumer' | 'provider'>;
export type PactV2ConsumerOptions = Omit<PactV2Options, 'consumer' | 'provider'>;
export type PactV3ConsumerOptions = Omit<PactV3Options, 'consumer' | 'provider'>;
export type PactPublicationOptions = PublisherOptions;

export interface PactConsumerOverallOptions {
consumer: PactConsumerOptions;
publication?: PactPublicationOptions;
}

export interface PactV2ConsumerOverallOptions {
consumer: PactV2ConsumerOptions;
publication?: PactPublicationOptions;
}

export interface PactV3ConsumerOverallOptions {
consumer: PactV3ConsumerOptions;
publication?: PactPublicationOptions;
}

export interface PactConsumerOptionsFactory {
createPactConsumerOptions(): Promise<PactConsumerOverallOptions> | PactConsumerOverallOptions;
}
Expand All @@ -21,3 +33,25 @@ export interface PactConsumerModuleAsyncOptions extends Pick<ModuleMetadata, 'im
useExisting?: Type<PactConsumerOverallOptions>;
useFactory?: (...args: any[]) => Promise<PactConsumerOverallOptions> | PactConsumerOverallOptions;
}

export interface PactV2ConsumerOptionsFactory {
createPactV2ConsumerOptions(): Promise<PactV2ConsumerOverallOptions> | PactV2ConsumerOverallOptions;
}

export interface PactV2ConsumerModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
inject?: any[];
useClass?: Type<PactV2ConsumerOptionsFactory>;
useExisting?: Type<PactV2ConsumerOverallOptions>;
useFactory?: (...args: any[]) => Promise<PactV2ConsumerOverallOptions> | PactV2ConsumerOverallOptions;
}

export interface PactV3ConsumerOptionsFactory {
createPactV3ConsumerOptions(): Promise<PactV3ConsumerOverallOptions> | PactV3ConsumerOverallOptions;
}

export interface PactV3ConsumerModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
inject?: any[];
useClass?: Type<PactV3ConsumerOptionsFactory>;
useExisting?: Type<PactV3ConsumerOverallOptions>;
useFactory?: (...args: any[]) => Promise<PactV3ConsumerOverallOptions> | PactV3ConsumerOverallOptions;
}
Loading