Compare commits
38 Commits
github-oau
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0054a4c30b | ||
|
|
42a9ca6444 | ||
|
|
4ad822c42e | ||
|
|
a3601c9b3e | ||
|
|
1593793c27 | ||
|
|
1ff1fc0c58 | ||
|
|
c02f9d9396 | ||
|
|
a3c2ada96e | ||
|
|
403bb557c7 | ||
|
|
db8d556671 | ||
|
|
3471a99edf | ||
|
|
ca927fd5d8 | ||
|
|
4a8c1d1d0f | ||
|
|
aa38625b86 | ||
|
|
a3e2b2ff28 | ||
|
|
28cf98fff4 | ||
|
|
14a65ba38f | ||
|
|
ca0defa0a7 | ||
|
|
5e96598fcb | ||
|
|
926e62f927 | ||
|
|
0fe5839757 | ||
|
|
b69b13a72d | ||
|
|
8a9efb2fc9 | ||
|
|
e9153ab97a | ||
|
|
d79cf79f8a | ||
|
|
d0734a313a | ||
|
|
2c9068e976 | ||
|
|
8b6a61c764 | ||
|
|
2ae4d179df | ||
|
|
b171af4f82 | ||
|
|
74ffb87012 | ||
|
|
16620695e2 | ||
|
|
3d6c7971d1 | ||
|
|
f222668682 | ||
|
|
1f94f8d571 | ||
|
|
16813841b7 | ||
|
|
6dbee4457f | ||
|
|
c3dd1fbc5c |
20
.github/actions/close-bot/package-lock.json
generated
vendored
20
.github/actions/close-bot/package-lock.json
generated
vendored
@@ -9,14 +9,17 @@
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.5.0",
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz",
|
||||
"integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ=="
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/github": {
|
||||
"version": "5.0.0",
|
||||
@@ -193,9 +196,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz",
|
||||
"integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ=="
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"@actions/github": {
|
||||
"version": "5.0.0",
|
||||
|
||||
2
.github/actions/close-bot/package.json
vendored
2
.github/actions/close-bot/package.json
vendored
@@ -10,7 +10,7 @@
|
||||
"author": "chris48s",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.5.0",
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -4,6 +4,20 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2021-10-04
|
||||
|
||||
- feat: add 2021 support to GitHub Hacktoberfest [#7086](https://github.com/badges/shields/issues/7086)
|
||||
- Add [ClearlyDefined] service [#6944](https://github.com/badges/shields/issues/6944)
|
||||
- handle null licenses in crates.io response schema, run [crates] [#7074](https://github.com/badges/shields/issues/7074)
|
||||
- [OBS] add Open Build Service service-badge [#6993](https://github.com/badges/shields/issues/6993)
|
||||
- Correction of badges url in self-hosting configuration with a custom port. Issue 7025 [#7036](https://github.com/badges/shields/issues/7036)
|
||||
- fix: support gitlab token via env var [#7023](https://github.com/badges/shields/issues/7023)
|
||||
- Add API-based support for [GitLab] badges, add new GitLab Tag badge [#6988](https://github.com/badges/shields/issues/6988)
|
||||
- [freecodecamp]: allow + symbol in username [#7016](https://github.com/badges/shields/issues/7016)
|
||||
- Rename Riot to Element in Matrix badge help [#6996](https://github.com/badges/shields/issues/6996)
|
||||
- Fixed Reddit Negative Karma Issue [#6992](https://github.com/badges/shields/issues/6992)
|
||||
- Dependency updates
|
||||
|
||||
## server-2021-09-01
|
||||
|
||||
- use multi-stage build to reduce size of docker images [#6938](https://github.com/badges/shields/issues/6938)
|
||||
|
||||
@@ -50,6 +50,8 @@ public:
|
||||
authorizedOrigins: 'NEXUS_ORIGINS'
|
||||
npm:
|
||||
authorizedOrigins: 'NPM_ORIGINS'
|
||||
obs:
|
||||
authorizedOrigins: 'OBS_ORIGINS'
|
||||
sonar:
|
||||
authorizedOrigins: 'SONAR_ORIGINS'
|
||||
teamcity:
|
||||
@@ -87,6 +89,8 @@ private:
|
||||
nexus_user: 'NEXUS_USER'
|
||||
nexus_pass: 'NEXUS_PASS'
|
||||
npm_token: 'NPM_TOKEN'
|
||||
obs_user: 'OBS_USER'
|
||||
obs_pass: 'OBS_PASS'
|
||||
redis_url: 'REDIS_URL'
|
||||
sentry_dsn: 'SENTRY_DSN'
|
||||
shields_secret: 'SHIELDS_SECRET'
|
||||
|
||||
@@ -22,6 +22,8 @@ public:
|
||||
debug:
|
||||
enabled: false
|
||||
intervalSeconds: 200
|
||||
obs:
|
||||
authorizedOrigins: 'https://api.opensuse.org'
|
||||
weblate:
|
||||
authorizedOrigins: 'https://hosted.weblate.org'
|
||||
trace: false
|
||||
|
||||
@@ -6,6 +6,8 @@ private:
|
||||
# preferable for self hosting.
|
||||
gh_token: '...'
|
||||
gitlab_token: '...'
|
||||
obs_user: '...'
|
||||
obs_pass: '...'
|
||||
twitch_client_id: '...'
|
||||
twitch_client_secret: '...'
|
||||
weblate_api_key: '...'
|
||||
|
||||
@@ -124,15 +124,11 @@ describe('BaseService', function () {
|
||||
})
|
||||
|
||||
describe('Logging', function () {
|
||||
let sandbox
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
sinon.stub(trace, 'logTrace')
|
||||
})
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function () {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
sinon.restore()
|
||||
})
|
||||
it('Invokes the logger as expected', async function () {
|
||||
await DummyService.invoke(
|
||||
@@ -426,15 +422,11 @@ describe('BaseService', function () {
|
||||
})
|
||||
|
||||
describe('request', function () {
|
||||
let sandbox
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
sinon.stub(trace, 'logTrace')
|
||||
})
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function () {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('logs appropriate information', async function () {
|
||||
|
||||
@@ -99,14 +99,11 @@ describe('Cache header functions', function () {
|
||||
})
|
||||
|
||||
describe('setHeadersForCacheLength', function () {
|
||||
let sandbox
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
sandbox.useFakeTimers()
|
||||
sinon.useFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
sandbox = undefined
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should set the correct Date header', function () {
|
||||
|
||||
@@ -10,15 +10,11 @@ describe('validate', function () {
|
||||
requiredString: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
let sandbox
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
sinon.stub(trace, 'logTrace')
|
||||
})
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function () {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const ErrorClass = InvalidParameter
|
||||
|
||||
@@ -134,6 +134,7 @@ const publicConfigSchema = Joi.object({
|
||||
}).default({ authorizedOrigins: [] }),
|
||||
nexus: defaultService,
|
||||
npm: defaultService,
|
||||
obs: defaultService,
|
||||
sonar: defaultService,
|
||||
teamcity: defaultService,
|
||||
weblate: defaultService,
|
||||
@@ -172,6 +173,8 @@ const privateConfigSchema = Joi.object({
|
||||
nexus_user: Joi.string(),
|
||||
nexus_pass: Joi.string(),
|
||||
npm_token: Joi.string(),
|
||||
obs_user: Joi.string(),
|
||||
obs_pass: Joi.string(),
|
||||
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
||||
sentry_dsn: Joi.string(),
|
||||
shields_secret: Joi.string(),
|
||||
|
||||
@@ -188,10 +188,6 @@ class TokenPool {
|
||||
this.priorityQueue = new PriorityQueue(this.constructor.compareTokens)
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.tokenIds.size
|
||||
}
|
||||
|
||||
/**
|
||||
* compareTokens
|
||||
*
|
||||
|
||||
22
doc/adding-new-config-values.md
Normal file
22
doc/adding-new-config-values.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Adding New Config Values
|
||||
|
||||
The Badge Server supports a [variety of methods for defining configuration settings and secrets](./server-secrets.md), and provides a framework for loading those values during bootstrapping.
|
||||
|
||||
Any new configuration setting or secret must be correctly registered so that it will be loaded at startup along with the others.
|
||||
|
||||
This generally includes adding the corresponding information for your new setting(s)/secret(s) to the following locations:
|
||||
|
||||
- [core/server/server.js](https://github.com/badges/shields/blob/master/core/server/server.js) - Add the new values to the [schemas](https://github.com/badges/shields/blob/master/core/server/server.js#L118-L193). Secrets/tokens/etc. should go in the `privateConfigSchema` while non-secret configuration settings should go in the `publicConfigSchema`.
|
||||
- [config/custom-environment-variables.yml](https://github.com/badges/shields/blob/master/config/custom-environment-variables.yml)
|
||||
- [docs/server-secrets.md](https://github.com/badges/shields/blob/master/doc/server-secrets.md) (only applicable for secrets)
|
||||
- [config/default.yml](https://github.com/badges/shields/blob/master/config/default.yml) (optional)
|
||||
- Any other template config files (e.g. `config/local.template.yml`) (optional)
|
||||
|
||||
The exact values needed will depend on what type of secret/setting you are adding, but for reference a few commits are included below which added secrets and or settings:
|
||||
|
||||
- (secret) [8a9efb2fc99f97e78ab133c836ab1685803bf4df](https://github.com/badges/shields/commit/8a9efb2fc99f97e78ab133c836ab1685803bf4df)
|
||||
- (secret) [bd6f4ee1465d14a8f188c37823748a21b6a46762](https://github.com/badges/shields/commit/bd6f4ee1465d14a8f188c37823748a21b6a46762)
|
||||
- (secret) [0fd557d7bb623e3852c92cebac586d5f6d6d89d8](https://github.com/badges/shields/commit/0fd557d7bb623e3852c92cebac586d5f6d6d89d8)
|
||||
- (configuration setting) [b1fc4925928c061234e9492f3794c0797467e123](https://github.com/badges/shields/commit/b1fc4925928c061234e9492f3794c0797467e123)
|
||||
|
||||
Don't hesitate to reach out if you're unsure of the exact values needed for your new secret/setting, or have any other questions. Feel free to post questions on your corresponding Issue/Pull Request, and/or ping us on the `contributing` channel on our Discord server.
|
||||
@@ -16,7 +16,7 @@ Production hosting is managed by the Shields ops team:
|
||||
|
||||
| Component | Subcomponent | People with access |
|
||||
| ----------------------------- | ------------------------------- | --------------------------------------------------------------- |
|
||||
| shields-production-us | Account owner | @paulmelnikow |
|
||||
| shields-production-us | Account owner | @calebcartwright, @paulmelnikow |
|
||||
| shields-production-us | Full access | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
| shields-production-us | Access management | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
| Compose.io Redis | Account owner | @paulmelnikow |
|
||||
|
||||
@@ -193,6 +193,21 @@ installation access to private npm packages
|
||||
|
||||
[npm token]: https://docs.npmjs.com/getting-started/working_with_tokens
|
||||
|
||||
## Open Build Service
|
||||
|
||||
- `OBS_USER` (yml: `private.obs_user`)
|
||||
- `OBS_PASS` (yml: `private.obs_user`)
|
||||
|
||||
Only authenticated users are allowed to access the Open Build Service API.
|
||||
Authentication is done by sending a Basic HTTP Authorisation header. A user
|
||||
account for the [reference instance](https://build.opensuse.org) is a SUSE
|
||||
IdP account, which can be created [here](https://idp-portal.suse.com/univention/self-service/#page=createaccount).
|
||||
|
||||
While OBS supports [API tokens](https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.authorization.token.html#id-1.5.10.16.4),
|
||||
they can only be scoped to execute specific actions on a POST request. This
|
||||
means however, that an actual account is required to read the build status
|
||||
of a package.
|
||||
|
||||
### SymfonyInsight (formerly Sensiolabs)
|
||||
|
||||
- `SL_INSIGHT_USER_UUID` (yml: `private.sl_insight_userUuid`)
|
||||
|
||||
@@ -21,11 +21,11 @@ export function getBaseUrl(): string {
|
||||
https://img.shields.io/
|
||||
*/
|
||||
try {
|
||||
const { protocol, hostname } = window.location
|
||||
const { protocol, hostname, port } = window.location
|
||||
if (['shields.io', 'www.shields.io'].includes(hostname)) {
|
||||
return 'https://img.shields.io'
|
||||
}
|
||||
return `${protocol}//${hostname}`
|
||||
return `${protocol}//${hostname}:${port}`
|
||||
} catch (e) {
|
||||
// server-side rendering
|
||||
return ''
|
||||
|
||||
2597
package-lock.json
generated
2597
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -24,7 +24,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^4.5.0",
|
||||
"@fontsource/lekton": "^4.5.0",
|
||||
"@sentry/node": "^6.12.0",
|
||||
"@sentry/node": "^6.13.2",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.0",
|
||||
@@ -34,14 +34,14 @@
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"decamelize": "^5.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"emojic": "^1.1.16",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^3.20.0",
|
||||
"glob": "^7.1.7",
|
||||
"fast-xml-parser": "^3.20.3",
|
||||
"glob": "^7.2.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "11.8.2",
|
||||
"graphql": "^15.5.3",
|
||||
"graphql": "^15.6.0",
|
||||
"graphql-tag": "^2.12.5",
|
||||
"ioredis": "4.27.9",
|
||||
"joi": "17.4.2",
|
||||
@@ -57,12 +57,12 @@
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^13.2.0",
|
||||
"prom-client": "^14.0.0",
|
||||
"qs": "^6.10.1",
|
||||
"query-string": "^7.0.1",
|
||||
"request": "~2.88.2",
|
||||
"semver": "~7.3.5",
|
||||
"simple-icons": "5.14.0",
|
||||
"simple-icons": "5.16.0",
|
||||
"webextension-store-meta": "^1.0.4",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -146,7 +146,7 @@
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.15.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.groupby": "^4.6.6",
|
||||
"@types/mocha": "^9.0.0",
|
||||
@@ -155,11 +155,11 @@
|
||||
"@types/react-modal": "^3.12.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.14",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-plugin-istanbul": "^6.0.0",
|
||||
"babel-preset-gatsby": "^1.13.0",
|
||||
"babel-preset-gatsby": "^1.14.0",
|
||||
"c8": "^7.9.0",
|
||||
"caller": "^1.0.1",
|
||||
"chai": "^4.3.4",
|
||||
@@ -168,8 +168,8 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^6.2.1",
|
||||
"cypress": "^8.4.0",
|
||||
"concurrently": "^6.2.2",
|
||||
"cypress": "^8.5.0",
|
||||
"danger": "^10.6.6",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
@@ -192,12 +192,12 @@
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "3.13.1",
|
||||
"gatsby-plugin-catch-links": "^3.13.0",
|
||||
"gatsby-plugin-page-creator": "^3.13.0",
|
||||
"gatsby-plugin-react-helmet": "^4.13.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^3.13.0",
|
||||
"gatsby-plugin-styled-components": "^4.13.0",
|
||||
"gatsby-plugin-typescript": "^3.2.0",
|
||||
"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",
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
@@ -208,13 +208,13 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.5",
|
||||
"mocha": "^9.1.1",
|
||||
"mocha": "^9.1.2",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.0.0",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.1.3",
|
||||
"node-mocks-http": "^1.10.1",
|
||||
"nodemon": "^2.0.12",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"nodemon": "^2.0.13",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
import {
|
||||
testResultQueryParamSchema,
|
||||
renderTestResultBadge,
|
||||
documentation,
|
||||
} from '../test-results.js'
|
||||
import AppVeyorBase from './appveyor-base.js'
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
You may change the "passed", "failed" and "skipped" text on this badge by supplying query parameters <code>&passed_label=</code>, <code>&failed_label=</code> and <code>&skipped_label=</code> respectively.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For example, if you want to use a different terminology:
|
||||
<br>
|
||||
<code>/appveyor/tests/NZSmartie/coap-net-iu0to.svg?passed_label=good&failed_label=bad&skipped_label=n%2Fa</code>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Or symbols:
|
||||
<br>
|
||||
<code>/appveyor/tests/NZSmartie/coap-net-iu0to.svg?compact_message&passed_label=%F0%9F%8E%89&failed_label=%F0%9F%92%A2&skipped_label=%F0%9F%A4%B7</code>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
There is also a <code>&compact_message</code> query parameter, which will default to displaying ✔, ✘ and ➟, separated by a horizontal bar |.
|
||||
</p>
|
||||
`
|
||||
|
||||
const commonPreviewProps = {
|
||||
passed: 477,
|
||||
failed: 2,
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
import queryString from 'querystring'
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
isDefaultTestTotals,
|
||||
isDefaultCompactTestTotals,
|
||||
isCustomTestTotals,
|
||||
isCustomCompactTestTotals,
|
||||
} from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
const isAppveyorTestTotals = Joi.string().regex(
|
||||
/^[0-9]+ passed(, [0-9]+ failed)?(, [0-9]+ skipped)?$/
|
||||
)
|
||||
|
||||
const isCompactAppveyorTestTotals = Joi.string().regex(
|
||||
/^✔ [0-9]+( \| ✘ [0-9]+)?( \| ➟ [0-9]+)?$/
|
||||
)
|
||||
|
||||
const isCustomAppveyorTestTotals = Joi.string().regex(
|
||||
/^[0-9]+ good(, [0-9]+ bad)?(, [0-9]+ n\/a)?$/
|
||||
)
|
||||
|
||||
const isCompactCustomAppveyorTestTotals = Joi.string().regex(
|
||||
/^💃 [0-9]+( \| 🤦♀️ [0-9]+)?( \| 🤷 [0-9]+)?$/
|
||||
)
|
||||
|
||||
t.create('Test status')
|
||||
.timeout(10000)
|
||||
.get('/NZSmartie/coap-net-iu0to.json')
|
||||
.expectBadge({ label: 'tests', message: isAppveyorTestTotals })
|
||||
.expectBadge({ label: 'tests', message: isDefaultTestTotals })
|
||||
|
||||
t.create('Test status on branch')
|
||||
.timeout(10000)
|
||||
.get('/NZSmartie/coap-net-iu0to/master.json')
|
||||
.expectBadge({ label: 'tests', message: isAppveyorTestTotals })
|
||||
.expectBadge({ label: 'tests', message: isDefaultTestTotals })
|
||||
|
||||
t.create('Test status with compact message')
|
||||
.timeout(10000)
|
||||
.get('/NZSmartie/coap-net-iu0to.json?compact_message')
|
||||
.expectBadge({ label: 'tests', message: isCompactAppveyorTestTotals })
|
||||
.expectBadge({ label: 'tests', message: isDefaultCompactTestTotals })
|
||||
|
||||
t.create('Test status with custom labels')
|
||||
.timeout(10000)
|
||||
@@ -43,21 +31,21 @@ t.create('Test status with custom labels')
|
||||
skipped_label: 'n/a',
|
||||
},
|
||||
})
|
||||
.expectBadge({ label: 'tests', message: isCustomAppveyorTestTotals })
|
||||
.expectBadge({ label: 'tests', message: isCustomTestTotals })
|
||||
|
||||
t.create('Test status with compact message and custom labels')
|
||||
.timeout(10000)
|
||||
.get(
|
||||
`/NZSmartie/coap-net-iu0to.json?${queryString.stringify({
|
||||
.get('/NZSmartie/coap-net-iu0to.json', {
|
||||
qs: {
|
||||
compact_message: null,
|
||||
passed_label: '💃',
|
||||
failed_label: '🤦♀️',
|
||||
skipped_label: '🤷',
|
||||
})}`
|
||||
)
|
||||
},
|
||||
})
|
||||
.expectBadge({
|
||||
label: 'tests',
|
||||
message: isCompactCustomAppveyorTestTotals,
|
||||
message: isCustomCompactTestTotals,
|
||||
})
|
||||
|
||||
t.create('Test status on non-existent project')
|
||||
|
||||
@@ -12,6 +12,7 @@ const greenStatuses = [
|
||||
const orangeStatuses = ['partially succeeded', 'unstable', 'timeout']
|
||||
|
||||
const redStatuses = [
|
||||
'broken',
|
||||
'error',
|
||||
'errored',
|
||||
'failed',
|
||||
|
||||
@@ -53,6 +53,7 @@ test(renderBuildStatusBadge, () => {
|
||||
|
||||
test(renderBuildStatusBadge, () => {
|
||||
forCases([
|
||||
given({ status: 'broken' }),
|
||||
given({ status: 'error' }),
|
||||
given({ status: 'errored' }),
|
||||
given({ status: 'failed' }),
|
||||
|
||||
74
services/clearlydefined/clearlydefined-score.service.js
Normal file
74
services/clearlydefined/clearlydefined-score.service.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
nonNegativeInteger,
|
||||
optionalNonNegativeInteger,
|
||||
} from '../validators.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { BaseJsonService, NotFound } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
scores: Joi.object({
|
||||
effective: nonNegativeInteger,
|
||||
}).required(),
|
||||
described: Joi.object({
|
||||
files: optionalNonNegativeInteger,
|
||||
}),
|
||||
}).required()
|
||||
|
||||
// This service based on the REST API for clearlydefined.io
|
||||
// https://api.clearlydefined.io/api-docs/
|
||||
export default class ClearlyDefinedService extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
static route = {
|
||||
base: 'clearlydefined',
|
||||
pattern: 'score/:type/:provider/:namespace/:name/:revision',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'ClearlyDefined Score',
|
||||
namedParams: {
|
||||
type: 'npm',
|
||||
provider: 'npmjs',
|
||||
namespace: '-',
|
||||
name: 'jquery',
|
||||
revision: '3.4.1',
|
||||
},
|
||||
staticPreview: this.render({ score: 88 }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'score' }
|
||||
|
||||
static render({ score }) {
|
||||
score = Math.round(score)
|
||||
return {
|
||||
label: 'score',
|
||||
message: `${score}/100`,
|
||||
color: floorCountColor(score, 40, 60, 100),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ type, provider, namespace, name, revision }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://api.clearlydefined.io/definitions/${type}/${provider}/${namespace}/${name}/${revision}`,
|
||||
errorMessages: {
|
||||
500: 'unknown type, provider, or upstream issue',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ type, provider, namespace, name, revision }) {
|
||||
const data = await this.fetch({ type, provider, namespace, name, revision })
|
||||
// Return score only if definition contains some files,
|
||||
// else it was an incomplete response due to unknown coordinates
|
||||
if (data.described.files > 0) {
|
||||
return this.constructor.render({ score: data.scores.effective })
|
||||
} else {
|
||||
throw new NotFound({
|
||||
prettyMessage: 'unknown namespace, name, or revision',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
24
services/clearlydefined/clearlydefined-score.tester.js
Normal file
24
services/clearlydefined/clearlydefined-score.tester.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('ClearlyDefined Score')
|
||||
.get('/score/npm/npmjs/-/jquery/3.4.1.json')
|
||||
.expectBadge({
|
||||
label: 'score',
|
||||
message: Joi.string().regex(/^\d+\/\d+$/),
|
||||
})
|
||||
|
||||
t.create('ClearlyDefined Score (name not found)')
|
||||
.get('/score/npm/npmjs/-/not-a-real-package/0.0.0.json')
|
||||
.expectBadge({
|
||||
label: 'score',
|
||||
message: 'unknown namespace, name, or revision',
|
||||
})
|
||||
|
||||
t.create('ClearlyDefined Score (type not found)')
|
||||
.get('/score/abc/xyz/-/not-a-real-package/0.0.0.json')
|
||||
.expectBadge({
|
||||
label: 'score',
|
||||
message: 'unknown type, provider, or upstream issue',
|
||||
})
|
||||
@@ -14,7 +14,7 @@ const crateSchema = Joi.object({
|
||||
.items(
|
||||
Joi.object({
|
||||
downloads: nonNegativeInteger,
|
||||
license: Joi.string().required(),
|
||||
license: Joi.string().required().allow(null),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
@@ -25,7 +25,7 @@ const versionSchema = Joi.object({
|
||||
version: Joi.object({
|
||||
downloads: nonNegativeInteger,
|
||||
num: Joi.string().required(),
|
||||
license: Joi.string().required(),
|
||||
license: Joi.string().required().allow(null),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import { isMetric } from '../test-validators.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'crates',
|
||||
title: 'crates.io',
|
||||
pathPrefix: '/crates',
|
||||
})
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('total downloads')
|
||||
.get('/d/libc.json')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import { BaseCratesService, keywords } from './crates-base.js'
|
||||
|
||||
export default class CratesLicense extends BaseCratesService {
|
||||
@@ -21,28 +22,30 @@ export default class CratesLicense extends BaseCratesService {
|
||||
},
|
||||
]
|
||||
|
||||
static render({ license }) {
|
||||
return {
|
||||
label: 'license',
|
||||
message: license,
|
||||
color: 'blue',
|
||||
static defaultBadgeData = { label: 'license', color: 'blue' }
|
||||
|
||||
static render({ license: message }) {
|
||||
return { message }
|
||||
}
|
||||
|
||||
static transform({ errors, version, versions }) {
|
||||
// crates.io returns a 200 response with an errors object in
|
||||
// error scenarios, e.g. https://crates.io/api/v1/crates/libc/0.1
|
||||
if (errors) {
|
||||
throw new InvalidResponse({ prettyMessage: errors[0].detail })
|
||||
}
|
||||
|
||||
const license = version ? version.license : versions[0].license
|
||||
if (!license) {
|
||||
throw new InvalidResponse({ prettyMessage: 'invalid null license' })
|
||||
}
|
||||
|
||||
return { license }
|
||||
}
|
||||
|
||||
async handle({ crate, version }) {
|
||||
const json = await this.fetch({ crate, version })
|
||||
|
||||
if (json.errors) {
|
||||
/* a call like
|
||||
https://crates.io/api/v1/crates/libc/0.1
|
||||
or
|
||||
https://crates.io/api/v1/crates/libc/0.1.76
|
||||
returns a 200 OK with an errors object */
|
||||
return { message: json.errors[0].detail }
|
||||
}
|
||||
|
||||
return this.constructor.render({
|
||||
license: json.version ? json.version.license : json.versions[0].license,
|
||||
})
|
||||
const { license } = this.constructor.transform(json)
|
||||
return this.constructor.render({ license })
|
||||
}
|
||||
}
|
||||
|
||||
38
services/crates/crates-license.spec.js
Normal file
38
services/crates/crates-license.spec.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import CratesLicense from './crates-license.service.js'
|
||||
|
||||
describe('CratesLicense', function () {
|
||||
test(CratesLicense.transform, () => {
|
||||
given({
|
||||
version: { num: '1.0.0', license: 'MIT' },
|
||||
versions: [{ license: 'MIT/Apache 2.0' }],
|
||||
}).expect({ license: 'MIT' })
|
||||
given({
|
||||
versions: [{ license: 'MIT/Apache 2.0' }],
|
||||
}).expect({ license: 'MIT/Apache 2.0' })
|
||||
})
|
||||
|
||||
it('throws InvalidResponse on error response', function () {
|
||||
expect(() =>
|
||||
CratesLicense.transform({ errors: [{ detail: 'invalid semver' }] })
|
||||
)
|
||||
.to.throw(InvalidResponse)
|
||||
.with.property('prettyMessage', 'invalid semver')
|
||||
})
|
||||
|
||||
it('throws InvalidResponse on null license with specific version', function () {
|
||||
expect(() =>
|
||||
CratesLicense.transform({ version: { num: '1.2.3', license: null } })
|
||||
)
|
||||
.to.throw(InvalidResponse)
|
||||
.with.property('prettyMessage', 'invalid null license')
|
||||
})
|
||||
|
||||
it('throws InvalidResponse on null license with latest version', function () {
|
||||
expect(() => CratesLicense.transform({ versions: [{ license: null }] }))
|
||||
.to.throw(InvalidResponse)
|
||||
.with.property('prettyMessage', 'invalid null license')
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,5 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'crates',
|
||||
title: 'crates.io',
|
||||
pathPrefix: '/crates/l',
|
||||
})
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('license')
|
||||
.get('/libc.json')
|
||||
@@ -16,4 +11,13 @@ t.create('license (with version)')
|
||||
|
||||
t.create('license (not found)')
|
||||
.get('/not-a-real-package.json')
|
||||
.expectBadge({ label: 'crates.io', message: 'not found' })
|
||||
.expectBadge({ label: 'license', message: 'not found' })
|
||||
|
||||
// https://github.com/badges/shields/issues/7073
|
||||
t.create('license (null licenses in history)')
|
||||
.get('/stun.json')
|
||||
.expectBadge({ label: 'license', message: 'MIT/Apache-2.0' })
|
||||
|
||||
t.create('license (version with null license)')
|
||||
.get('/stun/0.0.1.json')
|
||||
.expectBadge({ label: 'license', message: 'invalid null license' })
|
||||
|
||||
@@ -71,22 +71,19 @@ export default class David extends BaseJsonService {
|
||||
}
|
||||
|
||||
async fetch({ kind, user, repo, path }) {
|
||||
const url = `https://david-dm.org/${user}/${repo}/${
|
||||
kind ? `${kind}-` : ''
|
||||
}info.json`
|
||||
// Note: David does not return canonical 404 response codes for 'not found'
|
||||
// cases, but will instead return various 50x errors. Accordingly we account
|
||||
// for both 'not found' as well as typical/real internal server errors.
|
||||
const notFoundError = 'repo or path not found or david internal error'
|
||||
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
options: { qs: { path } },
|
||||
url: `https://status.david-dm.org/gh/${user}/${repo}`,
|
||||
options: { qs: { path, type: kind } },
|
||||
errorMessages: {
|
||||
/* note:
|
||||
david returns a 504 response for 'not found'
|
||||
e.g: https://david-dm.org/foo/barbaz/info.json
|
||||
not a 404 so we can't handle 'not found' cleanly
|
||||
because this might also be some other error.
|
||||
*/
|
||||
504: 'repo or path not found or david internal error',
|
||||
502: notFoundError,
|
||||
503: notFoundError,
|
||||
504: notFoundError,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import request from 'request'
|
||||
import { userAgent } from '../../../core/base-service/legacy-request-handler.js'
|
||||
import log from '../../../core/server/log.js'
|
||||
|
||||
function setRoutes({ server, authHelper, onTokenAccepted, tokenScopes }) {
|
||||
function setRoutes({ server, authHelper, onTokenAccepted }) {
|
||||
const baseUrl = process.env.GATSBY_BASE_URL || 'https://img.shields.io'
|
||||
|
||||
server.route(/^\/github-auth$/, (data, match, end, ask) => {
|
||||
@@ -15,7 +15,6 @@ function setRoutes({ server, authHelper, onTokenAccepted, tokenScopes }) {
|
||||
// it's not setting a bad example.
|
||||
client_id: authHelper._user,
|
||||
redirect_uri: `${baseUrl}/github-auth/done`,
|
||||
scope: tokenScopes,
|
||||
})
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
|
||||
@@ -41,7 +41,6 @@ describe('Github token acceptor', function () {
|
||||
server: camp,
|
||||
authHelper: oauthHelper,
|
||||
onTokenAccepted,
|
||||
tokenScopes: 'read:packages',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,7 +52,6 @@ describe('Github token acceptor', function () {
|
||||
const qs = queryString.stringify({
|
||||
client_id: fakeClientId,
|
||||
redirect_uri: 'https://img.shields.io/github-auth/done',
|
||||
scope: 'read:packages',
|
||||
})
|
||||
const expectedLocationHeader = `https://github.com/login/oauth/authorize?${qs}`
|
||||
expect(res.headers.location).to.equal(expectedLocationHeader)
|
||||
|
||||
@@ -30,10 +30,11 @@ 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.requestAsPromise({
|
||||
await githubApiProvider.requestAsPromise(
|
||||
request,
|
||||
url: '/repos/rust-lang/rust',
|
||||
})
|
||||
'/repos/rust-lang/rust',
|
||||
{}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -51,10 +52,11 @@ describe('Github API provider', function () {
|
||||
|
||||
const headers = []
|
||||
async function performOneRequest() {
|
||||
const { res } = await githubApiProvider.requestAsPromise({
|
||||
const { res } = await githubApiProvider.requestAsPromise(
|
||||
request,
|
||||
url: '/repos/rust-lang/rust',
|
||||
})
|
||||
'/repos/rust-lang/rust',
|
||||
{}
|
||||
)
|
||||
expect(res.statusCode).to.equal(200)
|
||||
headers.push(res.headers)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ class GithubApiProvider {
|
||||
onTokenInvalidated = tokenString => {},
|
||||
globalToken,
|
||||
reserveFraction = 0.25,
|
||||
tokenScopeNames = {},
|
||||
}) {
|
||||
Object.assign(this, {
|
||||
baseUrl,
|
||||
@@ -46,14 +45,12 @@ class GithubApiProvider {
|
||||
onTokenInvalidated,
|
||||
globalToken,
|
||||
reserveFraction,
|
||||
tokenScopeNames,
|
||||
})
|
||||
|
||||
if (this.withPooling) {
|
||||
this.standardTokens = new TokenPool({ batchSize: 25 })
|
||||
this.searchTokens = new TokenPool({ batchSize: 5 })
|
||||
this.graphqlTokens = new TokenPool({ batchSize: 25 })
|
||||
this.packageScopedTokens = new TokenPool({ batchSize: 25 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,41 +60,17 @@ class GithubApiProvider {
|
||||
standardTokens: this.standardTokens.serializeDebugInfo({ sanitize }),
|
||||
searchTokens: this.searchTokens.serializeDebugInfo({ sanitize }),
|
||||
graphqlTokens: this.graphqlTokens.serializeDebugInfo({ sanitize }),
|
||||
packageScopedTokens: this.packageScopedTokens.serializeDebugInfo({
|
||||
sanitize,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
numReservedScopedTokens() {
|
||||
return this.packageScopedTokens.count()
|
||||
}
|
||||
|
||||
addReservedScopedToken(tokenString, data) {
|
||||
if (!this.withPooling) {
|
||||
throw Error('When not using a token pool, do not provide tokens')
|
||||
}
|
||||
|
||||
const { scopes } = data
|
||||
if (!scopes) {
|
||||
throw new Error('Cannot add unscoped token to reserved token pools')
|
||||
}
|
||||
|
||||
scopes.split('%20').forEach(scope => {
|
||||
if (scope === this.tokenScopeNames.readPackages) {
|
||||
this.packageScopedTokens.add(tokenString, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addToken(tokenString, data) {
|
||||
addToken(tokenString) {
|
||||
if (this.withPooling) {
|
||||
this.standardTokens.add(tokenString, data)
|
||||
this.searchTokens.add(tokenString, data)
|
||||
this.graphqlTokens.add(tokenString, data)
|
||||
this.standardTokens.add(tokenString)
|
||||
this.searchTokens.add(tokenString)
|
||||
this.graphqlTokens.add(tokenString)
|
||||
} else {
|
||||
throw Error('When not using a token pool, do not provide tokens')
|
||||
}
|
||||
@@ -168,11 +141,7 @@ class GithubApiProvider {
|
||||
this.onTokenInvalidated(token.id)
|
||||
}
|
||||
|
||||
tokenForUrl(url, { needsPackageScope }) {
|
||||
if (needsPackageScope) {
|
||||
return this.packageScopedTokens.next()
|
||||
}
|
||||
|
||||
tokenForUrl(url) {
|
||||
if (url.startsWith('/search')) {
|
||||
return this.searchTokens.next()
|
||||
} else if (url.startsWith('/graphql')) {
|
||||
@@ -185,14 +154,14 @@ class GithubApiProvider {
|
||||
// Act like request(), but tweak headers and query to avoid hitting a rate
|
||||
// limit. Inject `request` so we can pass in `cachingRequest` from
|
||||
// `request-handler.js`.
|
||||
request({ request, url, options = {}, neededScopes = {}, callback }) {
|
||||
request(request, url, options = {}, callback) {
|
||||
const { baseUrl } = this
|
||||
|
||||
let token
|
||||
let tokenString
|
||||
if (this.withPooling) {
|
||||
try {
|
||||
token = this.tokenForUrl(url, neededScopes)
|
||||
token = this.tokenForUrl(url)
|
||||
} catch (e) {
|
||||
callback(e)
|
||||
return
|
||||
@@ -229,20 +198,14 @@ class GithubApiProvider {
|
||||
})
|
||||
}
|
||||
|
||||
requestAsPromise({ request, url, options, neededScopes }) {
|
||||
requestAsPromise(request, url, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.request({
|
||||
request,
|
||||
url,
|
||||
options,
|
||||
neededScopes,
|
||||
callback: (err, res, buffer) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve({ res, buffer })
|
||||
}
|
||||
},
|
||||
this.request(request, url, options, (err, res, buffer) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve({ res, buffer })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ describe('Github API provider', function () {
|
||||
const baseUrl = 'https://github-api.example.com'
|
||||
const reserveFraction = 0.333
|
||||
|
||||
let mockStandardToken,
|
||||
mockSearchToken,
|
||||
mockGraphqlToken,
|
||||
mockPackagesScopedToken,
|
||||
provider
|
||||
let mockStandardToken, mockSearchToken, mockGraphqlToken, provider
|
||||
beforeEach(function () {
|
||||
provider = new GithubApiProvider({ baseUrl, reserveFraction })
|
||||
|
||||
@@ -22,11 +18,6 @@ describe('Github API provider', function () {
|
||||
|
||||
mockGraphqlToken = { update: sinon.spy(), invalidate: sinon.spy() }
|
||||
sinon.stub(provider.graphqlTokens, 'next').returns(mockGraphqlToken)
|
||||
|
||||
mockPackagesScopedToken = { update: sinon.spy(), invalidate: sinon.spy() }
|
||||
sinon
|
||||
.stub(provider.packageScopedTokens, 'next')
|
||||
.returns(mockPackagesScopedToken)
|
||||
})
|
||||
|
||||
context('a search API request', function () {
|
||||
@@ -34,16 +25,12 @@ describe('Github API provider', function () {
|
||||
callback()
|
||||
}
|
||||
it('should obtain an appropriate token', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/search',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).to.have.been.calledOnce
|
||||
expect(provider.standardTokens.next).not.to.have.been.called
|
||||
expect(provider.graphqlTokens.next).not.to.have.been.called
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/search', {}, (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).to.have.been.calledOnce
|
||||
expect(provider.standardTokens.next).not.to.have.been.called
|
||||
expect(provider.graphqlTokens.next).not.to.have.been.called
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -53,37 +40,12 @@ describe('Github API provider', function () {
|
||||
callback()
|
||||
}
|
||||
it('should obtain an appropriate token', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/graphql',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).not.to.have.been.called
|
||||
expect(provider.standardTokens.next).not.to.have.been.called
|
||||
expect(provider.graphqlTokens.next).to.have.been.calledOnce
|
||||
done()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('a request requiring the read:packages scope', function () {
|
||||
const mockRequest = (options, callback) => {
|
||||
callback()
|
||||
}
|
||||
it('should obtain an appropriate token', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/graphql',
|
||||
neededScopes: { needsPackageScope: true },
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).not.to.have.been.called
|
||||
expect(provider.standardTokens.next).not.to.have.been.called
|
||||
expect(provider.graphqlTokens.next).not.to.have.been.called
|
||||
expect(provider.packageScopedTokens.next).to.have.been.calledOnce
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/graphql', {}, (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).not.to.have.been.called
|
||||
expect(provider.standardTokens.next).not.to.have.been.called
|
||||
expect(provider.graphqlTokens.next).to.have.been.calledOnce
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -93,16 +55,12 @@ describe('Github API provider', function () {
|
||||
callback()
|
||||
}
|
||||
it('should obtain an appropriate token', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/repo',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).not.to.have.been.called
|
||||
expect(provider.standardTokens.next).to.have.been.calledOnce
|
||||
expect(provider.graphqlTokens.next).not.to.have.been.called
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/repo', {}, (err, res, buffer) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(provider.searchTokens.next).not.to.have.been.called
|
||||
expect(provider.standardTokens.next).to.have.been.calledOnce
|
||||
expect(provider.graphqlTokens.next).not.to.have.been.called
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -126,33 +84,25 @@ describe('Github API provider', function () {
|
||||
}
|
||||
|
||||
it('should invoke the callback', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/foo',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
expect(Object.is(res, mockResponse)).to.be.true
|
||||
expect(Object.is(buffer, mockBuffer)).to.be.true
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/foo', {}, (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
expect(Object.is(res, mockResponse)).to.be.true
|
||||
expect(Object.is(buffer, mockBuffer)).to.be.true
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the token with the expected values', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/foo',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
const expectedUsesRemaining =
|
||||
remaining - Math.ceil(reserveFraction * rateLimit)
|
||||
expect(mockStandardToken.update).to.have.been.calledWith(
|
||||
expectedUsesRemaining,
|
||||
nextReset
|
||||
)
|
||||
expect(mockStandardToken.invalidate).not.to.have.been.called
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/foo', {}, (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
const expectedUsesRemaining =
|
||||
remaining - Math.ceil(reserveFraction * rateLimit)
|
||||
expect(mockStandardToken.update).to.have.been.calledWith(
|
||||
expectedUsesRemaining,
|
||||
nextReset
|
||||
)
|
||||
expect(mockStandardToken.invalidate).not.to.have.been.called
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -182,33 +132,25 @@ describe('Github API provider', function () {
|
||||
}
|
||||
|
||||
it('should invoke the callback', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/graphql',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
expect(Object.is(res, mockResponse)).to.be.true
|
||||
expect(Object.is(buffer, mockBuffer)).to.be.true
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/graphql', {}, (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
expect(Object.is(res, mockResponse)).to.be.true
|
||||
expect(Object.is(buffer, mockBuffer)).to.be.true
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the token with the expected values', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/graphql',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
const expectedUsesRemaining =
|
||||
remaining - Math.ceil(reserveFraction * rateLimit)
|
||||
expect(mockGraphqlToken.update).to.have.been.calledWith(
|
||||
expectedUsesRemaining,
|
||||
nextReset
|
||||
)
|
||||
expect(mockGraphqlToken.invalidate).not.to.have.been.called
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/graphql', {}, (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
const expectedUsesRemaining =
|
||||
remaining - Math.ceil(reserveFraction * rateLimit)
|
||||
expect(mockGraphqlToken.update).to.have.been.calledWith(
|
||||
expectedUsesRemaining,
|
||||
nextReset
|
||||
)
|
||||
expect(mockGraphqlToken.invalidate).not.to.have.been.called
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -222,15 +164,11 @@ describe('Github API provider', function () {
|
||||
}
|
||||
|
||||
it('should invoke the callback and update the token with the expected values', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/foo',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
expect(mockStandardToken.invalidate).to.have.been.calledOnce
|
||||
expect(mockStandardToken.update).not.to.have.been.called
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/foo', {}, (err, res, buffer) => {
|
||||
expect(err).to.equal(null)
|
||||
expect(mockStandardToken.invalidate).to.have.been.calledOnce
|
||||
expect(mockStandardToken.update).not.to.have.been.called
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -242,14 +180,10 @@ describe('Github API provider', function () {
|
||||
}
|
||||
|
||||
it('should pass the error to the callback', function (done) {
|
||||
provider.request({
|
||||
request: mockRequest,
|
||||
url: '/foo',
|
||||
callback: (err, res, buffer) => {
|
||||
expect(err).to.be.an.instanceof(Error)
|
||||
expect(err.message).to.equal('connection timeout')
|
||||
done()
|
||||
},
|
||||
provider.request(mockRequest, '/foo', {}, (err, res, buffer) => {
|
||||
expect(err).to.be.an.instanceof(Error)
|
||||
expect(err.message).to.equal('connection timeout')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,22 +2,21 @@ import gql from 'graphql-tag'
|
||||
import { mergeQueries } from '../../core/base-service/graphql.js'
|
||||
import { BaseGraphqlService, BaseJsonService } from '../index.js'
|
||||
|
||||
function createRequestFetcher(context, config, neededScopes) {
|
||||
function createRequestFetcher(context, config) {
|
||||
const { sendAndCacheRequestWithCallbacks, githubApiProvider } = context
|
||||
|
||||
return async (url, options) =>
|
||||
githubApiProvider.requestAsPromise({
|
||||
request: sendAndCacheRequestWithCallbacks,
|
||||
githubApiProvider.requestAsPromise(
|
||||
sendAndCacheRequestWithCallbacks,
|
||||
url,
|
||||
options,
|
||||
neededScopes,
|
||||
})
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
class GithubAuthV3Service extends BaseJsonService {
|
||||
constructor(context, config, neededScopes) {
|
||||
constructor(context, config) {
|
||||
super(context, config)
|
||||
this._requestFetcher = createRequestFetcher(context, config, neededScopes)
|
||||
this._requestFetcher = createRequestFetcher(context, config)
|
||||
this.staticAuthConfigured = true
|
||||
}
|
||||
}
|
||||
@@ -28,10 +27,10 @@ class GithubAuthV3Service extends BaseJsonService {
|
||||
// useful when consuming GitHub endpoints which are not rate-limited: it
|
||||
// avoids wasting API quota on them in production.
|
||||
class ConditionalGithubAuthV3Service extends BaseJsonService {
|
||||
constructor(context, config, neededScopes) {
|
||||
constructor(context, config) {
|
||||
super(context, config)
|
||||
if (context.githubApiProvider.globalToken) {
|
||||
this._requestFetcher = createRequestFetcher(context, config, neededScopes)
|
||||
this._requestFetcher = createRequestFetcher(context, config)
|
||||
this.staticAuthConfigured = true
|
||||
} else {
|
||||
this.staticAuthConfigured = false
|
||||
@@ -40,9 +39,9 @@ class ConditionalGithubAuthV3Service extends BaseJsonService {
|
||||
}
|
||||
|
||||
class GithubAuthV4Service extends BaseGraphqlService {
|
||||
constructor(context, config, neededScopes) {
|
||||
constructor(context, config) {
|
||||
super(context, config)
|
||||
this._requestFetcher = createRequestFetcher(context, config, neededScopes)
|
||||
this._requestFetcher = createRequestFetcher(context, config)
|
||||
this.staticAuthConfigured = true
|
||||
}
|
||||
|
||||
|
||||
@@ -25,32 +25,17 @@ describe('GithubAuthV3Service', function () {
|
||||
}
|
||||
}
|
||||
|
||||
class ScopedDummyGithubAuthV3Service extends DummyGithubAuthV3Service {
|
||||
constructor(context, config) {
|
||||
super(context, config, { needsPackageScope: true })
|
||||
}
|
||||
}
|
||||
|
||||
let sendAndCacheRequestWithCallbacks, mockToken
|
||||
const githubApiProvider = new GithubApiProvider({
|
||||
baseUrl: 'https://github-api.example.com',
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequestWithCallbacks = sinon.stub().returns(
|
||||
it('forwards custom Accept header', async function () {
|
||||
const sendAndCacheRequestWithCallbacks = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '{"requiredString": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
)
|
||||
mockToken = { id: 'abc123', update: sinon.mock(), invalidate: sinon.mock() }
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('forwards custom Accept header', async function () {
|
||||
const githubApiProvider = new GithubApiProvider({
|
||||
baseUrl: 'https://github-api.example.com',
|
||||
})
|
||||
const mockToken = { update: sinon.mock(), invalidate: sinon.mock() }
|
||||
sinon.stub(githubApiProvider.standardTokens, 'next').returns(mockToken)
|
||||
|
||||
DummyGithubAuthV3Service.invoke({
|
||||
@@ -62,26 +47,7 @@ describe('GithubAuthV3Service', function () {
|
||||
headers: {
|
||||
'User-Agent': 'Shields.io/2003a',
|
||||
Accept: 'application/vnd.github.antiope-preview+json',
|
||||
Authorization: 'token abc123',
|
||||
},
|
||||
url: 'https://github-api.example.com/repos/badges/shields/check-runs',
|
||||
baseUrl: 'https://github-api.example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('uses token with correct read scope', function () {
|
||||
sinon.stub(githubApiProvider.packageScopedTokens, 'next').returns(mockToken)
|
||||
|
||||
ScopedDummyGithubAuthV3Service.invoke({
|
||||
sendAndCacheRequestWithCallbacks,
|
||||
githubApiProvider,
|
||||
})
|
||||
|
||||
expect(sendAndCacheRequestWithCallbacks).to.have.been.calledOnceWith({
|
||||
headers: {
|
||||
'User-Agent': 'Shields.io/2003a',
|
||||
Accept: 'application/vnd.github.antiope-preview+json',
|
||||
Authorization: 'token abc123',
|
||||
Authorization: 'token undefined',
|
||||
},
|
||||
url: 'https://github-api.example.com/repos/badges/shields/check-runs',
|
||||
baseUrl: 'https://github-api.example.com',
|
||||
|
||||
@@ -5,11 +5,6 @@ import GithubApiProvider from './github-api-provider.js'
|
||||
import { setRoutes as setAdminRoutes } from './auth/admin.js'
|
||||
import { setRoutes as setAcceptorRoutes } from './auth/acceptor.js'
|
||||
|
||||
const readPackagesScope = 'read:packages'
|
||||
// Multiple scopes need to be uri-encoded space delimited
|
||||
const tokenScopes = `${readPackagesScope}`
|
||||
const persistenceScopeDelimiter = '.scopes.'
|
||||
|
||||
// Convenience class with all the stuff related to the Github API and its
|
||||
// authorization tokens, to simplify server initialization.
|
||||
class GithubConstellation {
|
||||
@@ -29,8 +24,6 @@ class GithubConstellation {
|
||||
this._debugEnabled = config.service.debug.enabled
|
||||
this._debugIntervalSeconds = config.service.debug.intervalSeconds
|
||||
this.shieldsSecret = config.private.shields_secret
|
||||
this._tokenScopes = {}
|
||||
this._maxNumReservedScopedTokens = 0
|
||||
|
||||
const { redis_url: redisUrl, gh_token: globalToken } = config.private
|
||||
if (redisUrl) {
|
||||
@@ -45,9 +38,6 @@ class GithubConstellation {
|
||||
baseUrl: process.env.GITHUB_URL || 'https://api.github.com',
|
||||
globalToken,
|
||||
withPooling: !globalToken,
|
||||
tokenScopeNames: {
|
||||
readPackages: readPackagesScope,
|
||||
},
|
||||
onTokenInvalidated: tokenString => this.onTokenInvalidated(tokenString),
|
||||
})
|
||||
|
||||
@@ -80,21 +70,8 @@ class GithubConstellation {
|
||||
log.error(e)
|
||||
}
|
||||
|
||||
// Reserve a subset of scoped tokens from the total set
|
||||
// to be used for queries which require an explicit scope,
|
||||
// while leaving a sufficient amount of tokens (scoped or unscoped)
|
||||
// for the bulk of our requests which don't care about scopes.
|
||||
this._maxNumReservedScopedTokens = Math.floor(tokens.length * 0.15)
|
||||
tokens.forEach(tokenString => {
|
||||
const [token, scopes] = tokenString.split(persistenceScopeDelimiter)
|
||||
this._tokenScopes[token] = scopes || null
|
||||
const data = { scopes }
|
||||
const numReserved = this.apiProvider.numReservedScopedTokens()
|
||||
if (scopes && numReserved < this._maxNumReservedScopedTokens) {
|
||||
this.apiProvider.addReservedScopedToken(token, data)
|
||||
} else {
|
||||
this.apiProvider.addToken(token, data)
|
||||
}
|
||||
this.apiProvider.addToken(tokenString)
|
||||
})
|
||||
|
||||
const { shieldsSecret, apiProvider } = this
|
||||
@@ -104,53 +81,19 @@ class GithubConstellation {
|
||||
setAcceptorRoutes({
|
||||
server,
|
||||
authHelper: this.oauthHelper,
|
||||
tokenScopes,
|
||||
onTokenAccepted: tokenString =>
|
||||
this.onTokenAdded(tokenString, tokenScopes),
|
||||
onTokenAccepted: tokenString => this.onTokenAdded(tokenString),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onTokenAdded(tokenString, tokenScopes) {
|
||||
onTokenAdded(tokenString) {
|
||||
if (!this.persistence) {
|
||||
throw Error('Token persistence is not configured')
|
||||
}
|
||||
const data = { scopes: tokenScopes }
|
||||
const numReserved = this.apiProvider.numReservedScopedTokens()
|
||||
if (numReserved < this._maxNumReservedScopedTokens) {
|
||||
this.apiProvider.addReservedScopedToken(tokenString, data)
|
||||
} else {
|
||||
this.apiProvider.addToken(tokenString, data)
|
||||
}
|
||||
|
||||
this.apiProvider.addToken(tokenString)
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
// To avoid having multiple set entries for re-authorized/re-scoped
|
||||
// tokens we need to first remove the previous entry that had different scopes
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(this._tokenScopes, tokenString)
|
||||
) {
|
||||
const currentScopes = this._tokenScopes[tokenString]
|
||||
// These scopes shouldn't match in practice, as that would
|
||||
// indicate the function has somehow been invoked with an existing
|
||||
// token but without any scope changes. Nevertheless, the conditional
|
||||
// guard is here in case there are circumstances that assumption fails
|
||||
// to be upheld.
|
||||
if (currentScopes !== tokenScopes) {
|
||||
const token = currentScopes
|
||||
? `${tokenString}${persistenceScopeDelimiter}${currentScopes}`
|
||||
: tokenString
|
||||
await this.persistence.noteTokenRemoved(token)
|
||||
}
|
||||
}
|
||||
// It's unlikely that we'd evert revert back to no longer requesting any scopes
|
||||
// but handling that scenario regardless so we don't end up
|
||||
// with junk like `abc123.scopes.undefined` in redis
|
||||
const token = tokenScopes
|
||||
? `${tokenString}${persistenceScopeDelimiter}${tokenScopes}`
|
||||
: tokenString
|
||||
await this.persistence.noteTokenAdded(token)
|
||||
this._tokenScopes[tokenString] = tokenScopes || null
|
||||
await this.persistence.noteTokenAdded(tokenString)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
@@ -161,12 +104,7 @@ class GithubConstellation {
|
||||
if (this.persistence) {
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
const scopes = this._tokenScopes[tokenString]
|
||||
const token = scopes
|
||||
? `${tokenString}${persistenceScopeDelimiter}${scopes}`
|
||||
: tokenString
|
||||
await this.persistence.noteTokenRemoved(token)
|
||||
delete this._tokenScopes[tokenString]
|
||||
await this.persistence.noteTokenRemoved(tokenString)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import log from '../../core/server/log.js'
|
||||
import RedisTokenPersistence from '../../core/token-pooling/redis-token-persistence.js'
|
||||
import GithubConstellation from './github-constellation.js'
|
||||
import GithubApiProvider from './github-api-provider.js'
|
||||
|
||||
describe('GithubConstellation', function () {
|
||||
const tokens = [
|
||||
'abc123',
|
||||
'def4567.scopes.read:packages%20read:user',
|
||||
'def789.scopes.read:packages',
|
||||
'ghi012',
|
||||
'fff444.scopes.read:packages',
|
||||
'555eee.scopes.read:packages',
|
||||
'ddd666',
|
||||
'777ccc',
|
||||
'bbb888',
|
||||
'999aaa',
|
||||
'000111.scopes.read:packages',
|
||||
'222333.scopes.read:packages',
|
||||
'111111',
|
||||
'888888',
|
||||
]
|
||||
const config = {
|
||||
private: {
|
||||
redis_url: 'localhost',
|
||||
},
|
||||
service: {
|
||||
debug: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
const server = { ajax: { on: sinon.stub() } }
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(log, 'log')
|
||||
sinon
|
||||
.stub(GithubConstellation, '_createOauthHelper')
|
||||
.returns({ isConfigured: false })
|
||||
sinon.stub(GithubConstellation.prototype, 'scheduleDebugLogging')
|
||||
sinon.stub(RedisTokenPersistence.prototype, 'initialize').returns(tokens)
|
||||
sinon.stub(RedisTokenPersistence.prototype, 'noteTokenAdded')
|
||||
sinon.stub(RedisTokenPersistence.prototype, 'noteTokenRemoved')
|
||||
sinon.spy(GithubApiProvider.prototype, 'addToken')
|
||||
sinon.spy(GithubApiProvider.prototype, 'addReservedScopedToken')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
context('initialize', function () {
|
||||
it('does not fetch tokens when pooling disabled', async function () {
|
||||
const constellation = new GithubConstellation({
|
||||
...config,
|
||||
...{ private: { gh_token: 'secret' } },
|
||||
})
|
||||
await constellation.initialize(server)
|
||||
expect(RedisTokenPersistence.prototype.initialize).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('loads both scoped and unscoped tokens', async function () {
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
expect(constellation.apiProvider.graphqlTokens.count()).to.equal(12)
|
||||
expect(constellation.apiProvider.searchTokens.count()).to.equal(12)
|
||||
expect(constellation.apiProvider.standardTokens.count()).to.equal(12)
|
||||
expect(constellation.apiProvider.packageScopedTokens.count()).to.equal(2)
|
||||
expect(
|
||||
GithubApiProvider.prototype.addReservedScopedToken
|
||||
).to.be.calledWithExactly('def4567', {
|
||||
scopes: 'read:packages%20read:user',
|
||||
})
|
||||
expect(
|
||||
GithubApiProvider.prototype.addReservedScopedToken
|
||||
).to.be.calledWithExactly('def789', {
|
||||
scopes: 'read:packages',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('onTokenAdded', function () {
|
||||
it('adds new scoped token with met reserves', async function () {
|
||||
const token = 'shh_secret'
|
||||
sinon
|
||||
.stub(GithubApiProvider.prototype, 'numReservedScopedTokens')
|
||||
.returns(2)
|
||||
const clock = sinon.useFakeTimers()
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
constellation._maxNumReservedScopedTokens = 2
|
||||
constellation.onTokenAdded(token, 'read:packages')
|
||||
await clock.tickAsync()
|
||||
expect(GithubApiProvider.prototype.addToken).to.be.calledWithExactly(
|
||||
token,
|
||||
{ scopes: 'read:packages' }
|
||||
)
|
||||
expect(
|
||||
GithubApiProvider.prototype.addReservedScopedToken
|
||||
).to.not.be.calledWith(token)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenAdded).to.be.calledWith(
|
||||
`${token}.scopes.read:packages`
|
||||
)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenRemoved).to.not.be.called
|
||||
expect(Object.keys(constellation._tokenScopes).length).to.equal(15)
|
||||
expect(constellation._tokenScopes[token]).to.equal('read:packages')
|
||||
})
|
||||
|
||||
it('adds new scoped token with unmet reserves', async function () {
|
||||
const token = 'shh_secret'
|
||||
sinon
|
||||
.stub(GithubApiProvider.prototype, 'numReservedScopedTokens')
|
||||
.returns(2)
|
||||
const clock = sinon.useFakeTimers()
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
constellation._maxNumReservedScopedTokens = 3
|
||||
constellation.onTokenAdded(token, 'read:packages')
|
||||
await clock.tickAsync()
|
||||
expect(
|
||||
GithubApiProvider.prototype.addReservedScopedToken
|
||||
).to.be.calledWithExactly(token, { scopes: 'read:packages' })
|
||||
expect(GithubApiProvider.prototype.addToken).to.not.be.calledWith(token)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenAdded).to.be.calledWith(
|
||||
`${token}.scopes.read:packages`
|
||||
)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenRemoved).to.not.be.called
|
||||
expect(Object.keys(constellation._tokenScopes).length).to.equal(15)
|
||||
expect(constellation._tokenScopes[token]).to.equal('read:packages')
|
||||
})
|
||||
|
||||
it('adds new unscoped token', async function () {
|
||||
const token = '1234567890987654321'
|
||||
const clock = sinon.useFakeTimers()
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
constellation.onTokenAdded(token)
|
||||
await clock.tickAsync()
|
||||
expect(GithubApiProvider.prototype.addToken).to.be.calledWithExactly(
|
||||
token,
|
||||
{ scopes: undefined }
|
||||
)
|
||||
expect(
|
||||
GithubApiProvider.prototype.addReservedScopedToken
|
||||
).to.not.be.calledWith(token)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenAdded).to.be.calledWith(
|
||||
token
|
||||
)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenRemoved).to.not.be.called
|
||||
expect(Object.keys(constellation._tokenScopes).length).to.equal(15)
|
||||
expect(constellation._tokenScopes[token]).to.equal(null)
|
||||
})
|
||||
|
||||
it('updates scopes on existing token', async function () {
|
||||
const existingToken = 'abc123'
|
||||
const clock = sinon.useFakeTimers()
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
sinon
|
||||
.stub(GithubApiProvider.prototype, 'numReservedScopedTokens')
|
||||
.returns(1)
|
||||
constellation.onTokenAdded(existingToken, 'read:packages')
|
||||
await clock.tickAsync()
|
||||
expect(
|
||||
GithubApiProvider.prototype.addReservedScopedToken
|
||||
).to.be.calledWithExactly(existingToken, { scopes: 'read:packages' })
|
||||
expect(GithubApiProvider.prototype.addToken.callCount).to.equal(12)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenAdded).to.be.calledWith(
|
||||
`${existingToken}.scopes.read:packages`
|
||||
)
|
||||
expect(RedisTokenPersistence.prototype.noteTokenRemoved).to.be.calledWith(
|
||||
existingToken
|
||||
)
|
||||
expect(Object.keys(constellation._tokenScopes).length).to.equal(14)
|
||||
expect(constellation._tokenScopes[existingToken]).to.equal(
|
||||
'read:packages'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
context('onTokenInvalidated', function () {
|
||||
it('removes scoped token', async function () {
|
||||
const clock = sinon.useFakeTimers()
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
constellation.onTokenInvalidated('def789')
|
||||
await clock.tickAsync()
|
||||
expect(RedisTokenPersistence.prototype.noteTokenRemoved).to.be.calledWith(
|
||||
'def789.scopes.read:packages'
|
||||
)
|
||||
expect(Object.keys(constellation._tokenScopes).length).to.equal(13)
|
||||
})
|
||||
|
||||
it('removes unscoped token', async function () {
|
||||
const clock = sinon.useFakeTimers()
|
||||
const constellation = new GithubConstellation(config)
|
||||
await constellation.initialize(server)
|
||||
constellation.onTokenInvalidated('888888')
|
||||
await clock.tickAsync()
|
||||
expect(
|
||||
RedisTokenPersistence.prototype.noteTokenRemoved
|
||||
).to.be.calledWithExactly('888888')
|
||||
expect(Object.keys(constellation._tokenScopes).length).to.equal(13)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -60,7 +60,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
static category = 'issue-tracking'
|
||||
static route = {
|
||||
base: 'github/hacktoberfest',
|
||||
pattern: ':year(2019|2020)/:user/:repo',
|
||||
pattern: ':year(2019|2020|2021)/:user/:repo',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
{
|
||||
title: 'GitHub Hacktoberfest combined status',
|
||||
namedParams: {
|
||||
year: '2020',
|
||||
year: '2021',
|
||||
user: 'snyk',
|
||||
repo: 'snyk',
|
||||
},
|
||||
@@ -82,7 +82,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
{
|
||||
title: 'GitHub Hacktoberfest combined status (suggestion label override)',
|
||||
namedParams: {
|
||||
year: '2020',
|
||||
year: '2021',
|
||||
user: 'tmrowco',
|
||||
repo: 'tmrowapp-contrib',
|
||||
},
|
||||
@@ -90,7 +90,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
suggestion_label: 'help wanted',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
year: '2020',
|
||||
year: '2021',
|
||||
suggestedIssueCount: 12,
|
||||
contributionCount: 8,
|
||||
daysLeft: 15,
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class JenkinsTests extends JenkinsBase {
|
||||
passed_label: 'passed',
|
||||
failed_label: 'failed',
|
||||
skipped_label: 'skipped',
|
||||
jobUrl: 'https://jenkins.sqlalchemy.org/job/alembic_coverage',
|
||||
jobUrl: 'https://jenkins.sqlalchemy.org/job/alembic_gerrit',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
passed: 477,
|
||||
|
||||
@@ -13,43 +13,34 @@ export const t = await createServiceTester()
|
||||
// https://wiki.jenkins.io/pages/viewpage.action?pageId=58001258
|
||||
|
||||
t.create('Test status')
|
||||
.get('/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage')
|
||||
.get('/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_gerrit')
|
||||
.expectBadge({ label: 'tests', message: isDefaultTestTotals })
|
||||
|
||||
t.create('Test status with compact message')
|
||||
.get(
|
||||
'/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
|
||||
{
|
||||
qs: { compact_message: null },
|
||||
}
|
||||
)
|
||||
.get('/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_gerrit', {
|
||||
qs: { compact_message: null },
|
||||
})
|
||||
.expectBadge({ label: 'tests', message: isDefaultCompactTestTotals })
|
||||
|
||||
t.create('Test status with custom labels')
|
||||
.get(
|
||||
'/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
|
||||
{
|
||||
qs: {
|
||||
passed_label: 'good',
|
||||
failed_label: 'bad',
|
||||
skipped_label: 'n/a',
|
||||
},
|
||||
}
|
||||
)
|
||||
.get('/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_gerrit', {
|
||||
qs: {
|
||||
passed_label: 'good',
|
||||
failed_label: 'bad',
|
||||
skipped_label: 'n/a',
|
||||
},
|
||||
})
|
||||
.expectBadge({ label: 'tests', message: isCustomTestTotals })
|
||||
|
||||
t.create('Test status with compact message and custom labels')
|
||||
.get(
|
||||
'/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
|
||||
{
|
||||
qs: {
|
||||
compact_message: null,
|
||||
passed_label: '💃',
|
||||
failed_label: '🤦♀️',
|
||||
skipped_label: '🤷',
|
||||
},
|
||||
}
|
||||
)
|
||||
.get('/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_gerrit', {
|
||||
qs: {
|
||||
compact_message: null,
|
||||
passed_label: '💃',
|
||||
failed_label: '🤦♀️',
|
||||
skipped_label: '🤷',
|
||||
},
|
||||
})
|
||||
.expectBadge({
|
||||
label: 'tests',
|
||||
message: isCustomCompactTestTotals,
|
||||
|
||||
34
services/obs/obs-build-status.js
Normal file
34
services/obs/obs-build-status.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
isBuildStatus as gIsBuildStatus,
|
||||
renderBuildStatusBadge as gRenderBuildStatusBadge,
|
||||
} from '../build-status.js'
|
||||
|
||||
const localStatuses = {
|
||||
blocked: 'inactive',
|
||||
disabled: 'inactive',
|
||||
finished: 'orange',
|
||||
'scheduled-warning': 'orange',
|
||||
signing: 'orange',
|
||||
unknown: 'inactive',
|
||||
unresolvable: 'red',
|
||||
}
|
||||
|
||||
const isBuildStatus = Joi.alternatives().try(
|
||||
gIsBuildStatus,
|
||||
Joi.equal(...Object.keys(localStatuses))
|
||||
)
|
||||
|
||||
function renderBuildStatusBadge({ repository, status }) {
|
||||
const color = localStatuses[status]
|
||||
if (color) {
|
||||
return {
|
||||
message: status.toLowerCase(),
|
||||
color,
|
||||
}
|
||||
} else {
|
||||
return gRenderBuildStatusBadge({ status: status.toLowerCase() })
|
||||
}
|
||||
}
|
||||
|
||||
export { isBuildStatus, renderBuildStatusBadge }
|
||||
81
services/obs/obs.service.js
Normal file
81
services/obs/obs.service.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseXmlService } from '../index.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { isBuildStatus, renderBuildStatusBadge } from './obs-build-status.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
status: Joi.object({
|
||||
'@_code': isBuildStatus,
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class ObsService extends BaseXmlService {
|
||||
static category = 'build'
|
||||
static route = {
|
||||
base: 'obs',
|
||||
pattern: ':project/:packageName/:repository/:arch',
|
||||
queryParamSchema: Joi.object({
|
||||
instance: optionalUrl,
|
||||
}).required(),
|
||||
}
|
||||
|
||||
static auth = {
|
||||
userKey: 'obs_user',
|
||||
passKey: 'obs_pass',
|
||||
serviceKey: 'obs',
|
||||
isRequired: true,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'OBS package build status',
|
||||
namedParams: {
|
||||
project: 'openSUSE:Tools',
|
||||
packageName: 'osc',
|
||||
repository: 'Debian_11',
|
||||
arch: 'x86_64',
|
||||
},
|
||||
queryParams: { instance: 'https://api.opensuse.org' },
|
||||
staticPreview: this.render({
|
||||
repository: 'Debian_11',
|
||||
status: 'succeeded',
|
||||
}),
|
||||
keywords: ['open build service'],
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'build' }
|
||||
|
||||
static render({ repository, status }) {
|
||||
return renderBuildStatusBadge({ repository, status })
|
||||
}
|
||||
|
||||
async fetch({ instance, project, packageName, repository, arch }) {
|
||||
return this._requestXml(
|
||||
this.authHelper.withBasicAuth({
|
||||
schema,
|
||||
url: `${instance}/build/${project}/${repository}/${arch}/${packageName}/_status`,
|
||||
parserOptions: {
|
||||
ignoreAttributes: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async handle(
|
||||
{ project, packageName, repository, arch },
|
||||
{ instance = 'https://api.opensuse.org' }
|
||||
) {
|
||||
const resp = await this.fetch({
|
||||
instance,
|
||||
project,
|
||||
packageName,
|
||||
repository,
|
||||
arch,
|
||||
})
|
||||
return this.constructor.render({
|
||||
repository,
|
||||
status: resp.status['@_code'],
|
||||
})
|
||||
}
|
||||
}
|
||||
25
services/obs/obs.tester.js
Normal file
25
services/obs/obs.tester.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import { noToken } from '../test-helpers.js'
|
||||
import ObsService from './obs.service.js'
|
||||
import { isBuildStatus } from './obs-build-status.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'obs',
|
||||
title: 'openSUSE Open Build Service',
|
||||
})
|
||||
|
||||
t.create('status (valid)')
|
||||
.skipWhen(noToken(ObsService))
|
||||
.get('/openSUSE:Factory/aaa_base/standard/x86_64.json?label=standard')
|
||||
.expectBadge({
|
||||
label: 'standard',
|
||||
message: isBuildStatus,
|
||||
})
|
||||
|
||||
t.create('status (invalid)')
|
||||
.skipWhen(noToken(ObsService))
|
||||
.get('/home:sp1rit/this_package_will_never_exist/repo/arch.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'not found',
|
||||
})
|
||||
@@ -75,10 +75,10 @@ async function githubLicense(githubApiProvider, user, repo) {
|
||||
|
||||
let link = `https://github.com/${repoSlug}`
|
||||
|
||||
const { buffer } = await githubApiProvider.requestAsPromise({
|
||||
const { buffer } = await githubApiProvider.requestAsPromise(
|
||||
request,
|
||||
url: `/repos/${repoSlug}/license`,
|
||||
})
|
||||
`/repos/${repoSlug}/license`
|
||||
)
|
||||
try {
|
||||
const data = JSON.parse(buffer)
|
||||
if ('html_url' in data) {
|
||||
|
||||
@@ -15,7 +15,10 @@ export default class WeblateBase extends BaseJsonService {
|
||||
|
||||
async fetch(requestParams) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withBearerAuthHeader(requestParams, 'Token')
|
||||
this.authHelper.withBearerAuthHeader(
|
||||
requestParams,
|
||||
'Token' // lgtm [js/hardcoded-credentials]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user