diff --git a/.changeset/metal-glasses-join.md b/.changeset/metal-glasses-join.md new file mode 100644 index 0000000000..aa7c6ecb9e --- /dev/null +++ b/.changeset/metal-glasses-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +add support for ETag at `BitbucketUrlReader.readUrl` diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 9580780196..eff982581b 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -75,20 +75,74 @@ describe('BitbucketUrlReader', () => { setupRequestMockHandlers(worker); describe('readUrl', () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (_, res, ctx) => res(ctx.status(200), ctx.body('foo')), - ), - ); + it('should be able to readUrl without ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBeNull(); + return res( + ctx.status(200), + ctx.body('foo'), + ctx.set('ETag', 'etag-value'), + ); + }, + ), + ); - it('should be able to readUrl', async () => { const result = await bitbucketProcessor.readUrl( 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', ); const buffer = await result.buffer(); expect(buffer.toString()).toBe('foo'); }); + + it('should be able to readUrl with matching ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBe( + 'matching-etag-value', + ); + return res(ctx.status(304)); + }, + ), + ); + + await expect( + bitbucketProcessor.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + { etag: 'matching-etag-value' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + + it('should be able to readUrl without matching ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBe( + 'previous-etag-value', + ); + return res( + ctx.status(200), + ctx.body('foo'), + ctx.set('ETag', 'new-etag-value'), + ); + }, + ), + ); + + const result = await bitbucketProcessor.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + { etag: 'previous-etag-value' }, + ); + const buffer = await result.buffer(); + expect(buffer.toString()).toBe('foo'); + expect(result.etag).toBe('new-etag-value'); + }); }); describe('read', () => { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index e22137ea69..df22f1633f 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -80,15 +80,17 @@ export class BitbucketUrlReader implements UrlReader { url: string, options?: ReadUrlOptions, ): Promise { - // TODO: etag is not supported yet - const { signal } = options ?? {}; + const { etag, signal } = options ?? {}; const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config); const requestOptions = getBitbucketRequestOptions(this.integration.config); let response: Response; try { response = await fetch(bitbucketUrl.toString(), { - ...requestOptions, + headers: { + ...requestOptions.headers, + ...(etag && { 'If-None-Match': etag }), + }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can be @@ -101,9 +103,14 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(`Unable to read ${url}, ${e}`); } + if (response.status === 304) { + throw new NotModifiedError(); + } + if (response.ok) { return { buffer: async () => Buffer.from(await response.arrayBuffer()), + etag: response.headers.get('ETag') ?? undefined, }; }