From 0da4ccc3e1540c21c1fd9b5e805ba71d1488d707 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 22 Aug 2023 14:04:00 +0100 Subject: [PATCH 1/4] feat: support auth mechanisms via env var 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 --- README.md | 18 ++++++++++++----- lib/cli.ts | 13 +++++++++++- .../clients/http-client.ts | 20 ++++++++++++++++--- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e554058..a8648c3 100644 --- a/README.md +++ b/README.md @@ -76,14 +76,16 @@ the specified provider name. The 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 @@ -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) diff --git a/lib/cli.ts b/lib/cli.ts index 07b6678..b45f5fd 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -72,7 +72,18 @@ json file. Optionally, pass a --tag option alongside a --provider option to filt 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.` +(i.e. THE_USERNAME:THE_PASSWORD) to access the pact broker resources. + +Alternatively you can set 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 { diff --git a/lib/swagger-mock-validator/clients/http-client.ts b/lib/swagger-mock-validator/clients/http-client.ts index 3a3ff10..73c11d1 100644 --- a/lib/swagger-mock-validator/clients/http-client.ts +++ b/lib/swagger-mock-validator/clients/http-client.ts @@ -2,13 +2,27 @@ import axios from 'axios'; export class HttpClient { public async get(url: string, auth?: string): Promise { + console.log(auth); + 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' + ); + } else if (auth) { + authHeader = auth.includes(':') ? 'Bearer ' + auth : 'Basic ' + Buffer.from(auth).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; } @@ -16,7 +30,7 @@ export class HttpClient { public async post(url: string, body: any): Promise { await axios.post(url, body, { timeout: 5000, - validateStatus: (status) => status >= 200 && status <= 299 + validateStatus: (status) => status >= 200 && status <= 299, }); } } From f583ac7f89f39d81b562e4e07bd36e38821c35e3 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 22 Aug 2023 16:12:57 +0100 Subject: [PATCH 2/4] fix!: remove --user option which doesnt work from cli replace with PACT_BROKER_USERNAME/PACT_BROKER_PASSWORD env vars if using OS Pact Broker. Use PACT_BROKER_TOKEN if using Pact Broker with Bearer auth (such as PactFlow) --- lib/cli.ts | 8 ++--- lib/swagger-mock-validator-factory.ts | 4 +-- .../clients/http-client.ts | 5 +-- .../clients/pact-broker-client.ts | 6 ++-- test/unit/reading-urls.spec.ts | 34 +++++++++---------- .../support/swagger-mock-validator-loader.ts | 3 +- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/lib/cli.ts b/lib/cli.ts index b45f5fd..bd0d173 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -47,7 +47,6 @@ program .arguments(' ') .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. ' + @@ -71,10 +70,7 @@ the specified provider name. The 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. - -Alternatively you can set access pact broker resources, by setting the following env vars +If the pact broker has auth enabled, you can access pact broker resources, by setting the following env vars Basic Auth @@ -87,7 +83,7 @@ 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, diff --git a/lib/swagger-mock-validator-factory.ts b/lib/swagger-mock-validator-factory.ts index f901cd8..2f0aef8 100644 --- a/lib/swagger-mock-validator-factory.ts +++ b/lib/swagger-mock-validator-factory.ts @@ -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); diff --git a/lib/swagger-mock-validator/clients/http-client.ts b/lib/swagger-mock-validator/clients/http-client.ts index 73c11d1..a188577 100644 --- a/lib/swagger-mock-validator/clients/http-client.ts +++ b/lib/swagger-mock-validator/clients/http-client.ts @@ -1,8 +1,7 @@ import axios from 'axios'; export class HttpClient { - public async get(url: string, auth?: string): Promise { - console.log(auth); + public async get(url: string): Promise { let authHeader: string | undefined; if (process.env.PACT_BROKER_TOKEN != '') { authHeader = 'Bearer ' + process.env.PACT_BROKER_TOKEN; @@ -12,8 +11,6 @@ export class HttpClient { Buffer.from(`${process.env.PACT_BROKER_USERNAME}:${process.env.PACT_BROKER_PASSWORD}`).toString( 'base64' ); - } else if (auth) { - authHeader = auth.includes(':') ? 'Bearer ' + auth : 'Basic ' + Buffer.from(auth).toString('base64'); } const response = await axios.get(url, { diff --git a/lib/swagger-mock-validator/clients/pact-broker-client.ts b/lib/swagger-mock-validator/clients/pact-broker-client.ts index 61077af..25d05eb 100644 --- a/lib/swagger-mock-validator/clients/pact-broker-client.ts +++ b/lib/swagger-mock-validator/clients/pact-broker-client.ts @@ -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(url: string): Promise { try { - const content = await this.httpClient.get(url, this.auth); + const content = await this.httpClient.get(url); return transformStringToObject(content, url); } catch (error) { @@ -20,7 +20,7 @@ export class PactBrokerClient { public async loadAsString(url: string): Promise { 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 diff --git a/test/unit/reading-urls.spec.ts b/test/unit/reading-urls.spec.ts index 124e429..7381671 100644 --- a/test/unit/reading-urls.spec.ts +++ b/test/unit/reading-urls.spec.ts @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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'); }); }); @@ -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 () => { @@ -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 () => { @@ -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'); }); }); @@ -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 () => { @@ -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'); }); }); }); diff --git a/test/unit/support/swagger-mock-validator-loader.ts b/test/unit/support/swagger-mock-validator-loader.ts index ea1691f..06255d9 100644 --- a/test/unit/support/swagger-mock-validator-loader.ts +++ b/test/unit/support/swagger-mock-validator-loader.ts @@ -32,7 +32,6 @@ export type MockUuidGeneratorResponses = string[]; export interface SwaggerMockValidatorLoaderInvokeWithMocksOptions { analyticsUrl?: string; - auth?: string; fileSystem?: FileSystem; httpClient?: HttpClient; metadata?: Metadata; @@ -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); From b26e63223061a06f2fe09f18b140a15091d8f949 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 22 Aug 2023 16:15:35 +0100 Subject: [PATCH 3/4] feat: support reading OAS from PactFlow notes:- leverages PactFlow internal url for BDCT internal/contracts/bi-directional curl -H "Authorization: Bearer $PACT_BROKER_TOKEN" https://testdemo.pactflow.io/internal/contracts/bi-directional/provider/pact-provider-poc/version/64898db/consumer/pact-consumer-poc/version/9191e17/provider-contract | jq . relies on the following resource _embedded[providerContract][content] --- lib/swagger-mock-validator/file-store.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/swagger-mock-validator/file-store.ts b/lib/swagger-mock-validator/file-store.ts index 5eafc17..595a800 100644 --- a/lib/swagger-mock-validator/file-store.ts +++ b/lib/swagger-mock-validator/file-store.ts @@ -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 { @@ -11,10 +11,18 @@ export class FileStore { public async loadFile(pathOrUrl: string): Promise { 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 ); } } From ccf62723e17f70e5db7711bb7c926fdf21e99153 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 22 Aug 2023 16:27:20 +0100 Subject: [PATCH 4/4] chore: update tests --- test/e2e/cli.spec.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/e2e/cli.spec.ts b/test/e2e/cli.spec.ts index 320ba20..5c8eece 100644 --- a/test/e2e/cli.spec.ts +++ b/test/e2e/cli.spec.ts @@ -7,7 +7,6 @@ import {expectToFail} from '../helpers/expect-to-fail'; interface InvokeCommandOptions { analyticsUrl?: string; - auth?: string; mock: string; providerName?: string; swagger: string; @@ -42,10 +41,6 @@ const invokeCommand = (options: InvokeCommandOptions): Promise => { command += ` --analyticsUrl ${options.analyticsUrl}`; } - if (options.auth) { - command += ` --user ${options.auth}`; - } - if (options.outputDepth) { command += ` --outputDepth ${options.outputDepth}`; } @@ -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',