Compare commits

...

59 Commits

Author SHA1 Message Date
Paul Melnikow
fc68b88a04 Merge branch 'master' into express 2022-04-23 21:55:55 -04:00
dependabot[bot]
ec2bbf41b1 chore(deps-dev): bump eslint-plugin-jsdoc from 39.2.1 to 39.2.7 (#7865)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 39.2.1 to 39.2.7.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v39.2.1...v39.2.7)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-23 19:23:56 +00:00
Rohan Khandelwal
482be064c1 [OSSFScorecard] Create scorecard badge service (#7687)
* security scorecards badge

* Create scorecards.yml

* Delete scorecards.yml

* api functional + color changing

* fixed test

* security scorecards badge

* Create scorecards.yml

* Delete scorecards.yml

* api functional + color changing

* fixed test

* Create scorecards.yml

* Delete scorecards.yml

* reverted package-lock

* renamed to ossf-scorecard + fixups

* fix packages

* renamed to OSSFScorecard

* reverted package-lock

* fixed dependencies

* force peer deps

* install force

* force install

* reset package lock

* pkg

* original package lock

* security scorecards badge

* Create scorecards.yml

* Delete scorecards.yml

* api functional + color changing

* fixed test

* Create scorecards.yml

* Delete scorecards.yml

* Create scorecards.yml

* Delete scorecards.yml

* renamed to ossf-scorecard + fixups

* renamed to OSSFScorecard

* pkg

* original package lock

* reset other files

* rerun tests

* fixed pkg err

* handle response errors

* 404 test, removed 500 err

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-23 18:59:26 +00:00
Paul Melnikow
4f219f1b92 Stringify [githublanguagecount] message (#7881) 2022-04-23 13:52:52 -05:00
Paul Melnikow
ca6ae88504 Tweak style 2022-04-22 15:26:25 -07:00
Paul Melnikow
fc13e8e90a Merge branch 'master' into express 2022-04-22 15:19:21 -07:00
Paul Melnikow
1fcc7e4b98 Stringify and trim whitespace from a few services (#7880) 2022-04-22 18:19:04 -04:00
Paul Melnikow
8b82e6d7fc Reclassify test for versionColorForWordpressVersion as an integration test (#7879)
This test requires hitting a web URL, and doesn't work offline, so it seems better classified as an integration test.
2022-04-22 18:14:33 -04:00
Paul Melnikow
d6d5c02ed5 Remove obsolete monitor.html (#7878) 2022-04-22 18:09:55 -04:00
Paul Melnikow
1761aab020 Wee bit of cleanup 2022-04-22 15:09:39 -07:00
Paul Melnikow
416ef3920a Don't alias this 2022-04-22 13:09:50 -04:00
Paul Melnikow
25ab4806c0 Clean up makeBadge tests 2022-04-22 13:01:58 -04:00
Paul Melnikow
9d6b3e0985 Clean lint 2022-04-22 12:54:21 -04:00
Paul Melnikow
18c76e392f Clean up acceptor test 2022-04-22 12:49:04 -04:00
Paul Melnikow
78b886c010 Merge branch 'master' into express 2022-04-22 12:36:39 -04:00
Paul Melnikow
b19ca203e8 Update docs and suggest test 2022-04-22 12:35:08 -04:00
Paul Melnikow
00df3e1136 Clean diff 2022-04-22 12:23:04 -04:00
Paul Melnikow
b4f7ec383e Update integration tests 2022-04-22 12:20:25 -04:00
Paul Melnikow
6d77534709 Progress / cleanup 2022-04-22 12:12:37 -04:00
chris48s
560d267844 add labels to Dockerfile (#7862)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-18 12:33:05 +01:00
Paul Melnikow
e8f59a2645 More fixes 2022-04-17 11:10:15 -04:00
Paul Melnikow
8cbc8cb926 More tests passing 2022-04-17 10:46:10 -04:00
Paul Melnikow
be9d49083d More tests passing 2022-04-17 10:41:33 -04:00
Paul Melnikow
aa046cb510 Fixes 2022-04-17 01:55:57 -04:00
Paul Melnikow
903bef2a4c Finish rename 2022-04-17 01:40:27 -04:00
Paul Melnikow
15043dfc92 Remove more obsolete code 2022-04-17 01:39:42 -04:00
Paul Melnikow
cf6b5b14c7 Remove obsolete code 2022-04-17 01:38:33 -04:00
Paul Melnikow
aeebfaa51f Renames 2022-04-17 01:38:28 -04:00
Paul Melnikow
c3097aad0d Fix / remove obsolete tests 2022-04-17 01:27:38 -04:00
Paul Melnikow
6e6cec9b2b Move one test 2022-04-17 01:16:16 -04:00
Paul Melnikow
7c071e352e Core tests passing 2022-04-17 01:10:05 -04:00
Paul Melnikow
6d72fd68e8 Begin to replace scoutcamp with express 2022-04-16 23:29:11 -04:00
dependabot[bot]
eeacb2f80a chore(deps): bump semver from 7.3.6 to 7.3.7 (#7860)
Bumps [semver](https://github.com/npm/node-semver) from 7.3.6 to 7.3.7.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.6...v7.3.7)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-16 19:33:30 +00:00
dependabot[bot]
87157d47b8 chore(deps-dev): bump eslint-plugin-jsdoc from 39.0.1 to 39.2.1 (#7859)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 39.0.1 to 39.2.1.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v39.0.1...v39.2.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 19:17:28 +00:00
dependabot[bot]
3ffaa74879 chore(deps): bump ioredis from 5.0.3 to 5.0.4 (#7853)
Bumps [ioredis](https://github.com/luin/ioredis) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/luin/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/luin/ioredis/compare/v5.0.3...v5.0.4)

---
updated-dependencies:
- dependency-name: ioredis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 17:25:09 +00:00
dependabot[bot]
8b48b8fd61 chore(deps-dev): bump react-error-overlay from 6.0.10 to 6.0.11 (#7856)
Bumps [react-error-overlay](https://github.com/facebook/create-react-app/tree/HEAD/packages/react-error-overlay) from 6.0.10 to 6.0.11.
- [Release notes](https://github.com/facebook/create-react-app/releases)
- [Changelog](https://github.com/facebook/create-react-app/blob/main/CHANGELOG-1.x.md)
- [Commits](https://github.com/facebook/create-react-app/commits/react-error-overlay@6.0.11/packages/react-error-overlay)

---
updated-dependencies:
- dependency-name: react-error-overlay
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 17:17:50 +00:00
dependabot[bot]
2a2e96a8fd chore(deps): bump simple-icons from 6.17.0 to 6.18.0 (#7852)
Bumps [simple-icons](https://github.com/simple-icons/simple-icons) from 6.17.0 to 6.18.0.
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/6.17.0...6.18.0)

---
updated-dependencies:
- dependency-name: simple-icons
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 17:11:38 +00:00
dependabot[bot]
6608cf318d chore(deps): bump glob from 7.2.0 to 8.0.1 (#7851)
Bumps [glob](https://github.com/isaacs/node-glob) from 7.2.0 to 8.0.1.
- [Release notes](https://github.com/isaacs/node-glob/releases)
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v7.2.0...v8.0.1)

---
updated-dependencies:
- dependency-name: glob
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 17:03:18 +00:00
dependabot[bot]
dbd82d6dcb chore(deps-dev): bump cypress from 9.5.3 to 9.5.4 (#7850)
Bumps [cypress](https://github.com/cypress-io/cypress) from 9.5.3 to 9.5.4.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v9.5.3...v9.5.4)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 16:46:38 +00:00
dependabot[bot]
a56a71c623 chore(deps-dev): bump sinon from 13.0.1 to 13.0.2 (#7847)
Bumps [sinon](https://github.com/sinonjs/sinon) from 13.0.1 to 13.0.2.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v13.0.1...v13.0.2)

---
updated-dependencies:
- dependency-name: sinon
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 16:38:29 +00:00
dependabot[bot]
58f5506bfb chore(deps-dev): bump @types/chai from 4.3.0 to 4.3.1 (#7848)
Bumps [@types/chai](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chai) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chai)

---
updated-dependencies:
- dependency-name: "@types/chai"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 16:30:33 +00:00
dependabot[bot]
35f60cfd42 chore(deps-dev): bump @typescript-eslint/eslint-plugin (#7845)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.18.0 to 5.19.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.19.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-16 04:37:32 +00:00
dependabot[bot]
607e27f213 chore(deps-dev): bump lint-staged from 12.3.7 to 12.3.8 (#7841)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.3.7 to 12.3.8.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.3.7...v12.3.8)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-15 23:27:21 -05:00
Caleb Cartwright
6ea690c446 fix: various sonar service tests (#7836)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-14 22:08:05 +00:00
Caleb Cartwright
9a8c05de18 fix: bitrise example & service test (#7837) 2022-04-14 17:00:27 -05:00
dependabot[bot]
a550fc60bf chore(deps-dev): bump eslint-plugin-jsdoc from 38.1.6 to 39.0.1 (#7832)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 38.1.6 to 39.0.1.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v38.1.6...v39.0.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 19:13:19 +00:00
dependabot[bot]
0f388f687f chore(deps-dev): bump @typescript-eslint/eslint-plugin (#7830)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.17.0 to 5.18.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.18.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 14:52:42 +00:00
dependabot[bot]
6bcf2fd264 chore(deps-dev): bump eslint-plugin-import from 2.25.4 to 2.26.0 (#7829)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.25.4 to 2.26.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.25.4...v2.26.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 14:42:08 +00:00
dependabot[bot]
1e76507ebd chore(deps-dev): bump concurrently from 7.0.0 to 7.1.0 (#7827)
Bumps [concurrently](https://github.com/open-cli-tools/concurrently) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](https://github.com/open-cli-tools/concurrently/compare/v7.0.0...v7.1.0)

---
updated-dependencies:
- dependency-name: concurrently
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 02:23:55 +00:00
dependabot[bot]
a24c2be1b9 chore(deps-dev): bump @types/styled-components from 5.1.24 to 5.1.25 (#7828)
Bumps [@types/styled-components](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/styled-components) from 5.1.24 to 5.1.25.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/styled-components)

---
updated-dependencies:
- dependency-name: "@types/styled-components"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-08 21:17:46 -05:00
dependabot[bot]
a78226f754 chore(deps-dev): bump prettier from 2.6.1 to 2.6.2 (#7826)
Bumps [prettier](https://github.com/prettier/prettier) from 2.6.1 to 2.6.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.6.1...2.6.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 01:57:05 +00:00
dependabot[bot]
4c68d830ec chore(deps-dev): bump @babel/core from 7.17.8 to 7.17.9 (#7821)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.8 to 7.17.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.9/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 01:01:18 +00:00
dependabot[bot]
7b51bc6859 chore(deps): bump @sentry/node from 6.19.3 to 6.19.6 (#7820)
Bumps [@sentry/node](https://github.com/getsentry/sentry-javascript) from 6.19.3 to 6.19.6.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.19.3...6.19.6)

---
updated-dependencies:
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 00:37:44 +00:00
dependabot[bot]
c6586f0f50 chore(deps-dev): bump tsd from 0.19.1 to 0.20.0 (#7819)
Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.19.1 to 0.20.0.
- [Release notes](https://github.com/SamVerschueren/tsd/releases)
- [Commits](https://github.com/SamVerschueren/tsd/compare/v0.19.1...v0.20.0)

---
updated-dependencies:
- dependency-name: tsd
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-09 00:29:53 +00:00
dependabot[bot]
7da986f60f chore(deps): bump moment from 2.29.1 to 2.29.2 (#7818)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-08 19:24:07 -05:00
dependabot[bot]
75bb074258 chore(deps): bump semver from 7.3.5 to 7.3.6 (#7817)
Bumps [semver](https://github.com/npm/node-semver) from 7.3.5 to 7.3.6.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.5...v7.3.6)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-08 18:55:55 -05:00
chris48s
ac52741b2d add dependency review action (#7816)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2022-04-08 18:07:22 -05:00
chris48s
8821ff85fc handle missing 'fly-client-ip' (#7814) 2022-04-05 20:29:28 +01:00
chris48s
4382706352 note ioredis breaking change in changelog (#7813)
add note to server-2022-04-03 changelog about
ioredis connection string breaking change
2022-04-05 18:32:15 +01:00
48 changed files with 1810 additions and 2881 deletions

View File

@@ -0,0 +1,14 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v1

View File

@@ -6,6 +6,12 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
## server-2022-04-03
- Breaking change: This release updates ioredis from v4 to v5.
If you are using redis for GitHub token pooling, redis connection strings of the form
`redis://junkusername:authpassword@example.com:1234` will need to be updated to
`redis://:authpassword@example.com:1234`. See the
[ioredis upgrade guide](https://github.com/luin/ioredis/wiki/Upgrading-from-v4-to-v5)
for further details.
- fix installation issue on npm >= 8.5.5 [#7809](https://github.com/badges/shields/issues/7809)
- two fixes for [packagist] schemas [#7782](https://github.com/badges/shields/issues/7782)
- allow requireCloudflare setting to work when hosted on fly.io [#7781](https://github.com/badges/shields/issues/7781)

View File

@@ -23,6 +23,8 @@ FROM node:16-alpine
ARG version=dev
ENV DOCKER_SHIELDS_VERSION=$version
LABEL version=$version
LABEL fly.version=$version
# Run the server using production configs.
ENV NODE_ENV production

View File

@@ -1,6 +1,6 @@
'use strict'
const { normalizeColor, toSvgColor } = require('./color')
const { toSvgColor } = require('./color')
const badgeRenderers = require('./badge-renderers')
const { stripXmlWhitespace } = require('./xml')
@@ -9,7 +9,6 @@ note: makeBadge() is fairly thinly wrapped so if we are making changes here
it is likely this will impact on the package's public interface in index.js
*/
module.exports = function makeBadge({
format,
style = 'flat',
label,
message,
@@ -24,22 +23,6 @@ module.exports = function makeBadge({
label = `${label}`.trim()
message = `${message}`.trim()
// This ought to be the responsibility of the server, not `makeBadge`.
if (format === 'json') {
return JSON.stringify({
label,
message,
logoWidth,
// Only call normalizeColor for the JSON case: this is handled
// internally by toSvgColor in the SVG case.
color: normalizeColor(color),
labelColor: normalizeColor(labelColor),
link: links,
name: label,
value: message,
})
}
const render = badgeRenderers[style]
if (!render) {
throw new Error(`Unknown badge style: '${style}'`)

View File

@@ -1,143 +1,48 @@
'use strict'
const { test, given, forCases } = require('sazerac')
const { expect } = require('chai')
const snapshot = require('snap-shot-it')
const isSvg = require('is-svg')
const prettier = require('prettier')
const makeBadge = require('./make-badge')
function expectBadgeToMatchSnapshot(format) {
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
}
function testColor(color = '', colorAttr = 'color') {
return JSON.parse(
makeBadge({
label: 'name',
message: 'Bob',
[colorAttr]: color,
format: 'json',
})
).color
function expectBadgeToMatchSnapshot(badgeData) {
snapshot(prettier.format(makeBadge(badgeData), { parser: 'html' }))
}
describe('The badge generator', function () {
describe('color test', function () {
test(testColor, () => {
// valid hex
forCases([
given('#4c1'),
given('#4C1'),
given('4C1'),
given('4c1'),
]).expect('#4c1')
forCases([
given('#abc123'),
given('#ABC123'),
given('abc123'),
given('ABC123'),
]).expect('#abc123')
// valid rgb(a)
given('rgb(0,128,255)').expect('rgb(0,128,255)')
given('rgb(220,128,255,0.5)').expect('rgb(220,128,255,0.5)')
given('rgba(0,0,255)').expect('rgba(0,0,255)')
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
// valid hsl(a)
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
given('hsl(360,50%,50%,0.5)').expect('hsl(360,50%,50%,0.5)')
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
given('hsla(0,50%,101%)').expect('hsla(0,50%,101%)')
// CSS named color.
given('papayawhip').expect('papayawhip')
// Shields named color.
given('red').expect('red')
given('green').expect('green')
given('blue').expect('blue')
given('yellow').expect('yellow')
// Semantic color alias
given('success').expect('brightgreen')
given('informational').expect('blue')
forCases(
// invalid hex
given('#123red'), // contains letter above F
given('#red'), // contains letter above F
// neither a css named color nor colorscheme
given('notacolor'),
given('bluish'),
given('almostred'),
given('brightmaroon'),
given('cactus')
).expect(undefined)
})
})
describe('color aliases', function () {
test(testColor, () => {
forCases([given('#4c1', 'color')]).expect('#4c1')
})
})
describe('SVG', function () {
it('should produce SVG', function () {
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
expect(makeBadge({ label: 'cactus', message: 'grown' }))
.to.satisfy(isSvg)
.and.to.include('cactus')
.and.to.include('grown')
})
it('should match snapshot', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
})
})
})
describe('JSON', function () {
it('should produce the expected JSON', function () {
const json = makeBadge({
label: 'cactus',
message: 'grown',
format: 'json',
links: ['https://example.com/', 'https://other.example.com/'],
})
expect(JSON.parse(json)).to.deep.equal({
name: 'cactus',
label: 'cactus',
value: 'grown',
message: 'grown',
link: ['https://example.com/', 'https://other.example.com/'],
})
expectBadgeToMatchSnapshot({ label: 'cactus', message: 'grown' })
})
it('should replace undefined svg badge style with "flat"', function () {
const jsonBadgeWithUnknownStyle = makeBadge({
label: 'name',
message: 'Bob',
format: 'svg',
})
const jsonBadgeWithDefaultStyle = makeBadge({
label: 'name',
message: 'Bob',
format: 'svg',
style: 'flat',
})
expect(jsonBadgeWithUnknownStyle)
.to.equal(jsonBadgeWithDefaultStyle)
.and.to.satisfy(isSvg)
expect(
makeBadge({
label: 'name',
message: 'Bob',
})
)
.to.satisfy(isSvg)
.and.to.equal(
makeBadge({
label: 'name',
message: 'Bob',
style: 'flat',
})
)
})
it('should fail with unknown svg badge style', function () {
expect(() =>
makeBadge({
label: 'name',
message: 'Bob',
format: 'svg',
style: 'unknown_style',
})
makeBadge({ label: 'name', message: 'Bob', style: 'unknown_style' })
).to.throw(Error, "Unknown badge style: 'unknown_style'")
})
})
@@ -147,7 +52,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
@@ -158,7 +62,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
@@ -170,7 +73,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#b3e',
})
@@ -180,7 +82,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -191,7 +92,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
@@ -203,7 +103,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
@@ -215,7 +114,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#000',
labelColor: '#f3f3f3',
@@ -226,7 +124,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#e2ffe1',
labelColor: '#000',
@@ -239,7 +136,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
@@ -250,7 +146,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
@@ -262,7 +157,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#b3e',
})
@@ -272,7 +166,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -283,7 +176,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
@@ -295,7 +187,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
@@ -307,7 +198,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#000',
labelColor: '#f3f3f3',
@@ -318,7 +208,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat-square',
color: '#e2ffe1',
labelColor: '#000',
@@ -331,7 +220,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
@@ -342,7 +230,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
@@ -354,7 +241,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#b3e',
})
@@ -364,7 +250,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -375,7 +260,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
@@ -387,7 +271,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
@@ -399,7 +282,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#000',
labelColor: '#f3f3f3',
@@ -410,7 +292,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#e2ffe1',
labelColor: '#000',
@@ -425,7 +306,6 @@ describe('The badge generator', function () {
makeBadge({
label: 1998,
message: 1999,
format: 'svg',
style: 'for-the-badge',
})
)
@@ -438,7 +318,6 @@ describe('The badge generator', function () {
makeBadge({
label: 'Label',
message: '1 string',
format: 'svg',
style: 'for-the-badge',
})
)
@@ -450,7 +329,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
@@ -461,7 +339,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
@@ -473,7 +350,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#b3e',
})
@@ -483,7 +359,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -494,7 +369,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
@@ -506,7 +380,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
@@ -518,7 +391,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#000',
labelColor: '#f3f3f3',
@@ -529,7 +401,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'for-the-badge',
color: '#e2ffe1',
labelColor: '#000',
@@ -543,7 +414,6 @@ describe('The badge generator', function () {
makeBadge({
label: 'some-key',
message: 'some-value',
format: 'svg',
style: 'social',
})
)
@@ -557,11 +427,10 @@ describe('The badge generator', function () {
makeBadge({
label: '',
message: 'some-value',
format: 'json',
style: 'social',
})
)
.to.include('""')
.to.include('></text>')
.and.to.include('some-value')
})
@@ -569,7 +438,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
@@ -580,7 +448,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
@@ -592,7 +459,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'social',
color: '#b3e',
})
@@ -602,7 +468,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'social',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -613,7 +478,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
@@ -625,7 +489,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
@@ -639,7 +502,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({
label: 'label',
message: 'message',
format: 'svg',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})

View File

@@ -1,58 +1,29 @@
import makeBadge from '../../badge-maker/lib/make-badge.js'
import BaseService from './base.js'
import {
serverHasBeenUpSinceResourceCached,
setCacheHeadersForStaticResource,
} from './cache-headers.js'
import { makeSend } from './legacy-result-sender.js'
import { MetricHelper } from './metric-helper.js'
import coalesceBadge from './coalesce-badge.js'
import { prepareRoute, namedParamsForMatch } from './route.js'
import { prepareRoute } from './route.js'
export default class BaseStaticService extends BaseService {
static register({ camp, metricInstance }, serviceConfig) {
const { regex, captureNames } = prepareRoute(this.route)
static _applyCacheHeaders({ res }) {
setCacheHeadersForStaticResource(res)
}
const metricHelper = MetricHelper.create({
metricInstance,
ServiceClass: this,
})
camp.route(regex, async (queryParams, match, end, ask) => {
if (serverHasBeenUpSinceResourceCached(ask.req)) {
// Send Not Modified.
ask.res.statusCode = 304
ask.res.end()
return
}
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke(
{},
serviceConfig,
namedParams,
queryParams
)
const badgeData = coalesceBadge(
queryParams,
serviceData,
this.defaultBadgeData,
this
)
// The final capture group is the extension.
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
badgeData.format = format
setCacheHeadersForStaticResource(ask.res)
const svg = makeBadge(badgeData)
makeSend(format, ask.res, end)(svg)
metricHandle.noteResponseSent()
})
static register({ app, ...serviceContext }, serviceConfig) {
const { regex } = prepareRoute(this.route)
app.get(
regex,
(req, res, next) => {
if (serverHasBeenUpSinceResourceCached(req)) {
// Send Not Modified.
res.status(304)
res.end()
} else {
next()
}
},
this.makeExpressHandler(serviceContext, serviceConfig)
)
}
}

View File

@@ -6,8 +6,13 @@
import emojic from 'emojic'
import Joi from 'joi'
import log from '../server/log.js'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import { AuthHelper } from './auth-helper.js'
import { MetricHelper, MetricNames } from './metric-helper.js'
import {
coalesceCacheLength,
setHeadersForCacheLength,
} from './cache-headers.js'
import { assertValidCategory } from './categories.js'
import checkErrorResponse from './check-error-response.js'
import coalesceBadge from './coalesce-badge.js'
@@ -21,11 +26,12 @@ import {
} from './errors.js'
import { validateExample, transformExample } from './examples.js'
import { fetch } from './got.js'
import { makeJsonBadge } from './make-json-badge.js'
import {
makeFullUrl,
assertValidRoute,
paramsForReq,
prepareRoute,
namedParamsForMatch,
getQueryParamNames,
} from './route.js'
import { assertValidServiceDefinition } from './service-definitions.js'
@@ -423,60 +429,90 @@ class BaseService {
return serviceData
}
static register(
{
camp,
handleRequest,
githubApiProvider,
librariesIoApiProvider,
metricInstance,
},
// `defaultCacheLengthSeconds` can be overridden by
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
// by-badge basis). Then in turn that can be overridden by
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
// query param can also override both of those but again only if `cacheSeconds`
// is longer.
//
// Ref: https://github.com/badges/shields/pull/2755
static _applyCacheHeaders({
cacheHeaderConfig,
req,
res,
serviceOverrideCacheLengthSeconds,
}) {
const cacheLengthSeconds = coalesceCacheLength({
cacheHeaderConfig,
serviceDefaultCacheLengthSeconds: this._cacheLength,
serviceOverrideCacheLengthSeconds,
queryParams: req.query,
})
setHeadersForCacheLength(res, cacheLengthSeconds)
}
static makeExpressHandler(
{ githubApiProvider, librariesIoApiProvider, metricInstance },
serviceConfig
) {
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route)
const metricHelper = MetricHelper.create({
metricInstance,
ServiceClass: this,
})
const { captureNames } = prepareRoute(this.route)
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
camp.route(
regex,
handleRequest(cacheHeaderConfig, {
queryParams,
handler: async (queryParams, match, sendBadge) => {
const metricHandle = metricHelper.startRequest()
return async (req, res) => {
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke(
{
requestFetcher: fetch,
githubApiProvider,
librariesIoApiProvider,
metricHelper,
},
serviceConfig,
namedParams,
queryParams
)
const badgeData = coalesceBadge(
queryParams,
serviceData,
this.defaultBadgeData,
this
)
// The final capture group is the extension.
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
sendBadge(format, badgeData)
metricHandle.noteResponseSent()
const { namedParams, format } = paramsForReq(captureNames, req, this)
const serviceData = await this.invoke(
{
requestFetcher: fetch,
githubApiProvider,
librariesIoApiProvider,
metricHelper,
},
cacheLength: this._cacheLength,
serviceConfig,
namedParams,
req.query
)
const badgeData = coalesceBadge(
req.query,
serviceData,
this.defaultBadgeData,
this
)
this._applyCacheHeaders({
cacheHeaderConfig,
req,
res,
serviceOverrideCacheLengthSeconds: badgeData.cacheLengthSeconds,
})
)
if (format === 'svg') {
res.setHeader('Content-Type', 'image/svg+xml')
res.send(makeBadge(badgeData))
} else if (format === 'json') {
res.json(makeJsonBadge(badgeData))
} else {
throw Error(`Unrecognized format: ${format}`)
}
res.end()
metricHandle.noteResponseSent()
}
}
static register({ app, ...serviceContext }, serviceConfig) {
const { regex } = prepareRoute(this.route)
app.get(regex, this.makeExpressHandler(serviceContext, serviceConfig))
}
}

View File

@@ -1,9 +1,11 @@
import Joi from 'joi'
import chai from 'chai'
import isSvg from 'is-svg'
import sinon from 'sinon'
import prometheus from 'prom-client'
import chaiAsPromised from 'chai-as-promised'
import PrometheusMetrics from '../server/prometheus-metrics.js'
import { ExpressTestHarness } from '../express-test-harness.js'
import trace from './trace.js'
import {
NotFound,
@@ -15,6 +17,7 @@ import {
import BaseService from './base.js'
import { MetricHelper, MetricNames } from './metric-helper.js'
import '../register-chai-plugins.spec.js'
const { expect } = chai
chai.use(chaiAsPromised)
@@ -59,9 +62,12 @@ class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
describe('BaseService', function () {
const defaultConfig = {
handleInternalErrors: false,
cacheHeaders: { defaultCacheLengthSeconds: 120 },
public: {
handleInternalErrors: false,
services: {},
cacheHeaders: { defaultCacheLengthSeconds: 120 },
},
private: {},
}
@@ -321,62 +327,45 @@ describe('BaseService', function () {
})
})
describe('ScoutCamp integration', function () {
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
// eslint-disable-next-line no-useless-escape
const expectedRouteRegex = /^\/foo(?:\/([^\/#\?]+?))(|\.svg|\.json)$/
describe('Express integration', function () {
let harness
beforeEach(async function () {
harness = new ExpressTestHarness()
DummyService.register({ app: harness.app }, defaultConfig)
await harness.start()
})
let mockCamp
let mockHandleRequest
afterEach(async function () {
await harness.stop()
})
beforeEach(function () {
mockCamp = {
route: sinon.spy(),
}
mockHandleRequest = sinon.spy()
DummyService.register(
{ camp: mockCamp, handleRequest: mockHandleRequest },
defaultConfig
it('fulfills the request for an SVG badge', async function () {
const { headers, body } = await harness.get(
'/foo/bar.svg?queryParamA=%3F'
)
expect(headers).to.include({
'content-type': 'image/svg+xml; charset=utf-8',
})
expect(body)
.to.satisfy(isSvg)
.and.to.include('cat: Hello namedParamA: bar with queryParamA: ?')
})
it('registers the service', function () {
expect(mockCamp.route).to.have.been.calledOnce
expect(mockCamp.route).to.have.been.calledWith(expectedRouteRegex)
})
it('fulfills the request for a JSON badge', async function () {
const { headers, body } = await harness.get(
'/foo/bar.json?queryParamA=%3F',
{ responseType: 'json' }
)
it('handles the request', async function () {
expect(mockHandleRequest).to.have.been.calledOnce
expect(headers).to.include({
'content-type': 'application/json; charset=utf-8',
})
const { queryParams: serviceQueryParams, handler: requestHandler } =
mockHandleRequest.getCall(0).args[1]
expect(serviceQueryParams).to.deep.equal([
'queryParamA',
'legacyQueryParamA',
])
const mockSendBadge = sinon.spy()
const mockRequest = {
asPromise: sinon.spy(),
}
const queryParams = { queryParamA: '?' }
const match = '/foo/bar.svg'.match(expectedRouteRegex)
await requestHandler(queryParams, match, mockSendBadge, mockRequest)
const expectedFormat = 'svg'
expect(mockSendBadge).to.have.been.calledOnce
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
expect(body).to.include({
label: 'cat',
message: 'Hello namedParamA: bar with queryParamA: ?',
color: 'lightgrey',
style: 'flat',
namedLogo: undefined,
logo: undefined,
logoWidth: undefined,
logoPosition: undefined,
links: [],
labelColor: undefined,
cacheLengthSeconds: undefined,
})
})
})
@@ -574,9 +563,7 @@ describe('BaseService', function () {
},
private: {},
},
{
namedParamA: 'bar.bar.bar',
}
{ namedParamA: 'bar.bar.bar' }
)
).to.deep.equal({
color: 'lightgray',

View File

@@ -1,143 +0,0 @@
import makeBadge from '../../badge-maker/lib/make-badge.js'
import { setCacheHeaders } from './cache-headers.js'
import { makeSend } from './legacy-result-sender.js'
import coalesceBadge from './coalesce-badge.js'
// These query parameters are available to any badge. They are handled by
// `coalesceBadge`.
const globalQueryParams = new Set([
'label',
'style',
'link',
'logo',
'logoColor',
'logoPosition',
'logoWidth',
'link',
'colorA',
'colorB',
'color',
'labelColor',
])
function flattenQueryParams(queryParams) {
const union = new Set(globalQueryParams)
;(queryParams || []).forEach(name => {
union.add(name)
})
return Array.from(union).sort()
}
// 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
//
// For safety, the service must declare the query parameters it wants to use.
// Only the declared parameters (and the global parameters) are provided to
// the service. Consequently, failure to declare a parameter results in the
// parameter not working at all (which is undesirable, but easy to debug)
// rather than indeterminate behavior that depends on the cache state
// (undesirable and hard to debug).
//
// Pass just the handler function as shorthand.
function handleRequest(cacheHeaderConfig, handlerOptions) {
if (!cacheHeaderConfig) {
throw Error('cacheHeaderConfig is required')
}
if (typeof handlerOptions === 'function') {
handlerOptions = { handler: handlerOptions }
}
const allowedKeys = flattenQueryParams(handlerOptions.queryParams)
const { cacheLength: serviceDefaultCacheLengthSeconds } = handlerOptions
return (queryParams, match, end, ask) => {
/*
This is here for legacy reasons. The badge server and frontend used to live
on two different servers. When we merged them there was a conflict so we
did this to avoid moving the endpoint docs to another URL.
Never ever do this again.
*/
if (match[0] === '/endpoint' && Object.keys(queryParams).length === 0) {
ask.res.statusCode = 301
ask.res.setHeader('Location', '/endpoint/')
ask.res.end()
return
}
// `defaultCacheLengthSeconds` can be overridden by
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
// by-badge basis). Then in turn that can be overridden by
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
// query param can also override both of those but again only if `cacheSeconds`
// is longer.
//
// When the legacy services have been rewritten, all the code in here
// will go away, which should achieve this goal in a simpler way.
//
// Ref: https://github.com/badges/shields/pull/2755
function setCacheHeadersOnResponse(res, serviceOverrideCacheLengthSeconds) {
setCacheHeaders({
cacheHeaderConfig,
serviceDefaultCacheLengthSeconds,
serviceOverrideCacheLengthSeconds,
queryParams,
res,
})
}
const filteredQueryParams = {}
allowedKeys.forEach(key => {
filteredQueryParams[key] = queryParams[key]
})
// In case our vendor servers are unresponsive.
let serverUnresponsive = false
const serverResponsive = setTimeout(() => {
serverUnresponsive = true
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
const badgeData = coalesceBadge(
filteredQueryParams,
{ label: 'vendor', message: 'unresponsive' },
{}
)
const svg = makeBadge(badgeData)
const extension = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
setCacheHeadersOnResponse(ask.res)
makeSend(extension, ask.res, end)(svg)
}, 25000)
const result = handlerOptions.handler(
filteredQueryParams,
match,
// eslint-disable-next-line mocha/prefer-arrow-callback
function sendBadge(format, badgeData) {
if (serverUnresponsive) {
return
}
clearTimeout(serverResponsive)
// Add format to badge data.
badgeData.format = format
const svg = makeBadge(badgeData)
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
makeSend(format, ask.res, end)(svg)
}
)
// eslint-disable-next-line promise/prefer-await-to-then
if (result && result.catch) {
// eslint-disable-next-line promise/prefer-await-to-then
result.catch(err => {
throw err
})
}
}
}
export { handleRequest }

View File

@@ -1,251 +0,0 @@
import { expect } from 'chai'
import portfinder from 'portfinder'
import Camp from '@shields_io/camp'
import got from '../got-test-client.js'
import coalesceBadge from './coalesce-badge.js'
import { handleRequest } from './legacy-request-handler.js'
async function performTwoRequests(baseUrl, first, second) {
expect((await got(`${baseUrl}${first}`)).statusCode).to.equal(200)
expect((await got(`${baseUrl}${second}`)).statusCode).to.equal(200)
}
function fakeHandler(queryParams, match, sendBadge, request) {
const [, someValue, format] = match
const badgeData = coalesceBadge(
queryParams,
{
label: 'testing',
message: someValue,
},
{}
)
sendBadge(format, badgeData)
}
function createFakeHandlerWithCacheLength(cacheLengthSeconds) {
return function fakeHandler(queryParams, match, sendBadge, request) {
const [, someValue, format] = match
const badgeData = coalesceBadge(
queryParams,
{
label: 'testing',
message: someValue,
},
{},
{
_cacheLength: cacheLengthSeconds,
}
)
sendBadge(format, badgeData)
}
}
describe('The request handler', function () {
let port, baseUrl
beforeEach(async function () {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
})
let camp
beforeEach(function (done) {
camp = Camp.start({ port, hostname: '::' })
camp.on('listening', () => done())
})
afterEach(function (done) {
if (camp) {
camp.close(() => done())
camp = null
}
})
const standardCacheHeaders = { defaultCacheLengthSeconds: 120 }
describe('the options object calling style', function () {
beforeEach(function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(standardCacheHeaders, { handler: fakeHandler })
)
})
it('should return the expected response', async function () {
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: [],
})
})
})
describe('the function shorthand calling style', function () {
beforeEach(function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(standardCacheHeaders, fakeHandler)
)
})
it('should return the expected response', async function () {
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: [],
})
})
})
describe('caching', function () {
describe('standard query parameters', function () {
function register({ cacheHeaderConfig }) {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(
cacheHeaderConfig,
(queryParams, match, sendBadge, request) => {
fakeHandler(queryParams, match, sendBadge, request)
}
)
)
}
it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
const { headers } = await got(`${baseUrl}/testing/123.json`)
const expectedExpiry = new Date(
+new Date(headers.date) + 900000
).toGMTString()
expect(headers.expires).to.equal(expectedExpiry)
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
})
it('should set the expected cache headers on cached responses', async function () {
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
// Make first request.
await got(`${baseUrl}/testing/123.json`)
const { headers } = await got(`${baseUrl}/testing/123.json`)
const expectedExpiry = new Date(
+new Date(headers.date) + 900000
).toGMTString()
expect(headers.expires).to.equal(expectedExpiry)
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
})
it('should let live service data override the default cache headers with longer value', async function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(
{ defaultCacheLengthSeconds: 300 },
(queryParams, match, sendBadge, request) => {
createFakeHandlerWithCacheLength(400)(
queryParams,
match,
sendBadge,
request
)
}
)
)
const { headers } = await got(`${baseUrl}/testing/123.json`)
expect(headers['cache-control']).to.equal('max-age=400, s-maxage=400')
})
it('should not let live service data override the default cache headers with shorter value', async function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(
{ defaultCacheLengthSeconds: 300 },
(queryParams, match, sendBadge, request) => {
createFakeHandlerWithCacheLength(200)(
queryParams,
match,
sendBadge,
request
)
}
)
)
const { headers } = await got(`${baseUrl}/testing/123.json`)
expect(headers['cache-control']).to.equal('max-age=300, s-maxage=300')
})
it('should set the expires header to current time + cacheSeconds', async function () {
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
const { headers } = await got(
`${baseUrl}/testing/123.json?cacheSeconds=3600`
)
const expectedExpiry = new Date(
+new Date(headers.date) + 3600000
).toGMTString()
expect(headers.expires).to.equal(expectedExpiry)
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
})
it('should ignore cacheSeconds when shorter than defaultCacheLengthSeconds', async function () {
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 600 } })
const { headers } = await got(
`${baseUrl}/testing/123.json?cacheSeconds=300`
)
const expectedExpiry = new Date(
+new Date(headers.date) + 600000
).toGMTString()
expect(headers.expires).to.equal(expectedExpiry)
expect(headers['cache-control']).to.equal('max-age=600, s-maxage=600')
})
it('should set Cache-Control: no-cache, no-store, must-revalidate if cache seconds is 0', async function () {
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
const { headers } = await got(`${baseUrl}/testing/123.json`)
expect(headers.expires).to.equal(headers.date)
expect(headers['cache-control']).to.equal(
'no-cache, no-store, must-revalidate'
)
})
})
describe('custom query parameters', function () {
let handlerCallCount
beforeEach(function () {
handlerCallCount = 0
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(standardCacheHeaders, {
queryParams: ['foo'],
handler: (queryParams, match, sendBadge, request) => {
++handlerCallCount
fakeHandler(queryParams, match, sendBadge, request)
},
})
)
})
it('should differentiate them', async function () {
await performTwoRequests(
baseUrl,
'/testing/123.svg?foo=1',
'/testing/123.svg?foo=2'
)
expect(handlerCallCount).to.equal(2)
})
})
})
})

View File

@@ -1,35 +0,0 @@
import stream from 'stream'
function streamFromString(str) {
const newStream = new stream.Readable()
newStream._read = () => {
newStream.push(str)
newStream.push(null)
}
return newStream
}
function sendSVG(res, askres, end) {
askres.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
end(null, { template: streamFromString(res) })
}
function sendJSON(res, askres, end) {
askres.setHeader('Content-Type', 'application/json')
askres.setHeader('Access-Control-Allow-Origin', '*')
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
end(null, { template: streamFromString(res) })
}
function makeSend(format, askres, end) {
if (format === 'svg') {
return res => sendSVG(res, askres, end)
} else if (format === 'json') {
return res => sendJSON(res, askres, end)
} else {
throw Error(`Unrecognized format: ${format}`)
}
}
export { makeSend }

View File

@@ -0,0 +1,16 @@
import { normalizeColor } from 'badge-maker/lib/color.js'
export function makeJsonBadge(badgeData) {
const { label, message, logoWidth, color, labelColor, links } = badgeData
return {
label,
message,
logoWidth,
color: normalizeColor(color),
labelColor: normalizeColor(labelColor),
link: links,
name: label,
value: message,
}
}

View File

@@ -0,0 +1,23 @@
import { expect } from 'chai'
import { makeJsonBadge } from './make-json-badge.js'
describe('makeJsonBadge()', function () {
it('should produce the expected JSON', function () {
expect(
makeJsonBadge({
label: 'cactus',
message: 'grown',
links: ['https://example.com/', 'https://other.example.com/'],
})
).to.deep.equal({
name: 'cactus',
label: 'cactus',
value: 'grown',
message: 'grown',
link: ['https://example.com/', 'https://other.example.com/'],
color: undefined,
labelColor: undefined,
logoWidth: undefined,
})
})
})

View File

@@ -1,3 +1,4 @@
import url from 'url'
import camelcase from 'camelcase'
import emojic from 'emojic'
import Joi from 'joi'
@@ -9,7 +10,7 @@ import {
} from './cache-headers.js'
import { isValidCategory } from './categories.js'
import { MetricHelper } from './metric-helper.js'
import { isValidRoute, prepareRoute, namedParamsForMatch } from './route.js'
import { isValidRoute, prepareRoute, paramsForReq } from './route.js'
import trace from './trace.js'
const attrSchema = Joi.object({
@@ -54,7 +55,7 @@ export default function redirector(attrs) {
static route = route
static examples = examples
static register({ camp, metricInstance }, { rasterUrl }) {
static register({ app, metricInstance }, { rasterUrl }) {
const { regex, captureNames } = prepareRoute({
...this.route,
withPng: Boolean(rasterUrl),
@@ -65,17 +66,17 @@ export default function redirector(attrs) {
ServiceClass: this,
})
camp.route(regex, async (queryParams, match, end, ask) => {
if (serverHasBeenUpSinceResourceCached(ask.req)) {
app.get(regex, async (req, res) => {
if (serverHasBeenUpSinceResourceCached(req)) {
// Send Not Modified.
ask.res.statusCode = 304
ask.res.end()
res.status(304)
res.end()
return
}
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this)
const { namedParams, format } = paramsForReq(captureNames, req, this)
trace.logTrace(
'inbound',
emojic.arrowHeadingUp,
@@ -83,12 +84,12 @@ export default function redirector(attrs) {
route.base
)
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
trace.logTrace('inbound', emojic.crayon, 'Query params', queryParams)
trace.logTrace('inbound', emojic.crayon, 'Query params', req.query)
const targetPath = encodeURI(transformPath(namedParams))
trace.logTrace('validate', emojic.dart, 'Target', targetPath)
let urlSuffix = ask.uri.search || ''
let urlSuffix = url.parse(req.url).search ?? '' // eslint-disable-line node/no-deprecated-api
if (transformQueryParams) {
const specifiedParams = queryString.parse(urlSuffix)
@@ -100,21 +101,18 @@ export default function redirector(attrs) {
urlSuffix = `?${outQueryString}`
}
// The final capture group is the extension.
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
const redirectUrl = `${
format === 'png' ? rasterUrl : ''
}${targetPath}.${format}${urlSuffix}`
const baseUrl = format === 'png' ? rasterUrl : ''
const redirectUrl = `${baseUrl}${targetPath}.${format}${urlSuffix}`
trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl)
ask.res.statusCode = 301
ask.res.setHeader('Location', redirectUrl)
res.status(301)
res.setHeader('Location', redirectUrl)
// To avoid caching mistakes for a long time, and to make this simpler
// to reason about, use the same cache semantics as the static badge.
setCacheHeadersForStaticResource(ask.res)
setCacheHeadersForStaticResource(res)
ask.res.end()
res.end()
metricHandle.noteResponseSent()
})

View File

@@ -1,7 +1,5 @@
import Camp from '@shields_io/camp'
import portfinder from 'portfinder'
import { expect } from 'chai'
import got from '../got-test-client.js'
import { ExpressTestHarness } from '../express-test-harness.js'
import redirector from './redirector.js'
describe('Redirector', function () {
@@ -63,28 +61,12 @@ describe('Redirector', function () {
expect(redirector({ ...attrs, examples }).examples).to.equal(examples)
})
describe('ScoutCamp integration', function () {
let port, baseUrl
beforeEach(async function () {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
})
let camp
beforeEach(async function () {
camp = Camp.start({ port, hostname: '::' })
await new Promise(resolve => camp.on('listening', () => resolve()))
})
afterEach(async function () {
if (camp) {
await new Promise(resolve => camp.close(resolve))
camp = undefined
}
})
describe('Express integration', function () {
const transformPath = ({ namedParamA }) => `/new/service/${namedParamA}`
beforeEach(function () {
let harness
beforeEach(async function () {
harness = new ExpressTestHarness()
const ServiceClass = redirector({
category,
route,
@@ -92,17 +74,20 @@ describe('Redirector', function () {
dateAdded,
})
ServiceClass.register(
{ camp },
{ app: harness.app },
{ rasterUrl: 'http://raster.example.test' }
)
await harness.start()
})
afterEach(async function () {
await harness.stop()
})
it('should redirect as configured', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/very/old/service/hello-world.svg`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/very/old/service/hello-world.svg',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -110,11 +95,9 @@ describe('Redirector', function () {
})
it('should redirect raster extensions to the canonical path as configured', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/very/old/service/hello-world.png`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/very/old/service/hello-world.png',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -124,11 +107,9 @@ describe('Redirector', function () {
})
it('should forward the query params', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/very/old/service/hello-world.svg?color=123&style=flat-square`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/very/old/service/hello-world.svg?color=123&style=flat-square',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -138,11 +119,9 @@ describe('Redirector', function () {
})
it('should correctly encode the redirect URL', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/very/old/service/hello%0Dworld.svg?foobar=a%0Db`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/very/old/service/hello%0Dworld.svg?foobar=a%0Db',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -166,15 +145,13 @@ describe('Redirector', function () {
transformQueryParams,
dateAdded,
})
ServiceClass.register({ camp }, {})
ServiceClass.register({ app: harness.app }, {})
})
it('should forward the transformed query params', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/another/old/service/token/abc123/hello-world.svg`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/another/old/service/token/abc123/hello-world.svg',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -184,11 +161,9 @@ describe('Redirector', function () {
})
it('should forward the specified and transformed query params', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -198,11 +173,9 @@ describe('Redirector', function () {
})
it('should use transformed query params on param conflicts by default', async function () {
const { statusCode, headers } = await got(
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456`,
{
followRedirect: false,
}
const { statusCode, headers } = await harness.get(
'/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
@@ -224,12 +197,10 @@ describe('Redirector', function () {
overrideTransformedQueryParams: true,
dateAdded,
})
ServiceClass.register({ camp }, {})
const { statusCode, headers } = await got(
`${baseUrl}/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456`,
{
followRedirect: false,
}
ServiceClass.register({ app: harness.app }, {})
const { statusCode, headers } = await harness.get(
'/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456',
{ followRedirect: false }
)
expect(statusCode).to.equal(301)

View File

@@ -44,23 +44,29 @@ function prepareRoute({ base, pattern, format, capture, withPng }) {
return { regex, captureNames }
}
function namedParamsForMatch(captureNames = [], match, ServiceClass) {
// Assume the last match is the format, and drop match[0], which is the
// entire match.
const captures = match.slice(1, -1)
if (captureNames.length !== captures.length) {
function paramsForReq(captureNames = [], req, ServiceClass) {
// In addition to the parameters declared by the service, we have one match
// for the format.
const expectedNamedParamCount = Object.keys(req.params).length - 1
if (captureNames.length !== expectedNamedParamCount) {
throw new Error(
`Service ${ServiceClass.name} declares incorrect number of named params ` +
`(expected ${captures.length}, got ${captureNames.length})`
`(expected ${expectedNamedParamCount}, got ${captureNames.length})`
)
}
const result = {}
const namedParams = {}
captureNames.forEach((name, index) => {
result[name] = captures[index]
namedParams[name] = req.params[index]
})
return result
// The final capture group is the extension.
const format = (req.params[expectedNamedParamCount] || '.svg').replace(
/^\./,
''
)
return { namedParams, format }
}
function getQueryParamNames({ queryParamSchema }) {
@@ -77,6 +83,6 @@ export {
isValidRoute,
assertValidRoute,
prepareRoute,
namedParamsForMatch,
paramsForReq,
getQueryParamNames,
}

View File

@@ -1,13 +1,25 @@
import { expect } from 'chai'
import Joi from 'joi'
import { test, given, forCases } from 'sazerac'
import {
prepareRoute,
namedParamsForMatch,
getQueryParamNames,
} from './route.js'
import { test, given } from 'sazerac'
import { prepareRoute, paramsForReq, getQueryParamNames } from './route.js'
function paramsForPath({ regex, captureNames, ServiceClass }, path) {
// Prepare a mock express `req` object.
const params = {}
regex.exec(path).forEach((param, i) => {
// regex.exec(path)[0] contains the entire path. We want [1] ... [n].
if (i > 0) {
params[i - 1] = param
}
})
const req = { params }
return paramsForReq(captureNames, req, ServiceClass)
}
describe('Route helpers', function () {
const ServiceClass = { name: 'MyService' }
context('A `pattern` with a named param is declared', function () {
const { regex, captureNames } = prepareRoute({
base: 'foo',
@@ -15,22 +27,31 @@ describe('Route helpers', function () {
queryParamSchema: Joi.object({ queryParamA: Joi.string() }).required(),
})
const regexExec = str => regex.exec(str)
const regexExec = path => regex.exec(path)
test(regexExec, () => {
given('/foo/bar/bar.svg').expect(null)
})
const namedParams = str =>
namedParamsForMatch(captureNames, regex.exec(str))
test(namedParams, () => {
forCases([
given('/foo/bar.bar.bar.svg'),
given('/foo/bar.bar.bar.json'),
]).expect({ namedParamA: 'bar.bar.bar' })
const params = path =>
paramsForPath({ regex, captureNames, ServiceClass }, path)
test(params, () => {
given('/foo/bar.bar.bar.svg').expect({
namedParams: { namedParamA: 'bar.bar.bar' },
format: 'svg',
})
given('/foo/bar.bar.bar.json').expect({
namedParams: { namedParamA: 'bar.bar.bar' },
format: 'json',
})
// This pattern catches bugs related to escaping the extension separator.
given('/foo/bar.bar.bar_svg').expect({ namedParamA: 'bar.bar.bar_svg' })
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' })
given('/foo/bar.bar.bar_svg').expect({
namedParams: { namedParamA: 'bar.bar.bar_svg' },
format: 'svg',
})
given('/foo/bar.bar.bar.zip').expect({
namedParams: { namedParamA: 'bar.bar.bar.zip' },
format: 'svg',
})
})
})
@@ -46,33 +67,41 @@ describe('Route helpers', function () {
given('/foo/bar/bar.svg').expect(null)
})
const namedParams = str =>
namedParamsForMatch(captureNames, regex.exec(str))
test(namedParams, () => {
forCases([
given('/foo/bar.bar.bar.svg'),
given('/foo/bar.bar.bar.json'),
]).expect({ namedParamA: 'bar.bar.bar' })
const params = path =>
paramsForPath({ regex, captureNames, ServiceClass }, path)
test(params, () => {
given('/foo/bar.bar.bar.svg').expect({
namedParams: { namedParamA: 'bar.bar.bar' },
format: 'svg',
})
given('/foo/bar.bar.bar.json').expect({
namedParams: { namedParamA: 'bar.bar.bar' },
format: 'json',
})
// This pattern catches bugs related to escaping the extension separator.
given('/foo/bar.bar.bar_svg').expect({ namedParamA: 'bar.bar.bar_svg' })
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' })
given('/foo/bar.bar.bar_svg').expect({
namedParams: { namedParamA: 'bar.bar.bar_svg' },
format: 'svg',
})
given('/foo/bar.bar.bar.zip').expect({
namedParams: { namedParamA: 'bar.bar.bar.zip' },
format: 'svg',
})
})
})
context('No named params are declared', function () {
const { regex, captureNames } = prepareRoute({
base: 'foo',
format: '(?:[^/]+)',
format: '(?:[^/]+?)',
})
const namedParams = str =>
namedParamsForMatch(captureNames, regex.exec(str))
test(namedParams, () => {
forCases([
given('/foo/bar.bar.bar.svg'),
given('/foo/bar.bar.bar.json'),
]).expect({})
const params = path =>
paramsForPath({ regex, captureNames, ServiceClass }, path)
test(params, () => {
given('/foo/bar.bar.bar.svg').expect({ namedParams: {}, format: 'svg' })
given('/foo/bar.bar.bar.json').expect({ namedParams: {}, format: 'json' })
})
})
@@ -83,13 +112,13 @@ describe('Route helpers', function () {
capture: ['namedParamA'],
})
expect(() =>
namedParamsForMatch(captureNames, regex.exec('/foo/bar/baz.svg'), {
name: 'MyService',
})
).to.throw(
'Service MyService declares incorrect number of named params (expected 2, got 1)'
)
it('Throws the expected error', function () {
expect(() =>
paramsForPath({ regex, captureNames, ServiceClass }, '/foo/bar/baz.svg')
).to.throw(
'Service MyService declares incorrect number of named params (expected 2, got 1)'
)
})
})
it('getQueryParamNames', function () {

View File

@@ -0,0 +1,37 @@
import express from 'express'
import portfinder from 'portfinder'
import got from './got-test-client.js'
export class ExpressTestHarness {
constructor() {
this.app = express()
}
async start() {
const port = (this.port = await portfinder.getPortPromise())
this.baseUrl = `http://127.0.0.1:${port}`
await new Promise(resolve => {
this.server = this.app.listen({ host: '::', port }, () => resolve())
})
}
async stop() {
await new Promise(resolve => this.server.close(resolve))
}
ensureStarted() {
if (!this.server) {
throw Error('Server has not been started')
}
}
async get(url, options) {
this.ensureStarted()
return got.get(`${this.baseUrl}${url}`, options)
}
async post(url, options) {
this.ensureStarted()
return got.post(`${this.baseUrl}${url}`, options)
}
}

View File

@@ -37,12 +37,13 @@ export default class PrometheusMetrics {
})
}
async registerMetricsEndpoint(server) {
async registerMetricsEndpoint(app) {
const { register } = this
server.route(/^\/metrics$/, async (data, match, end, ask) => {
ask.res.setHeader('Content-Type', register.contentType)
ask.res.end(await register.metrics())
app.get('/metrics', async (req, res) => {
res.setHeader('Content-Type', register.contentType)
res.send(await register.metrics())
res.end()
})
}

View File

@@ -1,32 +1,24 @@
import { expect } from 'chai'
import Camp from '@shields_io/camp'
import portfinder from 'portfinder'
import got from '../got-test-client.js'
import { ExpressTestHarness } from '../express-test-harness.js'
import Metrics from './prometheus-metrics.js'
describe('Prometheus metrics route', function () {
let port, baseUrl, camp, metrics
let harness, metrics
beforeEach(async function () {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
camp = Camp.start({ port, hostname: '::' })
await new Promise(resolve => camp.on('listening', () => resolve()))
harness = new ExpressTestHarness()
metrics = new Metrics()
metrics.registerMetricsEndpoint(harness.app)
await harness.start()
})
afterEach(async function () {
if (metrics) {
metrics.stop()
}
if (camp) {
await new Promise(resolve => camp.close(resolve))
camp = undefined
}
await harness.stop()
})
it('returns default metrics', async function () {
metrics = new Metrics()
metrics.registerMetricsEndpoint(camp)
const { statusCode, body } = await got(`${baseUrl}/metrics`)
const { statusCode, body } = await harness.get('/metrics')
expect(statusCode).to.be.equal(200)
expect(body).to.contain('nodejs_version_info')

View File

@@ -1,66 +0,0 @@
<!doctype html><meta charset=utf-8>
<title> Shields.io Admin Monitoring Interface </title>
<style>
#monitorPlatform { display: none; }
</style>
<div id=passwordRequest>
<p> Please enter your admin secret here:
<input type=password id=secretInput>
</div>
<div id=monitorPlatform>
</div>
<script>
(function() {
let network;
const onLoad = function() {
const secretInput = document.getElementById('secretInput');
const onSecretChange = function() {
const secret = secretInput.value;
const authentication = `monitor:${secret}`;
const headers = new Headers({
Authorization: `Basic ${btoa(authentication)}`
})
fetch('/sys/network', {headers})
.then(res => res.json())
.then(networkData => {
network = networkData;
// Show monitor platform.
monitorPlatform.style.display = 'block';
passwordRequest.parentNode.removeChild(passwordRequest);
// Show logs for each server.
network.ips.forEach(ip => {
const logger = document.createElement('div');
const pre = document.createElement('pre');
logger.textContent = ip;
logger.appendChild(pre);
monitorPlatform.appendChild(logger);
// Set up the websocket.
const setUpWebsocket = () => {
const websocket = new WebSocket(
(window.location.protocol === 'http:' ? 'ws' : 'wss') + '://' +
ip + ':' + window.location.port + '/sys/logs');
websocket.addEventListener('message', event => {
pre.textContent += event.data + '\n';
});
websocket.addEventListener('close', () => {
setTimeout(setUpWebsocket, 100);
});
websocket.addEventListener('open', () => {
websocket.send(JSON.stringify({secret}));
});
};
setUpWebsocket();
});
})
.catch(alert)
};
secretInput.addEventListener('change', onSecretChange);
};
addEventListener('DOMContentLoaded', onLoad);
}());
</script>

View File

@@ -2,19 +2,20 @@
* @module
*/
import http from 'http'
import https from 'https'
import path from 'path'
import url, { fileURLToPath } from 'url'
import express from 'express'
import { bootstrap } from 'global-agent'
import cloudflareMiddleware from 'cloudflare-middleware'
import Camp from '@shields_io/camp'
import originalJoi from 'joi'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import GithubConstellation from '../../services/github/github-constellation.js'
import LibrariesIoConstellation from '../../services/librariesio/librariesio-constellation.js'
import { setRoutes } from '../../services/suggest.js'
import { setRoutes as setSuggestRoutes } 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 { makeJsonBadge } from '../base-service/make-json-badge.js'
import { clearResourceCache } from '../base-service/resource-cache.js'
import { rasterRedirectUrl } from '../badge-urls/make-badge-url.js'
import { fileSize, nonNegativeInteger } from '../../services/validators.js'
@@ -140,7 +141,9 @@ const publicConfigSchema = Joi.object({
weblate: defaultService,
trace: Joi.boolean().required(),
}).required(),
cacheHeaders: { defaultCacheLengthSeconds: nonNegativeInteger },
cacheHeaders: Joi.object({
defaultCacheLengthSeconds: nonNegativeInteger,
}).required(),
handleInternalErrors: Joi.boolean().required(),
fetchLimit: fileSize,
userAgentBase: Joi.string().required(),
@@ -197,23 +200,11 @@ const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
influx_password: Joi.string().required(),
})
function addHandlerAtIndex(camp, index, handlerFn) {
camp.stack.splice(index, 0, handlerFn)
}
function isOnHeroku() {
return !!process.env.DYNO
}
function isOnFly() {
return !!process.env.FLY_APP_NAME
}
/**
* The Server is based on the web framework Scoutcamp. It creates
* an http server, sets up helpers for token persistence and monitoring.
* Then it loads all the services, injecting dependencies as it
* asks each one to register its route with Scoutcamp.
* The Server is based on Express. It creates an http server and sets up helpers
* for token persistence and monitoring. Then it loads all the services,
* injecting dependencies, as it asks each one to register its route with
* Express.
*/
class Server {
/**
@@ -306,43 +297,25 @@ class Server {
// See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
requireCloudflare() {
// Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
// by Express but not Scoutcamp.
addHandlerAtIndex(this.camp, 0, function (req, res, next) {
if (isOnHeroku()) {
// On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
// the router ensures that the last item in the `X-Forwarded-For` header
// is the real origin.
// https://stackoverflow.com/a/18517550/893113
req.ip = req.headers['x-forwarded-for'].split(', ').pop()
} else if (isOnFly()) {
// On Fly we can use the Fly-Client-IP header
// https://fly.io/docs/reference/runtime-environment/#request-headers
req.ip = req.headers['fly-client-ip']
} else {
req.ip = req.socket.remoteAddress
}
next()
})
addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
const { app } = this
app.use(cloudflareMiddleware())
}
/**
* Set up Scoutcamp routes for 404/not found responses
* Set up Express routes for 404/not found responses.
*/
registerErrorHandlers() {
const { camp, config } = this
const { app, config } = this
const {
public: { rasterUrl },
} = config
camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
const [, format] = match
makeSend(
'svg',
request.res,
end
)(
app.get(/\.(gif|jpg)$/, (req, res) => {
res.status(410)
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
const format = req.params[0]
res.send(
makeBadge({
label: '410',
message: `${format} no longer available`,
@@ -350,41 +323,53 @@ class Server {
format: 'svg',
})
)
res.end()
})
if (!rasterUrl) {
camp.route(/\.png$/, (query, match, end, request) => {
makeSend(
'svg',
request.res,
end
)(
app.get(/\.png$/, (req, res) => {
res.status(404)
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
res.send(
makeBadge({
label: '404',
message: 'raster badges not available',
color: 'lightgray',
format: 'svg',
})
)
res.end()
})
}
}
camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
const [, extension] = match
const format = (extension || '.svg').replace(/^\./, '')
registerNotFoundHandlers() {
const { app } = this
makeSend(
format,
request.res,
end
)(
app.get(/\.json$/, (req, res) => {
res.status(404)
res.setHeader('Content-Type', 'application/json')
res.json(
makeJsonBadge({
label: '404',
message: 'badge not found',
color: 'red',
})
)
res.end()
})
app.get(/(?:\.svg|)$/, (req, res) => {
res.status(404)
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
res.send(
makeBadge({
label: '404',
message: 'badge not found',
color: 'red',
format,
})
)
res.end()
})
}
@@ -396,54 +381,62 @@ class Server {
* to {@link https://shields.io/} )
*/
registerRedirects() {
const { config, camp } = this
const { config, app } = this
const {
public: { rasterUrl, redirectUrl },
} = config
if (rasterUrl) {
// Redirect to the raster server for raster versions of modern badges.
camp.route(/\.png$/, (queryParams, match, end, ask) => {
ask.res.statusCode = 301
ask.res.setHeader(
'Location',
rasterRedirectUrl({ rasterUrl }, ask.req.url)
)
app.get(/\.png$/, (req, res) => {
res.status(301)
res.setHeader('Location', rasterRedirectUrl({ rasterUrl }, req.url))
const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
ask.res.end()
res.end()
})
}
if (redirectUrl) {
camp.route(/^\/$/, (data, match, end, ask) => {
ask.res.statusCode = 302
ask.res.setHeader('Location', redirectUrl)
ask.res.end()
app.get('/', (req, res) => {
res.status(302)
res.setHeader('Location', redirectUrl)
res.end()
})
}
/*
This is here for legacy reasons. The badge server and frontend used to live
on two different servers. When we merged them there was a conflict so we did
this to avoid moving the endpoint docs to another URL.
Never ever do this again.
*/
app.use('/endpoint', (req, res, next) => {
if (Object.keys(req.query).length === 0) {
res.status(301)
res.setHeader('Location', '/endpoint/')
res.end()
} else {
next()
}
})
}
/**
* Iterate all the service classes defined in /services,
* load each service and register a Scoutcamp route for each service.
* load each service and register an Express route for each service.
*/
async registerServices() {
const { config, camp, metricInstance } = this
const { app, config, metricInstance } = this
const { apiProvider: githubApiProvider } = this.githubConstellation
const { apiProvider: librariesIoApiProvider } =
this.librariesioConstellation
;(await loadServiceClasses()).forEach(serviceClass =>
serviceClass.register(
{
camp,
handleRequest,
githubApiProvider,
librariesIoApiProvider,
metricInstance,
},
{ app, githubApiProvider, librariesIoApiProvider, metricInstance },
{
handleInternalErrors: config.public.handleInternalErrors,
cacheHeaders: config.public.cacheHeaders,
@@ -476,11 +469,14 @@ class Server {
/**
* Start the HTTP server:
* Bootstrap Scoutcamp,
* Bootstrap Express,
* Register handlers,
* Start listening for requests on this.baseUrl()
*
* @param {Function} registerExtras Optional function to register additional
* routes, used for testing.
*/
async start() {
async start(registerExtras) {
const {
bind: { port, address: hostname },
ssl: { isSecure: secure, cert, key },
@@ -492,25 +488,17 @@ class Server {
log.log(`Server is starting up: ${this.baseUrl}`)
const camp = (this.camp = Camp.create({
documentRoot: this.config.public.documentRoot,
port,
hostname,
secure,
staticMaxAge: 300,
cert,
key,
}))
const app = (this.app = express())
if (requireCloudflare) {
this.requireCloudflare()
}
const { githubConstellation, metricInstance } = this
await githubConstellation.initialize(camp)
await githubConstellation.initialize(app)
if (metricInstance) {
if (this.config.public.metrics.prometheus.endpointEnabled) {
metricInstance.registerMetricsEndpoint(camp)
metricInstance.registerMetricsEndpoint(app)
}
if (this.influxMetrics) {
this.influxMetrics.startPushingMetrics()
@@ -518,39 +506,47 @@ class Server {
}
const { apiProvider: githubApiProvider } = this.githubConstellation
setRoutes(allowedOrigin, githubApiProvider, camp)
setSuggestRoutes(allowedOrigin, githubApiProvider, app)
// https://github.com/badges/shields/issues/3273
camp.handle((req, res, next) => {
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
next()
})
this.registerErrorHandlers()
this.registerRedirects()
await this.registerServices()
camp.timeout = this.config.public.requestTimeoutSeconds * 1000
if (this.config.public.requestTimeoutSeconds > 0) {
camp.on('timeout', socket => {
const maxAge = this.config.public.requestTimeoutMaxAgeSeconds
socket.write('HTTP/1.1 408 Request Timeout\r\n')
socket.write('Content-Type: text/html; charset=UTF-8\r\n')
socket.write('Content-Encoding: UTF-8\r\n')
socket.write(`Cache-Control: max-age=${maxAge}, s-maxage=${maxAge}\r\n`)
socket.write('Connection: close\r\n\r\n')
socket.write('Request Timeout')
socket.end()
app.use(
express.static(this.config.public.documentRoot, {
// Since express's `maxAge` parameter sets `Cache-Control: public`, set
// the headers manually insetad.
cacheControl: false,
setHeaders: res =>
res.setHeader('Cache-Control', 'max-age=300, s-maxage=300'),
})
)
await this.registerServices()
if (registerExtras) {
registerExtras(app)
}
camp.listenAsConfigured()
this.registerNotFoundHandlers()
await new Promise(resolve => camp.on('listening', () => resolve()))
if (secure) {
this.server = https.createServer({ hostname, cert, key }, app)
} else {
this.server = http.createServer({ hostname }, app)
}
this.server.setTimeout(this.config.public.requestTimeoutSeconds * 1000)
await new Promise(resolve =>
this.server.listen({ host: hostname, port }, () => resolve())
)
}
static resetGlobalState() {
// This state should be migrated to instance state. When possible, do not add new
// global state.
// TODO: This state should be migrated to instance state. When possible, do
// not add new global state.
clearResourceCache()
}
@@ -562,10 +558,11 @@ class Server {
* Stop the HTTP server and clean up helpers
*/
async stop() {
if (this.camp) {
await new Promise(resolve => this.camp.close(resolve))
this.camp = undefined
if (this.server) {
await new Promise(resolve => this.server.close(() => resolve()))
this.server = undefined
}
this.app = undefined
if (this.cleanupMonitor) {
this.cleanupMonitor()

View File

@@ -73,9 +73,7 @@ describe('The server', function () {
it('should redirect colorscheme PNG badges as configured', async function () {
const { statusCode, headers } = await got(
`${baseUrl}:fruit-apple-green.png`,
{
followRedirect: false,
}
{ followRedirect: false }
)
expect(statusCode).to.equal(301)
expect(headers.location).to.equal(
@@ -98,7 +96,7 @@ describe('The server', function () {
`${baseUrl}:fruit-apple-green.svg`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('image/svg+xml;charset=utf-8')
expect(headers['content-type']).to.equal('image/svg+xml; charset=utf-8')
expect(headers['content-length']).to.equal('1130')
})
@@ -112,7 +110,9 @@ describe('The server', function () {
`${baseUrl}:fruit-apple-green.json`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('application/json')
expect(headers['content-type']).to.equal(
'application/json; charset=utf-8'
)
expect(headers['access-control-allow-origin']).to.equal('*')
expect(headers['content-length']).to.equal('92')
expect(() => JSON.parse(body)).not.to.throw()
@@ -200,19 +200,12 @@ describe('The server', function () {
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
throwHttpErrors: false,
})
// TODO It would be nice if this were 404 or 410.
expect(statusCode).to.equal(200)
expect(statusCode).to.equal(410)
expect(body)
.to.satisfy(isSvg)
.and.to.include('410')
.and.to.include('jpg no longer available')
})
it('should return cors header for the request', async function () {
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.svg`)
expect(statusCode).to.equal(200)
expect(headers['access-control-allow-origin']).to.equal('*')
})
})
context('`requireCloudflare` is enabled', function () {
@@ -245,22 +238,12 @@ describe('The server', function () {
// configure server to time out requests that take >2 seconds
server = await createTestServer({ public: { requestTimeoutSeconds: 2 } })
await server.start()
await server.start(app => {
// /fast returns a 200 OK after a 1 second delay
app.get('/fast', (req, res) => setTimeout(() => res.end(), 1000))
// /fast returns a 200 OK after a 1 second delay
server.camp.route(/^\/fast$/, (data, match, end, ask) => {
setTimeout(() => {
ask.res.statusCode = 200
ask.res.end()
}, 1000)
})
// /slow returns a 200 OK after a 3 second delay
server.camp.route(/^\/slow$/, (data, match, end, ask) => {
setTimeout(() => {
ask.res.statusCode = 200
ask.res.end()
}, 3000)
// /slow returns a 200 OK after a 3 second delay
app.get('/slow', (req, res) => setTimeout(() => res.end(), 3000))
})
})
@@ -273,11 +256,9 @@ describe('The server', function () {
it('should time out slow requests', async function () {
this.timeout(10000)
const { statusCode, body } = await got(`${server.baseUrl}slow`, {
throwHttpErrors: false,
})
expect(statusCode).to.be.equal(408)
expect(body).to.equal('Request Timeout')
await expect(got(`${server.baseUrl}slow`)).to.be.rejectedWith(
'socket hang up'
)
})
it('should not time out fast requests', async function () {

View File

@@ -80,29 +80,22 @@ test this kind of logic through unit tests (e.g. of `render()` and
reporting, loads config, and creates an instance of the server.
2. The Server, which is defined in
[`core/server/server.js`][core/server/server], is based on the web
framework [Scoutcamp][]. It creates an http server, sets up helpers for
token persistence and monitoring. Then it loads all the services,
injecting dependencies as it asks each one to register its route
with Scoutcamp.
[`core/server/server.js`][core/server/server], is based on [Express][].
It creates an http server, sets up helpers for token persistence and
monitoring. Then it loads all the services, injecting dependencies as it
asks each one to register its route with the Express app.
3. The service registration continues in `BaseService.register`. From its
`route` property, it derives a regular expression to match the route
path, and invokes `camp.route` with this value.
path, and invokes `app.get` with this value.
4. At this point the situation gets gnarly and hard to follow. For the
purpose of initialization, suffice it to say that `camp.route` invokes a
callback with the four parameters `( queryParams, match, end, ask )` which
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 )`, which
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
service and runs `BaseService#handle`.
4. TODO: Explain what happens here (i.e. now that we've migrated from Scoutcamp
to Express). `BaseService.invoke` instantiates the service and runs
`BaseService#handle`.
[entrypoint]: https://github.com/badges/shields/blob/master/server.js
[core/server/server]: https://github.com/badges/shields/blob/master/core/server/server.js
[scoutcamp]: https://github.com/espadrine/sc
[express]: https://expressjs.com/
[legacy-request-handler]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js
## Downstream caching
@@ -119,24 +112,15 @@ test this kind of logic through unit tests (e.g. of `render()` and
## How the server makes a badge
1. An HTTPS request arrives. Scoutcamp inspects the URL path and matches it
against the regexes for all the registered routes until it finds one that
matches. (See *Initialization* above for an explanation of how routes are
1. An HTTPS request arrives. Express inspects the URL path and matches it
against all the registered routes until it finds one that matches. (See
*Initialization* above for an explanation of how routes are
registered.)
2. Scoutcamp invokes a callback with the four parameters:
`( queryParams, match, end, ask )`. This callback is defined in
[`legacy-request-handler`][legacy-request-handler]. A timeout is set to
handle unresponsive service code and the next callback is invoked: the
legacy handler function.
3. The legacy handler function receives
`( 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
implemented by the service subclass.
5. The job of `handle()`, which should be implemented by each service
2. Invoke the request handler function, defined in `BaseService.register`,
which handles the request. It runs `BaseService.invoke`, which instantiates
the service, injects more dependencies, and invokes `BaseService.handle`
which is implemented by the service subclass.
3. The job of `handle()`, which should be implemented by each service
subclass, is to return an object which partially describes a badge or
throw one of the handled error classes. "Partially rendered" most
commonly means a non-empty message and an optional color. In the case
@@ -146,7 +130,7 @@ test this kind of logic through unit tests (e.g. of `render()` and
Throwing any other error is a programmer error which will be
[reported][error reporting] and described to the user as a **shields
internal error**.
6. A typical `handle()` function delegates to one or more helpers to
4. A typical `handle()` function delegates to one or more helpers to
handle stages of the request:
1. **fetch**: load the needed data from the upstream service and
validate it
@@ -154,13 +138,13 @@ test this kind of logic through unit tests (e.g. of `render()` and
into a few properties which will be displayed on the badge
3. **render**: given a few properties, return a message, optional
color, and optional label.
7. When an error is thrown, BaseService steps in and converts the error
5. When an error is thrown, BaseService steps in and converts the error
object to renderable properties: `{ isError, message, color }`.
8. The service invokes [`coalesceBadge`][coalescebadge] whose job is to
6. The service invokes [`coalesceBadge`][coalescebadge] whose job is to
coalesce query string overrides with values from the service and the
services defaults to produce an object that fully describes the badge to
be rendered.
9. `sendBadge` is invoked with that object. It does some housekeeping on the
7. `sendBadge` is invoked with that object. It does some housekeeping on the
timeout. Then it renders the badge to svg or raster and pushes out the
result over the HTTPS connection.

2472
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,7 @@
"@fontsource/lato": "^4.5.5",
"@fontsource/lekton": "^4.5.6",
"@renovate/pep440": "^1.0.0",
"@sentry/node": "^6.19.3",
"@shields_io/camp": "^18.1.1",
"@sentry/node": "^6.19.6",
"badge-maker": "file:badge-maker",
"bytes": "^3.1.2",
"camelcase": "^6.3.0",
@@ -37,13 +36,14 @@
"decamelize": "^3.2.0",
"emojic": "^1.1.17",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.3",
"fast-xml-parser": "^4.0.7",
"glob": "^7.2.0",
"glob": "^8.0.1",
"global-agent": "^3.0.0",
"got": "^12.0.3",
"graphql": "^15.6.1",
"graphql-tag": "^2.12.6",
"ioredis": "5.0.3",
"ioredis": "5.0.4",
"joi": "17.6.0",
"joi-extension-semver": "5.0.0",
"js-yaml": "^4.1.0",
@@ -51,7 +51,8 @@
"lodash.countby": "^4.6.0",
"lodash.groupby": "^4.6.0",
"lodash.times": "^4.3.2",
"moment": "^2.29.1",
"moment": "^2.29.2",
"multer": "^1.4.4",
"node-env-flag": "^0.1.0",
"parse-link-header": "^2.0.0",
"path-to-regexp": "^6.2.0",
@@ -60,8 +61,8 @@
"prom-client": "^14.0.1",
"qs": "^6.10.3",
"query-string": "^7.1.1",
"semver": "~7.3.5",
"simple-icons": "6.17.0",
"semver": "~7.3.7",
"simple-icons": "6.18.0",
"webextension-store-meta": "^1.0.5",
"xmldom": "~0.6.0",
"xpath": "~0.0.32"
@@ -141,12 +142,12 @@
]
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@babel/core": "^7.17.9",
"@babel/polyfill": "^7.12.1",
"@babel/register": "7.17.7",
"@istanbuljs/schema": "^0.1.3",
"@mapbox/react-click-to-select": "^2.2.1",
"@types/chai": "^4.3.0",
"@types/chai": "^4.3.1",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.groupby": "^4.6.6",
"@types/mocha": "^9.1.0",
@@ -154,8 +155,8 @@
"@types/react-helmet": "^6.1.5",
"@types/react-modal": "^3.13.1",
"@types/react-select": "^4.0.17",
"@types/styled-components": "5.1.24",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@types/styled-components": "5.1.25",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.15.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-preset-gatsby": "^2.11.1",
@@ -167,8 +168,8 @@
"chai-string": "^1.4.0",
"child-process-promise": "^2.2.1",
"clipboard-copy": "^4.0.1",
"concurrently": "^7.0.0",
"cypress": "^9.5.3",
"concurrently": "^7.1.0",
"cypress": "^9.5.4",
"danger": "^11.0.2",
"danger-plugin-no-test-shortcuts": "^2.0.0",
"deepmerge": "^4.2.2",
@@ -179,8 +180,8 @@
"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.4",
"eslint-plugin-jsdoc": "^38.1.6",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.2.7",
"eslint-plugin-mocha": "^10.0.3",
"eslint-plugin-no-extension-in-require": "^0.2.0",
"eslint-plugin-node": "^11.1.0",
@@ -203,7 +204,7 @@
"is-svg": "^4.3.2",
"js-yaml-loader": "^1.2.2",
"jsdoc": "^3.6.10",
"lint-staged": "^12.3.7",
"lint-staged": "^12.3.8",
"lodash.debounce": "^4.0.8",
"lodash.difference": "^4.5.0",
"minimist": "^1.2.6",
@@ -217,10 +218,10 @@
"npm-run-all": "^4.1.5",
"open-cli": "^7.0.1",
"portfinder": "^1.0.28",
"prettier": "2.6.1",
"prettier": "2.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-overlay": "^6.0.10",
"react-error-overlay": "^6.0.11",
"react-helmet": "^6.1.0",
"react-modal": "^3.14.4",
"react-pose": "^4.0.10",
@@ -230,13 +231,13 @@
"rimraf": "^3.0.2",
"sazerac": "^2.0.0",
"simple-git-hooks": "^2.7.0",
"sinon": "^13.0.1",
"sinon": "^13.0.2",
"sinon-chai": "^3.7.0",
"snap-shot-it": "^7.9.6",
"start-server-and-test": "1.14.0",
"styled-components": "^5.3.5",
"ts-mocha": "^9.0.2",
"tsd": "^0.19.1",
"tsd": "^0.20.0",
"typescript": "^4.6.3",
"url": "^0.11.0"
},

View File

@@ -21,8 +21,8 @@ export default class Bitrise extends BaseJsonService {
static examples = [
{
title: 'Bitrise',
namedParams: { appId: 'cde737473028420d', branch: 'master' },
queryParams: { token: 'GCIdEzacE4GW32jLVrZb7A' },
namedParams: { appId: '3ff11fe8457bd304', branch: 'master' },
queryParams: { token: 'lESRN9rEFFfDq92JtXs_jw' },
staticPreview: this.render({ status: 'success' }),
},
]

View File

@@ -3,14 +3,14 @@ import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('deploy status')
.get('/cde737473028420d.json?token=GCIdEzacE4GW32jLVrZb7A')
.get('/3ff11fe8457bd304.json?token=lESRN9rEFFfDq92JtXs_jw')
.expectBadge({
label: 'bitrise',
message: isBuildStatus,
})
t.create('deploy status with branch')
.get('/cde737473028420d/master.json?token=GCIdEzacE4GW32jLVrZb7A')
.get('/3ff11fe8457bd304/master.json?token=lESRN9rEFFfDq92JtXs_jw')
.expectBadge({
label: 'bitrise',
message: isBuildStatus,

View File

@@ -1,5 +1,6 @@
import Joi from 'joi'
import { floorCount as floorCountColor } from '../color-formatters.js'
import { metric } from '../text-formatters.js'
import { BaseJsonService } from '../index.js'
const ownerSchema = Joi.array().required()
@@ -20,7 +21,7 @@ export default class GemOwner extends BaseJsonService {
static render({ count }) {
return {
message: count,
message: metric(count),
color: floorCountColor(count, 10, 50, 100),
}
}

View File

@@ -1,12 +1,13 @@
import queryString from 'query-string'
import multer from 'multer'
import { fetch } from '../../../core/base-service/got.js'
import log from '../../../core/server/log.js'
function setRoutes({ server, authHelper, onTokenAccepted }) {
function setRoutes({ app, authHelper, onTokenAccepted }) {
const baseUrl = process.env.GATSBY_BASE_URL || 'https://img.shields.io'
server.route(/^\/github-auth$/, (data, match, end, ask) => {
ask.res.statusCode = 302 // Found.
app.post('/github-auth', (req, res) => {
res.status(302) // Found.
const query = queryString.stringify({
// TODO The `_user` property bypasses security checks in AuthHelper.
// (e.g: enforceStrictSsl and shouldAuthenticateRequest).
@@ -15,56 +16,64 @@ function setRoutes({ server, authHelper, onTokenAccepted }) {
client_id: authHelper._user,
redirect_uri: `${baseUrl}/github-auth/done`,
})
ask.res.setHeader(
res.setHeader(
'Location',
`https://github.com/login/oauth/authorize?${query}`
)
end('')
res.end()
})
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.')
}
app.post('/github-auth/done', multer().none(), async (req, res) => {
const code = (req.body ?? {}).code
const options = {
method: 'POST',
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
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
// this up so it's not setting a bad example.
client_id: authHelper._user,
client_secret: authHelper._pass,
code: data.code,
},
if (!code) {
log.log(`GitHub OAuth data: ${JSON.stringify(req.body)}`)
res.send('GitHub OAuth authentication failed to provide a code.')
res.end()
return
}
let resp
try {
resp = await fetch('https://github.com/login/oauth/access_token', options)
resp = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
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
// this up so it's not setting a bad example.
client_id: authHelper._user,
client_secret: authHelper._pass,
code,
},
})
} catch (e) {
return end('The connection to GitHub failed.')
res.send('The connection to GitHub failed.')
res.end()
return
}
let content
try {
content = queryString.parse(resp.buffer)
} catch (e) {
return end('The GitHub OAuth token could not be parsed.')
res.send('The GitHub OAuth token could not be parsed.')
res.end()
return
}
const { access_token: token } = content
if (!token) {
return end('The GitHub OAuth process did not return a user token.')
res.send('The GitHub OAuth process did not return a user token.')
res.end()
return
}
ask.res.setHeader('Content-Type', 'text/html')
end(
res.setHeader('Content-Type', 'text/html')
res.send(
'<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>' +
@@ -75,6 +84,7 @@ function setRoutes({ server, authHelper, onTokenAccepted }) {
'everyone!</p>' +
'<p><a href="/">Back to the website</a></p>'
)
res.end()
onTokenAccepted(token)
})

View File

@@ -1,11 +1,9 @@
import { expect } from 'chai'
import Camp from '@shields_io/camp'
import FormData from 'form-data'
import sinon from 'sinon'
import portfinder from 'portfinder'
import queryString from 'query-string'
import nock from 'nock'
import got from '../../../core/got-test-client.js'
import { ExpressTestHarness } from '../../../core/express-test-harness.js'
import GithubConstellation from '../github-constellation.js'
import { setRoutes } from './acceptor.js'
@@ -17,36 +15,26 @@ describe('Github token acceptor', function () {
private: { gh_client_id: fakeClientId, gh_client_secret: fakeClientSecret },
})
let port, baseUrl
let harness, onTokenAccepted
beforeEach(async function () {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
})
harness = new ExpressTestHarness()
let camp
beforeEach(async function () {
camp = Camp.start({ port, hostname: '::' })
await new Promise(resolve => camp.on('listening', () => resolve()))
})
afterEach(async function () {
if (camp) {
await new Promise(resolve => camp.close(resolve))
camp = undefined
}
})
let onTokenAccepted
beforeEach(function () {
onTokenAccepted = sinon.stub()
setRoutes({
server: camp,
app: harness.app,
authHelper: oauthHelper,
onTokenAccepted,
})
await harness.start()
})
afterEach(async function () {
await harness.stop()
})
it('should start the OAuth process', async function () {
const res = await got(`${baseUrl}/github-auth`, { followRedirect: false })
const res = await harness.post('/github-auth', { followRedirect: false })
expect(res.statusCode).to.equal(302)
@@ -61,8 +49,8 @@ describe('Github token acceptor', function () {
describe('Finishing the OAuth process', function () {
context('no code is provided', function () {
it('should return an error', async function () {
const res = await got(`${baseUrl}/github-auth/done`)
expect(res.body).to.equal(
const { body } = await harness.post('/github-auth/done')
expect(body).to.equal(
'GitHub OAuth authentication failed to provide a code.'
)
})
@@ -111,9 +99,7 @@ describe('Github token acceptor', function () {
const form = new FormData()
form.append('code', fakeCode)
const res = await got.post(`${baseUrl}/github-auth/done`, {
body: form,
})
const res = await harness.post('/github-auth/done', { body: form })
expect(res.body).to.startWith(
'<p>Shields.io has received your app-specific GitHub user token.'
)

View File

@@ -50,7 +50,7 @@ class GithubConstellation {
}
}
async initialize(server) {
async initialize(app) {
if (!this.apiProvider.withPooling) {
return
}
@@ -74,7 +74,7 @@ class GithubConstellation {
if (this.oauthHelper.isConfigured) {
setAcceptorRoutes({
server,
app,
authHelper: this.oauthHelper,
onTokenAccepted: tokenString => this.onTokenAdded(tokenString),
})

View File

@@ -307,7 +307,9 @@ export default class GithubIssues extends GithubAuthV4Service {
return {
label: `${labelPrefix}${labelText}${labelSuffix}`,
message: `${metric(issueCount)} ${messageSuffix}`,
message: `${metric(issueCount)}${
messageSuffix ? ' ' : ''
}${messageSuffix}`,
color: issueCount > 0 ? 'yellow' : 'brightgreen',
}
}

View File

@@ -1,3 +1,4 @@
import { metric } from '../text-formatters.js'
import { BaseGithubLanguage } from './github-languages-base.js'
import { documentation } from './github-helpers.js'
@@ -20,7 +21,7 @@ export default class GithubLanguageCount extends BaseGithubLanguage {
static render({ count }) {
return {
message: count,
message: metric(count),
color: 'blue',
}
}

View File

@@ -75,7 +75,7 @@ export default class GithubMilestoneDetail extends GithubAuthV3Service {
}
return {
label: `${milestone.title} ${label}`,
label: `${milestone.title}${label ? ' ' : ''}${label}`,
message: metric(milestoneMetric),
color,
}

View File

@@ -43,7 +43,7 @@ export default class GithubMilestone extends GithubAuthV3Service {
static render({ user, repo, variant, milestones }) {
const milestoneLength = milestones.length
let color
let label = ''
let qualifier = ''
switch (variant) {
case 'all':
@@ -51,16 +51,16 @@ export default class GithubMilestone extends GithubAuthV3Service {
break
case 'open':
color = 'red'
label = 'active'
qualifier = 'active'
break
case 'closed':
color = 'green'
label = 'completed'
qualifier = 'completed'
break
}
return {
label: `${label} milestones`,
label: `${qualifier}${qualifier ? ' ' : ''}milestones`,
message: metric(milestoneLength),
color,
}

View File

@@ -1,6 +1,7 @@
import Joi from 'joi'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService } from '../index.js'
import { metric } from '../text-formatters.js'
// https://developer.opencollective.com/#/api/collectives?id=get-info
const collectiveDetailsSchema = Joi.object().keys({
@@ -30,12 +31,11 @@ export default class OpencollectiveBase extends BaseJsonService {
}
static render(backersCount, label) {
const badge = {
message: backersCount,
return {
label,
message: metric(backersCount),
color: backersCount > 0 ? 'brightgreen' : 'lightgrey',
}
if (label) badge.label = label
return badge
}
async fetchCollectiveInfo(collective) {

View File

@@ -0,0 +1,55 @@
import Joi from 'joi'
import { BaseJsonService } from '../index.js'
import { colorScale } from '../color-formatters.js'
const schema = Joi.object({
score: Joi.number().min(0).required(),
}).required()
const ossfScorecardColorScale = colorScale(
[2, 5, 8, 10],
['red', 'yellow', 'yellowgreen', 'green', 'brightgreen']
)
export default class OSSFScorecard extends BaseJsonService {
static category = 'analysis'
static route = { base: 'ossf-scorecard', pattern: ':host/:orgName/:repoName' }
static examples = [
{
title: 'OSSF-Scorecard Score',
namedParams: {
host: 'github.com',
orgName: 'rohankh532',
repoName: 'org-workflow-add',
},
staticPreview: this.render({ score: '7.5' }),
},
]
static defaultBadgeData = { label: 'score' }
static render({ score }) {
return {
message: score,
color: ossfScorecardColorScale(score),
}
}
async fetch({ host, orgName, repoName }) {
return this._requestJson({
schema,
url: `https://api.securityscorecards.dev/projects/${host}/${orgName}/${repoName}`,
errorMessages: {
404: 'invalid repo path',
},
})
}
async handle({ host, orgName, repoName }) {
const { score } = await this.fetch({ host, orgName, repoName })
return this.constructor.render({ score })
}
}

View File

@@ -0,0 +1,25 @@
import Joi from 'joi'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('score valid')
.get('/github.com/rohankh532/org-workflow-add.json')
.expectBadge({
label: 'score',
message: Joi.number().min(0),
color: Joi.string().allow(
'red',
'yellow',
'yellowgreen',
'green',
'brightgreen'
),
})
t.create('score ivalid')
.get('/github.com/invalid-user/invalid-repo.json')
.expectBadge({
label: 'score',
message: 'invalid repo path',
color: 'red',
})

View File

@@ -12,9 +12,7 @@ export const t = await createServiceTester()
// https://docs.sonarqube.org/7.0/MetricDefinitions.html
// https://sonarcloud.io/api/measures/component?componentKey=org.sonarsource.sonarqube:sonarqube&metricKeys=public_documented_api_density
t.create('Documented API Density (not found)')
.get(
'/org.sonarsource.sonarqube%3Asonarqube.json?server=https://sonarcloud.io'
)
.get('/brave_brave-core.json?server=https://sonarcloud.io')
.expectBadge({
label: 'public documented api density',
message: 'metric not found',

View File

@@ -10,7 +10,7 @@ export const t = await createServiceTester()
t.create('Tech Debt')
.get(
'/tech_debt/org.sonarsource.sonarqube%3Asonarqube.json?server=https://sonarcloud.io'
'/tech_debt/brave_brave-core.json?server=https://sonarcloud.io&sonarVersion=9.0'
)
.expectBadge({
label: 'tech debt',
@@ -18,9 +18,7 @@ t.create('Tech Debt')
})
t.create('Tech Debt (branch)')
.get(
'/tech_debt/org.sonarsource.sonarqube%3Asonarqube/master.json?server=https://sonarcloud.io'
)
.get('/tech_debt/brave_brave-core/master.json?server=https://sonarcloud.io')
.expectBadge({
label: 'tech debt',
message: isPercentage,

View File

@@ -17,9 +17,7 @@ const isViolationsLongFormMetric = Joi.alternatives(
t.create('Violations')
.timeout(10000)
.get(
'/violations/org.sonarsource.sonarqube%3Asonarqube.json?server=https://sonarcloud.io'
)
.get('/violations/brave_brave-core.json?server=https://sonarcloud.io')
.expectBadge({
label: 'violations',
message: isMetric,
@@ -27,9 +25,7 @@ t.create('Violations')
t.create('Violations (branch)')
.timeout(10000)
.get(
'/violations/org.sonarsource.sonarqube%3Asonarqube/master.json?server=https://sonarcloud.io'
)
.get('/violations/brave_brave-core/master.json?server=https://sonarcloud.io')
.expectBadge({
label: 'violations',
message: isMetric,
@@ -67,7 +63,7 @@ t.create('Violations (legacy API supported)')
t.create('Violations Long Format')
.timeout(10000)
.get(
'/violations/org.sonarsource.sonarqube%3Asonarqube.json?server=https://sonarcloud.io&format=long'
'/violations/brave_brave-core.json?server=https://sonarcloud.io&format=long'
)
.expectBadge({
label: 'violations',
@@ -126,9 +122,7 @@ t.create('Violations Long Format (legacy API supported)')
t.create('Blocker Violations')
.timeout(10000)
.get(
'/blocker_violations/org.sonarsource.sonarqube%3Asonarqube.json?server=https://sonarcloud.io'
)
.get('/blocker_violations/brave_brave-core.json?server=https://sonarcloud.io')
.expectBadge({
label: 'blocker violations',
message: isMetric,
@@ -166,7 +160,7 @@ t.create('Blocker Violations (legacy API supported)')
t.create('Critical Violations')
.timeout(10000)
.get(
'/critical_violations/org.sonarsource.sonarqube%3Asonarqube.json?server=https://sonarcloud.io'
'/critical_violations/brave_brave-core.json?server=https://sonarcloud.io'
)
.expectBadge({
label: 'critical violations',

View File

@@ -1,12 +1,10 @@
import { expect } from 'chai'
import Camp from '@shields_io/camp'
import portfinder from 'portfinder'
import config from 'config'
import got from '../core/got-test-client.js'
import { ExpressTestHarness } from '../core/express-test-harness.js'
import { setRoutes } from './suggest.js'
import GithubApiProvider from './github/github-api-provider.js'
describe('Badge suggestions for', function () {
describe('Badge suggestions', function () {
const githubApiBaseUrl = process.env.GITHUB_URL || 'https://api.github.com'
let token, apiProvider
@@ -22,38 +20,27 @@ describe('Badge suggestions for', function () {
})
})
let port, baseUrl
before(async function () {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
})
let camp
before(async function () {
camp = Camp.start({ port, hostname: '::' })
await new Promise(resolve => camp.on('listening', () => resolve()))
})
after(async function () {
if (camp) {
await new Promise(resolve => camp.close(resolve))
camp = undefined
}
})
const origin = 'https://example.test'
before(function () {
setRoutes([origin], apiProvider, camp)
let harness
before(async function () {
harness = new ExpressTestHarness()
setRoutes([origin], apiProvider, harness.app)
await harness.start()
})
after(async function () {
await harness.stop()
})
describe('GitHub', function () {
context('with an existing project', function () {
it('returns the expected suggestions', async function () {
const { statusCode, body } = await got(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(
const { statusCode, body } = await harness.get(
`/$suggest/v1?url=${encodeURIComponent(
'https://github.com/atom/atom'
)}`,
{
responseType: 'json',
}
{ responseType: 'json' }
)
expect(statusCode).to.equal(200)
expect(body).to.deep.equal({
@@ -117,13 +104,11 @@ describe('Badge suggestions for', function () {
it('returns the expected suggestions', async function () {
this.timeout(5000)
const { statusCode, body } = await got(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(
const { statusCode, body } = await harness.get(
`/$suggest/v1?url=${encodeURIComponent(
'https://github.com/badges/not-a-real-project'
)}`,
{
responseType: 'json',
}
{ responseType: 'json' }
)
expect(statusCode).to.equal(200)
expect(body).to.deep.equal({
@@ -187,13 +172,11 @@ describe('Badge suggestions for', function () {
describe('GitLab', function () {
context('with an existing project', function () {
it('returns the expected suggestions', async function () {
const { statusCode, body } = await got(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(
const { statusCode, body } = await harness.get(
`/$suggest/v1?url=${encodeURIComponent(
'https://gitlab.com/gitlab-org/gitlab'
)}`,
{
responseType: 'json',
}
{ responseType: 'json' }
)
expect(statusCode).to.equal(200)
expect(body).to.deep.equal({
@@ -228,8 +211,8 @@ describe('Badge suggestions for', function () {
context('with an nonexisting project', function () {
it('returns the expected suggestions', async function () {
const { statusCode, body } = await got(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(
const { statusCode, body } = await harness.get(
`/$suggest/v1?url=${encodeURIComponent(
'https://gitlab.com/gitlab-org/not-gitlab'
)}`,
{

View File

@@ -146,8 +146,8 @@ async function findSuggestions(githubApiProvider, url) {
// - link: target as a string URL
// - preview: object (optional)
// - style: string
function setRoutes(allowedOrigin, githubApiProvider, server) {
server.ajax.on('suggest/v1', (data, end, ask) => {
function setRoutes(allowedOrigin, githubApiProvider, app) {
app.get('/[$]suggest/v1', (req, res) => {
// The typical dev and production setups are cross-origin. However, in
// Heroku deploys and some self-hosted deploys these requests may come from
// the same host. Chrome does not send an Origin header on same-origin
@@ -155,23 +155,25 @@ function setRoutes(allowedOrigin, githubApiProvider, server) {
//
// It would be better to solve this problem using some well-tested
// middleware.
const origin = ask.req.headers.origin
const origin = req.headers.origin
if (origin) {
let host
try {
host = new URL(origin).hostname
} catch (e) {
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
end({ err: 'Disallowed' })
res.setHeader('Access-Control-Allow-Origin', 'null')
res.json({ err: 'Disallowed' })
res.end()
return
}
if (host !== ask.req.headers.host) {
if (host !== req.headers.host) {
if (allowedOrigin.includes(origin)) {
ask.res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Access-Control-Allow-Origin', origin)
} else {
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
end({ err: 'Disallowed' })
res.setHeader('Access-Control-Allow-Origin', 'null')
res.json({ err: 'Disallowed' })
res.end()
return
}
}
@@ -179,9 +181,10 @@ function setRoutes(allowedOrigin, githubApiProvider, server) {
let url
try {
url = new URL(data.url)
url = new URL(req.query.url)
} catch (e) {
end({ err: `${e}` })
res.json({ err: `${e}` })
res.end()
return
}
@@ -189,11 +192,13 @@ function setRoutes(allowedOrigin, githubApiProvider, server) {
// This interacts with callback code and can't use async/await.
// eslint-disable-next-line promise/prefer-await-to-then
.then(suggestions => {
end({ suggestions })
res.json({ suggestions })
res.end()
})
// eslint-disable-next-line promise/prefer-await-to-then
.catch(err => {
end({ suggestions: [], err })
res.json({ suggestions: [], err })
res.end()
})
})
}

View File

@@ -1,8 +1,6 @@
import Camp from '@shields_io/camp'
import { expect } from 'chai'
import nock from 'nock'
import portfinder from 'portfinder'
import got from '../core/got-test-client.js'
import { ExpressTestHarness } from '../core/express-test-harness.js'
import { setRoutes, githubLicense } from './suggest.js'
import GithubApiProvider from './github/github-api-provider.js'
@@ -67,28 +65,20 @@ describe('Badge suggestions', function () {
})
})
describe('Scoutcamp integration', function () {
let port, baseUrl
before(async function () {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
})
let camp
before(async function () {
camp = Camp.start({ port, hostname: '::' })
await new Promise(resolve => camp.on('listening', () => resolve()))
})
after(async function () {
if (camp) {
await new Promise(resolve => camp.close(resolve))
camp = undefined
}
describe('Express integration', function () {
let harness
beforeEach(async function () {
harness = new ExpressTestHarness()
await harness.start()
})
const origin = 'https://example.test'
before(function () {
setRoutes([origin], apiProvider, camp)
beforeEach(function () {
setRoutes([origin], apiProvider, harness.app)
})
afterEach(async function () {
await harness.stop()
})
context('without an origin header', function () {
@@ -106,13 +96,11 @@ describe('Badge suggestions', function () {
},
})
const { statusCode, body } = await got(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(
const { statusCode, body } = await harness.get(
`/$suggest/v1?url=${encodeURIComponent(
'https://github.com/atom/atom'
)}`,
{
responseType: 'json',
}
{ responseType: 'json' }
)
expect(statusCode).to.equal(200)
expect(body).to.deep.equal({

View File

@@ -69,7 +69,7 @@ export default class SwaggerValidatorService extends BaseJsonService {
} else if (valMessages.length === 1) {
const { message, level } = valMessages[0]
if (level === 'error' && message === `Can't read from file ${specUrl}`) {
throw new NotFound({ prettyMessage: 'spec not found or unreadable ' })
throw new NotFound({ prettyMessage: 'spec not found or unreadable' })
}
}
if (valMessages.every(msg => msg.level === 'warning')) {

View File

@@ -0,0 +1,23 @@
import { expect } from 'chai'
import { versionColorForWordpressVersion } from './wordpress-version-color.js'
describe('versionColorForWordpressVersion()', function () {
it('generates correct colours for given versions', async function () {
this.timeout(5e3)
expect(await versionColorForWordpressVersion('11.2.0')).to.equal(
'brightgreen'
)
expect(await versionColorForWordpressVersion('11.2')).to.equal(
'brightgreen'
)
expect(await versionColorForWordpressVersion('3.2.0')).to.equal('yellow')
expect(await versionColorForWordpressVersion('3.2')).to.equal('yellow')
expect(await versionColorForWordpressVersion('4.7-beta.3')).to.equal(
'yellow'
)
expect(await versionColorForWordpressVersion('cheese')).to.equal(
'lightgrey'
)
})
})

View File

@@ -1,8 +1,5 @@
import { expect } from 'chai'
import {
toSemver,
versionColorForWordpressVersion,
} from './wordpress-version-color.js'
import { toSemver } from './wordpress-version-color.js'
describe('toSemver() function', function () {
it('coerces versions', function () {
@@ -13,24 +10,3 @@ describe('toSemver() function', function () {
expect(toSemver('foobar')).to.equal('foobar')
})
})
describe('versionColorForWordpressVersion()', function () {
it('generates correct colours for given versions', async function () {
this.timeout(5e3)
expect(await versionColorForWordpressVersion('11.2.0')).to.equal(
'brightgreen'
)
expect(await versionColorForWordpressVersion('11.2')).to.equal(
'brightgreen'
)
expect(await versionColorForWordpressVersion('3.2.0')).to.equal('yellow')
expect(await versionColorForWordpressVersion('3.2')).to.equal('yellow')
expect(await versionColorForWordpressVersion('4.7-beta.3')).to.equal(
'yellow'
)
expect(await versionColorForWordpressVersion('cheese')).to.equal(
'lightgrey'
)
})
})