-
Notifications
You must be signed in to change notification settings - Fork 3
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
Separating github api calls into new file and adding some tests #68
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import axios, { AxiosResponse } from 'axios'; | ||
import { DEFAULT_USER_AGENT_HEADERS } from '../http/getAxiosConfig'; | ||
import { GithubReleaseData, GithubRepoFile } from '../types/Github'; | ||
|
||
const GITHUB_REPOS_API = 'https://api.github.com/repos'; | ||
const GITHUB_RAW_CONTENT_API_PATH = 'https://raw.githubusercontent.com'; | ||
|
||
declare global { | ||
// eslint-disable-next-line no-var | ||
var githubToken: string; | ||
} | ||
|
||
type RepoPath = `${string}/${string}`; | ||
|
||
const GITHUB_AUTH_HEADERS = { | ||
authorization: | ||
global && global.githubToken ? `Bearer ${global.githubToken}` : null, | ||
}; | ||
|
||
// Returns information about the repo's releases. Defaults to "latest" if no tag is provided | ||
// https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name | ||
export async function fetchRepoReleaseData( | ||
repoPath: RepoPath, | ||
tag = '' | ||
): Promise<AxiosResponse<GithubReleaseData>> { | ||
const URL = `${GITHUB_REPOS_API}/${repoPath}/releases`; | ||
|
||
return axios.get<GithubReleaseData>( | ||
`${URL}/${tag ? `tags/${tag}` : 'latest'}`, | ||
{ | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
} | ||
); | ||
} | ||
|
||
// Returns the entire repo content as a zip, using the zipball_url from fetchRepoReleaseData() | ||
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#download-a-repository-archive-zip | ||
export async function fetchRepoAsZip( | ||
zipUrl: string | ||
): Promise<AxiosResponse<Buffer>> { | ||
return axios.get<Buffer>(zipUrl, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
}); | ||
} | ||
|
||
// Returns the raw file contents via the raw.githubusercontent endpoint | ||
export async function fetchRepoFile( | ||
repoPath: RepoPath, | ||
filePath: string, | ||
ref: string | ||
): Promise<AxiosResponse<Buffer>> { | ||
return axios.get<Buffer>( | ||
`${GITHUB_RAW_CONTENT_API_PATH}/${repoPath}/${ref}/${filePath}`, | ||
{ | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
} | ||
); | ||
} | ||
|
||
// Returns the raw file contents via the raw.githubusercontent endpoint | ||
export async function fetchRepoFileByDownloadUrl( | ||
downloadUrl: string | ||
): Promise<AxiosResponse<Buffer>> { | ||
return axios.get<Buffer>(downloadUrl, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
}); | ||
} | ||
|
||
// Returns the contents of a file or directory in a repository by path | ||
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content | ||
export async function fetchRepoContents( | ||
repoPath: RepoPath, | ||
path: string, | ||
ref?: string | ||
): Promise<AxiosResponse<Array<GithubRepoFile>>> { | ||
const refQuery = ref ? `?ref=${ref}` : ''; | ||
|
||
return axios.get<Array<GithubRepoFile>>( | ||
`${GITHUB_REPOS_API}/${repoPath}/contents/${path}${refQuery}`, | ||
{ | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
} | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { fetchFileFromRepository } from '../github'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests for the github utils will come in a follow up PR |
||
import { fetchRepoFile as __fetchRepoFile } from '../../api/github'; | ||
|
||
jest.mock('../../api/github'); | ||
|
||
const fetchRepoFile = __fetchRepoFile as jest.MockedFunction< | ||
typeof __fetchRepoFile | ||
>; | ||
|
||
describe('lib/github', () => { | ||
describe('fetchFileFromRepository()', () => { | ||
beforeAll(() => { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
fetchRepoFile.mockResolvedValue({ data: null } as any); | ||
}); | ||
|
||
afterAll(() => { | ||
fetchRepoFile.mockReset(); | ||
}); | ||
|
||
it('downloads a github repo and writes it to a destination folder', async () => { | ||
await fetchFileFromRepository('owner/repo', 'file', 'ref'); | ||
expect(fetchRepoFile).toHaveBeenCalledWith('owner/repo', 'file', 'ref'); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import axios from 'axios'; | ||
import { trackUsage } from '../trackUsage'; | ||
import { | ||
getAccountConfig as __getAccountConfig, | ||
getAndLoadConfigIfNeeded as __getAndLoadConfigIfNeeded, | ||
} from '../../config'; | ||
import { AuthType } from '../../types/Accounts'; | ||
import { ENVIRONMENTS } from '../../constants/environments'; | ||
|
||
jest.mock('axios'); | ||
jest.mock('../../config'); | ||
|
||
const mockedAxios = jest.mocked(axios); | ||
const getAccountConfig = __getAccountConfig as jest.MockedFunction< | ||
typeof __getAccountConfig | ||
>; | ||
const getAndLoadConfigIfNeeded = | ||
__getAndLoadConfigIfNeeded as jest.MockedFunction< | ||
typeof __getAndLoadConfigIfNeeded | ||
>; | ||
|
||
mockedAxios.mockResolvedValue({}); | ||
getAndLoadConfigIfNeeded.mockReturnValue({}); | ||
|
||
const account = { | ||
accountId: 12345, | ||
authType: 'personalaccesskey' as AuthType, | ||
personalAccessKey: 'let-me-in-3', | ||
auth: { | ||
tokenInfo: { | ||
expiresAt: '', | ||
accessToken: 'test-token', | ||
}, | ||
}, | ||
env: ENVIRONMENTS.QA, | ||
}; | ||
|
||
const usageTrackingMeta = { | ||
action: 'cli-command', | ||
command: 'test-command', | ||
}; | ||
|
||
describe('lib/trackUsage', () => { | ||
describe('trackUsage()', () => { | ||
beforeEach(() => { | ||
mockedAxios.mockClear(); | ||
getAccountConfig.mockReset(); | ||
getAccountConfig.mockReturnValue(account); | ||
}); | ||
|
||
it('tracks correctly for unauthenticated accounts', async () => { | ||
await trackUsage('test-action', 'INTERACTION', usageTrackingMeta); | ||
const requestArgs = mockedAxios.mock.lastCall | ||
? mockedAxios.mock.lastCall[0] | ||
: ({} as any); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
|
||
expect(mockedAxios).toHaveBeenCalled(); | ||
expect(requestArgs!.data.eventName).toEqual('test-action'); | ||
expect(requestArgs!.url.includes('authenticated')).toBeFalsy(); | ||
expect(getAccountConfig).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('tracks correctly for authenticated accounts', async () => { | ||
await trackUsage('test-action', 'INTERACTION', usageTrackingMeta, 12345); | ||
const requestArgs = mockedAxios.mock.lastCall | ||
? mockedAxios.mock.lastCall[0] | ||
: ({} as any); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
|
||
expect(mockedAxios).toHaveBeenCalled(); | ||
expect(requestArgs!.data.eventName).toEqual('test-action'); | ||
expect(requestArgs!.url.includes('authenticated')).toBeTruthy(); | ||
expect(getAccountConfig).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
import fs from 'fs-extra'; | ||
import { HUBL_EXTENSIONS } from '../constants/extensions'; | ||
import { validateHubl } from '../api/validateHubl'; | ||
import { walk } from './fs'; | ||
import { getExt } from './path'; | ||
import { LintResult } from '../types/HublValidation'; | ||
import { HUBL_EXTENSIONS } from '../../constants/extensions'; | ||
import { validateHubl } from '../../api/validateHubl'; | ||
import { walk } from '../fs'; | ||
import { getExt } from '../path'; | ||
import { LintResult } from '../../types/HublValidation'; | ||
|
||
export async function lint( | ||
accountId: number, | ||
Comment on lines
1
to
9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate is already being used in the CLI, so this is technically a breaking change. Don't think it's a big deal to do now, we'll just have to remember to update it in the CLI whenever we pull in the next release. I do think it makes sense for it to live under /cms There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call out, thanks for the heads up. Is the currently-published version of the CLI using it? I think we should be safe if we push this out as a minor update right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it's live on the current beta release. I think bumping the minor version should be safe |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
import axios from 'axios'; | ||
import path from 'path'; | ||
import fs from 'fs-extra'; | ||
|
||
|
@@ -7,42 +6,37 @@ import { throwError, throwErrorWithMessage } from '../errors/standardErrors'; | |
import { extractZipArchive } from './archive'; | ||
|
||
import { GITHUB_RELEASE_TYPES } from '../constants/github'; | ||
import { DEFAULT_USER_AGENT_HEADERS } from '../http/getAxiosConfig'; | ||
import { BaseError } from '../types/Error'; | ||
import { GithubReleaseData, GithubRepoFile } from '../types/Github'; | ||
import { ValueOf } from '../types/Utils'; | ||
import { LogCallbacksArg } from '../types/LogCallbacks'; | ||
import { | ||
fetchRepoFile, | ||
fetchRepoFileByDownloadUrl, | ||
fetchRepoAsZip, | ||
fetchRepoReleaseData, | ||
fetchRepoContents, | ||
} from '../api/github'; | ||
|
||
const i18nKey = 'lib.github'; | ||
|
||
declare global { | ||
// eslint-disable-next-line no-var | ||
var githubToken: string; | ||
} | ||
|
||
type RepoPath = `${string}/${string}`; | ||
|
||
const GITHUB_AUTH_HEADERS = { | ||
authorization: | ||
global && global.githubToken ? `Bearer ${global.githubToken}` : null, | ||
}; | ||
|
||
export async function fetchJsonFromRepository( | ||
export async function fetchFileFromRepository( | ||
repoPath: RepoPath, | ||
filePath: string, | ||
ref: string | ||
): Promise<JSON> { | ||
): Promise<Buffer> { | ||
try { | ||
const URL = `https://raw.githubusercontent.com/${repoPath}/${ref}/${filePath}`; | ||
debug(`${i18nKey}.fetchJsonFromRepository.fetching`, { url: URL }); | ||
|
||
const { data } = await axios.get<JSON>(URL, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
debug(`${i18nKey}.fetchFileFromRepository.fetching`, { | ||
path: `${repoPath}/${ref}/${filePath}`, | ||
}); | ||
|
||
const { data } = await fetchRepoFile(repoPath, filePath, ref); | ||
return data; | ||
} catch (err) { | ||
throwErrorWithMessage( | ||
`${i18nKey}.fetchJsonFromRepository.errors.fetchFail`, | ||
`${i18nKey}.fetchFileFromRepository.errors.fetchFail`, | ||
{}, | ||
err as BaseError | ||
); | ||
|
@@ -57,13 +51,8 @@ export async function fetchReleaseData( | |
if (tag.length && tag[0] !== 'v') { | ||
tag = `v${tag}`; | ||
} | ||
const URI = tag | ||
? `https://api.github.com/repos/${repoPath}/releases/tags/${tag}` | ||
: `https://api.github.com/repos/${repoPath}/releases/latest`; | ||
try { | ||
const { data } = await axios.get<GithubReleaseData>(URI, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
}); | ||
const { data } = await fetchRepoReleaseData(repoPath, tag); | ||
return data; | ||
} catch (err) { | ||
const error = err as BaseError; | ||
|
@@ -99,9 +88,7 @@ async function downloadGithubRepoZip( | |
const { name } = releaseData; | ||
debug(`${i18nKey}.downloadGithubRepoZip.fetchingName`, { name }); | ||
} | ||
const { data } = await axios.get<Buffer>(zipUrl, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
}); | ||
const { data } = await fetchRepoAsZip(zipUrl); | ||
debug(`${i18nKey}.downloadGithubRepoZip.completed`); | ||
return data; | ||
} catch (err) { | ||
|
@@ -144,29 +131,11 @@ export async function cloneGithubRepo( | |
return success; | ||
} | ||
|
||
async function getGitHubRepoContentsAtPath( | ||
repoPath: RepoPath, | ||
path: string, | ||
ref?: string | ||
): Promise<Array<GithubRepoFile>> { | ||
const refQuery = ref ? `?ref=${ref}` : ''; | ||
const contentsRequestUrl = `https://api.github.com/repos/${repoPath}/contents/${path}${refQuery}`; | ||
|
||
const response = await axios.get<Array<GithubRepoFile>>(contentsRequestUrl, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
}); | ||
|
||
return response.data; | ||
} | ||
|
||
async function fetchGitHubRepoContentFromDownloadUrl( | ||
dest: string, | ||
downloadUrl: string | ||
): Promise<void> { | ||
const resp = await axios.get<Buffer>(downloadUrl, { | ||
headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, | ||
}); | ||
|
||
const resp = await fetchRepoFileByDownloadUrl(downloadUrl); | ||
fs.writeFileSync(dest, resp.data, 'utf8'); | ||
} | ||
|
||
|
@@ -181,7 +150,7 @@ export async function downloadGithubRepoContents( | |
fs.ensureDirSync(path.dirname(dest)); | ||
|
||
try { | ||
const contentsResp = await getGitHubRepoContentsAtPath( | ||
const { data: contentsResp } = await fetchRepoContents( | ||
repoPath, | ||
contentPath, | ||
ref | ||
|
@@ -190,7 +159,11 @@ export async function downloadGithubRepoContents( | |
const downloadContent = async ( | ||
contentPiece: GithubRepoFile | ||
): Promise<void> => { | ||
const { path: contentPiecePath, download_url } = contentPiece; | ||
const { | ||
path: contentPiecePath, | ||
download_url, | ||
type: contentPieceType, | ||
} = contentPiece; | ||
const downloadPath = path.join( | ||
dest, | ||
contentPiecePath.replace(contentPath, '') | ||
|
@@ -206,6 +179,16 @@ export async function downloadGithubRepoContents( | |
downloadPath, | ||
}); | ||
|
||
if (contentPieceType === 'dir') { | ||
const { data: innerDirContent } = await fetchRepoContents( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this to get parity between the util here and in cli-lib. I'd appreciate a second pair of eyes on this, but I'm pretty sure we need this in order to be able to handle downloading deeply nested folders and files. Otherwise our recursion only ever goes one level deep. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh good catch. It looks like the check for 'dir' was added after this file was initially ported to local dev lib so it must have gotten skipped over here |
||
repoPath, | ||
contentPiecePath, | ||
ref | ||
); | ||
await Promise.all(innerDirContent.map(downloadContent)); | ||
return Promise.resolve(); | ||
} | ||
|
||
return fetchGitHubRepoContentFromDownloadUrl(downloadPath, download_url); | ||
}; | ||
|
||
|
@@ -217,7 +200,7 @@ export async function downloadGithubRepoContents( | |
contentPromises = [downloadContent(contentsResp)]; | ||
} | ||
|
||
Promise.all(contentPromises); | ||
await Promise.all(contentPromises); | ||
} catch (e) { | ||
const error = e as BaseError; | ||
if (error?.error?.message) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving the github api calls into here did a few things: