diff --git a/README.md b/README.md index 17b204f..5cf3126 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,10 @@ Options: -A, --additionalPropertiesInResponse [boolean] allow additional properties in response bodies, default false -R, --requiredPropertiesInResponse [boolean] allows required properties in response bodies, default false --publish Allows publication of verification result to pact broker, default false - --providerApplicationVersion [string] Version of provider, used when publishing result to broker + --providerApplicationVersion [string] Version of provider, used when publishing result to broker, required if --publish is set --buildUrl [string] Url to build/pipeline, used when publishing result to broker + --providerBranch [string] Branch of provider, used when publishing result to broker + --providerTags [string] Tags of provider, used when publishing result to broker, comma separated -h, --help display help for command ``` diff --git a/lib/cli.ts b/lib/cli.ts index 706dc3a..afc178d 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -56,6 +56,8 @@ program .option('-R, --requiredPropertiesInResponse [boolean]', 'allows required properties in response bodies, default false') .option('--publish', 'Allows publication of verification result to pact broker, default false') .option('--providerApplicationVersion [string]', 'Version of provider, used when publishing result to broker') + .option('--providerBranch [string]', 'Branch of provider, used when publishing result to broker') + .option('--providerTags [string]', 'Tags of provider, used when publishing result to broker, comma seperated') .option('--buildUrl [string]', 'Url to build/pipeline, used when publishing result to broker') .description( `Confirms the swagger spec and mock are compatible with each other. @@ -91,6 +93,8 @@ If the pact broker has basic auth enabled, pass a --user option with username an additionalPropertiesInResponse: options.additionalPropertiesInResponse, requiredPropertiesInResponse: options.requiredPropertiesInResponse, providerApplicationVersion: options.providerApplicationVersion, + providerBranch: options.providerBranch, + providerTags: options.providerTags, buildUrl: options.buildUrl, publish: options.publish }); diff --git a/lib/swagger-mock-validator/clients/http-client.ts b/lib/swagger-mock-validator/clients/http-client.ts index 5be1446..8ea6118 100644 --- a/lib/swagger-mock-validator/clients/http-client.ts +++ b/lib/swagger-mock-validator/clients/http-client.ts @@ -22,4 +22,13 @@ export class HttpClient { validateStatus: (status) => status >= 200 && status <= 299 }); } + public async put(url: string, body: any, auth?: string): Promise { + await axios.put(url, body, { + headers: { + ...(auth ? {authorization: 'Basic ' + Buffer.from(auth).toString('base64')} : {}) + }, + timeout: 5000, + validateStatus: (status) => status >= 200 && status <= 299 + }); + } } diff --git a/lib/swagger-mock-validator/clients/pact-broker-client.ts b/lib/swagger-mock-validator/clients/pact-broker-client.ts index c4e56bc..01d8d3b 100644 --- a/lib/swagger-mock-validator/clients/pact-broker-client.ts +++ b/lib/swagger-mock-validator/clients/pact-broker-client.ts @@ -37,4 +37,13 @@ export class PactBrokerClient { ); } } + public async put(url: string, body: object): Promise { + try { + await this.httpClient.put(url, body, this.auth); + } catch (error) { + throw new SwaggerMockValidatorErrorImpl( + 'SWAGGER_MOCK_VALIDATOR_READ_ERROR', `Unable to put "${url}"`, error + ); + } + } } diff --git a/lib/swagger-mock-validator/mock-parser/parsed-mock.d.ts b/lib/swagger-mock-validator/mock-parser/parsed-mock.d.ts index 4974a1a..2ce1cda 100644 --- a/lib/swagger-mock-validator/mock-parser/parsed-mock.d.ts +++ b/lib/swagger-mock-validator/mock-parser/parsed-mock.d.ts @@ -4,6 +4,8 @@ export interface ParsedMock { pathOrUrl: string; provider: string; verificationUrl?: string; + verificationBranchVersionUrl?: string; + verificationTagVersionUrl?: string; } export interface ParsedMockInteraction extends ParsedMockValue { diff --git a/lib/swagger-mock-validator/pact-broker.ts b/lib/swagger-mock-validator/pact-broker.ts index 7315258..602c157 100644 --- a/lib/swagger-mock-validator/pact-broker.ts +++ b/lib/swagger-mock-validator/pact-broker.ts @@ -5,6 +5,7 @@ import { PactBrokerProviderPacts, PactBrokerProviderPactsLinksPact, PactBrokerRootResponse, + PactBrokerPacticipantResponse, PactBrokerUserOptions, PactBrokerUserOptionsWithTag, ParsedSwaggerMockValidatorOptions, @@ -18,8 +19,7 @@ export class PactBroker { return _.get(pactBrokerRootResponse, template); } - public constructor(private readonly pactBrokerClient: PactBrokerClient) { - } + public constructor(private readonly pactBrokerClient: PactBrokerClient) {} public async loadPacts(options: PactBrokerUserOptions): Promise { const providerPactsUrl = await this.getUrlForProviderPacts(options); @@ -29,17 +29,78 @@ export class PactBroker { } public async publishVerificationResult( - {providerApplicationVersion, buildUrl}: ParsedSwaggerMockValidatorOptions, - {verificationUrl}: ParsedMock, - {success}: ValidationOutcome + { + providerApplicationVersion, + buildUrl, + providerBranch, + providerTags, + mockPathOrUrl, + mockSource, + providerName, + }: ParsedSwaggerMockValidatorOptions, + { verificationUrl }: ParsedMock, + { success }: ValidationOutcome, ): Promise { + // if (mockSource !== 'pactBroker') { + // throw new SwaggerMockValidatorErrorImpl( + // 'SWAGGER_MOCK_VALIDATOR_READ_ERROR', + // `verification results can only be published for pacts sourced from a Pact Broker`, + // ); + // } + if (!providerApplicationVersion) { + throw new SwaggerMockValidatorErrorImpl( + 'SWAGGER_MOCK_VALIDATOR_READ_ERROR', + `providerApplicationVersion is required to publish verification results`, + ); + } if (!verificationUrl) { throw new SwaggerMockValidatorErrorImpl( 'SWAGGER_MOCK_VALIDATOR_READ_ERROR', - `No verification publication url available in pact` + `No verification publication url available in pact`, + ); + } + + let branchVersionUrl; + let versionTagUrl; + if ((providerBranch || providerTags) && providerName) { + const pactBrokerRootResponse = await this.pactBrokerClient.loadAsObject( + mockPathOrUrl + ); + const pactBrokerPacticipantUrl = pactBrokerRootResponse._links['pb:pacticipant'].href; + const pactBrokerPacticipantResponse = await this.pactBrokerClient.loadAsObject( + this.getSpecificUrlFromTemplate(pactBrokerPacticipantUrl,{pacticipant: providerName}), + ); + branchVersionUrl = pactBrokerPacticipantResponse._links['pb:branch-version'].href; + versionTagUrl = pactBrokerPacticipantResponse._links['pb:version-tag'].href; + } + + if (providerBranch && providerName && branchVersionUrl) { + await this.pactBrokerClient.put( + this.getSpecificUrlFromTemplate(branchVersionUrl, { + provider: providerName, + branch: providerBranch, + version: providerApplicationVersion, + }), + { + version: providerApplicationVersion, + branch: providerBranch, + }, ); } + if (providerTags && providerName && versionTagUrl) { + const tags = providerTags.split(','); + for (const tag of tags) { + await this.pactBrokerClient.put( + this.getSpecificUrlFromTemplate(versionTagUrl, { tag: tag, version: providerApplicationVersion }), + { + version: providerApplicationVersion, + tag: tag, + }, + ); + } + } + return this.pactBrokerClient.post(verificationUrl, { success, providerApplicationVersion, @@ -48,60 +109,61 @@ export class PactBroker { } private async getUrlForProviderPacts(options: PactBrokerUserOptions): Promise { - const pactBrokerRootResponse = - await this.pactBrokerClient.loadAsObject(options.pactBrokerUrl); + const pactBrokerRootResponse = await this.pactBrokerClient.loadAsObject( + options.pactBrokerUrl, + ); return options.tag ? this.getUrlForProviderPactsByTag(pactBrokerRootResponse, { - pactBrokerUrl: options.pactBrokerUrl, - providerName: options.providerName, - tag: options.tag - }) : this.getUrlForAllProviderPacts(pactBrokerRootResponse, options); + pactBrokerUrl: options.pactBrokerUrl, + providerName: options.providerName, + tag: options.tag, + }) + : this.getUrlForAllProviderPacts(pactBrokerRootResponse, options); } - private getUrlForProviderPactsByTag(pactBrokerRootResponse: PactBrokerRootResponse, - options: PactBrokerUserOptionsWithTag): string { + private getUrlForProviderPactsByTag( + pactBrokerRootResponse: PactBrokerRootResponse, + options: PactBrokerUserOptionsWithTag, + ): string { const providerTemplateUrl = PactBroker.getProviderTemplateUrl( pactBrokerRootResponse, - '_links.pb:latest-provider-pacts-with-tag.href' + '_links.pb:latest-provider-pacts-with-tag.href', ); if (!providerTemplateUrl) { throw new SwaggerMockValidatorErrorImpl( 'SWAGGER_MOCK_VALIDATOR_READ_ERROR', - `Unable to read "${options.pactBrokerUrl}": No latest pact file url found for tag` + `Unable to read "${options.pactBrokerUrl}": No latest pact file url found for tag`, ); } - return this.getSpecificUrlFromTemplate( - providerTemplateUrl, {provider: options.providerName, tag: options.tag} - ); + return this.getSpecificUrlFromTemplate(providerTemplateUrl, { + provider: options.providerName, + tag: options.tag, + }); } private getUrlForAllProviderPacts( pactBrokerRootResponse: PactBrokerRootResponse, - options: PactBrokerUserOptions + options: PactBrokerUserOptions, ): string { const providerTemplateUrl = PactBroker.getProviderTemplateUrl( pactBrokerRootResponse, - '_links.pb:latest-provider-pacts.href' + '_links.pb:latest-provider-pacts.href', ); if (!providerTemplateUrl) { throw new SwaggerMockValidatorErrorImpl( 'SWAGGER_MOCK_VALIDATOR_READ_ERROR', - `Unable to read "${options.pactBrokerUrl}": No latest pact file url found` + `Unable to read "${options.pactBrokerUrl}": No latest pact file url found`, ); } - return this.getSpecificUrlFromTemplate( - providerTemplateUrl, {provider: options.providerName} - ); + return this.getSpecificUrlFromTemplate(providerTemplateUrl, { provider: options.providerName }); } - private getSpecificUrlFromTemplate( - providerTemplateUrl: string, parameters: { [key: string]: string } - ): string { + private getSpecificUrlFromTemplate(providerTemplateUrl: string, parameters: { [key: string]: string }): string { let specificUrl = providerTemplateUrl; Object.keys(parameters).forEach((key) => { const encodedParameterValue = encodeURIComponent(parameters[key]); @@ -112,18 +174,21 @@ export class PactBroker { } private async getPactUrls(providerPactsUrl: string): Promise { - const providerUrlResponse = - await this.pactBrokerClient.loadAsObject(providerPactsUrl); + const providerUrlResponse = await this.pactBrokerClient.loadAsObject(providerPactsUrl); const providerPactEntries: PactBrokerProviderPactsLinksPact[] = _.get(providerUrlResponse, '_links.pacts', []); return _.map(providerPactEntries, (providerPact) => providerPact.href); } private async getPacts(pactUrls: string[]): Promise { - return Promise.all(pactUrls.map(async (mockPathOrUrl): Promise => ({ - content: await this.pactBrokerClient.loadAsString(mockPathOrUrl), - format: 'auto-detect', - pathOrUrl: mockPathOrUrl - }))); + return Promise.all( + pactUrls.map( + async (mockPathOrUrl): Promise => ({ + content: await this.pactBrokerClient.loadAsString(mockPathOrUrl), + format: 'auto-detect', + pathOrUrl: mockPathOrUrl, + }), + ), + ); } } diff --git a/lib/swagger-mock-validator/types.d.ts b/lib/swagger-mock-validator/types.d.ts index d1c9769..6ef7433 100644 --- a/lib/swagger-mock-validator/types.d.ts +++ b/lib/swagger-mock-validator/types.d.ts @@ -3,12 +3,29 @@ import {SwaggerMockValidatorOptionsMockType, SwaggerMockValidatorOptionsSpecType export interface PactBrokerRootResponse { _links: PactBrokerLinks; } +export interface PactBrokerPacticipantResponse { + _links: PactBrokerPacticipantResponseLinks; +} interface PactBrokerLinks { + 'pb:pacticipant': PactBrokerLinksPacticipant; 'pb:latest-provider-pacts': PactBrokerLinksLatestProviderPacts; 'pb:latest-provider-pacts-with-tag': PactBrokerLinksLatestProviderPacts; } +interface PactBrokerPacticipantResponseLinks { + 'pb:version-tag': PactBrokerLinksPacticipantVersionTag; + 'pb:branch-version': PactBrokerLinksPacticipantBranchVersion; +} +interface PactBrokerLinksPacticipant { + href: string; +} +interface PactBrokerLinksPacticipantVersionTag { + href: string; +} +interface PactBrokerLinksPacticipantBranchVersion { + href: string; +} interface PactBrokerLinksLatestProviderPacts { href: string; } @@ -36,6 +53,8 @@ export interface SwaggerMockValidatorUserOptions { providerApplicationVersion?: string; buildUrl?: string; publish?: string; + providerBranch?: string; + providerTags?: string; } export interface PactBrokerUserOptions { @@ -80,6 +99,8 @@ interface ParsedSwaggerMockValidatorOptions { additionalPropertiesInResponse: boolean; requiredPropertiesInResponse: boolean; providerApplicationVersion?: string; + providerBranch?: string; + providerTags?: string; buildUrl?: string; publish: boolean; } diff --git a/test/e2e/cli.spec.ts b/test/e2e/cli.spec.ts index bcc0cd5..9628049 100644 --- a/test/e2e/cli.spec.ts +++ b/test/e2e/cli.spec.ts @@ -76,11 +76,12 @@ describe('swagger-mock-validator/cli', () => { let mockPactBroker: jasmine.SpyObj<{ get: (requestHeaders: object, requestUrl: string) => void, post: (body: object, requestUrl: string) => void + put: (body: object, requestUrl: string) => void }>; let mockAnalytics: jasmine.SpyObj<{ post: (body: object) => void }>; beforeAll((done) => { - mockPactBroker = jasmine.createSpyObj('mockPactBroker', ['get', 'post']); + mockPactBroker = jasmine.createSpyObj('mockPactBroker', ['get', 'post', 'put']); mockAnalytics = jasmine.createSpyObj('mockAnalytics', ['post']); const expressApp = express(); @@ -93,10 +94,14 @@ describe('swagger-mock-validator/cli', () => { mockAnalytics.post(request.body); response.status(201).end(); }); - expressApp.post('/*', bodyParser.json(), (request, response) => { + expressApp.post(/(.*)/, bodyParser.json(), (request, response) => { mockPactBroker.post(request.body, request.url); response.status(201).end(); }); + expressApp.post(/(.*)/, bodyParser.json(), (request, response) => { + mockPactBroker.put(request.body, request.url); + response.status(201).end(); + }); expressApp.use(express.static('.')); mockServer = expressApp.listen(serverPort, done); @@ -106,6 +111,7 @@ describe('swagger-mock-validator/cli', () => { mockAnalytics.post.calls.reset(); mockPactBroker.get.calls.reset(); mockPactBroker.post.calls.reset(); + mockPactBroker.post.calls.reset(); }); afterAll((done) => mockServer.close(done));