mirror of https://github.com/renovatebot/renovate
953 lines
28 KiB
TypeScript
953 lines
28 KiB
TypeScript
import { Buffer } from 'node:buffer';
|
|
import { codeBlock } from 'common-tags';
|
|
import { DateTime } from 'luxon';
|
|
import * as httpMock from '../../../test/http-mock';
|
|
import { mocked } from '../../../test/util';
|
|
import {
|
|
EXTERNAL_HOST_ERROR,
|
|
PLATFORM_BAD_CREDENTIALS,
|
|
PLATFORM_INTEGRATION_UNAUTHORIZED,
|
|
PLATFORM_RATE_LIMIT_EXCEEDED,
|
|
REPOSITORY_CHANGED,
|
|
} from '../../constants/error-messages';
|
|
import { GithubReleasesDatasource } from '../../modules/datasource/github-releases';
|
|
import * as _repositoryCache from '../cache/repository';
|
|
import type { RepoCacheData } from '../cache/repository/types';
|
|
import * as hostRules from '../host-rules';
|
|
import { GithubHttp, setBaseUrl } from './github';
|
|
import type { GraphqlPageCache } from './github';
|
|
|
|
jest.mock('../cache/repository');
|
|
const repositoryCache = mocked(_repositoryCache);
|
|
|
|
const githubApiHost = 'https://api.github.com';
|
|
|
|
const graphqlQuery = codeBlock`
|
|
query(
|
|
$owner: String!,
|
|
$name: String!,
|
|
$count: Int,
|
|
$cursor: String
|
|
) {
|
|
repository(owner: $name, name: $name) {
|
|
testItem (
|
|
orderBy: { field: UPDATED_AT, direction: DESC },
|
|
filterBy: { createdBy: "someone" },
|
|
first: $count,
|
|
after: $cursor,
|
|
) {
|
|
pageInfo {
|
|
endCursor
|
|
hasNextPage
|
|
}
|
|
nodes {
|
|
number state title body
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
describe('util/http/github', () => {
|
|
let githubApi: GithubHttp;
|
|
let repoCache: RepoCacheData = {};
|
|
|
|
beforeEach(() => {
|
|
delete process.env.RENOVATE_X_REBASE_PAGINATION_LINKS;
|
|
githubApi = new GithubHttp();
|
|
setBaseUrl(githubApiHost);
|
|
repoCache = {};
|
|
repositoryCache.getCache.mockReturnValue(repoCache);
|
|
});
|
|
|
|
afterEach(() => {
|
|
hostRules.clear();
|
|
});
|
|
|
|
describe('HTTP', () => {
|
|
it('supports app mode', async () => {
|
|
hostRules.add({ hostType: 'github', token: 'x-access-token:123test' });
|
|
httpMock.scope(githubApiHost).get('/some-url').reply(200);
|
|
await githubApi.get('/some-url', {
|
|
headers: { accept: 'some-accept' },
|
|
});
|
|
const [req] = httpMock.getTrace();
|
|
expect(req).toBeDefined();
|
|
expect(req.headers.accept).toBe(
|
|
'some-accept, application/vnd.github.machine-man-preview+json',
|
|
);
|
|
expect(req.headers.authorization).toBe('token 123test');
|
|
});
|
|
|
|
it('supports different datasources', async () => {
|
|
const githubApiDatasource = new GithubHttp(GithubReleasesDatasource.id);
|
|
hostRules.add({ hostType: 'github', token: 'abc' });
|
|
hostRules.add({
|
|
hostType: GithubReleasesDatasource.id,
|
|
token: 'def',
|
|
});
|
|
httpMock.scope(githubApiHost).get('/some-url').reply(200);
|
|
await githubApiDatasource.get('/some-url');
|
|
const [req] = httpMock.getTrace();
|
|
expect(req).toBeDefined();
|
|
expect(req.headers.authorization).toBe('token def');
|
|
});
|
|
|
|
it('paginates', async () => {
|
|
const url = '/some-url?per_page=2';
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.get(url)
|
|
.reply(200, ['a', 'b'], {
|
|
link: `<${url}&page=2>; rel="next", <${url}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${url}&page=2`)
|
|
.reply(200, ['c', 'd'], {
|
|
link: `<${url}&page=3>; rel="next", <${url}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${url}&page=3`)
|
|
.reply(200, ['e']);
|
|
const res = await githubApi.getJson(url, { paginate: true });
|
|
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('uses paginationField', async () => {
|
|
const url = '/some-url';
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.get(url)
|
|
.reply(
|
|
200,
|
|
{ the_field: ['a'], total: 4 },
|
|
{
|
|
link: `<${url}?page=2>; rel="next", <${url}?page=3>; rel="last"`,
|
|
},
|
|
)
|
|
.get(`${url}?page=2`)
|
|
.reply(
|
|
200,
|
|
{ the_field: ['b', 'c'], total: 4 },
|
|
{
|
|
link: `<${url}?page=3>; rel="next", <${url}?page=3>; rel="last"`,
|
|
},
|
|
)
|
|
.get(`${url}?page=3`)
|
|
.reply(200, { the_field: ['d'], total: 4 });
|
|
const res: any = await githubApi.getJson('some-url', {
|
|
paginate: true,
|
|
paginationField: 'the_field',
|
|
});
|
|
expect(res.body.the_field).toEqual(['a', 'b', 'c', 'd']);
|
|
});
|
|
|
|
it('paginates with auth and repo', async () => {
|
|
const url = '/some-url?per_page=2';
|
|
hostRules.add({
|
|
hostType: 'github',
|
|
token: 'test',
|
|
matchHost: 'github.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'github',
|
|
token: 'abc',
|
|
matchHost: 'https://api.github.com/repos/some/repo',
|
|
});
|
|
httpMock
|
|
.scope(githubApiHost, {
|
|
reqheaders: {
|
|
authorization: 'token abc',
|
|
accept: 'application/json, application/vnd.github.v3+json',
|
|
},
|
|
})
|
|
.get(url)
|
|
.reply(200, ['a', 'b'], {
|
|
link: `<${url}&page=2>; rel="next", <${url}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${url}&page=2`)
|
|
.reply(200, ['c', 'd'], {
|
|
link: `<${url}&page=3>; rel="next", <${url}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${url}&page=3`)
|
|
.reply(200, ['e']);
|
|
const res = await githubApi.getJson(url, {
|
|
paginate: true,
|
|
repository: 'some/repo',
|
|
});
|
|
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('paginates with auth and repo on GHE', async () => {
|
|
const url = '/api/v3/some-url?per_page=2';
|
|
hostRules.add({
|
|
hostType: 'github',
|
|
token: 'test',
|
|
matchHost: 'github.domain.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'github',
|
|
token: 'abc',
|
|
matchHost: 'https://github.domain.com/api/v3/repos/some/repo',
|
|
});
|
|
httpMock
|
|
.scope('https://github.domain.com', {
|
|
reqheaders: {
|
|
authorization: 'token abc',
|
|
accept:
|
|
'application/vnd.github.antiope-preview+json, application/vnd.github.v3+json',
|
|
},
|
|
})
|
|
.get(url)
|
|
.reply(200, ['a', 'b'], {
|
|
link: `<${url}&page=2>; rel="next", <${url}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${url}&page=2`)
|
|
.reply(200, ['c', 'd'], {
|
|
link: `<${url}&page=3>; rel="next", <${url}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${url}&page=3`)
|
|
.reply(200, ['e']);
|
|
const res = await githubApi.getJson(url, {
|
|
paginate: true,
|
|
repository: 'some/repo',
|
|
baseUrl: 'https://github.domain.com',
|
|
headers: {
|
|
accept: 'application/vnd.github.antiope-preview+json',
|
|
},
|
|
});
|
|
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('attempts to paginate', async () => {
|
|
const url = '/some-url';
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.get(url)
|
|
.reply(200, ['a'], {
|
|
link: `<${url}?page=34>; rel="last"`,
|
|
});
|
|
const res = await githubApi.getJson('some-url', { paginate: true });
|
|
expect(res).toBeDefined();
|
|
expect(res.body).toEqual(['a']);
|
|
});
|
|
|
|
it('rebases GHE Server pagination links', async () => {
|
|
process.env.RENOVATE_X_REBASE_PAGINATION_LINKS = '1';
|
|
// The origin and base URL which Renovate uses (from its config) to reach GHE:
|
|
const baseUrl = 'http://ghe.alternative.domain.com/api/v3';
|
|
setBaseUrl(baseUrl);
|
|
// The hostname from GHE settings, which users use through their browsers to reach GHE:
|
|
// https://docs.github.com/en/enterprise-server@3.5/admin/configuration/configuring-network-settings/configuring-a-hostname
|
|
const gheHostname = 'ghe.mycompany.com';
|
|
// GHE replies to paginated requests with a Link response header whose URLs have this base
|
|
const gheBaseUrl = `https://${gheHostname}/api/v3`;
|
|
const apiUrl = '/some-url?per_page=2';
|
|
httpMock
|
|
.scope(baseUrl)
|
|
.get(apiUrl)
|
|
.reply(200, ['a', 'b'], {
|
|
link: `<${gheBaseUrl}${apiUrl}&page=2>; rel="next", <${gheBaseUrl}${apiUrl}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${apiUrl}&page=2`)
|
|
.reply(200, ['c', 'd'], {
|
|
link: `<${gheBaseUrl}${apiUrl}&page=3>; rel="next", <${gheBaseUrl}${apiUrl}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${apiUrl}&page=3`)
|
|
.reply(200, ['e']);
|
|
const res = await githubApi.getJson(apiUrl, { paginate: true });
|
|
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('preserves pagination links by default', async () => {
|
|
const baseUrl = 'http://ghe.alternative.domain.com/api/v3';
|
|
setBaseUrl(baseUrl);
|
|
const apiUrl = '/some-url?per_page=2';
|
|
httpMock
|
|
.scope(baseUrl)
|
|
.get(apiUrl)
|
|
.reply(200, ['a', 'b'], {
|
|
link: `<${baseUrl}${apiUrl}&page=2>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${apiUrl}&page=2`)
|
|
.reply(200, ['c', 'd'], {
|
|
link: `<${baseUrl}${apiUrl}&page=3>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
|
|
})
|
|
.get(`${apiUrl}&page=3`)
|
|
.reply(200, ['e']);
|
|
const res = await githubApi.getJson(apiUrl, { paginate: true });
|
|
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('preserves pagination links for github.com', async () => {
|
|
process.env.RENOVATE_X_REBASE_PAGINATION_LINKS = '1';
|
|
const baseUrl = 'https://api.github.com/';
|
|
|
|
setBaseUrl(baseUrl);
|
|
const apiUrl = 'some-url?per_page=2';
|
|
httpMock
|
|
.scope(baseUrl)
|
|
.get('/' + apiUrl)
|
|
.reply(200, ['a', 'b'], {
|
|
link: `<${baseUrl}${apiUrl}&page=2>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
|
|
})
|
|
.get(`/${apiUrl}&page=2`)
|
|
.reply(200, ['c', 'd'], {
|
|
link: `<${baseUrl}${apiUrl}&page=3>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
|
|
})
|
|
.get(`/${apiUrl}&page=3`)
|
|
.reply(200, ['e']);
|
|
const res = await githubApi.getJson(apiUrl, { paginate: true });
|
|
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
describe('handleGotError', () => {
|
|
async function fail(
|
|
code: number,
|
|
body: any = undefined,
|
|
headers: httpMock.ReplyHeaders = {},
|
|
) {
|
|
const url = '/some-url';
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.get(url)
|
|
.reply(
|
|
code,
|
|
function reply() {
|
|
// https://github.com/nock/nock/issues/1979
|
|
if (typeof body === 'object' && 'message' in body) {
|
|
(this.req as any).response.statusMessage = body?.message;
|
|
}
|
|
return body;
|
|
},
|
|
headers,
|
|
);
|
|
await githubApi.getJson(url);
|
|
}
|
|
|
|
async function failWithError(error: string | Record<string, unknown>) {
|
|
const url = '/some-url';
|
|
httpMock.scope(githubApiHost).get(url).replyWithError(error);
|
|
await githubApi.getJson(url);
|
|
}
|
|
|
|
it('should throw Not found', async () => {
|
|
await expect(fail(404)).rejects.toThrow(
|
|
'Response code 404 (Not Found)',
|
|
);
|
|
});
|
|
|
|
it('should throw 410', async () => {
|
|
await expect(
|
|
fail(410, { message: 'Issues are disabled for this repo' }),
|
|
).rejects.toThrow(
|
|
'Response code 410 (Issues are disabled for this repo)',
|
|
);
|
|
});
|
|
|
|
it('should throw rate limit exceeded', async () => {
|
|
await expect(
|
|
fail(403, {
|
|
message:
|
|
'Error updating branch: API rate limit exceeded for installation ID 48411. (403)',
|
|
}),
|
|
).rejects.toThrow(PLATFORM_RATE_LIMIT_EXCEEDED);
|
|
});
|
|
|
|
it('should throw secondary rate limit exceeded', async () => {
|
|
await expect(
|
|
fail(403, {
|
|
message:
|
|
'You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.',
|
|
}),
|
|
).rejects.toThrow(PLATFORM_RATE_LIMIT_EXCEEDED);
|
|
});
|
|
|
|
it('should throw Bad credentials', async () => {
|
|
await expect(
|
|
fail(401, { message: 'Bad credentials. (401)' }),
|
|
).rejects.toThrow(PLATFORM_BAD_CREDENTIALS);
|
|
});
|
|
|
|
it('should throw platform failure', async () => {
|
|
await expect(
|
|
fail(
|
|
401,
|
|
{ message: 'Bad credentials. (401)' },
|
|
{
|
|
'x-ratelimit-limit': '60',
|
|
},
|
|
),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw platform failure for ENOTFOUND, ETIMEDOUT or EAI_AGAIN', async () => {
|
|
const codes = ['ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN'];
|
|
for (let idx = 0; idx < codes.length; idx += 1) {
|
|
const code = codes[idx];
|
|
await expect(failWithError({ code })).rejects.toThrow(
|
|
EXTERNAL_HOST_ERROR,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should throw platform failure for 500', async () => {
|
|
await expect(fail(500)).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw platform failure ParseError', async () => {
|
|
await expect(fail(200, '{{')).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw for unauthorized integration', async () => {
|
|
await expect(
|
|
fail(403, {
|
|
message: 'Resource not accessible by integration (403)',
|
|
}),
|
|
).rejects.toThrow(PLATFORM_INTEGRATION_UNAUTHORIZED);
|
|
});
|
|
|
|
it('should throw for unauthorized integration2', async () => {
|
|
await expect(
|
|
fail(403, { message: 'Upgrade to GitHub Pro' }),
|
|
).rejects.toThrow('Upgrade to GitHub Pro');
|
|
});
|
|
|
|
it('should throw on abuse', async () => {
|
|
await expect(
|
|
fail(403, {
|
|
message: 'You have triggered an abuse detection mechanism',
|
|
}),
|
|
).rejects.toThrow(PLATFORM_RATE_LIMIT_EXCEEDED);
|
|
});
|
|
|
|
it('should throw on repository change', async () => {
|
|
await expect(
|
|
fail(422, {
|
|
message: 'foobar',
|
|
errors: [{ code: 'invalid' }],
|
|
}),
|
|
).rejects.toThrow(REPOSITORY_CHANGED);
|
|
});
|
|
|
|
it('should throw platform failure on 422 response', async () => {
|
|
await expect(
|
|
fail(422, {
|
|
message: 'foobar',
|
|
}),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw original error when failed to add reviewers', async () => {
|
|
await expect(
|
|
fail(422, {
|
|
message: 'Review cannot be requested from pull request author.',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Review cannot be requested from pull request author.',
|
|
);
|
|
});
|
|
|
|
it('should throw original error when pull requests aleady existed', async () => {
|
|
await expect(
|
|
fail(422, {
|
|
message: 'Validation error',
|
|
errors: [{ message: 'A pull request already exists' }],
|
|
}),
|
|
).rejects.toThrow('Validation error');
|
|
});
|
|
|
|
it('should throw original error of unknown type', async () => {
|
|
await expect(
|
|
fail(418, {
|
|
message: 'Sorry, this is a teapot',
|
|
}),
|
|
).rejects.toThrow('Sorry, this is a teapot');
|
|
});
|
|
|
|
it('should throw original error when milestone not found', async () => {
|
|
const milestoneNotFoundError = {
|
|
message: 'Validation Failed',
|
|
errors: [
|
|
{
|
|
value: 1,
|
|
resource: 'Issue',
|
|
field: 'milestone',
|
|
code: 'invalid',
|
|
},
|
|
],
|
|
documentation_url:
|
|
'https://docs.github.com/rest/issues/issues#update-an-issue',
|
|
};
|
|
|
|
await expect(fail(422, milestoneNotFoundError)).rejects.toThrow(
|
|
'Validation Failed',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GraphQL', () => {
|
|
const page1 = {
|
|
data: {
|
|
repository: {
|
|
testItem: {
|
|
pageInfo: {
|
|
endCursor: 'cursor1',
|
|
hasNextPage: true,
|
|
},
|
|
nodes: [
|
|
{
|
|
number: 1,
|
|
state: 'OPEN',
|
|
title: 'title-1',
|
|
body: 'the body 1',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const page2 = {
|
|
data: {
|
|
repository: {
|
|
testItem: {
|
|
pageInfo: {
|
|
endCursor: 'cursor2',
|
|
hasNextPage: true,
|
|
},
|
|
nodes: [
|
|
{
|
|
number: 2,
|
|
state: 'CLOSED',
|
|
title: 'title-2',
|
|
body: 'the body 2',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const page3 = {
|
|
data: {
|
|
repository: {
|
|
testItem: {
|
|
pageInfo: {
|
|
endCursor: 'cursor3',
|
|
hasNextPage: false,
|
|
},
|
|
nodes: [
|
|
{
|
|
number: 3,
|
|
state: 'OPEN',
|
|
title: 'title-3',
|
|
body: 'the body 3',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
it('strips path from baseUrl', async () => {
|
|
setBaseUrl('https://ghe.mycompany.com/api/v3/');
|
|
const repository = { foo: 'foo', bar: 'bar' };
|
|
httpMock
|
|
.scope('https://ghe.mycompany.com')
|
|
.post('/api/graphql')
|
|
.reply(200, { data: { repository } });
|
|
await githubApi.requestGraphql(graphqlQuery);
|
|
const [req] = httpMock.getTrace();
|
|
expect(req).toBeDefined();
|
|
expect(req.url).toBe('https://ghe.mycompany.com/api/graphql');
|
|
});
|
|
|
|
it('supports app mode', async () => {
|
|
hostRules.add({ hostType: 'github', token: 'x-access-token:123test' });
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, { data: { repository: { testItem: 'XXX' } } });
|
|
await githubApi.queryRepoField(graphqlQuery, 'testItem', {
|
|
paginate: false,
|
|
});
|
|
const [req] = httpMock.getTrace();
|
|
expect(req).toBeDefined();
|
|
expect(req.headers.accept).toBe(
|
|
'application/vnd.github.machine-man-preview+json',
|
|
);
|
|
});
|
|
|
|
it('returns empty array for undefined data', async () => {
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, {
|
|
data: {
|
|
someprop: 'someval',
|
|
},
|
|
});
|
|
expect(
|
|
await githubApi.queryRepoField(graphqlQuery, 'testItem', {
|
|
paginate: false,
|
|
}),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('returns empty array for undefined data.', async () => {
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, {
|
|
data: { repository: { otherField: 'someval' } },
|
|
});
|
|
expect(
|
|
await githubApi.queryRepoField(graphqlQuery, 'testItem', {
|
|
paginate: false,
|
|
}),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('throws errors for invalid responses', async () => {
|
|
httpMock.scope(githubApiHost).post('/graphql').reply(418);
|
|
await expect(
|
|
githubApi.queryRepoField(graphqlQuery, 'someItem', {
|
|
paginate: false,
|
|
}),
|
|
).rejects.toThrow("Response code 418 (I'm a Teapot)");
|
|
});
|
|
|
|
it('halves node count and retries request', async () => {
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.persist()
|
|
.post('/graphql')
|
|
.reply(200, {
|
|
data: {
|
|
someprop: 'someval',
|
|
},
|
|
});
|
|
expect(
|
|
await githubApi.queryRepoField(graphqlQuery, 'testItem'),
|
|
).toMatchInlineSnapshot(`[]`);
|
|
});
|
|
|
|
it('queryRepo', async () => {
|
|
const repository = {
|
|
foo: 'foo',
|
|
bar: 'bar',
|
|
};
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, { data: { repository } });
|
|
|
|
const res = await githubApi.requestGraphql(graphqlQuery);
|
|
expect(res?.data).toEqual({ repository });
|
|
});
|
|
|
|
it('queryRepoField', async () => {
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, page1)
|
|
.post('/graphql')
|
|
.reply(200, page2)
|
|
.post('/graphql')
|
|
.reply(200, page3);
|
|
|
|
const items = await githubApi.queryRepoField(graphqlQuery, 'testItem');
|
|
expect(items).toHaveLength(3);
|
|
});
|
|
|
|
it('limit result size', async () => {
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, page1)
|
|
.post('/graphql')
|
|
.reply(200, page2);
|
|
|
|
const items = await githubApi.queryRepoField(graphqlQuery, 'testItem', {
|
|
limit: 2,
|
|
});
|
|
expect(items).toHaveLength(2);
|
|
});
|
|
|
|
it('shrinks items count on 50x', async () => {
|
|
repoCache.platform ??= {};
|
|
repoCache.platform.github ??= {};
|
|
repoCache.platform.github.graphqlPageCache = {
|
|
testItem: {
|
|
pageLastResizedAt: DateTime.local().toISO(),
|
|
pageSize: 50,
|
|
},
|
|
};
|
|
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, page1)
|
|
.post('/graphql')
|
|
.reply(500)
|
|
.post('/graphql')
|
|
.reply(200, page2)
|
|
.post('/graphql')
|
|
.reply(200, page3);
|
|
|
|
const items = await githubApi.queryRepoField(graphqlQuery, 'testItem');
|
|
expect(items).toHaveLength(3);
|
|
|
|
const graphqlPageCache = repoCache?.platform?.github?.[
|
|
'graphqlPageCache'
|
|
] as GraphqlPageCache;
|
|
expect(graphqlPageCache?.['testItem']?.pageSize).toBe(25);
|
|
});
|
|
|
|
it('expands items count on timeout', async () => {
|
|
repoCache.platform ??= {};
|
|
repoCache.platform.github ??= {};
|
|
repoCache.platform.github.graphqlPageCache = {
|
|
testItem: {
|
|
pageLastResizedAt: DateTime.local()
|
|
.minus({ hours: 24, seconds: 1 })
|
|
.toISO(),
|
|
pageSize: 42,
|
|
},
|
|
};
|
|
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, page1)
|
|
.post('/graphql')
|
|
.reply(200, page2)
|
|
.post('/graphql')
|
|
.reply(200, page3);
|
|
|
|
const items = await githubApi.queryRepoField(graphqlQuery, 'testItem');
|
|
expect(items).toHaveLength(3);
|
|
const graphqlPageCache = repoCache?.platform?.github?.[
|
|
'graphqlPageCache'
|
|
] as GraphqlPageCache;
|
|
expect(graphqlPageCache?.['testItem']?.pageSize).toBe(84);
|
|
});
|
|
|
|
it('continues to iterate with a lower page size on error 502', async () => {
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(502)
|
|
.post('/graphql')
|
|
.reply(200, page1)
|
|
.post('/graphql')
|
|
.reply(200, page2)
|
|
.post('/graphql')
|
|
.reply(200, page3);
|
|
|
|
const items = await githubApi.queryRepoField(graphqlQuery, 'testItem');
|
|
expect(items).toHaveLength(3);
|
|
});
|
|
|
|
it('removes cache record once expanded to the maximum', async () => {
|
|
repoCache.platform ??= {};
|
|
repoCache.platform.github ??= {};
|
|
repoCache.platform.github.graphqlPageCache = {
|
|
testItem: {
|
|
pageLastResizedAt: DateTime.local()
|
|
.minus({ hours: 24, seconds: 1 })
|
|
.toISO(),
|
|
pageSize: 50,
|
|
},
|
|
};
|
|
|
|
httpMock
|
|
.scope(githubApiHost)
|
|
.post('/graphql')
|
|
.reply(200, page1)
|
|
.post('/graphql')
|
|
.reply(200, page2)
|
|
.post('/graphql')
|
|
.reply(200, page3);
|
|
|
|
const items = await githubApi.queryRepoField(graphqlQuery, 'testItem');
|
|
expect(items).toHaveLength(3);
|
|
const graphqlPageCache = repoCache?.platform?.github?.[
|
|
'graphqlPageCache'
|
|
] as GraphqlPageCache;
|
|
expect(graphqlPageCache?.['testItem']).toBeUndefined();
|
|
});
|
|
|
|
it('throws on 50x if count < 10', async () => {
|
|
httpMock.scope(githubApiHost).post('/graphql').reply(500);
|
|
await expect(
|
|
githubApi.queryRepoField(graphqlQuery, 'testItem', {
|
|
count: 9,
|
|
}),
|
|
).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();
|
|
});
|
|
});
|
|
});
|