Compare commits
71 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a850e9ce56 | ||
|
|
feb1682814 | ||
|
|
8355793520 | ||
|
|
bdfbe3d59b | ||
|
|
a2758608d4 | ||
|
|
2eb3e236a6 | ||
|
|
a91129ea6e | ||
|
|
6f2374c958 | ||
|
|
036c701312 | ||
|
|
99bffd3a86 | ||
|
|
95a439a1cc | ||
|
|
92090bd447 | ||
|
|
e4bbea7a0f | ||
|
|
41028d3f9b | ||
|
|
c4cee3062e | ||
|
|
ec4fdff9f3 | ||
|
|
f5c0a4d05e | ||
|
|
b84ef44dd6 | ||
|
|
4a23c489ab | ||
|
|
f981c12a6a | ||
|
|
64201121e3 | ||
|
|
4fbbf75286 | ||
|
|
d1017a5a4b | ||
|
|
b83415f413 | ||
|
|
45b106eb06 | ||
|
|
6024d08012 | ||
|
|
763a4149aa | ||
|
|
f4677c5118 | ||
|
|
08d4dce92c | ||
|
|
d844681034 | ||
|
|
14ad049f33 | ||
|
|
6d22389e88 | ||
|
|
53762c7ccd | ||
|
|
c73072deed | ||
|
|
42b0033bc8 | ||
|
|
313dc983f1 | ||
|
|
be013e175b | ||
|
|
32f049e3ce | ||
|
|
798368c2a4 | ||
|
|
ed5e4cb03a | ||
|
|
b8c063e983 | ||
|
|
aa2d5f51b5 | ||
|
|
6845dba6af | ||
|
|
8aeb99c260 | ||
|
|
75e31e9d56 | ||
|
|
aa88e4ed89 | ||
|
|
c08c09997b | ||
|
|
268031c69e | ||
|
|
502848c95b | ||
|
|
0e38eab8df | ||
|
|
9f322f55f6 | ||
|
|
f1b643df0d | ||
|
|
db505156ca | ||
|
|
0d70fd97df | ||
|
|
ad9fc3468a | ||
|
|
90c2a0cc5b | ||
|
|
a44e5ab388 | ||
|
|
b8665000f2 | ||
|
|
aa5b6bd734 | ||
|
|
f3dc455c3b | ||
|
|
32cfe6a393 | ||
|
|
a4cdf608b5 | ||
|
|
6dc8aac451 | ||
|
|
002333fb04 | ||
|
|
812b3b4ddd | ||
|
|
eb686ae0bf | ||
|
|
0568b1bf9e | ||
|
|
64f7cf52a5 | ||
|
|
88d4b7e83c | ||
|
|
dfa5ee8535 | ||
|
|
a4dd959aab |
@@ -137,33 +137,33 @@ package_steps: &package_steps
|
||||
jobs:
|
||||
main:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:16
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
main@node-16:
|
||||
main@node-17:
|
||||
docker:
|
||||
- image: circleci/node:16
|
||||
- image: circleci/node:17
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:16
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
integration@node-16:
|
||||
integration@node-17:
|
||||
docker:
|
||||
- image: circleci/node:16
|
||||
- image: circleci/node:17
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:16
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:16
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -224,19 +224,19 @@ jobs:
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:16
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
services@node-16:
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: circleci/node:16
|
||||
- image: circleci/node:17
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
e2e:
|
||||
docker:
|
||||
- image: cypress/base:14.16.0
|
||||
- image: cypress/base:16.13.0
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -285,11 +285,11 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- main@node-16:
|
||||
- main@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- integration@node-16:
|
||||
- integration@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
@@ -307,7 +307,7 @@ workflows:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- services@node-16:
|
||||
- services@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
|
||||
2
.github/workflows/build-docker-image.yml
vendored
2
.github/workflows/build-docker-image.yml
vendored
@@ -18,3 +18,5 @@ jobs:
|
||||
context: .
|
||||
push: false
|
||||
tags: shieldsio/shields:pr-validation
|
||||
build-args: |
|
||||
version=${GITHUB_SHA::7}
|
||||
|
||||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
@@ -45,3 +45,5 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
|
||||
build-args: |
|
||||
version=server-${{ steps.date.outputs.date }}
|
||||
|
||||
2
.github/workflows/publish-docker-next.yml
vendored
2
.github/workflows/publish-docker-next.yml
vendored
@@ -26,3 +26,5 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
tags: shieldsio/shields:next
|
||||
build-args: |
|
||||
version=${GITHUB_SHA::7}
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -4,6 +4,18 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2021-12-01
|
||||
|
||||
- Send better user-agent values [#7309](https://github.com/badges/shields/issues/7309)
|
||||
Self-hosting users now send a user agent which indicates the server version and starts `shields (self-hosted)/` by default.
|
||||
This can be configured using the env var `USER_AGENT_BASE`
|
||||
- upgrade to node 16 [#7271](https://github.com/badges/shields/issues/7271)
|
||||
- feat: deprecate dependabot badges [#7274](https://github.com/badges/shields/issues/7274)
|
||||
- fix: npmversion tagged service test [#7269](https://github.com/badges/shields/issues/7269)
|
||||
- feat: create new Test Results category [#7218](https://github.com/badges/shields/issues/7218)
|
||||
- Migration from Request to Got for all HTTP requests is completed in this release
|
||||
- Dependency updates
|
||||
|
||||
## server-2021-11-04
|
||||
|
||||
- migrate regularUpdate() from request-->got [#7215](https://github.com/badges/shields/issues/7215)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:14-alpine AS Builder
|
||||
FROM node:16-alpine AS Builder
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
RUN mkdir /usr/src/app/private
|
||||
@@ -8,6 +8,7 @@ COPY package.json package-lock.json /usr/src/app/
|
||||
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
|
||||
COPY badge-maker /usr/src/app/badge-maker/
|
||||
|
||||
RUN apk add python3 make g++
|
||||
RUN npm install -g "npm@>=7"
|
||||
# We need dev deps to build the front end. We don't need Cypress, though.
|
||||
RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
|
||||
@@ -18,7 +19,11 @@ RUN npm prune --production
|
||||
RUN npm cache clean --force
|
||||
|
||||
# Use multi-stage build to reduce size
|
||||
FROM node:14-alpine
|
||||
FROM node:16-alpine
|
||||
|
||||
ARG version=dev
|
||||
ENV DOCKER_SHIELDS_VERSION=$version
|
||||
|
||||
# Run the server using production configs.
|
||||
ENV NODE_ENV production
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ You can read a [tutorial on how to add a badge][tutorial].
|
||||
|
||||
## Development
|
||||
|
||||
1. Install Node 14 or later. You can use the [package manager][] of your choice.
|
||||
Tests need to pass in Node 14 and 16.
|
||||
1. Install Node 16 or later. You can use the [package manager][] of your choice.
|
||||
Tests need to pass in Node 16 and 17.
|
||||
2. Clone this repository.
|
||||
3. Run `npm ci` to install the dependencies.
|
||||
4. Run `npm start` to start the badge server and the frontend dev server.
|
||||
|
||||
@@ -64,6 +64,7 @@ public:
|
||||
defaultCacheLengthSeconds: 'BADGE_MAX_AGE_SECONDS'
|
||||
|
||||
fetchLimit: 'FETCH_LIMIT'
|
||||
userAgentBase: 'USER_AGENT_BASE'
|
||||
|
||||
requestTimeoutSeconds: 'REQUEST_TIMEOUT_SECONDS'
|
||||
requestTimeoutMaxAgeSeconds: 'REQUEST_TIMEOUT_MAX_AGE_SECONDS'
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
handleInternalErrors: true
|
||||
|
||||
fetchLimit: '10MB'
|
||||
userAgentBase: 'shields (self-hosted)'
|
||||
|
||||
requestTimeoutSeconds: 120
|
||||
requestTimeoutMaxAgeSeconds: 30
|
||||
|
||||
@@ -74,7 +74,7 @@ class AuthHelper {
|
||||
}
|
||||
|
||||
static _isInsecureSslRequest({ options = {} }) {
|
||||
const { strictSSL = true } = options
|
||||
const strictSSL = options?.https?.rejectUnauthorized ?? true
|
||||
return strictSSL !== true
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ class AuthHelper {
|
||||
}
|
||||
|
||||
get _basicAuth() {
|
||||
const { _user: user, _pass: pass } = this
|
||||
return this.isConfigured ? { user, pass } : undefined
|
||||
const { _user: username, _pass: password } = this
|
||||
return this.isConfigured ? { username, password } : undefined
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -131,7 +131,7 @@ class AuthHelper {
|
||||
const { options, ...rest } = requestParams
|
||||
return {
|
||||
options: {
|
||||
auth,
|
||||
...auth,
|
||||
...options,
|
||||
},
|
||||
...rest,
|
||||
@@ -181,11 +181,13 @@ class AuthHelper {
|
||||
}
|
||||
|
||||
static _mergeQueryParams(requestParams, query) {
|
||||
const { options: { qs: existingQuery, ...restOptions } = {}, ...rest } =
|
||||
requestParams
|
||||
const {
|
||||
options: { searchParams: existingQuery, ...restOptions } = {},
|
||||
...rest
|
||||
} = requestParams
|
||||
return {
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
...existingQuery,
|
||||
...query,
|
||||
},
|
||||
|
||||
@@ -104,14 +104,14 @@ describe('AuthHelper', function () {
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass' },
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' }
|
||||
),
|
||||
]).expect({ user: 'admin', pass: 'abc123' })
|
||||
]).expect({ username: 'admin', password: 'abc123' })
|
||||
given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({
|
||||
user: 'admin',
|
||||
pass: undefined,
|
||||
username: 'admin',
|
||||
password: undefined,
|
||||
})
|
||||
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({
|
||||
user: undefined,
|
||||
pass: 'abc123',
|
||||
username: undefined,
|
||||
password: 'abc123',
|
||||
})
|
||||
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
|
||||
undefined
|
||||
@@ -120,8 +120,8 @@ describe('AuthHelper', function () {
|
||||
{ passKey: 'myci_pass', defaultToEmptyStringForUser: true },
|
||||
{ myci_pass: 'abc123' }
|
||||
).expect({
|
||||
user: '',
|
||||
pass: 'abc123',
|
||||
username: '',
|
||||
password: 'abc123',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -131,15 +131,18 @@ describe('AuthHelper', function () {
|
||||
forCases([
|
||||
given({ url: 'http://example.test' }),
|
||||
given({ url: 'http://example.test', options: {} }),
|
||||
given({ url: 'http://example.test', options: { strictSSL: true } }),
|
||||
given({
|
||||
url: 'http://example.test',
|
||||
options: { strictSSL: undefined },
|
||||
options: { https: { rejectUnauthorized: true } },
|
||||
}),
|
||||
given({
|
||||
url: 'http://example.test',
|
||||
options: { https: { rejectUnauthorized: undefined } },
|
||||
}),
|
||||
]).expect(false)
|
||||
given({
|
||||
url: 'http://example.test',
|
||||
options: { strictSSL: false },
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
}).expect(true)
|
||||
})
|
||||
})
|
||||
@@ -163,7 +166,9 @@ describe('AuthHelper', function () {
|
||||
})
|
||||
it('throws for insecure requests', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
|
||||
authHelper.enforceStrictSsl({
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
})
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
})
|
||||
@@ -185,7 +190,9 @@ describe('AuthHelper', function () {
|
||||
})
|
||||
it('does not throw for insecure requests', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
|
||||
authHelper.enforceStrictSsl({
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
})
|
||||
).not.to.throw()
|
||||
})
|
||||
})
|
||||
@@ -220,7 +227,7 @@ describe('AuthHelper', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({
|
||||
url: 'https://myci.test/api',
|
||||
options: { strictSSL: false },
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
}).expect(false)
|
||||
})
|
||||
})
|
||||
@@ -258,7 +265,7 @@ describe('AuthHelper', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({
|
||||
url: 'https://myci.test',
|
||||
options: { strictSSL: false },
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
}).expect(true)
|
||||
})
|
||||
})
|
||||
@@ -323,7 +330,8 @@ describe('AuthHelper', function () {
|
||||
}).expect({
|
||||
url: 'https://myci.test/api',
|
||||
options: {
|
||||
auth: { user: 'admin', pass: 'abc123' },
|
||||
username: 'admin',
|
||||
password: 'abc123',
|
||||
},
|
||||
})
|
||||
given({
|
||||
@@ -335,7 +343,8 @@ describe('AuthHelper', function () {
|
||||
url: 'https://myci.test/api',
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
auth: { user: 'admin', pass: 'abc123' },
|
||||
username: 'admin',
|
||||
password: 'abc123',
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -366,7 +375,7 @@ describe('AuthHelper', function () {
|
||||
expect(() =>
|
||||
withBasicAuth({
|
||||
url: 'https://myci.test/api',
|
||||
options: { strictSSL: false },
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
})
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
|
||||
@@ -38,8 +38,8 @@ class BaseGraphqlService extends BaseService {
|
||||
* representing the query clause of GraphQL POST body
|
||||
* e.g. gql`{ query { ... } }`
|
||||
* @param {object} attrs.variables Variables clause of GraphQL POST body
|
||||
* @param {object} [attrs.options={}] Options to pass to request. See
|
||||
* [documentation](https://github.com/request/request#requestoptions-callback)
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.httpErrorMessages={}] Key-value map of HTTP status codes
|
||||
* and custom error messages e.g: `{ 404: 'package not found' }`.
|
||||
* This can be used to extend or override the
|
||||
@@ -53,7 +53,7 @@ class BaseGraphqlService extends BaseService {
|
||||
* The default is to return the first entry of the `errors` array as
|
||||
* an InvalidResponse.
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/request/request#requestoptions-callback
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
*/
|
||||
async _requestGraphql({
|
||||
schema,
|
||||
|
||||
@@ -29,9 +29,9 @@ class DummyGraphqlService extends BaseGraphqlService {
|
||||
|
||||
describe('BaseGraphqlService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
let requestFetcher
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
requestFetcher = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '{"some": "json"}',
|
||||
res: { statusCode: 200 },
|
||||
@@ -39,13 +39,13 @@ describe('BaseGraphqlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/graphql',
|
||||
{
|
||||
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
|
||||
@@ -55,7 +55,7 @@ describe('BaseGraphqlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
it('forwards options to _requestFetcher', async function () {
|
||||
class WithOptions extends DummyGraphqlService {
|
||||
async handle() {
|
||||
const { value } = await this._requestGraphql({
|
||||
@@ -66,24 +66,24 @@ describe('BaseGraphqlService', function () {
|
||||
requiredString
|
||||
}
|
||||
`,
|
||||
options: { qs: { queryParam: 123 } },
|
||||
options: { searchParams: { queryParam: 123 } },
|
||||
})
|
||||
return { message: value }
|
||||
}
|
||||
}
|
||||
|
||||
await WithOptions.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/graphql',
|
||||
{
|
||||
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
|
||||
headers: { Accept: 'application/json' },
|
||||
method: 'POST',
|
||||
qs: { queryParam: 123 },
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -91,13 +91,13 @@ describe('BaseGraphqlService', function () {
|
||||
|
||||
describe('Making badges', function () {
|
||||
it('handles valid json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '{"requiredString": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -106,13 +106,13 @@ describe('BaseGraphqlService', function () {
|
||||
})
|
||||
|
||||
it('handles json responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '{"unexpectedKey": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -123,13 +123,13 @@ describe('BaseGraphqlService', function () {
|
||||
})
|
||||
|
||||
it('handles unparseable json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: 'not json',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -142,13 +142,13 @@ describe('BaseGraphqlService', function () {
|
||||
|
||||
describe('Error handling', function () {
|
||||
it('handles generic error', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -181,13 +181,13 @@ describe('BaseGraphqlService', function () {
|
||||
}
|
||||
}
|
||||
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await WithErrorHandler.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
|
||||
@@ -28,14 +28,14 @@ class BaseJsonService extends BaseService {
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {Joi} attrs.schema Joi schema to validate the response against
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to request. See
|
||||
* [documentation](https://github.com/request/request#requestoptions-callback)
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* and custom error messages e.g: `{ 404: 'package not found' }`.
|
||||
* This can be used to extend or override the
|
||||
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/request/request#requestoptions-callback
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
*/
|
||||
async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
|
||||
const mergedOptions = {
|
||||
|
||||
@@ -22,9 +22,9 @@ class DummyJsonService extends BaseJsonService {
|
||||
|
||||
describe('BaseJsonService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
let requestFetcher
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
requestFetcher = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '{"some": "json"}',
|
||||
res: { statusCode: 200 },
|
||||
@@ -32,13 +32,13 @@ describe('BaseJsonService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyJsonService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.json',
|
||||
{
|
||||
headers: { Accept: 'application/json' },
|
||||
@@ -46,29 +46,29 @@ describe('BaseJsonService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
it('forwards options to _requestFetcher', async function () {
|
||||
class WithOptions extends DummyJsonService {
|
||||
async handle() {
|
||||
const { value } = await this._requestJson({
|
||||
schema: dummySchema,
|
||||
url: 'http://example.com/foo.json',
|
||||
options: { method: 'POST', qs: { queryParam: 123 } },
|
||||
options: { method: 'POST', searchParams: { queryParam: 123 } },
|
||||
})
|
||||
return { message: value }
|
||||
}
|
||||
}
|
||||
|
||||
await WithOptions.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.json',
|
||||
{
|
||||
headers: { Accept: 'application/json' },
|
||||
method: 'POST',
|
||||
qs: { queryParam: 123 },
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -76,13 +76,13 @@ describe('BaseJsonService', function () {
|
||||
|
||||
describe('Making badges', function () {
|
||||
it('handles valid json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '{"requiredString": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyJsonService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -91,13 +91,13 @@ describe('BaseJsonService', function () {
|
||||
})
|
||||
|
||||
it('handles json responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '{"unexpectedKey": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyJsonService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -108,13 +108,13 @@ describe('BaseJsonService', function () {
|
||||
})
|
||||
|
||||
it('handles unparseable json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: 'not json',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyJsonService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
|
||||
@@ -51,14 +51,14 @@ class BaseSvgScrapingService extends BaseService {
|
||||
* @param {RegExp} attrs.valueMatcher
|
||||
* RegExp to match the value we want to parse from the SVG
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to request. See
|
||||
* [documentation](https://github.com/request/request#requestoptions-callback)
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* and custom error messages e.g: `{ 404: 'package not found' }`.
|
||||
* This can be used to extend or override the
|
||||
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/request/request#requestoptions-callback
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
*/
|
||||
async _requestSvg({
|
||||
schema,
|
||||
|
||||
@@ -34,9 +34,9 @@ describe('BaseSvgScrapingService', function () {
|
||||
})
|
||||
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
let requestFetcher
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
requestFetcher = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: exampleSvg,
|
||||
res: { statusCode: 200 },
|
||||
@@ -44,13 +44,13 @@ describe('BaseSvgScrapingService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest with the expected header', async function () {
|
||||
it('invokes _requestFetcher with the expected header', async function () {
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.svg',
|
||||
{
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
@@ -58,7 +58,7 @@ describe('BaseSvgScrapingService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
it('forwards options to _requestFetcher', async function () {
|
||||
class WithCustomOptions extends DummySvgScrapingService {
|
||||
async handle() {
|
||||
const { message } = await this._requestSvg({
|
||||
@@ -66,7 +66,7 @@ describe('BaseSvgScrapingService', function () {
|
||||
url: 'http://example.com/foo.svg',
|
||||
options: {
|
||||
method: 'POST',
|
||||
qs: { queryParam: 123 },
|
||||
searchParams: { queryParam: 123 },
|
||||
},
|
||||
})
|
||||
return { message }
|
||||
@@ -74,16 +74,16 @@ describe('BaseSvgScrapingService', function () {
|
||||
}
|
||||
|
||||
await WithCustomOptions.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.svg',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
qs: { queryParam: 123 },
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -91,13 +91,13 @@ describe('BaseSvgScrapingService', function () {
|
||||
|
||||
describe('Making badges', function () {
|
||||
it('handles valid svg responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: exampleSvg,
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -117,13 +117,13 @@ describe('BaseSvgScrapingService', function () {
|
||||
})
|
||||
}
|
||||
}
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '<desc>a different message</desc>',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await WithValueMatcher.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -132,13 +132,13 @@ describe('BaseSvgScrapingService', function () {
|
||||
})
|
||||
|
||||
it('handles unparseable svg responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: 'not svg yo',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
|
||||
@@ -22,8 +22,8 @@ class BaseXmlService extends BaseService {
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {Joi} attrs.schema Joi schema to validate the response against
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to request. See
|
||||
* [documentation](https://github.com/request/request#requestoptions-callback)
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* and custom error messages e.g: `{ 404: 'package not found' }`.
|
||||
* This can be used to extend or override the
|
||||
@@ -31,7 +31,7 @@ class BaseXmlService extends BaseService {
|
||||
* @param {object} [attrs.parserOptions={}] Options to pass to fast-xml-parser. See
|
||||
* [documentation](https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json)
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/request/request#requestoptions-callback
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
* @see https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json
|
||||
*/
|
||||
async _requestXml({
|
||||
|
||||
@@ -22,9 +22,9 @@ class DummyXmlService extends BaseXmlService {
|
||||
|
||||
describe('BaseXmlService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
let requestFetcher
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
requestFetcher = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '<requiredString>some-string</requiredString>',
|
||||
res: { statusCode: 200 },
|
||||
@@ -32,13 +32,13 @@ describe('BaseXmlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyXmlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.xml',
|
||||
{
|
||||
headers: { Accept: 'application/xml, text/xml' },
|
||||
@@ -46,7 +46,7 @@ describe('BaseXmlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
it('forwards options to _requestFetcher', async function () {
|
||||
class WithCustomOptions extends BaseXmlService {
|
||||
static route = {}
|
||||
|
||||
@@ -54,23 +54,23 @@ describe('BaseXmlService', function () {
|
||||
const { requiredString } = await this._requestXml({
|
||||
schema: dummySchema,
|
||||
url: 'http://example.com/foo.xml',
|
||||
options: { method: 'POST', qs: { queryParam: 123 } },
|
||||
options: { method: 'POST', searchParams: { queryParam: 123 } },
|
||||
})
|
||||
return { message: requiredString }
|
||||
}
|
||||
}
|
||||
|
||||
await WithCustomOptions.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.xml',
|
||||
{
|
||||
headers: { Accept: 'application/xml, text/xml' },
|
||||
method: 'POST',
|
||||
qs: { queryParam: 123 },
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -78,13 +78,13 @@ describe('BaseXmlService', function () {
|
||||
|
||||
describe('Making badges', function () {
|
||||
it('handles valid xml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '<requiredString>some-string</requiredString>',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyXmlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -104,14 +104,14 @@ describe('BaseXmlService', function () {
|
||||
return { message: requiredString }
|
||||
}
|
||||
}
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer:
|
||||
'<requiredString>some-string with trailing whitespace </requiredString>',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyXmlServiceWithParserOption.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -120,13 +120,13 @@ describe('BaseXmlService', function () {
|
||||
})
|
||||
|
||||
it('handles xml responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '<unexpectedAttribute>some-string</unexpectedAttribute>',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyXmlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -137,13 +137,13 @@ describe('BaseXmlService', function () {
|
||||
})
|
||||
|
||||
it('handles unparseable xml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: 'not xml',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyXmlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
|
||||
@@ -21,15 +21,15 @@ class BaseYamlService extends BaseService {
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {Joi} attrs.schema Joi schema to validate the response against
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to request. See
|
||||
* [documentation](https://github.com/request/request#requestoptions-callback)
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* and custom error messages e.g: `{ 404: 'package not found' }`.
|
||||
* This can be used to extend or override the
|
||||
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
|
||||
* @param {object} [attrs.encoding='utf8'] Character encoding
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/request/request#requestoptions-callback
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
*/
|
||||
async _requestYaml({
|
||||
schema,
|
||||
|
||||
@@ -38,9 +38,9 @@ foo: baz
|
||||
|
||||
describe('BaseYamlService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
let requestFetcher
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
requestFetcher = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: expectedYaml,
|
||||
res: { statusCode: 200 },
|
||||
@@ -48,13 +48,13 @@ describe('BaseYamlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyYamlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.yaml',
|
||||
{
|
||||
headers: {
|
||||
@@ -65,24 +65,24 @@ describe('BaseYamlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
it('forwards options to _requestFetcher', async function () {
|
||||
class WithOptions extends DummyYamlService {
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestYaml({
|
||||
schema: dummySchema,
|
||||
url: 'http://example.com/foo.yaml',
|
||||
options: { method: 'POST', qs: { queryParam: 123 } },
|
||||
options: { method: 'POST', searchParams: { queryParam: 123 } },
|
||||
})
|
||||
return { message: requiredString }
|
||||
}
|
||||
}
|
||||
|
||||
await WithOptions.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.yaml',
|
||||
{
|
||||
headers: {
|
||||
@@ -90,7 +90,7 @@ describe('BaseYamlService', function () {
|
||||
'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
|
||||
},
|
||||
method: 'POST',
|
||||
qs: { queryParam: 123 },
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -98,13 +98,13 @@ describe('BaseYamlService', function () {
|
||||
|
||||
describe('Making badges', function () {
|
||||
it('handles valid yaml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: expectedYaml,
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyYamlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -113,13 +113,13 @@ describe('BaseYamlService', function () {
|
||||
})
|
||||
|
||||
it('handles yaml responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: unexpectedYaml,
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyYamlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
@@ -130,13 +130,13 @@ describe('BaseYamlService', function () {
|
||||
})
|
||||
|
||||
it('handles unparseable yaml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: invalidYaml,
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
expect(
|
||||
await DummyYamlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
).to.deep.equal({
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Deprecated,
|
||||
} from './errors.js'
|
||||
import { validateExample, transformExample } from './examples.js'
|
||||
import { fetchFactory } from './got.js'
|
||||
import { fetch } from './got.js'
|
||||
import {
|
||||
makeFullUrl,
|
||||
assertValidRoute,
|
||||
@@ -108,11 +108,14 @@ class BaseService {
|
||||
*
|
||||
* See also the config schema in `./server.js` and `doc/server-secrets.md`.
|
||||
*
|
||||
* To use the configured auth in the handler or fetch method, pass the
|
||||
* credentials to the request. For example:
|
||||
* - `{ options: { auth: this.authHelper.basicAuth } }`
|
||||
* - `{ options: { headers: this.authHelper.bearerAuthHeader } }`
|
||||
* - `{ options: { qs: { token: this.authHelper._pass } } }`
|
||||
* To use the configured auth in the handler or fetch method, wrap the
|
||||
* _request() input params in a call to one of:
|
||||
* - this.authHelper.withBasicAuth()
|
||||
* - this.authHelper.withBearerAuthHeader()
|
||||
* - this.authHelper.withQueryStringAuth()
|
||||
*
|
||||
* For example:
|
||||
* this._request(this.authHelper.withBasicAuth({ url, schema, options }))
|
||||
*
|
||||
* @abstract
|
||||
* @type {module:core/base-service/base~Auth}
|
||||
@@ -204,10 +207,10 @@ class BaseService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
{ sendAndCacheRequest, authHelper, metricHelper },
|
||||
{ requestFetcher, authHelper, metricHelper },
|
||||
{ handleInternalErrors }
|
||||
) {
|
||||
this._requestFetcher = sendAndCacheRequest
|
||||
this._requestFetcher = requestFetcher
|
||||
this.authHelper = authHelper
|
||||
this._handleInternalErrors = handleInternalErrors
|
||||
this._metricHelper = metricHelper
|
||||
@@ -217,10 +220,10 @@ class BaseService {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
let logUrl = url
|
||||
const logOptions = Object.assign({}, options)
|
||||
if ('qs' in options) {
|
||||
const params = new URLSearchParams(options.qs)
|
||||
if ('searchParams' in options) {
|
||||
const params = new URLSearchParams(options.searchParams)
|
||||
logUrl = `${url}?${params.toString()}`
|
||||
delete logOptions.qs
|
||||
delete logOptions.searchParams
|
||||
}
|
||||
logTrace(
|
||||
emojic.bowAndArrow,
|
||||
@@ -275,7 +278,7 @@ class BaseService {
|
||||
/**
|
||||
* Asynchronous function to handle requests for this service. Take the route
|
||||
* parameters (as defined in the `route` property), perform a request using
|
||||
* `this._sendAndCacheRequest`, and return the badge data.
|
||||
* `this._requestFetcher`, and return the badge data.
|
||||
*
|
||||
* @abstract
|
||||
* @param {object} namedParams Params parsed from route pattern
|
||||
@@ -429,7 +432,7 @@ class BaseService {
|
||||
},
|
||||
serviceConfig
|
||||
) {
|
||||
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
|
||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
@@ -438,19 +441,17 @@ class BaseService {
|
||||
ServiceClass: this,
|
||||
})
|
||||
|
||||
const fetcher = fetchFactory(fetchLimitBytes)
|
||||
|
||||
camp.route(
|
||||
regex,
|
||||
handleRequest(cacheHeaderConfig, {
|
||||
queryParams,
|
||||
handler: async (queryParams, match, sendBadge, request) => {
|
||||
handler: async (queryParams, match, sendBadge) => {
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
const serviceData = await this.invoke(
|
||||
{
|
||||
sendAndCacheRequest: fetcher, // TODO: rename sendAndCacheRequest
|
||||
requestFetcher: fetch,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricHelper,
|
||||
@@ -473,7 +474,6 @@ class BaseService {
|
||||
metricHandle.noteResponseSent()
|
||||
},
|
||||
cacheLength: this._cacheLength,
|
||||
fetchLimitBytes,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -430,12 +430,12 @@ describe('BaseService', function () {
|
||||
})
|
||||
|
||||
it('logs appropriate information', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
@@ -458,12 +458,12 @@ describe('BaseService', function () {
|
||||
})
|
||||
|
||||
it('handles errors', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: '',
|
||||
res: { statusCode: 404 },
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ sendAndCacheRequest },
|
||||
{ requestFetcher },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
@@ -490,13 +490,13 @@ describe('BaseService', function () {
|
||||
metricInstance: new PrometheusMetrics({ register }),
|
||||
ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled,
|
||||
})
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: 'x'.repeat(65536 + 1),
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
const serviceInstance =
|
||||
new DummyServiceWithServiceResponseSizeMetricEnabled(
|
||||
{ sendAndCacheRequest, metricHelper },
|
||||
{ requestFetcher, metricHelper },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
@@ -516,12 +516,12 @@ describe('BaseService', function () {
|
||||
metricInstance: new PrometheusMetrics({ register }),
|
||||
ServiceClass: DummyService,
|
||||
})
|
||||
const sendAndCacheRequest = async () => ({
|
||||
const requestFetcher = async () => ({
|
||||
buffer: 'x',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ sendAndCacheRequest, metricHelper },
|
||||
{ requestFetcher, metricHelper },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
|
||||
26
core/base-service/got-config.js
Normal file
26
core/base-service/got-config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import bytes from 'bytes'
|
||||
import configModule from 'config'
|
||||
import Joi from 'joi'
|
||||
import { fileSize } from '../../services/validators.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
fetchLimit: fileSize,
|
||||
userAgentBase: Joi.string().required(),
|
||||
}).required()
|
||||
const config = configModule.util.toObject()
|
||||
const publicConfig = Joi.attempt(config.public, schema, { allowUnknown: true })
|
||||
|
||||
const fetchLimitBytes = bytes(publicConfig.fetchLimit)
|
||||
|
||||
function getUserAgent(userAgentBase = publicConfig.userAgentBase) {
|
||||
let version = 'dev'
|
||||
if (process.env.DOCKER_SHIELDS_VERSION) {
|
||||
version = process.env.DOCKER_SHIELDS_VERSION
|
||||
}
|
||||
if (process.env.HEROKU_SLUG_COMMIT) {
|
||||
version = process.env.HEROKU_SLUG_COMMIT.substring(0, 7)
|
||||
}
|
||||
return `${userAgentBase}/${version}`
|
||||
}
|
||||
|
||||
export { fetchLimitBytes, getUserAgent }
|
||||
27
core/base-service/got-config.spec.js
Normal file
27
core/base-service/got-config.spec.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect } from 'chai'
|
||||
import { getUserAgent } from './got-config.js'
|
||||
|
||||
describe('getUserAgent function', function () {
|
||||
afterEach(function () {
|
||||
delete process.env.HEROKU_SLUG_COMMIT
|
||||
delete process.env.DOCKER_SHIELDS_VERSION
|
||||
})
|
||||
|
||||
it('uses the default userAgentBase', function () {
|
||||
expect(getUserAgent()).to.equal('shields (self-hosted)/dev')
|
||||
})
|
||||
|
||||
it('applies custom userAgentBase', function () {
|
||||
expect(getUserAgent('custom')).to.equal('custom/dev')
|
||||
})
|
||||
|
||||
it('uses short commit SHA from HEROKU_SLUG_COMMIT if available', function () {
|
||||
process.env.HEROKU_SLUG_COMMIT = '92090bd44742a5fac03bcb117002088fc7485834'
|
||||
expect(getUserAgent('custom')).to.equal('custom/92090bd')
|
||||
})
|
||||
|
||||
it('uses short commit SHA from DOCKER_SHIELDS_VERSION if available', function () {
|
||||
process.env.DOCKER_SHIELDS_VERSION = 'server-2021-11-22'
|
||||
expect(getUserAgent('custom')).to.equal('custom/server-2021-11-22')
|
||||
})
|
||||
})
|
||||
@@ -1,52 +1,14 @@
|
||||
import got from 'got'
|
||||
import { Inaccessible, InvalidResponse } from './errors.js'
|
||||
import {
|
||||
fetchLimitBytes as fetchLimitBytesDefault,
|
||||
getUserAgent,
|
||||
} from './got-config.js'
|
||||
|
||||
const userAgent = 'Shields.io/2003a'
|
||||
|
||||
function requestOptions2GotOptions(options) {
|
||||
const requestOptions = Object.assign({}, options)
|
||||
const gotOptions = {}
|
||||
const interchangableOptions = ['body', 'form', 'headers', 'method', 'url']
|
||||
|
||||
interchangableOptions.forEach(function (opt) {
|
||||
if (opt in requestOptions) {
|
||||
gotOptions[opt] = requestOptions[opt]
|
||||
delete requestOptions[opt]
|
||||
}
|
||||
})
|
||||
|
||||
if ('qs' in requestOptions) {
|
||||
gotOptions.searchParams = requestOptions.qs
|
||||
delete requestOptions.qs
|
||||
}
|
||||
|
||||
if ('gzip' in requestOptions) {
|
||||
gotOptions.decompress = requestOptions.gzip
|
||||
delete requestOptions.gzip
|
||||
}
|
||||
|
||||
if ('strictSSL' in requestOptions) {
|
||||
gotOptions.https = {
|
||||
rejectUnauthorized: requestOptions.strictSSL,
|
||||
}
|
||||
delete requestOptions.strictSSL
|
||||
}
|
||||
|
||||
if ('auth' in requestOptions) {
|
||||
gotOptions.username = requestOptions.auth.user
|
||||
gotOptions.password = requestOptions.auth.pass
|
||||
delete requestOptions.auth
|
||||
}
|
||||
|
||||
if (Object.keys(requestOptions).length > 0) {
|
||||
throw new Error(`Found unrecognised options ${Object.keys(requestOptions)}`)
|
||||
}
|
||||
|
||||
return gotOptions
|
||||
}
|
||||
const userAgent = getUserAgent()
|
||||
|
||||
async function sendRequest(gotWrapper, url, options) {
|
||||
const gotOptions = requestOptions2GotOptions(options)
|
||||
const gotOptions = Object.assign({}, options)
|
||||
gotOptions.throwHttpErrors = false
|
||||
gotOptions.retry = 0
|
||||
gotOptions.headers = gotOptions.headers || {}
|
||||
@@ -64,8 +26,7 @@ async function sendRequest(gotWrapper, url, options) {
|
||||
}
|
||||
}
|
||||
|
||||
const TEN_MB = 10485760
|
||||
function fetchFactory(fetchLimitBytes = TEN_MB) {
|
||||
function _fetchFactory(fetchLimitBytes = fetchLimitBytesDefault) {
|
||||
const gotWithLimit = got.extend({
|
||||
handlers: [
|
||||
(options, next) => {
|
||||
@@ -94,4 +55,6 @@ function fetchFactory(fetchLimitBytes = TEN_MB) {
|
||||
return sendRequest.bind(sendRequest, gotWithLimit)
|
||||
}
|
||||
|
||||
export { requestOptions2GotOptions, fetchFactory }
|
||||
const fetch = _fetchFactory()
|
||||
|
||||
export { fetch, _fetchFactory }
|
||||
|
||||
@@ -1,50 +1,15 @@
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import { requestOptions2GotOptions, fetchFactory } from './got.js'
|
||||
import { _fetchFactory } from './got.js'
|
||||
import { Inaccessible, InvalidResponse } from './errors.js'
|
||||
|
||||
describe('requestOptions2GotOptions function', function () {
|
||||
it('translates valid options', function () {
|
||||
expect(
|
||||
requestOptions2GotOptions({
|
||||
body: 'body',
|
||||
form: 'form',
|
||||
headers: 'headers',
|
||||
method: 'method',
|
||||
url: 'url',
|
||||
qs: 'qs',
|
||||
gzip: 'gzip',
|
||||
strictSSL: 'strictSSL',
|
||||
auth: { user: 'user', pass: 'pass' },
|
||||
})
|
||||
).to.deep.equal({
|
||||
body: 'body',
|
||||
form: 'form',
|
||||
headers: 'headers',
|
||||
method: 'method',
|
||||
url: 'url',
|
||||
searchParams: 'qs',
|
||||
decompress: 'gzip',
|
||||
https: { rejectUnauthorized: 'strictSSL' },
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
})
|
||||
})
|
||||
|
||||
it('throws if unrecognised options are found', function () {
|
||||
expect(() =>
|
||||
requestOptions2GotOptions({ body: 'body', foobar: 'foobar' })
|
||||
).to.throw(Error, 'Found unrecognised options foobar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('got wrapper', function () {
|
||||
it('should not throw an error if the response <= fetchLimitBytes', async function () {
|
||||
nock('https://www.google.com')
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200, 'x'.repeat(100))
|
||||
const sendRequest = fetchFactory(100)
|
||||
const sendRequest = _fetchFactory(100)
|
||||
const { res } = await sendRequest('https://www.google.com/foo/bar')
|
||||
expect(res.statusCode).to.equal(200)
|
||||
})
|
||||
@@ -54,7 +19,7 @@ describe('got wrapper', function () {
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200, 'x'.repeat(101))
|
||||
const sendRequest = fetchFactory(100)
|
||||
const sendRequest = _fetchFactory(100)
|
||||
return expect(
|
||||
sendRequest('https://www.google.com/foo/bar')
|
||||
).to.be.rejectedWith(InvalidResponse, 'Maximum response size exceeded')
|
||||
@@ -62,7 +27,7 @@ describe('got wrapper', function () {
|
||||
|
||||
it('should throw an Inaccessible error if the request throws a (non-HTTP) error', async function () {
|
||||
nock('https://www.google.com').get('/foo/bar').replyWithError('oh no')
|
||||
const sendRequest = fetchFactory(1024)
|
||||
const sendRequest = _fetchFactory(1024)
|
||||
return expect(
|
||||
sendRequest('https://www.google.com/foo/bar')
|
||||
).to.be.rejectedWith(Inaccessible, 'oh no')
|
||||
@@ -71,7 +36,7 @@ describe('got wrapper', function () {
|
||||
it('should throw an Inaccessible error if the host can not be accessed', async function () {
|
||||
this.timeout(5000)
|
||||
nock.disableNetConnect()
|
||||
const sendRequest = fetchFactory(1024)
|
||||
const sendRequest = _fetchFactory(1024)
|
||||
return expect(
|
||||
sendRequest('https://www.google.com/foo/bar')
|
||||
).to.be.rejectedWith(
|
||||
@@ -84,14 +49,14 @@ describe('got wrapper', function () {
|
||||
nock('https://www.google.com', {
|
||||
reqheaders: {
|
||||
'user-agent': function (agent) {
|
||||
return agent.startsWith('Shields.io')
|
||||
return agent.startsWith('shields (self-hosted)')
|
||||
},
|
||||
},
|
||||
})
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200)
|
||||
const sendRequest = fetchFactory(1024)
|
||||
const sendRequest = _fetchFactory(1024)
|
||||
await sendRequest('https://www.google.com/foo/bar')
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import request from 'request'
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import { setCacheHeaders } from './cache-headers.js'
|
||||
import { Inaccessible, InvalidResponse, ShieldsRuntimeError } from './errors.js'
|
||||
import { makeSend } from './legacy-result-sender.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
|
||||
const userAgent = 'Shields.io/2003a'
|
||||
|
||||
// These query parameters are available to any badge. They are handled by
|
||||
// `coalesceBadge`.
|
||||
const globalQueryParams = new Set([
|
||||
@@ -32,32 +28,12 @@ function flattenQueryParams(queryParams) {
|
||||
return Array.from(union).sort()
|
||||
}
|
||||
|
||||
function promisify(cachingRequest) {
|
||||
return (uri, options) =>
|
||||
new Promise((resolve, reject) => {
|
||||
cachingRequest(uri, options, (err, res, buffer) => {
|
||||
if (err) {
|
||||
if (err instanceof ShieldsRuntimeError) {
|
||||
reject(err)
|
||||
} else {
|
||||
// Wrap the error in an Inaccessible so it can be identified
|
||||
// by the BaseService handler.
|
||||
reject(new Inaccessible({ underlyingError: err }))
|
||||
}
|
||||
} else {
|
||||
resolve({ res, buffer })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// handlerOptions can contain:
|
||||
// - handler: The service's request handler function
|
||||
// - queryParams: An array of the field names of any custom query parameters
|
||||
// the service uses
|
||||
// - cacheLength: An optional badge or category-specific cache length
|
||||
// (in number of seconds) to be used in preference to the default
|
||||
// - fetchLimitBytes: A limit on the response size we're willing to parse
|
||||
//
|
||||
// For safety, the service must declare the query parameters it wants to use.
|
||||
// Only the declared parameters (and the global parameters) are provided to
|
||||
@@ -77,8 +53,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
}
|
||||
|
||||
const allowedKeys = flattenQueryParams(handlerOptions.queryParams)
|
||||
const { cacheLength: serviceDefaultCacheLengthSeconds, fetchLimitBytes } =
|
||||
handlerOptions
|
||||
const { cacheLength: serviceDefaultCacheLengthSeconds } = handlerOptions
|
||||
|
||||
return (queryParams, match, end, ask) => {
|
||||
/*
|
||||
@@ -139,40 +114,6 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
makeSend(extension, ask.res, end)(svg)
|
||||
}, 25000)
|
||||
|
||||
function cachingRequest(uri, options, callback) {
|
||||
if (typeof options === 'function' && !callback) {
|
||||
callback = options
|
||||
}
|
||||
if (options && typeof options === 'object') {
|
||||
options.uri = uri
|
||||
} else if (typeof uri === 'string') {
|
||||
options = { uri }
|
||||
} else {
|
||||
options = uri
|
||||
}
|
||||
options.headers = options.headers || {}
|
||||
options.headers['User-Agent'] = userAgent
|
||||
|
||||
let bufferLength = 0
|
||||
const r = request(options, callback)
|
||||
r.on('data', chunk => {
|
||||
bufferLength += chunk.length
|
||||
if (bufferLength > fetchLimitBytes) {
|
||||
r.abort()
|
||||
r.emit(
|
||||
'error',
|
||||
new InvalidResponse({
|
||||
prettyMessage: 'Maximum response size exceeded',
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Wrapper around `cachingRequest` that returns a promise rather than needing
|
||||
// to pass a callback.
|
||||
cachingRequest.asPromise = promisify(cachingRequest)
|
||||
|
||||
const result = handlerOptions.handler(
|
||||
filteredQueryParams,
|
||||
match,
|
||||
@@ -187,8 +128,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
const svg = makeBadge(badgeData)
|
||||
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
},
|
||||
cachingRequest
|
||||
}
|
||||
)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
if (result && result.catch) {
|
||||
@@ -200,4 +140,4 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
export { handleRequest, promisify, userAgent }
|
||||
export { handleRequest }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import portfinder from 'portfinder'
|
||||
import Camp from '@shields_io/camp'
|
||||
import got from '../got-test-client.js'
|
||||
@@ -42,28 +41,6 @@ function createFakeHandlerWithCacheLength(cacheLengthSeconds) {
|
||||
}
|
||||
}
|
||||
|
||||
function fakeHandlerWithNetworkIo(queryParams, match, sendBadge, request) {
|
||||
const [, someValue, format] = match
|
||||
request('https://www.google.com/foo/bar', (err, res, buffer) => {
|
||||
let message
|
||||
if (err) {
|
||||
message = err.prettyMessage
|
||||
} else {
|
||||
message = someValue
|
||||
}
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
{
|
||||
label: 'testing',
|
||||
message,
|
||||
format,
|
||||
},
|
||||
{}
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
})
|
||||
}
|
||||
|
||||
describe('The request handler', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
@@ -133,60 +110,6 @@ describe('The request handler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('the response size limit', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, {
|
||||
handler: fakeHandlerWithNetworkIo,
|
||||
fetchLimitBytes: 100,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should not throw an error if the response <= fetchLimitBytes', async function () {
|
||||
nock('https://www.google.com')
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200, 'x'.repeat(100))
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
name: 'testing',
|
||||
value: '123',
|
||||
label: 'testing',
|
||||
message: '123',
|
||||
color: 'lightgrey',
|
||||
link: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the response is > fetchLimitBytes', async function () {
|
||||
nock('https://www.google.com')
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200, 'x'.repeat(101))
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
name: 'testing',
|
||||
value: 'Maximum response size exceeded',
|
||||
label: 'testing',
|
||||
message: 'Maximum response size exceeded',
|
||||
color: 'lightgrey',
|
||||
link: [],
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
nock.cleanAll()
|
||||
})
|
||||
})
|
||||
|
||||
describe('caching', function () {
|
||||
describe('standard query parameters', function () {
|
||||
function register({ cacheHeaderConfig }) {
|
||||
|
||||
@@ -2,37 +2,38 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { InvalidResponse } from '../base-service/errors.js'
|
||||
import { fetchFactory } from '../../core/base-service/got.js'
|
||||
import checkErrorResponse from '../../core/base-service/check-error-response.js'
|
||||
const fetcher = fetchFactory()
|
||||
import { InvalidResponse } from './errors.js'
|
||||
import { fetch } from './got.js'
|
||||
import checkErrorResponse from './check-error-response.js'
|
||||
|
||||
const oneDay = 24 * 3600 * 1000 // 1 day in milliseconds
|
||||
|
||||
// Map from URL to { timestamp: last fetch time, data: data }.
|
||||
let regularUpdateCache = Object.create(null)
|
||||
let resourceCache = Object.create(null)
|
||||
|
||||
/**
|
||||
* Make a HTTP request using an in-memory cache
|
||||
*
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {number} attrs.intervalMillis Number of milliseconds to keep cached value for
|
||||
* @param {number} attrs.ttl Number of milliseconds to keep cached value for
|
||||
* @param {boolean} [attrs.json=true] True if we expect to parse the response as JSON
|
||||
* @param {Function} [attrs.scraper=buffer => buffer] Function to extract value from the response
|
||||
* @param {object} [attrs.options={}] Options to pass to got
|
||||
* @param {Function} [attrs.requestFetcher=fetcher] Custom fetch function
|
||||
* @param {Function} [attrs.requestFetcher=fetch] Custom fetch function
|
||||
* @returns {*} Parsed response
|
||||
*/
|
||||
async function regularUpdate({
|
||||
async function getCachedResource({
|
||||
url,
|
||||
intervalMillis,
|
||||
ttl = oneDay,
|
||||
json = true,
|
||||
scraper = buffer => buffer,
|
||||
options = {},
|
||||
requestFetcher = fetcher,
|
||||
requestFetcher = fetch,
|
||||
}) {
|
||||
const timestamp = Date.now()
|
||||
const cached = regularUpdateCache[url]
|
||||
if (cached != null && timestamp - cached.timestamp < intervalMillis) {
|
||||
const cached = resourceCache[url]
|
||||
if (cached != null && timestamp - cached.timestamp < ttl) {
|
||||
return cached.data
|
||||
}
|
||||
|
||||
@@ -55,12 +56,12 @@ async function regularUpdate({
|
||||
}
|
||||
|
||||
const data = scraper(reqData)
|
||||
regularUpdateCache[url] = { timestamp, data }
|
||||
resourceCache[url] = { timestamp, data }
|
||||
return data
|
||||
}
|
||||
|
||||
function clearRegularUpdateCache() {
|
||||
regularUpdateCache = Object.create(null)
|
||||
function clearResourceCache() {
|
||||
resourceCache = Object.create(null)
|
||||
}
|
||||
|
||||
export { regularUpdate, clearRegularUpdateCache }
|
||||
export { getCachedResource, clearResourceCache }
|
||||
47
core/base-service/resource-cache.spec.js
Normal file
47
core/base-service/resource-cache.spec.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import { getCachedResource, clearResourceCache } from './resource-cache.js'
|
||||
|
||||
describe('Resource Cache', function () {
|
||||
beforeEach(function () {
|
||||
clearResourceCache()
|
||||
})
|
||||
|
||||
it('should use cached response if valid', async function () {
|
||||
let resp
|
||||
|
||||
nock('https://www.foobar.com').get('/baz').reply(200, { value: 1 })
|
||||
resp = await getCachedResource({ url: 'https://www.foobar.com/baz' })
|
||||
expect(resp).to.deep.equal({ value: 1 })
|
||||
expect(nock.isDone()).to.equal(true)
|
||||
nock.cleanAll()
|
||||
|
||||
nock('https://www.foobar.com').get('/baz').reply(200, { value: 2 })
|
||||
resp = await getCachedResource({ url: 'https://www.foobar.com/baz' })
|
||||
expect(resp).to.deep.equal({ value: 1 })
|
||||
expect(nock.isDone()).to.equal(false)
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it('should not use cached response if expired', async function () {
|
||||
let resp
|
||||
|
||||
nock('https://www.foobar.com').get('/baz').reply(200, { value: 1 })
|
||||
resp = await getCachedResource({
|
||||
url: 'https://www.foobar.com/baz',
|
||||
ttl: 1,
|
||||
})
|
||||
expect(resp).to.deep.equal({ value: 1 })
|
||||
expect(nock.isDone()).to.equal(true)
|
||||
nock.cleanAll()
|
||||
|
||||
nock('https://www.foobar.com').get('/baz').reply(200, { value: 2 })
|
||||
resp = await getCachedResource({
|
||||
url: 'https://www.foobar.com/baz',
|
||||
ttl: 1,
|
||||
})
|
||||
expect(resp).to.deep.equal({ value: 2 })
|
||||
expect(nock.isDone()).to.equal(true)
|
||||
nock.cleanAll()
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,6 @@ import path from 'path'
|
||||
import url, { fileURLToPath } from 'url'
|
||||
import { bootstrap } from 'global-agent'
|
||||
import cloudflareMiddleware from 'cloudflare-middleware'
|
||||
import bytes from 'bytes'
|
||||
import Camp from '@shields_io/camp'
|
||||
import originalJoi from 'joi'
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
@@ -16,9 +15,9 @@ import { setRoutes } from '../../services/suggest.js'
|
||||
import { loadServiceClasses } from '../base-service/loader.js'
|
||||
import { makeSend } from '../base-service/legacy-result-sender.js'
|
||||
import { handleRequest } from '../base-service/legacy-request-handler.js'
|
||||
import { clearRegularUpdateCache } from '../legacy/regular-update.js'
|
||||
import { clearResourceCache } from '../base-service/resource-cache.js'
|
||||
import { rasterRedirectUrl } from '../badge-urls/make-badge-url.js'
|
||||
import { nonNegativeInteger } from '../../services/validators.js'
|
||||
import { fileSize, nonNegativeInteger } from '../../services/validators.js'
|
||||
import log from './log.js'
|
||||
import PrometheusMetrics from './prometheus-metrics.js'
|
||||
import InfluxMetrics from './influx-metrics.js'
|
||||
@@ -143,7 +142,8 @@ const publicConfigSchema = Joi.object({
|
||||
}).required(),
|
||||
cacheHeaders: { defaultCacheLengthSeconds: nonNegativeInteger },
|
||||
handleInternalErrors: Joi.boolean().required(),
|
||||
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
|
||||
fetchLimit: fileSize,
|
||||
userAgentBase: Joi.string().required(),
|
||||
requestTimeoutSeconds: nonNegativeInteger,
|
||||
requestTimeoutMaxAgeSeconds: nonNegativeInteger,
|
||||
documentRoot: Joi.string().default(
|
||||
@@ -433,7 +433,6 @@ class Server {
|
||||
{
|
||||
handleInternalErrors: config.public.handleInternalErrors,
|
||||
cacheHeaders: config.public.cacheHeaders,
|
||||
fetchLimitBytes: bytes(config.public.fetchLimit),
|
||||
rasterUrl: config.public.rasterUrl,
|
||||
private: config.private,
|
||||
public: config.public,
|
||||
@@ -532,7 +531,7 @@ class Server {
|
||||
static resetGlobalState() {
|
||||
// This state should be migrated to instance state. When possible, do not add new
|
||||
// global state.
|
||||
clearRegularUpdateCache()
|
||||
clearResourceCache()
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
@@ -25,7 +25,7 @@ and learn about the [GitHub workflow](http://try.github.io/).
|
||||
|
||||
#### Node, NPM
|
||||
|
||||
Node >=14 and NPM >=7 is required. If you don't already have them,
|
||||
Node >=16 and NPM >=7 is required. If you don't already have them,
|
||||
install node and npm: https://nodejs.org/en/download/
|
||||
|
||||
### Setup a dev install
|
||||
@@ -228,14 +228,14 @@ Description of the code:
|
||||
9. Working our way upward, the `async fetch()` method is responsible for calling an API endpoint to get data. Extending `BaseJsonService` gives us the helper function `_requestJson()`. Note here that we pass the schema we defined in step 4 as an argument. `_requestJson()` will deal with validating the response against the schema and throwing an error if necessary.
|
||||
|
||||
- `_requestJson()` automatically adds an Accept header, checks the status code, parses the response as JSON, and returns the parsed response.
|
||||
- `_requestJson()` uses [request](https://github.com/request/request) to perform the HTTP request. Options can be passed to request, including method, query string, and headers. If headers are provided they will override the ones automatically set by `_requestJson()`. There is no need to specify json, as the JSON parsing is handled by `_requestJson()`. See the `request` docs for [supported options](https://github.com/request/request#requestoptions-callback).
|
||||
- `_requestJson()` uses [got](https://github.com/sindresorhus/got) to perform the HTTP request. Options can be passed to got, including method, query string, and headers. If headers are provided they will override the ones automatically set by `_requestJson()`. There is no need to specify json, as the JSON parsing is handled by `_requestJson()`. See the `got` docs for [supported options](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md).
|
||||
- Error messages corresponding to each status code can be returned by passing a dictionary of status codes -> messages in `errorMessages`.
|
||||
- A more complex call to `_requestJson()` might look like this:
|
||||
```js
|
||||
return this._requestJson({
|
||||
schema: mySchema,
|
||||
url,
|
||||
options: { qs: { branch: 'master' } },
|
||||
options: { searchParams: { branch: 'master' } },
|
||||
errorMessages: {
|
||||
401: 'private application not supported',
|
||||
404: 'application not found',
|
||||
|
||||
@@ -96,7 +96,7 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
is created in a legacy helper function in
|
||||
[`legacy-request-handler.js`][legacy-request-handler]. This callback
|
||||
delegates to a callback in `BaseService.register` with four different
|
||||
parameters `( queryParams, match, sendBadge, request )`, which
|
||||
parameters `( queryParams, match, sendBadge )`, which
|
||||
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
|
||||
service and runs `BaseService#handle`.
|
||||
|
||||
@@ -129,12 +129,12 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
handle unresponsive service code and the next callback is invoked: the
|
||||
legacy handler function.
|
||||
3. The legacy handler function receives
|
||||
`( queryParams, match, sendBadge, request )`. Its job is to extract data
|
||||
from the regex `match` and `queryParams`, invoke `request` to fetch
|
||||
whatever data it needs, and then invoke `sendBadge` with the result.
|
||||
`( queryParams, match, sendBadge )`. Its job is to extract data
|
||||
from the regex `match` and `queryParams`, and then invoke `sendBadge`
|
||||
with the result.
|
||||
4. The implementation of this function is in `BaseService.register`. It
|
||||
works by running `BaseService.invoke`, which instantiates the service,
|
||||
injects more dependencies, and invokes `BaseService#handle` which is
|
||||
injects more dependencies, and invokes `BaseService.handle` which is
|
||||
implemented by the service subclass.
|
||||
5. The job of `handle()`, which should be implemented by each service
|
||||
subclass, is to return an object which partially describes a badge or
|
||||
|
||||
@@ -49,11 +49,11 @@ Shields has mercifully little persistent state:
|
||||
1. The GitHub tokens we collect are saved on each server in a cloud Redis
|
||||
database. They can also be fetched from the [GitHub auth admin endpoint][]
|
||||
for debugging.
|
||||
2. The server keeps the [regular-update cache][] in memory. It is neither
|
||||
2. The server keeps the [resource cache][] in memory. It is neither
|
||||
persisted nor inspectable.
|
||||
|
||||
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
|
||||
[regular-update cache]: https://github.com/badges/shields/blob/master/core/legacy/regular-update.js
|
||||
[resource cache]: https://github.com/badges/shields/blob/master/core/base-service/resource-cache.js
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ This document describes how to host your own shields server either from source o
|
||||
|
||||
## Installing from Source
|
||||
|
||||
You will need Node 14 or later, which you can install using a
|
||||
You will need Node 16 or later, which you can install using a
|
||||
[package manager][].
|
||||
|
||||
On Ubuntu / Debian:
|
||||
|
||||
```sh
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -; sudo apt-get install -y nodejs
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -; sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
```sh
|
||||
|
||||
10391
package-lock.json
generated
10391
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@@ -24,26 +24,26 @@
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^4.5.0",
|
||||
"@fontsource/lekton": "^4.5.0",
|
||||
"@sentry/node": "^6.13.3",
|
||||
"@sentry/node": "^6.15.0",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.0",
|
||||
"camelcase": "^6.2.0",
|
||||
"chalk": "^4.1.2",
|
||||
"check-node-version": "^4.1.0",
|
||||
"bytes": "^3.1.1",
|
||||
"camelcase": "^6.2.1",
|
||||
"chalk": "^5.0.0",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"decamelize": "^6.0.0",
|
||||
"emojic": "^1.1.16",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^3.21.0",
|
||||
"fast-xml-parser": "^3.21.1",
|
||||
"glob": "^7.2.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "11.8.2",
|
||||
"got": "11.8.3",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.5",
|
||||
"ioredis": "4.28.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "4.28.1",
|
||||
"joi": "17.4.2",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -57,13 +57,12 @@
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.0.0",
|
||||
"prom-client": "^14.0.1",
|
||||
"qs": "^6.10.1",
|
||||
"query-string": "^7.0.1",
|
||||
"request": "~2.88.2",
|
||||
"semver": "~7.3.5",
|
||||
"simple-icons": "5.20.0",
|
||||
"webextension-store-meta": "^1.0.4",
|
||||
"simple-icons": "5.24.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
},
|
||||
@@ -99,7 +98,7 @@
|
||||
"test": "run-s --silent --continue-on-error lint test:frontend test:package test:core test:entrypoint check-types:package check-types:frontend prettier:check",
|
||||
"check-types:package": "tsd badge-maker",
|
||||
"check-types:frontend": "tsc --noEmit --project .",
|
||||
"depcheck": "check-node-version --node \">= 14.0\"",
|
||||
"depcheck": "check-node-version --node \">= 16.0\"",
|
||||
"prebuild": "run-s --silent depcheck",
|
||||
"features": "node scripts/export-supported-features-cli.js > ./frontend/supported-features.json",
|
||||
"defs": "node scripts/export-service-definitions-cli.js > ./frontend/service-definitions.yml",
|
||||
@@ -142,9 +141,9 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.15.3",
|
||||
"@babel/register": "7.16.0",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
@@ -155,11 +154,11 @@
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.15",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.3.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"babel-preset-gatsby": "^2.0.0",
|
||||
"babel-preset-gatsby": "^2.2.0",
|
||||
"c8": "^7.10.0",
|
||||
"caller": "^1.0.1",
|
||||
"chai": "^4.3.4",
|
||||
@@ -168,9 +167,9 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^6.3.0",
|
||||
"cypress": "^8.7.0",
|
||||
"danger": "^10.7.0",
|
||||
"concurrently": "^6.4.0",
|
||||
"cypress": "^9.1.0",
|
||||
"danger": "^10.7.1",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
@@ -180,31 +179,31 @@
|
||||
"eslint-config-standard-react": "^11.0.1",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jsdoc": "^37.0.3",
|
||||
"eslint-plugin-mocha": "^9.0.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-sort-class-members": "^1.12.0",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-sort-class-members": "^1.14.0",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "3.14.5",
|
||||
"gatsby-plugin-catch-links": "^3.14.0",
|
||||
"gatsby-plugin-page-creator": "^3.14.0",
|
||||
"gatsby-plugin-react-helmet": "^4.14.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^3.14.0",
|
||||
"gatsby-plugin-styled-components": "^4.14.0",
|
||||
"gatsby-plugin-typescript": "^3.14.0",
|
||||
"gatsby": "4.2.0",
|
||||
"gatsby-plugin-catch-links": "^4.2.0",
|
||||
"gatsby-plugin-page-creator": "^4.2.0",
|
||||
"gatsby-plugin-react-helmet": "^5.2.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^4.2.0",
|
||||
"gatsby-plugin-styled-components": "^5.2.0",
|
||||
"gatsby-plugin-typescript": "^4.2.0",
|
||||
"humanize-string": "^3.0.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^4.3.1",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^3.6.7",
|
||||
"lint-staged": "^11.2.6",
|
||||
"lint-staged": "^12.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.5",
|
||||
@@ -212,18 +211,18 @@
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.0.2",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.1.4",
|
||||
"nock": "13.2.1",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"nodemon": "^2.0.14",
|
||||
"nodemon": "^2.0.15",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"prettier": "2.4.1",
|
||||
"prettier": "2.5.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.9",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-modal": "^3.14.3",
|
||||
"react-modal": "^3.14.4",
|
||||
"react-pose": "^4.0.10",
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
@@ -231,17 +230,17 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.7.0",
|
||||
"sinon": "^11.1.2",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.6",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"ts-mocha": "^8.0.0",
|
||||
"tsd": "^0.18.0",
|
||||
"typescript": "^4.4.4"
|
||||
"tsd": "^0.19.0",
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.1",
|
||||
"node": "^16.13.0",
|
||||
"npm": ">=7.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -14,6 +14,7 @@ const commonPreviewProps = {
|
||||
}
|
||||
|
||||
export default class AppVeyorTests extends AppVeyorBase {
|
||||
static category = 'test-results'
|
||||
static route = {
|
||||
...this.buildRoute('appveyor/tests'),
|
||||
queryParamSchema: testResultQueryParamSchema,
|
||||
|
||||
@@ -43,7 +43,7 @@ class BaseAurService extends BaseJsonService {
|
||||
return this._requestJson({
|
||||
schema: aurSchema,
|
||||
url: 'https://aur.archlinux.org/rpc.php',
|
||||
options: { qs: { v: 5, type: 'info', arg: packageName } },
|
||||
options: { searchParams: { v: 5, type: 'info', arg: packageName } },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class AzureDevOpsBase extends BaseJsonService {
|
||||
// Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.0
|
||||
const url = `https://dev.azure.com/${organization}/${project}/_apis/build/builds`
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
definitions: definitionId,
|
||||
$top: 1,
|
||||
statusFilter: 'completed',
|
||||
@@ -49,7 +49,7 @@ export default class AzureDevOpsBase extends BaseJsonService {
|
||||
}
|
||||
|
||||
if (branch) {
|
||||
options.qs.branchName = `refs/heads/${branch}`
|
||||
options.searchParams.branchName = `refs/heads/${branch}`
|
||||
}
|
||||
|
||||
const json = await this.fetch({
|
||||
|
||||
@@ -104,7 +104,7 @@ export default class AzureDevOpsBuild extends BaseSvgScrapingService {
|
||||
// Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/vsts/build/status/get
|
||||
const { status } = await fetch(this, {
|
||||
url: `https://dev.azure.com/${organization}/${projectId}/_apis/build/status/${definitionId}`,
|
||||
qs: {
|
||||
searchParams: {
|
||||
branchName: branch,
|
||||
stageName: stage,
|
||||
jobName: job,
|
||||
|
||||
@@ -101,7 +101,7 @@ export default class AzureDevOpsCoverage extends AzureDevOpsBase {
|
||||
// Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/test/code%20coverage/get%20build%20code%20coverage?view=azure-devops-rest-5.0
|
||||
const url = `https://dev.azure.com/${organization}/${project}/_apis/test/codecoverage`
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
buildId,
|
||||
'api-version': '5.0-preview.1',
|
||||
},
|
||||
|
||||
@@ -15,12 +15,15 @@ const schema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
async function fetch(serviceInstance, { url, qs = {}, errorMessages }) {
|
||||
async function fetch(
|
||||
serviceInstance,
|
||||
{ url, searchParams = {}, errorMessages }
|
||||
) {
|
||||
// Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/vsts/build/status/get
|
||||
const { message: status } = await serviceInstance._requestSvg({
|
||||
schema,
|
||||
url,
|
||||
options: { qs },
|
||||
options: { searchParams },
|
||||
errorMessages,
|
||||
})
|
||||
return { status }
|
||||
|
||||
@@ -61,8 +61,7 @@ const buildTestResultSummarySchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
export default class AzureDevOpsTests extends AzureDevOpsBase {
|
||||
static category = 'build'
|
||||
|
||||
static category = 'test-results'
|
||||
static route = {
|
||||
base: 'azure-devops/tests',
|
||||
pattern: ':organization/:project/:definitionId/:branch*',
|
||||
@@ -161,7 +160,7 @@ export default class AzureDevOpsTests extends AzureDevOpsBase {
|
||||
return await this.fetch({
|
||||
url: `https://dev.azure.com/${organization}/${project}/_apis/test/ResultSummaryByBuild`,
|
||||
options: {
|
||||
qs: { buildId },
|
||||
searchParams: { buildId },
|
||||
},
|
||||
schema: buildTestResultSummarySchema,
|
||||
errorMessages,
|
||||
|
||||
@@ -44,7 +44,7 @@ function issueClassGenerator(raw) {
|
||||
schema: bitbucketIssuesSchema,
|
||||
// https://developer.atlassian.com/bitbucket/api/2/reference/meta/filtering#query-issues
|
||||
options: {
|
||||
qs: { limit: 0, q: '(state = "new" OR state = "open")' },
|
||||
searchParams: { limit: 0, q: '(state = "new" OR state = "open")' },
|
||||
},
|
||||
errorMessages: { 403: 'private repo' },
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ class BitbucketPipelines extends BaseJsonService {
|
||||
url,
|
||||
schema: bitbucketPipelinesSchema,
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
fields: 'values.state',
|
||||
page: 1,
|
||||
pagelen: 2,
|
||||
|
||||
@@ -86,7 +86,7 @@ function pullRequestClassGenerator(raw) {
|
||||
this.bitbucketAuthHelper.withBasicAuth({
|
||||
url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/pullrequests/`,
|
||||
schema,
|
||||
options: { qs: { state: 'OPEN', limit: 0 } },
|
||||
options: { searchParams: { state: 'OPEN', limit: 0 } },
|
||||
errorMessages,
|
||||
})
|
||||
)
|
||||
@@ -99,7 +99,7 @@ function pullRequestClassGenerator(raw) {
|
||||
url: `${server}/rest/api/1.0/projects/${user}/repos/${repo}/pull-requests`,
|
||||
schema,
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
state: 'OPEN',
|
||||
limit: 100,
|
||||
withProperties: false,
|
||||
|
||||
@@ -53,7 +53,7 @@ export default class Bitrise extends BaseJsonService {
|
||||
url: `https://app.bitrise.io/app/${encodeURIComponent(
|
||||
appId
|
||||
)}/status.json`,
|
||||
options: { qs: { token, branch } },
|
||||
options: { searchParams: { token, branch } },
|
||||
schema,
|
||||
errorMessages: {
|
||||
403: 'app not found or invalid token',
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class BStatsPlayers extends BaseJsonService {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
maxElements: 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class BStatsServers extends BaseJsonService {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
maxElements: 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class Buildkite extends BaseJsonService {
|
||||
|
||||
async fetch({ identifier, branch }) {
|
||||
const url = `https://badge.buildkite.com/${identifier}.json`
|
||||
const options = { qs: { branch } }
|
||||
const options = { searchParams: { branch } }
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class Bundlephobia extends BaseJsonService {
|
||||
const packageQuery = `${scope ? `${scope}/` : ''}${packageName}${
|
||||
version ? `@${version}` : ''
|
||||
}`
|
||||
const options = { qs: { package: packageQuery } }
|
||||
const options = { searchParams: { package: packageQuery } }
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: 'https://bundlephobia.com/api/size',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default [
|
||||
{ id: 'build', name: 'Build', keywords: ['build'] },
|
||||
{ id: 'coverage', name: 'Code Coverage', keywords: ['coverage'] },
|
||||
{ id: 'test-results', name: 'Test Results', keywords: ['tests'] },
|
||||
{ id: 'analysis', name: 'Analysis', keywords: ['analysis'] },
|
||||
{ id: 'chat', name: 'Chat', keywords: ['chat'] },
|
||||
{ id: 'dependencies', name: 'Dependencies', keywords: ['dependencies'] },
|
||||
|
||||
@@ -56,7 +56,7 @@ class CircleCi extends BaseSvgScrapingService {
|
||||
url: `https://circleci.com/${vcs}/${user}/${repo}${branchClause}.svg`,
|
||||
// Note that the unusual 'circle-token' query param name is required.
|
||||
// https://circleci.com/docs/api/#get-authenticated
|
||||
options: { qs: { style: 'shield', 'circle-token': token } },
|
||||
options: { searchParams: { style: 'shield', 'circle-token': token } },
|
||||
errorMessages: { 404: 'project not found' },
|
||||
})
|
||||
return this.constructor.render({ status: message })
|
||||
|
||||
@@ -65,7 +65,7 @@ export default class Cirrus extends BaseJsonService {
|
||||
const json = await this._requestJson({
|
||||
schema,
|
||||
url: `https://api.cirrus-ci.com/github/${user}/${repo}.json`,
|
||||
options: { qs: { branch, script, task } },
|
||||
options: { searchParams: { branch, script, task } },
|
||||
})
|
||||
|
||||
return this.constructor.render(json)
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class CodacyCoverage extends BaseSvgScrapingService {
|
||||
url: `https://api.codacy.com/project/badge/coverage/${encodeURIComponent(
|
||||
projectId
|
||||
)}`,
|
||||
options: { qs: { branch } },
|
||||
options: { searchParams: { branch } },
|
||||
valueMatcher: /text-anchor="middle">([^<>]+)<\/text>/,
|
||||
errorMessages: {
|
||||
404: 'project not found',
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class CodacyGrade extends BaseSvgScrapingService {
|
||||
url: `https://api.codacy.com/project/badge/grade/${encodeURIComponent(
|
||||
projectId
|
||||
)}`,
|
||||
options: { qs: { branch } },
|
||||
options: { searchParams: { branch } },
|
||||
errorMessages: { 404: 'project or branch not found' },
|
||||
valueMatcher: /visibility="hidden">([^<>]+)<\/text>/,
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ async function fetchRepo(serviceInstance, { user, repo }) {
|
||||
} = await serviceInstance._requestJson({
|
||||
schema: repoSchema,
|
||||
url: 'https://api.codeclimate.com/v1/repos',
|
||||
options: { qs: { github_slug: `${user}/${repo}` } },
|
||||
options: { searchParams: { github_slug: `${user}/${repo}` } },
|
||||
})
|
||||
if (repoInfo === undefined) {
|
||||
throw new NotFound({ prettyMessage: 'repo not found' })
|
||||
|
||||
@@ -151,7 +151,7 @@ export default class Codecov extends BaseSvgScrapingService {
|
||||
valueMatcher: svgValueMatcher,
|
||||
url,
|
||||
options: {
|
||||
qs: { token, flag },
|
||||
searchParams: { token, flag },
|
||||
},
|
||||
errorMessages: token ? { 400: 'invalid token pattern' } : {},
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class Codeship extends BaseSvgScrapingService {
|
||||
return this._requestSvg({
|
||||
schema,
|
||||
url,
|
||||
options: { qs: { branch } },
|
||||
options: { searchParams: { branch } },
|
||||
valueMatcher: /<g id="status_2">(?:[.\s\S]*)\/><\/g><g id="([\w\s]*)"/,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('codeship (valid, no branch)')
|
||||
.get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1.json')
|
||||
.get('/30419df0-80ff-0135-f7fb-06994b6b032d.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isBuildStatus,
|
||||
@@ -21,5 +21,5 @@ t.create('codeship (repo not found)')
|
||||
.expectBadge({ label: 'build', message: 'project not found' })
|
||||
|
||||
t.create('codeship (branch not found)')
|
||||
.get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1/not-a-branch.json')
|
||||
.get('/30419df0-80ff-0135-f7fb-06994b6b032d/not-a-branch.json')
|
||||
.expectBadge({ label: 'build', message: 'branch not found' })
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class Coveralls extends BaseJsonService {
|
||||
vcsType || 'github'
|
||||
}/${user}/${repo}.json`
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
// The API returns the latest result (across any branch) if no branch is explicitly specified,
|
||||
// whereas the Coveralls native badge (and the Shields.io badges for Coveralls) show
|
||||
// the coverage for the default branch if no branch is explicitly specified. If the user
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class Debian extends BaseJsonService {
|
||||
schema,
|
||||
url: 'https://api.ftp-master.debian.org/madison',
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
f: 'json',
|
||||
s: distribution,
|
||||
package: packageName,
|
||||
|
||||
@@ -1,53 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { deprecatedService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
status: Joi.string().required(),
|
||||
colour: Joi.string().required(),
|
||||
})
|
||||
|
||||
export default class DependabotSemverCompatibility extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
static route = {
|
||||
base: 'dependabot/semver',
|
||||
pattern: ':packageManager/:dependencyName',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Dependabot SemVer Compatibility',
|
||||
namedParams: { packageManager: 'bundler', dependencyName: 'puma' },
|
||||
staticPreview: {
|
||||
color: 'green',
|
||||
message: '98%',
|
||||
},
|
||||
export default [
|
||||
deprecatedService({
|
||||
category: 'analysis',
|
||||
route: {
|
||||
base: 'dependabot/semver',
|
||||
pattern: ':various+',
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'semver stability' }
|
||||
|
||||
_getQuery({ packageManager, dependencyName }) {
|
||||
return {
|
||||
'package-manager': packageManager,
|
||||
'dependency-name': dependencyName,
|
||||
'version-scheme': 'semver',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ packageManager, dependencyName }) {
|
||||
const url = `https://api.dependabot.com/badges/compatibility_score`
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
options: { qs: this._getQuery({ packageManager, dependencyName }) },
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ packageManager, dependencyName }) {
|
||||
const json = await this.fetch({ packageManager, dependencyName })
|
||||
return {
|
||||
color: json.colour,
|
||||
message: json.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
label: 'dependabot',
|
||||
dateAdded: new Date('2021-11-15'),
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import { isIntegerPercentage } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
t.create('semver stability (valid)').get('/bundler/puma.json').expectBadge({
|
||||
label: 'semver stability',
|
||||
message: isIntegerPercentage,
|
||||
export const t = new ServiceTester({
|
||||
id: 'dependabot',
|
||||
title: 'Dependabot',
|
||||
})
|
||||
|
||||
t.create('semver stability (invalid error)')
|
||||
.get('/invalid-manager/puma.json')
|
||||
t.create('no longer available (previously semver stability)')
|
||||
.get('/semver/bundler/puma.json')
|
||||
.expectBadge({
|
||||
label: 'semver stability',
|
||||
message: 'invalid',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
t.create('semver stability (missing dependency)')
|
||||
.get('/bundler/some-random-missing-dependency.json')
|
||||
.expectBadge({
|
||||
label: 'semver stability',
|
||||
message: 'unknown',
|
||||
label: 'dependabot',
|
||||
message: 'no longer available',
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ async function fetchBuild(serviceInstance, { user, repo }) {
|
||||
return serviceInstance._requestJson({
|
||||
schema: cloudBuildSchema,
|
||||
url: `https://cloud.docker.com/api/build/v1/source`,
|
||||
options: { qs: { image: `${user}/${repo}` } },
|
||||
options: { searchParams: { image: `${user}/${repo}` } },
|
||||
errorMessages: { 404: 'repo not found' },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class DroneBuild extends BaseJsonService {
|
||||
schema,
|
||||
url: `${server}/api/repos/${user}/${repo}/builds/latest`,
|
||||
options: {
|
||||
qs: { ref: branch ? `refs/heads/${branch}` : undefined },
|
||||
searchParams: { ref: branch ? `refs/heads/${branch}` : undefined },
|
||||
},
|
||||
errorMessages: {
|
||||
401: 'repo not found or not authorized',
|
||||
|
||||
@@ -64,7 +64,7 @@ async function fetchEndpointData(
|
||||
schema: anySchema,
|
||||
url,
|
||||
errorMessages,
|
||||
options: { gzip: true },
|
||||
options: { decompress: true },
|
||||
})
|
||||
return validateEndpointData(json, {
|
||||
prettyErrorMessage: validationPrettyErrorMessage,
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class FreeCodeCampPoints extends BaseJsonService {
|
||||
schema,
|
||||
url: `https://api.freecodecamp.org/api/users/get-public-profile`,
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
username,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import queryString from 'query-string'
|
||||
import request from 'request'
|
||||
import { userAgent } from '../../../core/base-service/legacy-request-handler.js'
|
||||
import { fetch } from '../../../core/base-service/got.js'
|
||||
import log from '../../../core/server/log.js'
|
||||
|
||||
function setRoutes({ server, authHelper, onTokenAccepted }) {
|
||||
@@ -23,20 +22,18 @@ function setRoutes({ server, authHelper, onTokenAccepted }) {
|
||||
end('')
|
||||
})
|
||||
|
||||
server.route(/^\/github-auth\/done$/, (data, match, end, ask) => {
|
||||
server.route(/^\/github-auth\/done$/, async (data, match, end, ask) => {
|
||||
if (!data.code) {
|
||||
log.log(`GitHub OAuth data: ${JSON.stringify(data)}`)
|
||||
return end('GitHub OAuth authentication failed to provide a code.')
|
||||
}
|
||||
|
||||
const options = {
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
form: queryString.stringify({
|
||||
form: {
|
||||
// TODO The `_user` and `_pass` properties bypass security checks in
|
||||
// AuthHelper (e.g: enforceStrictSsl and shouldAuthenticateRequest).
|
||||
// Do not use them elsewhere. It would be better to clean
|
||||
@@ -44,40 +41,42 @@ function setRoutes({ server, authHelper, onTokenAccepted }) {
|
||||
client_id: authHelper._user,
|
||||
client_secret: authHelper._pass,
|
||||
code: data.code,
|
||||
}),
|
||||
},
|
||||
}
|
||||
request(options, (err, res, body) => {
|
||||
if (err != null) {
|
||||
return end('The connection to GitHub failed.')
|
||||
}
|
||||
|
||||
let content
|
||||
try {
|
||||
content = queryString.parse(body)
|
||||
} catch (e) {
|
||||
return end('The GitHub OAuth token could not be parsed.')
|
||||
}
|
||||
let resp
|
||||
try {
|
||||
resp = await fetch('https://github.com/login/oauth/access_token', options)
|
||||
} catch (e) {
|
||||
return end('The connection to GitHub failed.')
|
||||
}
|
||||
|
||||
const { access_token: token } = content
|
||||
if (!token) {
|
||||
return end('The GitHub OAuth process did not return a user token.')
|
||||
}
|
||||
let content
|
||||
try {
|
||||
content = queryString.parse(resp.buffer)
|
||||
} catch (e) {
|
||||
return end('The GitHub OAuth token could not be parsed.')
|
||||
}
|
||||
|
||||
ask.res.setHeader('Content-Type', 'text/html')
|
||||
end(
|
||||
'<p>Shields.io has received your app-specific GitHub user token. ' +
|
||||
'You can revoke it by going to ' +
|
||||
'<a href="https://github.com/settings/applications">GitHub</a>.</p>' +
|
||||
'<p>Until you do, you have now increased the rate limit for GitHub ' +
|
||||
'requests going through Shields.io. GitHub-related badges are ' +
|
||||
'therefore more robust.</p>' +
|
||||
'<p>Thanks for contributing to a smoother experience for ' +
|
||||
'everyone!</p>' +
|
||||
'<p><a href="/">Back to the website</a></p>'
|
||||
)
|
||||
const { access_token: token } = content
|
||||
if (!token) {
|
||||
return end('The GitHub OAuth process did not return a user token.')
|
||||
}
|
||||
|
||||
onTokenAccepted(token)
|
||||
})
|
||||
ask.res.setHeader('Content-Type', 'text/html')
|
||||
end(
|
||||
'<p>Shields.io has received your app-specific GitHub user token. ' +
|
||||
'You can revoke it by going to ' +
|
||||
'<a href="https://github.com/settings/applications">GitHub</a>.</p>' +
|
||||
'<p>Until you do, you have now increased the rate limit for GitHub ' +
|
||||
'requests going through Shields.io. GitHub-related badges are ' +
|
||||
'therefore more robust.</p>' +
|
||||
'<p>Thanks for contributing to a smoother experience for ' +
|
||||
'everyone!</p>' +
|
||||
'<p><a href="/">Back to the website</a></p>'
|
||||
)
|
||||
|
||||
onTokenAccepted(token)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ import GithubConstellation from '../github-constellation.js'
|
||||
import { setRoutes } from './acceptor.js'
|
||||
|
||||
const fakeClientId = 'githubdabomb'
|
||||
const fakeClientSecret = 'foobar'
|
||||
|
||||
describe('Github token acceptor', function () {
|
||||
const oauthHelper = GithubConstellation._createOauthHelper({
|
||||
private: { gh_client_id: fakeClientId },
|
||||
private: { gh_client_id: fakeClientId, gh_client_secret: fakeClientSecret },
|
||||
})
|
||||
|
||||
let port, baseUrl
|
||||
@@ -78,7 +79,10 @@ describe('Github token acceptor', function () {
|
||||
scope = nock('https://github.com')
|
||||
.post('/login/oauth/access_token')
|
||||
.reply((url, requestBody) => {
|
||||
expect(queryString.parse(requestBody).code).to.equal(fakeCode)
|
||||
const parsedBody = queryString.parse(requestBody)
|
||||
expect(parsedBody.client_id).to.equal(fakeClientId)
|
||||
expect(parsedBody.client_secret).to.equal(fakeClientSecret)
|
||||
expect(parsedBody.code).to.equal(fakeCode)
|
||||
return [
|
||||
200,
|
||||
queryString.stringify({ access_token: fakeAccessToken }),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import config from 'config'
|
||||
import { fetchFactory } from '../../core/base-service/got.js'
|
||||
import { fetch } from '../../core/base-service/got.js'
|
||||
import GithubApiProvider from './github-api-provider.js'
|
||||
const requestFetcher = fetchFactory()
|
||||
|
||||
describe('Github API provider', function () {
|
||||
const baseUrl = process.env.GITHUB_URL || 'https://api.github.com'
|
||||
@@ -31,11 +30,7 @@ describe('Github API provider', function () {
|
||||
it('should be able to run 10 requests', async function () {
|
||||
this.timeout('20s')
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await githubApiProvider.fetch(
|
||||
requestFetcher,
|
||||
'/repos/rust-lang/rust',
|
||||
{}
|
||||
)
|
||||
await githubApiProvider.fetch(fetch, '/repos/rust-lang/rust', {})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -54,7 +49,7 @@ describe('Github API provider', function () {
|
||||
const headers = []
|
||||
async function performOneRequest() {
|
||||
const { res } = await githubApiProvider.fetch(
|
||||
requestFetcher,
|
||||
fetch,
|
||||
'/repos/rust-lang/rust',
|
||||
{}
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import Joi from 'joi'
|
||||
import log from '../../core/server/log.js'
|
||||
import { TokenPool } from '../../core/token-pooling/token-pool.js'
|
||||
import { userAgent } from '../../core/base-service/legacy-request-handler.js'
|
||||
import { getUserAgent } from '../../core/base-service/got-config.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { ImproperlyConfigured } from '../index.js'
|
||||
|
||||
const userAgent = getUserAgent()
|
||||
|
||||
const headerSchema = Joi.object({
|
||||
'x-ratelimit-limit': nonNegativeInteger,
|
||||
'x-ratelimit-remaining': nonNegativeInteger,
|
||||
|
||||
@@ -3,8 +3,8 @@ import { mergeQueries } from '../../core/base-service/graphql.js'
|
||||
import { BaseGraphqlService, BaseJsonService } from '../index.js'
|
||||
|
||||
function createRequestFetcher(context) {
|
||||
const { sendAndCacheRequest, githubApiProvider } = context
|
||||
return githubApiProvider.fetch.bind(githubApiProvider, sendAndCacheRequest)
|
||||
const { requestFetcher, githubApiProvider } = context
|
||||
return githubApiProvider.fetch.bind(githubApiProvider, requestFetcher)
|
||||
}
|
||||
|
||||
class GithubAuthV3Service extends BaseJsonService {
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('GithubAuthV3Service', function () {
|
||||
}
|
||||
|
||||
it('forwards custom Accept header', async function () {
|
||||
const sendAndCacheRequest = sinon.stub().returns(
|
||||
const requestFetcher = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '{"requiredString": "some-string"}',
|
||||
res: {
|
||||
@@ -46,15 +46,15 @@ describe('GithubAuthV3Service', function () {
|
||||
sinon.stub(githubApiProvider.standardTokens, 'next').returns(mockToken)
|
||||
|
||||
DummyGithubAuthV3Service.invoke({
|
||||
sendAndCacheRequest,
|
||||
requestFetcher,
|
||||
githubApiProvider,
|
||||
})
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'https://github-api.example.com/repos/badges/shields/check-runs',
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'Shields.io/2003a',
|
||||
'User-Agent': 'shields (self-hosted)/dev',
|
||||
Accept: 'application/vnd.github.antiope-preview+json',
|
||||
Authorization: 'token undefined',
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ async function fetchRepoContent(
|
||||
const { content } = await serviceInstance._requestJson({
|
||||
schema: contentSchema,
|
||||
url: `/repos/${user}/${repo}/contents/${filename}`,
|
||||
options: { qs: { ref: branch } },
|
||||
options: { searchParams: { ref: branch } },
|
||||
errorMessages,
|
||||
})
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class GithubContributors extends GithubAuthV3Service {
|
||||
|
||||
const { res, buffer } = await this._request({
|
||||
url: `/repos/${user}/${repo}/contributors`,
|
||||
options: { qs: { page: '1', per_page: '1', anon: isAnon } },
|
||||
options: { searchParams: { page: '1', per_page: '1', anon: isAnon } },
|
||||
errorMessages: errorMessagesFor('repo not found'),
|
||||
})
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ export default class GithubDownloads extends GithubAuthV3Service {
|
||||
const allReleases = await this._requestJson({
|
||||
schema: releaseArraySchema,
|
||||
url: `/repos/${user}/${repo}/releases`,
|
||||
options: { qs: { per_page: 500 } },
|
||||
options: { searchParams: { per_page: 500 } },
|
||||
errorMessages: errorMessagesFor('repo not found'),
|
||||
})
|
||||
releases = allReleases
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class GithubLastCommit extends GithubAuthV3Service {
|
||||
async fetch({ user, repo, branch }) {
|
||||
return this._requestJson({
|
||||
url: `/repos/${user}/${repo}/commits`,
|
||||
options: { qs: { sha: branch } },
|
||||
options: { searchParams: { sha: branch } },
|
||||
schema,
|
||||
errorMessages: errorMessagesFor(),
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class GithubSearch extends GithubAuthV3Service {
|
||||
const { total_count: totalCount } = await this._requestJson({
|
||||
url: '/search/code',
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
q: `${query} repo:${user}/${repo}`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@ export default class GithubWorkflowStatus extends BaseSvgScrapingService {
|
||||
url: `https://github.com/${user}/${repo}/workflows/${encodeURIComponent(
|
||||
workflow
|
||||
)}/badge.svg`,
|
||||
options: { qs: { branch, event } },
|
||||
options: { searchParams: { branch, event } },
|
||||
valueMatcher: />([^<>]+)<\/tspan><\/text><\/g><path/,
|
||||
errorMessages: {
|
||||
404: 'repo, branch, or workflow not found',
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class GitLabBase extends BaseJsonService {
|
||||
async fetchPage({ page, requestParams, schema }) {
|
||||
const { res, buffer } = await this._request({
|
||||
...requestParams,
|
||||
...{ options: { qs: { page } } },
|
||||
...{ options: { searchParams: { page } } },
|
||||
})
|
||||
|
||||
const json = this._parseJson(buffer)
|
||||
@@ -39,7 +39,7 @@ export default class GitLabBase extends BaseJsonService {
|
||||
url,
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
qs: { per_page: 100 },
|
||||
searchParams: { per_page: 100 },
|
||||
...options,
|
||||
},
|
||||
errorMessages,
|
||||
|
||||
@@ -95,13 +95,7 @@ export default class GitlabCoverage extends BaseSvgScrapingService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({
|
||||
user,
|
||||
repo,
|
||||
branch,
|
||||
gitlab_url: baseUrl = 'https://gitlab.com',
|
||||
job_name: jobName,
|
||||
}) {
|
||||
async fetch({ user, repo, branch, baseUrl = 'https://gitlab.com', jobName }) {
|
||||
// Since the URL doesn't return a usable value when an invalid job name is specified,
|
||||
// it is recommended to not use the query param at all if not required
|
||||
jobName = jobName ? `?job=${jobName}` : ''
|
||||
@@ -124,13 +118,16 @@ export default class GitlabCoverage extends BaseSvgScrapingService {
|
||||
return Number(coverage.slice(0, -1))
|
||||
}
|
||||
|
||||
async handle({ user, repo, branch }, { gitlab_url, job_name }) {
|
||||
async handle(
|
||||
{ user, repo, branch },
|
||||
{ gitlab_url: baseUrl, job_name: jobName }
|
||||
) {
|
||||
const { message: coverage } = await this.fetch({
|
||||
user,
|
||||
repo,
|
||||
branch,
|
||||
gitlab_url,
|
||||
job_name,
|
||||
baseUrl,
|
||||
jobName,
|
||||
})
|
||||
return this.constructor.render({
|
||||
coverage: this.constructor.transform({ coverage }),
|
||||
|
||||
@@ -106,7 +106,7 @@ export default class GitLabRelease extends GitLabBase {
|
||||
404: 'project not found',
|
||||
},
|
||||
options: {
|
||||
qs: { order_by: orderBy },
|
||||
searchParams: { order_by: orderBy },
|
||||
},
|
||||
firstPageOnly: !isSemver,
|
||||
})
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class GitlabTag extends GitLabBase {
|
||||
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
|
||||
project
|
||||
)}/repository/tags`,
|
||||
options: { qs: { order_by: 'updated' } },
|
||||
options: { searchParams: { order_by: 'updated' } },
|
||||
errorMessages: {
|
||||
404: 'repo not found',
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class HSTS extends BaseJsonService {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://hstspreload.org/api/v2/status`,
|
||||
options: { qs: { domain } },
|
||||
options: { searchParams: { domain } },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ export default class JenkinsBase extends BaseJsonService {
|
||||
async fetch({
|
||||
url,
|
||||
schema,
|
||||
qs,
|
||||
searchParams,
|
||||
errorMessages = { 404: 'instance or job not found' },
|
||||
}) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withBasicAuth({
|
||||
url,
|
||||
options: { qs },
|
||||
options: { searchParams },
|
||||
schema,
|
||||
errorMessages,
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ export default class JenkinsBuild extends JenkinsBase {
|
||||
const json = await this.fetch({
|
||||
url: buildUrl({ jobUrl, lastCompletedBuild: false }),
|
||||
schema,
|
||||
qs: buildTreeParamQueryString('color'),
|
||||
searchParams: buildTreeParamQueryString('color'),
|
||||
})
|
||||
const { status } = this.transform({ json })
|
||||
return this.constructor.render({ status })
|
||||
|
||||
@@ -118,7 +118,7 @@ export default class JenkinsCoverage extends JenkinsBase {
|
||||
const json = await this.fetch({
|
||||
url: buildUrl({ jobUrl, plugin: pluginSpecificPath }),
|
||||
schema,
|
||||
qs: buildTreeParamQueryString(treeQueryParam),
|
||||
searchParams: buildTreeParamQueryString(treeQueryParam),
|
||||
errorMessages: {
|
||||
404: 'job or coverage not found',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { regularUpdate } from '../../core/legacy/regular-update.js'
|
||||
import { getCachedResource } from '../../core/base-service/resource-cache.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { BaseService, NotFound } from '../index.js'
|
||||
|
||||
@@ -31,9 +31,9 @@ export default class JenkinsPluginVersion extends BaseService {
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
return regularUpdate({
|
||||
return getCachedResource({
|
||||
url: 'https://updates.jenkins-ci.org/current/update-center.actual.json',
|
||||
intervalMillis: 4 * 3600 * 1000,
|
||||
ttl: 4 * 3600 * 1000, // 4 hours in milliseconds
|
||||
scraper: json =>
|
||||
Object.keys(json.plugins).reduce((previous, current) => {
|
||||
previous[current] = json.plugins[current].version
|
||||
|
||||
@@ -34,8 +34,7 @@ const schema = Joi.object({
|
||||
}).required()
|
||||
|
||||
export default class JenkinsTests extends JenkinsBase {
|
||||
static category = 'build'
|
||||
|
||||
static category = 'test-results'
|
||||
static route = {
|
||||
base: 'jenkins',
|
||||
pattern: 'tests',
|
||||
@@ -117,7 +116,9 @@ export default class JenkinsTests extends JenkinsBase {
|
||||
const json = await this.fetch({
|
||||
url: buildUrl({ jobUrl }),
|
||||
schema,
|
||||
qs: buildTreeParamQueryString('actions[failCount,skipCount,totalCount]'),
|
||||
searchParams: buildTreeParamQueryString(
|
||||
'actions[failCount,skipCount,totalCount]'
|
||||
),
|
||||
})
|
||||
const { passed, failed, skipped, total } = this.transform({ json })
|
||||
return this.constructor.render({
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class JiraSprint extends BaseJsonService {
|
||||
url: `${baseUrl}/rest/api/2/search`,
|
||||
schema,
|
||||
options: {
|
||||
qs: {
|
||||
searchParams: {
|
||||
jql: `sprint=${sprintId} AND type IN (Bug,Improvement,Story,"Technical task")`,
|
||||
fields: 'resolution',
|
||||
maxResults: 500,
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class KeybaseBTC extends KeybaseProfile {
|
||||
|
||||
async handle({ username }) {
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
usernames: username,
|
||||
fields: 'cryptocurrency_addresses',
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class KeybasePGP extends KeybaseProfile {
|
||||
|
||||
async handle({ username }) {
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
usernames: username,
|
||||
fields: 'public_keys',
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class KeybaseXLM extends KeybaseProfile {
|
||||
|
||||
async handle({ username }) {
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
usernames: username,
|
||||
fields: 'stellar',
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class KeybaseZEC extends KeybaseProfile {
|
||||
|
||||
async handle({ username }) {
|
||||
const options = {
|
||||
qs: {
|
||||
searchParams: {
|
||||
usernames: username,
|
||||
fields: 'cryptocurrency_addresses',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ImproperlyConfigured } from '../index.js'
|
||||
import log from '../../core/server/log.js'
|
||||
import { TokenPool } from '../../core/token-pooling/token-pool.js'
|
||||
import { userAgent } from '../../core/base-service/legacy-request-handler.js'
|
||||
import { getUserAgent } from '../../core/base-service/got-config.js'
|
||||
|
||||
const userAgent = getUserAgent()
|
||||
|
||||
// Provides an interface to the Libraries.io API.
|
||||
export default class LibrariesIoApiProvider {
|
||||
@@ -89,9 +91,9 @@ export default class LibrariesIoApiProvider {
|
||||
'User-Agent': userAgent,
|
||||
...options.headers,
|
||||
},
|
||||
qs: {
|
||||
searchParams: {
|
||||
api_key: tokenString,
|
||||
...options.qs,
|
||||
...options.searchParams,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ const projectSchema = Joi.object({
|
||||
export default class LibrariesIoBase extends BaseJsonService {
|
||||
constructor(context, config) {
|
||||
super(context, config)
|
||||
const { sendAndCacheRequest, librariesIoApiProvider } = context
|
||||
const { requestFetcher, librariesIoApiProvider } = context
|
||||
this._requestFetcher = librariesIoApiProvider.fetch.bind(
|
||||
librariesIoApiProvider,
|
||||
sendAndCacheRequest
|
||||
requestFetcher
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class Localizely extends BaseJsonService {
|
||||
schema,
|
||||
url: `https://api.localizely.com/v1/projects/${projectId}/status`,
|
||||
options: {
|
||||
qs: { branch },
|
||||
searchParams: { branch },
|
||||
headers: { 'X-Api-Token': apiToken },
|
||||
},
|
||||
errorMessages: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user