Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Support retrieving OAS provider contracts from PactFlow #45

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@ the specified provider name. The <swagger> argument should be the path or url to
json file. Optionally, pass a --tag option alongside a --provider option to filter the retrieved
pacts from the broker by Pact Broker version tags.

If the pact broker has basic auth enabled, pass a --user option with username and password joined by a colon
(i.e. THE_USERNAME:THE_PASSWORD) to access the pact broker resources.
If the pact broker has auth enabled, set the necessary env vars to access the pact broker resources.

PACT_BROKER_USERNAME
PACT_BROKER_PASSWORD
PACT_BROKER_TOKEN

Options:
-V, --version output the version number
-p, --provider [string] The name of the provider in the pact broker
-t, --tag [string] The tag to filter pacts retrieved from the pact broker
-u, --user [USERNAME:PASSWORD] The basic auth username and password to access the pact broker
-a, --analyticsUrl [string] The url to send analytics events to as a http post
-o, --outputDepth [integer] Specifies the number of times to recurse while formatting the output objects. This is useful in case of large complicated objects or schemas. (default: 4)
-A, --additionalPropertiesInResponse [boolean] allow additional properties in response bodies, default false
Expand Down Expand Up @@ -351,9 +353,15 @@ Additionally, provide a Pact Broker version tag alongside the name of the provid
swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name --tag production
```

If the Pact Broker is behind basic auth, you can pass credentials with the `--user` option while invoking the tool.
If the Pact Broker is behind basic auth, you can pass credentials with env vars while invoking the tool.

```
PACT_BROKER_USERNAME=foo PACT_BROKER_PASSWORD=bar swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name
```

If the Pact Broker is behind bearer auth, you can pass credentials with env vars while invoking the tool.
```
swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name --user BASIC_AUTH_USER:BASIC_AUTH_PASSWORD
PACT_BROKER_TOKEN=bar swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name
```

### Analytics (Opt-In)
Expand Down
15 changes: 11 additions & 4 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ program
.arguments('<swagger> <mock>')
.option('-p, --provider [string]', 'The name of the provider in the pact broker')
.option('-t, --tag [string]', 'The tag to filter pacts retrieved from the pact broker')
.option('-u, --user [USERNAME:PASSWORD]', 'The basic auth username and password to access the pact broker')
.option('-a, --analyticsUrl [string]', 'The url to send analytics events to as a http post')
.option('-o, --outputDepth [integer]', 'Specifies the number of times to recurse ' +
'while formatting the output objects. ' +
Expand All @@ -71,12 +70,20 @@ the specified provider name. The <swagger> argument should be the path or url to
json file. Optionally, pass a --tag option alongside a --provider option to filter the retrieved
pacts from the broker by Pact Broker version tags.

If the pact broker has basic auth enabled, pass a --user option with username and password joined by a colon
(i.e. THE_USERNAME:THE_PASSWORD) to access the pact broker resources.`
If the pact broker has auth enabled, you can access pact broker resources, by setting the following env vars

Basic Auth

PACT_BROKER_USERNAME
PACT_BROKER_PASSWORD

Bearer Token Auth

PACT_BROKER_TOKEN`
)
.action(async (swagger, mock, options) => {
try {
const swaggerMockValidator = SwaggerMockValidatorFactory.create(options.user);
const swaggerMockValidator = SwaggerMockValidatorFactory.create();

const result = await swaggerMockValidator.validate({
analyticsUrl: options.analyticsUrl,
Expand Down
4 changes: 2 additions & 2 deletions lib/swagger-mock-validator-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {PactBroker} from './swagger-mock-validator/pact-broker';
import {UuidGenerator} from './swagger-mock-validator/uuid-generator';

export class SwaggerMockValidatorFactory {
public static create(auth?: string): SwaggerMockValidator {
public static create(): SwaggerMockValidator {
const fileSystem = new FileSystem();
const httpClient = new HttpClient();
const uuidGenerator = new UuidGenerator();
const metadata = new Metadata();
const fileStore = new FileStore(fileSystem, httpClient);
const pactBrokerClient = new PactBrokerClient(httpClient, auth);
const pactBrokerClient = new PactBrokerClient(httpClient);
const pactBroker = new PactBroker(pactBrokerClient);
const analytics = new Analytics(httpClient, uuidGenerator, metadata);
return new SwaggerMockValidator(fileStore, pactBroker, analytics);
Expand Down
19 changes: 15 additions & 4 deletions lib/swagger-mock-validator/clients/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import axios from 'axios';

export class HttpClient {
public async get(url: string, auth?: string): Promise<string> {
public async get(url: string): Promise<string> {
let authHeader: string | undefined;
if (process.env.PACT_BROKER_TOKEN != '') {
authHeader = 'Bearer ' + process.env.PACT_BROKER_TOKEN;
} else if (process.env.PACT_BROKER_USERNAME != '' && process.env.PACT_BROKER_PASSWORD != '') {
authHeader =
'Basic ' +
Buffer.from(`${process.env.PACT_BROKER_USERNAME}:${process.env.PACT_BROKER_PASSWORD}`).toString(
'base64'
);
}

const response = await axios.get(url, {
headers: {
...(auth ? {authorization: 'Basic ' + Buffer.from(auth).toString('base64')} : {})
...(authHeader ? { Authorization: authHeader } : {}),
},
timeout: 30000,
transformResponse: (data) => data,
validateStatus: (status) => status === 200
validateStatus: (status) => status === 200,
});
return response.data;
}

public async post(url: string, body: any): Promise<void> {

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (16.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (18.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (18.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (16.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (18.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (16.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (18.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (16.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (18.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (18.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (16.x)

Unexpected any. Specify a different type

Check warning on line 27 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (16.x)

Unexpected any. Specify a different type
await axios.post(url, body, {
timeout: 5000,
validateStatus: (status) => status >= 200 && status <= 299
validateStatus: (status) => status >= 200 && status <= 299,
});
}
}
6 changes: 3 additions & 3 deletions lib/swagger-mock-validator/clients/pact-broker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {transformStringToObject} from '../transform-string-to-object';
import {HttpClient} from './http-client';

export class PactBrokerClient {
public constructor(private readonly httpClient: HttpClient, private readonly auth?: string) {
public constructor(private readonly httpClient: HttpClient) {
}

public async loadAsObject<T>(url: string): Promise<T> {
try {
const content = await this.httpClient.get(url, this.auth);
const content = await this.httpClient.get(url);

return transformStringToObject<T>(content, url);
} catch (error) {
Expand All @@ -20,7 +20,7 @@ export class PactBrokerClient {

public async loadAsString(url: string): Promise<string> {
try {
return await this.httpClient.get(url, this.auth);
return await this.httpClient.get(url);
} catch (error) {
throw new SwaggerMockValidatorErrorImpl(
'SWAGGER_MOCK_VALIDATOR_READ_ERROR', `Unable to read "${url}"`, error
Expand Down
18 changes: 13 additions & 5 deletions lib/swagger-mock-validator/file-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {FileSystem} from './clients/file-system';
import {HttpClient} from './clients/http-client';
import {SwaggerMockValidatorErrorImpl} from './swagger-mock-validator-error-impl';
import { FileSystem } from './clients/file-system';
import { HttpClient } from './clients/http-client';
import { SwaggerMockValidatorErrorImpl } from './swagger-mock-validator-error-impl';

export class FileStore {
public static isUrl(pathOrUrl: string): boolean {
Expand All @@ -11,10 +11,18 @@ export class FileStore {

public async loadFile(pathOrUrl: string): Promise<string> {
try {
return await this.loadPathOrUrl(pathOrUrl);
if (process.env.PACT_BROKER_TOKEN != '' && pathOrUrl.includes('internal/contracts/bi-directional')) {
const result = await this.loadPathOrUrl(pathOrUrl);
const providerContractContent = JSON.parse(result)._embedded['providerContract']['content'];
return Promise.resolve(providerContractContent);
} else {
return await this.loadPathOrUrl(pathOrUrl);
}
} catch (error) {
throw new SwaggerMockValidatorErrorImpl(
'SWAGGER_MOCK_VALIDATOR_READ_ERROR', `Unable to read "${pathOrUrl}"`, error
'SWAGGER_MOCK_VALIDATOR_READ_ERROR',
`Unable to read "${pathOrUrl}"`,
error
);
}
}
Expand Down
21 changes: 0 additions & 21 deletions test/e2e/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {expectToFail} from '../helpers/expect-to-fail';

interface InvokeCommandOptions {
analyticsUrl?: string;
auth?: string;
mock: string;
providerName?: string;
swagger: string;
Expand Down Expand Up @@ -42,10 +41,6 @@ const invokeCommand = (options: InvokeCommandOptions): Promise<string> => {
command += ` --analyticsUrl ${options.analyticsUrl}`;
}

if (options.auth) {
command += ` --user ${options.auth}`;
}

if (options.outputDepth) {
command += ` --outputDepth ${options.outputDepth}`;
}
Expand Down Expand Up @@ -372,22 +367,6 @@ describe('swagger-mock-validator/cli', () => {
);
}, 30000);

it('should make an authenticated request to the provided pact broker url when asked to do so', async () => {
const auth = 'user:pass';

await invokeCommand({
auth,
mock: urlTo('test/e2e/fixtures/pact-broker.json'),
providerName: 'provider-1',
swagger: urlTo('test/e2e/fixtures/swagger-provider.json')
});

expect(mockPactBroker.get).toHaveBeenCalledWith(
jasmine.objectContaining({authorization: 'Basic dXNlcjpwYXNz'}),
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);

it('should format output objects to depth 0', async () => {
const result = await invokeCommand({
mock: 'test/e2e/fixtures/pact-working-consumer.json',
Expand Down
34 changes: 16 additions & 18 deletions test/unit/reading-urls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe('reading urls', () => {
it('should make a request to the root of the pact broker', async () => {
await invokeValidateWithPactBroker('http://pact-broker.com', 'provider-name');

expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com', undefined);
expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com');
});

it('should fail when the request to the root of the pact broker fails', async () => {
Expand Down Expand Up @@ -192,7 +192,7 @@ describe('reading urls', () => {

await invokeValidateWithPactBroker('http://pact-broker.com', 'provider-name');

expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com/provider-name/pacts', undefined);
expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com/provider-name/pacts');
});

it('should fail when the request for the latest pact files fails', async () => {
Expand Down Expand Up @@ -247,9 +247,9 @@ describe('reading urls', () => {
await invokeValidateWithPactBroker('http://pact-broker.com', 'provider-name');

expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-1/pact', undefined);
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-1/pact');
expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-2/pact', undefined);
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-2/pact');
});

it('should fail when the request for one of the provider pact files fails', async () => {
Expand Down Expand Up @@ -398,7 +398,7 @@ describe('reading urls', () => {

await invokeValidateWithPactBroker('http://pact-broker.com', 'provider/name');

expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com/provider%2Fname/pacts', undefined);
expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com/provider%2Fname/pacts');
});
});

Expand All @@ -415,19 +415,17 @@ describe('reading urls', () => {
Promise.resolve(JSON.stringify(pactBuilder.build()));
});

const invokeValidateWithPactBrokerAndAuth = (pactBrokerUrl: string, providerName: string, auth: string) => {
return invokeValidate({
auth,
mockPathOrUrl: pactBrokerUrl,
const invokeValidateWithPactBrokerAndAuth = (pactBrokerUrl: string, providerName: string) => {
return invokeValidate({mockPathOrUrl: pactBrokerUrl,
providerName,
specPathOrUrl: 'http://domain.com/swagger.json'
});
};

it('should make an authenticated request to the root of the pact broker', async () => {
await invokeValidateWithPactBrokerAndAuth('http://pact-broker.com', 'provider-name', 'user:password');
await invokeValidateWithPactBrokerAndAuth('http://pact-broker.com', 'provider-name');

expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com', 'user:password');
expect(mockHttpClient.get).toHaveBeenCalledWith('http://pact-broker.com');
});

it('should make an authenticated request for the latest pact files for the provider', async () => {
Expand All @@ -438,10 +436,10 @@ describe('reading urls', () => {
mockUrls['http://pact-broker.com/provider-name/pacts'] =
Promise.resolve(JSON.stringify(providerPactsBuilder.build()));

await invokeValidateWithPactBrokerAndAuth('http://pact-broker.com', 'provider-name', 'user:password');
await invokeValidateWithPactBrokerAndAuth('http://pact-broker.com', 'provider-name');

expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/pacts', 'user:password');
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/pacts');
});

it('should make a request for all the provider pact files', async () => {
Expand All @@ -459,12 +457,12 @@ describe('reading urls', () => {
mockUrls['http://pact-broker.com/provider-name/consumer-2/pact'] =
Promise.resolve(JSON.stringify(pactBuilder.build()));

await invokeValidateWithPactBrokerAndAuth('http://pact-broker.com', 'provider-name', 'user:password');
await invokeValidateWithPactBrokerAndAuth('http://pact-broker.com', 'provider-name');

expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-1/pact', 'user:password');
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-1/pact');
expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-2/pact', 'user:password');
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/consumer-2/pact');
});
});

Expand Down Expand Up @@ -509,7 +507,7 @@ describe('reading urls', () => {
await invokeValidateWithPactBrokerAndTag('http://pact-broker.com', 'provider-name', 'sample-tag');

expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/latest/sample-tag', undefined);
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/latest/sample-tag');
});

it('should pass but display a warning when there are no provider pact files for the given tag', async () => {
Expand Down Expand Up @@ -547,7 +545,7 @@ describe('reading urls', () => {
await invokeValidateWithPactBrokerAndTag('http://pact-broker.com', 'provider-name', 'sample/tag');

expect(mockHttpClient.get)
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/latest/sample%2Ftag', undefined);
.toHaveBeenCalledWith('http://pact-broker.com/provider-name/latest/sample%2Ftag');
});
});
});
3 changes: 1 addition & 2 deletions test/unit/support/swagger-mock-validator-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export type MockUuidGeneratorResponses = string[];

export interface SwaggerMockValidatorLoaderInvokeWithMocksOptions {
analyticsUrl?: string;
auth?: string;
fileSystem?: FileSystem;
httpClient?: HttpClient;
metadata?: Metadata;
Expand Down Expand Up @@ -111,7 +110,7 @@ export const swaggerMockValidatorLoader = {
const mockMetadata = options.metadata || swaggerMockValidatorLoader.createMockMetadata({});

const fileStore = new FileStore(mockFileSystem, mockHttpClient);
const pactBrokerClient = new PactBrokerClient(mockHttpClient, options.auth);
const pactBrokerClient = new PactBrokerClient(mockHttpClient);
const pactBroker = new PactBroker(pactBrokerClient);
const analytics = new Analytics(mockHttpClient, mockUuidGenerator, mockMetadata);
const swaggerMockValidator = new SwaggerMockValidator(fileStore, pactBroker, analytics);
Expand Down
Loading