From 1f3ab66d3355a2f932cfb40be2ccb2c35f7c4816 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer Date: Wed, 21 Aug 2024 14:56:27 +0200 Subject: [PATCH] chore(http/github): add utility function to fetch raw files (#30155) Co-authored-by: Rhys Arkins --- lib/util/http/github.spec.ts | 163 +++++++++++++++++++++++++++++++++++ lib/util/http/github.ts | 37 ++++++++ 2 files changed, 200 insertions(+) diff --git a/lib/util/http/github.spec.ts b/lib/util/http/github.spec.ts index 68af7bc831436d..31292e7b3a0487 100644 --- a/lib/util/http/github.spec.ts +++ b/lib/util/http/github.spec.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'node:buffer'; import { codeBlock } from 'common-tags'; import { DateTime } from 'luxon'; import * as httpMock from '../../../test/http-mock'; @@ -786,4 +787,166 @@ describe('util/http/github', () => { ).rejects.toThrow(EXTERNAL_HOST_ERROR); }); }); + + describe('getRawFile()', () => { + it('add header and return', async () => { + httpMock + .scope(githubApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile( + `${githubApiHost}/foo/bar/contents/lore/ipsum.txt`, + ), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support relative path', async () => { + httpMock + .scope(githubApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile( + `${githubApiHost}/foo/bar/contents/foo/../lore/ipsum.txt`, + ), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support default to api.github.com if no baseURL has been supplied', async () => { + httpMock + .scope(githubApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile(`foo/bar/contents/lore/ipsum.txt`), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support custom host if a baseURL has been supplied', async () => { + const customApiHost = 'https://my.comapny.com/api/v3/'; + httpMock + .scope(customApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile(`foo/bar/contents/lore/ipsum.txt`, { + baseUrl: customApiHost, + }), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support default to api.github.com if no baseURL, but repository has been supplied', async () => { + httpMock + .scope(githubApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile(`lore/ipsum.txt`, { + repository: 'foo/bar', + }), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support custom host if a baseURL and repository has been supplied', async () => { + const customApiHost = 'https://my.comapny.com/api/v3/'; + httpMock + .scope(customApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile(`lore/ipsum.txt`, { + baseUrl: customApiHost, + repository: 'foo/bar', + }), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support default to api.github.com if content path is used', async () => { + httpMock + .scope(githubApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'foo'); + await expect( + githubApi.getRawTextFile(`foo/bar/contents/lore/ipsum.txt`), + ).resolves.toMatchObject({ + body: 'foo', + }); + }); + + it('support custom host if content path is used', async () => { + const customApiHost = 'https://my.comapny.com/api/v3/'; + httpMock + .scope(customApiHost) + .get('/foo/bar/contents/lore/ipsum.txt') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, 'test'); + await expect( + githubApi.getRawTextFile(`foo/bar/contents/lore/ipsum.txt`, { + baseUrl: customApiHost, + }), + ).resolves.toMatchObject({ + body: 'test', + }); + }); + + it('throw error if a ', async () => { + httpMock + .scope(githubApiHost) + .get('/foo/bar/contents/lore/ipsum.bin') + .matchHeader( + 'accept', + 'application/vnd.github.raw+json, application/vnd.github.v3+json', + ) + .reply(200, Buffer.from('foo', 'binary')); + await expect( + githubApi.getRawTextFile(`foo/bar/contents/lore/ipsum.bin`, { + responseType: 'buffer', + }), + ).rejects.toThrow(); + }); + }); }); diff --git a/lib/util/http/github.ts b/lib/util/http/github.ts index 3db9d7b4a741dc..56db54db38716c 100644 --- a/lib/util/http/github.ts +++ b/lib/util/http/github.ts @@ -493,4 +493,41 @@ export class GithubHttp extends Http { return result; } + + /** + * Get the raw text file from a URL. + * Only use this method to fetch text files. + * + * @param url Full API URL, contents path or path inside the repository to the file + * @param options + * + * @example url = 'https://api.github.com/repos/renovatebot/renovate/contents/package.json' + * @example url = 'renovatebot/renovate/contents/package.json' + * @example url = 'package.json' & options.repository = 'renovatebot/renovate' + */ + public async getRawTextFile( + url: string, + options: InternalHttpOptions & GithubHttpOptions = {}, + ): Promise { + const newOptions: InternalHttpOptions & GithubHttpOptions = { + ...options, + headers: { + accept: 'application/vnd.github.raw+json', + }, + }; + + let newURL = url; + const httpRegex = regEx(/^https?:\/\//); + if (options.repository && !httpRegex.test(options.repository)) { + newURL = joinUrlParts(options.repository, 'contents', url); + } + + const result = await this.get(newURL, newOptions); + if (!is.string(result.body)) { + throw new Error( + `Expected raw text file but received ${typeof result.body}`, + ); + } + return result; + } }