Compare commits
104 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d4a8f87ce | ||
|
|
4346e37421 | ||
|
|
0ca8c95874 | ||
|
|
5aa0fdb821 | ||
|
|
49636311ba | ||
|
|
d0af75f721 | ||
|
|
1d3af05758 | ||
|
|
ac1cfd9fc9 | ||
|
|
1f87e181ce | ||
|
|
5ba86df981 | ||
|
|
7734784de3 | ||
|
|
7254e4a840 | ||
|
|
c86c9cd484 | ||
|
|
62ae049fa8 | ||
|
|
b3ac7e115b | ||
|
|
be20741e9a | ||
|
|
b739d57ee7 | ||
|
|
92d1d7dc21 | ||
|
|
1610226eb4 | ||
|
|
0457d078f8 | ||
|
|
838f284d04 | ||
|
|
7a5da5781c | ||
|
|
3ca22147c0 | ||
|
|
2e9812d0aa | ||
|
|
b1d765665b | ||
|
|
e3d6e38a26 | ||
|
|
1d5c9a109e | ||
|
|
d2b2c31962 | ||
|
|
e7488b4329 | ||
|
|
903c62ab9f | ||
|
|
5518521b73 | ||
|
|
692de451e3 | ||
|
|
1809d1ebbc | ||
|
|
9bc673f40b | ||
|
|
6b78b69fa5 | ||
|
|
4a38cfbdc7 | ||
|
|
d88ff2ad2a | ||
|
|
083caa1c83 | ||
|
|
b39afc1466 | ||
|
|
0987242032 | ||
|
|
1d4da9e1f8 | ||
|
|
3ad5900136 | ||
|
|
67fe4fa095 | ||
|
|
5273989a1e | ||
|
|
80d4fafd8e | ||
|
|
8b9bb470f5 | ||
|
|
fb7104b979 | ||
|
|
57eefd230f | ||
|
|
15deac06e8 | ||
|
|
94909ab29d | ||
|
|
4d9de9f760 | ||
|
|
3829d9ba27 | ||
|
|
d51b47a9fd | ||
|
|
2c32e02bd0 | ||
|
|
d8101f8c03 | ||
|
|
2e6fb0eabc | ||
|
|
d4e04e5442 | ||
|
|
8ba081a264 | ||
|
|
574206e8fd | ||
|
|
bdfdcd9c48 | ||
|
|
6c2ae7ce3a | ||
|
|
ff9a19268e | ||
|
|
223cb06ed6 | ||
|
|
0f1c6b0bc9 | ||
|
|
007a7e6c47 | ||
|
|
152b8e9a64 | ||
|
|
954147f7d9 | ||
|
|
49bcb52173 | ||
|
|
2c089f7ba6 | ||
|
|
4a75cf09a7 | ||
|
|
71f553e438 | ||
|
|
d0a64291ea | ||
|
|
082ad94a2b | ||
|
|
5698c29bc8 | ||
|
|
46e0c0952b | ||
|
|
ffe8d88bfd | ||
|
|
b6621e52e6 | ||
|
|
c7970fc310 | ||
|
|
cc5ca5873e | ||
|
|
4994199ddf | ||
|
|
0a8d844bb4 | ||
|
|
b93bbe322f | ||
|
|
d77e60a0f6 | ||
|
|
8dbfd58972 | ||
|
|
4ba06c0ec7 | ||
|
|
8cd1480e5f | ||
|
|
fb16e56c3f | ||
|
|
0d436c92d8 | ||
|
|
6d320eff48 | ||
|
|
c9e799ac8c | ||
|
|
1664b88ee6 | ||
|
|
973dac7f28 | ||
|
|
2127976c0f | ||
|
|
41d072e1c9 | ||
|
|
1b00489f57 | ||
|
|
4f49d545fb | ||
|
|
68d8287b62 | ||
|
|
644fb5b7b5 | ||
|
|
e713989884 | ||
|
|
2c653013c8 | ||
|
|
18e17233c4 | ||
|
|
e499030e28 | ||
|
|
de1382d44d | ||
|
|
3daef194fa |
2
.github/ISSUE_TEMPLATE/1_Bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/1_Bug_report.yml
vendored
@@ -41,4 +41,4 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## :heart: Love Shields?
|
||||
Please consider donating $10 to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)
|
||||
Please consider donating to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)
|
||||
|
||||
@@ -28,5 +28,5 @@ labels: 'keep-service-tests-green'
|
||||
|
||||
<!--- Optional: only if you have suggestions on a fix/reason for the bug -->
|
||||
|
||||
<!-- Love Shields? Please consider donating $10 to sustain our activities:
|
||||
<!-- Love Shields? Please consider donating to sustain our activities:
|
||||
👉 https://opencollective.com/shields -->
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
2
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
@@ -59,4 +59,4 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## :heart: Love Shields?
|
||||
Please consider donating $10 to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)
|
||||
Please consider donating to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/4_Feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/4_Feature_request.md
vendored
@@ -7,5 +7,5 @@ about: Ideas for other new features or improvements
|
||||
|
||||
<!-- A clear and concise description of the new feature. -->
|
||||
|
||||
<!-- Love Shields? Please consider donating $10 to sustain our activities:
|
||||
<!-- Love Shields? Please consider donating to sustain our activities:
|
||||
👉 https://opencollective.com/shields -->
|
||||
|
||||
122
.github/actions/docusaurus-swizzled-warning/package-lock.json
generated
vendored
122
.github/actions/docusaurus-swizzled-warning/package-lock.json
generated
vendored
@@ -89,18 +89,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.1.tgz",
|
||||
"integrity": "sha512-hRlOKAovtINHQPYHZlfyFwaM8OyetxeoC81lAkBy34uLb8exrZB50SQdeW3EROqiY9G9yxQTpp5OHTV54QD+vA==",
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
|
||||
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
|
||||
"version": "23.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
|
||||
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
|
||||
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^23.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz",
|
||||
@@ -115,22 +130,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.0.0.tgz",
|
||||
"integrity": "sha512-PclQ6JGMTE9iUStpzMkwLCISFn/wDeRjkZFIKALpvJQNBGwDoYYi2fFvuHwssoQ1rXI5mfh6jgTgWuddeUzfWw=="
|
||||
"version": "20.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
|
||||
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.0.0.tgz",
|
||||
"integrity": "sha512-oIJzCpttmBTlEhBmRvb+b9rlnGpmFgDtZ0bB6nq39qIod6A5DP+7RkVLMOixIgRCYSHDTeayWqmiJ2SZ6xgfdw==",
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz",
|
||||
"integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.0.0"
|
||||
"@octokit/types": "^12.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
"@octokit/core": "5"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
@@ -148,14 +165,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz",
|
||||
"integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==",
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
|
||||
"integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^9.0.0",
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"@octokit/endpoint": "^9.0.6",
|
||||
"@octokit/request-error": "^5.1.1",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -163,11 +180,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz",
|
||||
"integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
|
||||
"integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
@@ -175,12 +193,43 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.0.0.tgz",
|
||||
"integrity": "sha512-EzD434aHTFifGudYAygnFlS1Tl6KhbTynEWELQXIbTY8Msvb5nEqTZIm7sbPEt4mQYLZwu3zPKVdeIrw0g7ovg==",
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
|
||||
"version": "23.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
|
||||
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
|
||||
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^19.0.0"
|
||||
"@octokit/openapi-types": "^23.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
|
||||
"version": "23.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
|
||||
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/types": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
|
||||
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^23.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "12.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
|
||||
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
@@ -193,14 +242,6 @@
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -218,9 +259,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.28.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
|
||||
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
|
||||
"version": "5.28.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
|
||||
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
|
||||
2
.github/actions/service-tests/action.yml
vendored
2
.github/actions/service-tests/action.yml
vendored
@@ -67,6 +67,8 @@ runs:
|
||||
OBS_USER: '${{ inputs.obs-user }}'
|
||||
OBS_PASS: '${{ inputs.obs-pass }}'
|
||||
PEPY_KEY: '${{ inputs.pepy-key }}'
|
||||
REDDIT_CLIENT_ID: '${{ inputs.reddit-client-id }}'
|
||||
REDDIT_CLIENT_SECRET: '${{ inputs.reddit-client-secret }}'
|
||||
SL_INSIGHT_USER_UUID: '${{ inputs.sl-insight-user-uuid }}'
|
||||
SL_INSIGHT_API_TOKEN: '${{ inputs.sl-insight-api-token }}'
|
||||
TWITCH_CLIENT_ID: '${{ inputs.twitch-client-id }}'
|
||||
|
||||
@@ -60,6 +60,8 @@ jobs:
|
||||
OBS_USER: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
OBS_PASS: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
PEPY_KEY: '${{ secrets.SERVICETESTS_PEPY_KEY }}'
|
||||
REDDIT_CLIENT_ID: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}'
|
||||
REDDIT_CLIENT_SECRET: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}'
|
||||
SL_INSIGHT_USER_UUID: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
SL_INSIGHT_API_TOKEN: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
TWITCH_CLIENT_ID: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
|
||||
2
.github/workflows/daily-tests.yml
vendored
2
.github/workflows/daily-tests.yml
vendored
@@ -57,6 +57,8 @@ jobs:
|
||||
OBS_USER: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
OBS_PASS: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
PEPY_KEY: '${{ secrets.SERVICETESTS_PEPY_KEY }}'
|
||||
REDDIT_CLIENT_ID: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}'
|
||||
REDDIT_CLIENT_SECRET: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}'
|
||||
SL_INSIGHT_USER_UUID: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
SL_INSIGHT_API_TOKEN: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
TWITCH_CLIENT_ID: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
|
||||
2
.github/workflows/deploy-review-app.yml
vendored
2
.github/workflows/deploy-review-app.yml
vendored
@@ -36,6 +36,8 @@ jobs:
|
||||
OBS_USER=${{ secrets.SERVICETESTS_OBS_USER }}
|
||||
OBS_PASS=${{ secrets.SERVICETESTS_OBS_PASS }}
|
||||
PEPY_KEY=${{ secrets.SERVICETESTS_PEPY_KEY }}
|
||||
REDDIT_CLIENT_ID=${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}
|
||||
REDDIT_CLIENT_SECRET=${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}
|
||||
SL_INSIGHT_API_TOKEN=${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}
|
||||
SL_INSIGHT_USER_UUID=${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}
|
||||
TWITCH_CLIENT_ID=${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}
|
||||
|
||||
2
.github/workflows/test-package-lib.yml
vendored
2
.github/workflows/test-package-lib.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
npm: '^10'
|
||||
engine-strict: 'true'
|
||||
- node: '22'
|
||||
npm: '^10'
|
||||
npm: '^11'
|
||||
engine-strict: 'false'
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
2
.github/workflows/test-services-22.yml
vendored
2
.github/workflows/test-services-22.yml
vendored
@@ -30,6 +30,8 @@ jobs:
|
||||
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
pepy-key: '${{ secrets.SERVICETESTS_PEPY_KEY }}'
|
||||
reddit-client-id: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}'
|
||||
reddit-client-secret: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}'
|
||||
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
|
||||
2
.github/workflows/test-services.yml
vendored
2
.github/workflows/test-services.yml
vendored
@@ -28,6 +28,8 @@ jobs:
|
||||
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
pepy-key: '${{ secrets.SERVICETESTS_PEPY_KEY }}'
|
||||
reddit-client-id: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}'
|
||||
reddit-client-secret: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}'
|
||||
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -4,6 +4,26 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2025-03-01
|
||||
|
||||
- time out long running requests more aggressively [#10833](https://github.com/badges/shields/issues/10833)
|
||||
- Dependency updates
|
||||
|
||||
## server-2025-02-02
|
||||
|
||||
- Mark Stubs-only packages with [PypiTypes] badge [#10864](https://github.com/badges/shields/issues/10864)
|
||||
- fix badge style when logo only [#10794](https://github.com/badges/shields/issues/10794)
|
||||
- pass matching mime type to xmldom; test [dynamicxml] [#10830](https://github.com/badges/shields/issues/10830)
|
||||
- allow [chromewebstore] size to contain decimal point [#10812](https://github.com/badges/shields/issues/10812)
|
||||
- Add auth support to [Reddit] badges [#10790](https://github.com/badges/shields/issues/10790)
|
||||
- Fixed mixed up Code climate endpoints [#10813](https://github.com/badges/shields/issues/10813)
|
||||
- feat: add terraform registry providers and modules downloads [#10793](https://github.com/badges/shields/issues/10793)
|
||||
- Renew [Mastodon] docs and improve parameter handling [#10789](https://github.com/badges/shields/issues/10789)
|
||||
- Support [Matrix] summary endpoint [#10782](https://github.com/badges/shields/issues/10782)
|
||||
- use metric() in [coderabbit] badge [#10779](https://github.com/badges/shields/issues/10779)
|
||||
- cache matrix badges for 4 hours [#10778](https://github.com/badges/shields/issues/10778)
|
||||
- Dependency updates
|
||||
|
||||
## server-2025-01-01
|
||||
|
||||
- Add [PypiTypes] badge [#10774](https://github.com/badges/shields/issues/10774)
|
||||
|
||||
@@ -8,10 +8,7 @@ financial contributions, issues, and pull requests!
|
||||
### Financial contributions
|
||||
|
||||
We welcome financial contributions in full transparency on our
|
||||
[open collective](https://opencollective.com/shields). Anyone can file an
|
||||
expense. If the expense makes sense for the development of the community, it
|
||||
will be "merged" into the ledger of our open collective by the core
|
||||
contributors and the person who filed the expense will be reimbursed.
|
||||
[open collective](https://opencollective.com/shields).
|
||||
|
||||
### Contributing code
|
||||
|
||||
@@ -90,7 +87,7 @@ encourage you to contribute logos there. Please review their
|
||||
|
||||
Feel free to star the repository. This will help increase the visibility of the project, therefore attracting more users and contributors to Shields!
|
||||
|
||||
We're also asking for [one-time \$10 donations](https://opencollective.com/shields) from developers who use and love Shields, please spread the word!
|
||||
We're also asking for [donations](https://opencollective.com/shields) from developers who use and love Shields, please spread the word!
|
||||
|
||||
## Getting help
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ COPY package.json package-lock.json /usr/src/app/
|
||||
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
|
||||
COPY badge-maker /usr/src/app/badge-maker/
|
||||
|
||||
RUN npm install -g "npm@^9.0.0"
|
||||
RUN npm install -g "npm@^10"
|
||||
# We need dev deps to build the front end. We don't need Cypress, though.
|
||||
RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ This repo hosts:
|
||||
- amount of [Liberapay](https://liberapay.com/) donations per week: 
|
||||
- Python package downloads: 
|
||||
- Chrome Web Store extension rating: 
|
||||
- [Uptime Robot](https://uptimerobot.com) percentage: 
|
||||
- Uptime Robot uptime percentage: 
|
||||
|
||||
[Make your own badges!][custom badges]
|
||||
(Quick example: `https://img.shields.io/badge/left-right-f39f37`)
|
||||
|
||||
@@ -2161,75 +2161,6 @@ exports['The badge generator "social" template badge generation should match sna
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge badge with logo 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="113"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label="label: message"
|
||||
>
|
||||
<title>label: message</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="113" height="20" rx="3" fill="#fff" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="54" height="20" fill="#555" />
|
||||
<rect x="54" width="59" height="20" fill="#4c1" />
|
||||
<rect width="113" height="20" fill="url(#s)" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="365"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="270"
|
||||
>
|
||||
label
|
||||
</text>
|
||||
<text x="365" y="140" transform="scale(.1)" fill="#fff" textLength="270">
|
||||
label
|
||||
</text>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="825"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="490"
|
||||
>
|
||||
message
|
||||
</text>
|
||||
<text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="490">
|
||||
message
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message with custom suffix 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -2551,3 +2482,304 @@ exports['The badge generator "social" template badge generation should match sna
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge default badge with logo 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="113"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label="label: message"
|
||||
>
|
||||
<title>label: message</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="113" height="20" rx="3" fill="#fff" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="54" height="20" fill="#555" />
|
||||
<rect x="54" width="59" height="20" fill="#4c1" />
|
||||
<rect width="113" height="20" fill="url(#s)" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="365"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="270"
|
||||
>
|
||||
label
|
||||
</text>
|
||||
<text x="365" y="140" transform="scale(.1)" fill="#fff" textLength="270">
|
||||
label
|
||||
</text>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="825"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="490"
|
||||
>
|
||||
message
|
||||
</text>
|
||||
<text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="490">
|
||||
message
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logo-only should always produce the same badge flat badge, logo-only 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="25"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label=""
|
||||
>
|
||||
<title></title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<clipPath id="r"><rect width="25" height="20" rx="3" fill="#fff" /></clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="0" height="20" fill="#555" />
|
||||
<rect x="0" width="25" height="20" fill="#4c1" />
|
||||
<rect width="25" height="20" fill="url(#s)" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logo-only should always produce the same badge flat-square badge, logo-only 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="25"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label=""
|
||||
>
|
||||
<title></title>
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="0" height="20" fill="#555" />
|
||||
<rect x="0" width="25" height="20" fill="#4c1" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logo-only should always produce the same badge social badge, logo-only 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="26"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label=""
|
||||
>
|
||||
<title></title>
|
||||
<style>
|
||||
a:hover #llink {
|
||||
fill: url(#b);
|
||||
stroke: #ccc;
|
||||
}
|
||||
a:hover #rlink {
|
||||
fill: #4183c4;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="a" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fcfcfc" stop-opacity="0" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#ccc" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<g stroke="#d5d5d5">
|
||||
<rect
|
||||
stroke="none"
|
||||
fill="#fcfcfc"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="25"
|
||||
height="19"
|
||||
rx="2"
|
||||
/>
|
||||
</g>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<g
|
||||
aria-hidden="true"
|
||||
fill="#333"
|
||||
text-anchor="middle"
|
||||
font-family="Helvetica Neue,Helvetica,Arial,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-weight="700"
|
||||
font-size="110px"
|
||||
line-height="14px"
|
||||
>
|
||||
<rect
|
||||
id="llink"
|
||||
stroke="#d5d5d5"
|
||||
fill="url(#a)"
|
||||
x=".5"
|
||||
y=".5"
|
||||
width="25"
|
||||
height="19"
|
||||
rx="2"
|
||||
/>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="195"
|
||||
y="150"
|
||||
fill="#fff"
|
||||
transform="scale(.1)"
|
||||
textLength="10"
|
||||
></text>
|
||||
<text x="195" y="140" transform="scale(.1)" textLength="10"></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logo-only should always produce the same badge plastic badge, logo-only 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="25"
|
||||
height="18"
|
||||
role="img"
|
||||
aria-label=""
|
||||
>
|
||||
<title></title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".7" />
|
||||
<stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
|
||||
<stop offset=".9" stop-color="#000" stop-opacity=".3" />
|
||||
<stop offset="1" stop-color="#000" stop-opacity=".5" />
|
||||
</linearGradient>
|
||||
<clipPath id="r"><rect width="25" height="18" rx="4" fill="#fff" /></clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="0" height="18" fill="#555" />
|
||||
<rect x="0" width="25" height="18" fill="#4c1" />
|
||||
<rect width="25" height="18" fill="url(#s)" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="2"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logo-only should always produce the same badge for-the-badge badge, logo-only 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="35"
|
||||
height="28"
|
||||
role="img"
|
||||
aria-label=""
|
||||
>
|
||||
<title></title>
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="35" height="28" fill="#4c1" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="100"
|
||||
>
|
||||
<image
|
||||
x="9"
|
||||
y="7"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text
|
||||
transform="scale(.1)"
|
||||
x="230"
|
||||
y="175"
|
||||
textLength="0"
|
||||
fill="#fff"
|
||||
font-weight="bold"
|
||||
></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
@@ -67,7 +67,7 @@ The format is the following:
|
||||
message: 'passed', // (Required) Badge message
|
||||
labelColor: '#555', // (Optional) Label color
|
||||
color: '#4c1', // (Optional) Message color
|
||||
logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iI2IxY2U1NiIvPjxwYXRoIGQ9Ik04IDBoMjR2NjRIOGMtNC40MzIgMC04LTMuNTY4LTgtOFY4YzAtNC40MzIgMy41NjgtOCA4LTh6IiBmaWxsPSIjNWQ1ZDVkIi8+PC9zdmc+' // (Optional) Any custom logo can be passed in a URL parameter by base64 encoding
|
||||
logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iI2IxY2U1NiIvPjxwYXRoIGQ9Ik04IDBoMjR2NjRIOGMtNC40MzIgMC04LTMuNTY4LTgtOFY4YzAtNC40MzIgMy41NjgtOCA4LTh6IiBmaWxsPSIjNWQ1ZDVkIi8+PC9zdmc+', // (Optional) Any custom logo can be passed in a URL parameter by base64 encoding
|
||||
links: ['https://example.com', 'https://example.com'], // (Optional) Links array of maximum two links
|
||||
|
||||
// (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social'
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { spawn } from 'child-process-promise'
|
||||
import { expect, use } from 'chai'
|
||||
use(require('sinon-chai'))
|
||||
import sinonChai from 'sinon-chai'
|
||||
use(sinonChai)
|
||||
|
||||
const dirName = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function runCli(args) {
|
||||
return spawn('node', [path.join(__dirname, 'badge-cli.js'), ...args], {
|
||||
return spawn('node', [path.join(dirName, 'badge-cli.js'), ...args], {
|
||||
capture: ['stdout'],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class Badge {
|
||||
}
|
||||
let rightWidth = messageWidth + 2 * horizPadding
|
||||
if (hasLogo && !hasLabel) {
|
||||
rightWidth += totalLogoWidth + horizPadding - 1
|
||||
rightWidth += totalLogoWidth + (message.length ? horizPadding - 1 : 0)
|
||||
}
|
||||
|
||||
const width = leftWidth + rightWidth
|
||||
@@ -804,11 +804,13 @@ function forTheBadge({
|
||||
// there is no label. When `needsLabelRect` is true, render a label rect and a
|
||||
// message rect; when false, only a message rect.
|
||||
const hasLabel = Boolean(label.length)
|
||||
const noText = !hasLabel && !message
|
||||
const needsLabelRect = hasLabel || (logo && labelColor)
|
||||
const gutter = noText ? LOGO_TEXT_GUTTER - LOGO_MARGIN : LOGO_TEXT_GUTTER
|
||||
let logoMinX, labelTextMinX
|
||||
if (logo) {
|
||||
logoMinX = LOGO_MARGIN
|
||||
labelTextMinX = logoMinX + logoWidth + LOGO_TEXT_GUTTER
|
||||
labelTextMinX = logoMinX + logoWidth + gutter
|
||||
} else {
|
||||
labelTextMinX = TEXT_MARGIN
|
||||
}
|
||||
@@ -823,9 +825,8 @@ function forTheBadge({
|
||||
messageRectWidth = 2 * TEXT_MARGIN + messageTextWidth
|
||||
} else {
|
||||
if (logo) {
|
||||
messageTextMinX = TEXT_MARGIN + logoWidth + LOGO_TEXT_GUTTER
|
||||
messageRectWidth =
|
||||
2 * TEXT_MARGIN + logoWidth + LOGO_TEXT_GUTTER + messageTextWidth
|
||||
messageTextMinX = TEXT_MARGIN + logoWidth + gutter
|
||||
messageRectWidth = 2 * TEXT_MARGIN + logoWidth + gutter + messageTextWidth
|
||||
} else {
|
||||
messageTextMinX = TEXT_MARGIN
|
||||
messageRectWidth = 2 * TEXT_MARGIN + messageTextWidth
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { makeBadge, ValidationError } from '.'
|
||||
import { makeBadge, ValidationError } from './index.js'
|
||||
|
||||
describe('makeBadge function', function () {
|
||||
it('should produce badge with valid input', async function () {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { test, given, forCases } from 'sazerac'
|
||||
import { expect } from 'chai'
|
||||
import snapshot from 'snap-shot-it'
|
||||
import prettier from 'prettier'
|
||||
import makeBadge from './make-badge'
|
||||
import makeBadge from './make-badge.js'
|
||||
|
||||
async function expectBadgeToMatchSnapshot(format) {
|
||||
snapshot(await prettier.format(makeBadge(format), { parser: 'html' }))
|
||||
@@ -700,7 +700,7 @@ describe('The badge generator', function () {
|
||||
})
|
||||
|
||||
describe('badges with logos should always produce the same badge', function () {
|
||||
it('badge with logo', async function () {
|
||||
it('default badge with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'label',
|
||||
message: 'message',
|
||||
@@ -709,4 +709,56 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('badges with logo-only should always produce the same badge', function () {
|
||||
it('flat badge, logo-only', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: '',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
style: 'flat',
|
||||
})
|
||||
})
|
||||
|
||||
it('flat-square badge, logo-only', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: '',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
style: 'flat-square',
|
||||
})
|
||||
})
|
||||
|
||||
it('for-the-badge badge, logo-only', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: '',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
})
|
||||
|
||||
it('social badge, logo-only', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: '',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
style: 'social',
|
||||
})
|
||||
})
|
||||
|
||||
it('plastic badge, logo-only', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: '',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
style: 'plastic',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,6 +105,8 @@ private:
|
||||
opencollective_token: 'OPENCOLLECTIVE_TOKEN'
|
||||
pepy_key: 'PEPY_KEY'
|
||||
postgres_url: 'POSTGRES_URL'
|
||||
reddit_client_id: 'REDDIT_CLIENT_ID'
|
||||
reddit_client_secret: 'REDDIT_CLIENT_SECRET'
|
||||
sentry_dsn: 'SENTRY_DSN'
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
|
||||
@@ -5,6 +5,8 @@ private:
|
||||
gh_client_id: ...
|
||||
gh_client_secret: ...
|
||||
gitlab_token: ...
|
||||
reddit_client_id: ...
|
||||
reddit_client_secret: ...
|
||||
sentry_dsn: ...
|
||||
shields_secret: ...
|
||||
sl_insight_userUuid: ...
|
||||
|
||||
@@ -9,6 +9,8 @@ private:
|
||||
gitlab_token: '...'
|
||||
obs_user: '...'
|
||||
obs_pass: '...'
|
||||
reddit_client_id: '...'
|
||||
reddit_client_secret: '...'
|
||||
twitch_client_id: '...'
|
||||
twitch_client_secret: '...'
|
||||
weblate_api_key: '...'
|
||||
|
||||
@@ -22,4 +22,4 @@ public:
|
||||
rasterUrl: 'https://raster.shields.io'
|
||||
userAgentBase: 'Shields.io'
|
||||
requireCloudflare: true
|
||||
requestTimeoutSeconds: 20
|
||||
requestTimeoutSeconds: 8
|
||||
|
||||
@@ -16,7 +16,12 @@ import { makeSend } from '../base-service/legacy-result-sender.js'
|
||||
import { handleRequest } from '../base-service/legacy-request-handler.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'
|
||||
import {
|
||||
fileSize,
|
||||
nonNegativeInteger,
|
||||
optionalUrl,
|
||||
url as requiredUrl,
|
||||
} from '../../services/validators.js'
|
||||
import log from './log.js'
|
||||
import PrometheusMetrics from './prometheus-metrics.js'
|
||||
import InfluxMetrics from './influx-metrics.js'
|
||||
@@ -54,8 +59,6 @@ const Joi = originalJoi
|
||||
},
|
||||
}))
|
||||
|
||||
const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
|
||||
const requiredUrl = optionalUrl.required()
|
||||
const origins = Joi.arrayFromString().items(Joi.string().origin())
|
||||
const defaultService = Joi.object({ authorizedOrigins: origins }).default({
|
||||
authorizedOrigins: [],
|
||||
@@ -194,6 +197,8 @@ const privateConfigSchema = Joi.object({
|
||||
opencollective_token: Joi.string(),
|
||||
pepy_key: Joi.string(),
|
||||
postgres_url: Joi.string().uri({ scheme: 'postgresql' }),
|
||||
reddit_client_id: Joi.string(),
|
||||
reddit_client_secret: Joi.string(),
|
||||
sentry_dsn: Joi.string(),
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
|
||||
@@ -10,4 +10,6 @@ export default defineConfig({
|
||||
baseUrl: 'http://localhost:3000',
|
||||
supportFile: false,
|
||||
},
|
||||
video: true,
|
||||
videoCompression: true,
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ Production hosting is managed by the Shields ops team:
|
||||
| Cloudflare (CDN) | Access management | @espadrine |
|
||||
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
||||
| Twitch | OAuth app | @PyvesB |
|
||||
| Reddit | OAuth app | @chris48s, @PyvesB |
|
||||
| Discord | OAuth app | @PyvesB |
|
||||
| YouTube | Account owner | @PyvesB |
|
||||
| GitLab | Account owner | @calebcartwright |
|
||||
@@ -32,7 +33,6 @@ Production hosting is managed by the Shields ops team:
|
||||
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
|
||||
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
||||
| Metrics server | Owner | @platan |
|
||||
| UptimeRobot | Account owner | @paulmelnikow |
|
||||
| More metrics | Owner | @RedSparr0w |
|
||||
|
||||
## Attached state
|
||||
@@ -119,19 +119,15 @@ The canonical and only recommended domain for badge URLs is `img.shields.io`. Cu
|
||||
## Monitoring
|
||||
|
||||
Overall server performance and requests by service are monitored using
|
||||
[Prometheus and Grafana][metrics].
|
||||
[Prometheus and Grafana][server metrics].
|
||||
|
||||
Request performance is monitored in two places:
|
||||
|
||||
- [Status][] (using [UptimeRobot][])
|
||||
- [Status][] (using NodePing)
|
||||
- [Server metrics][] using Prometheus and Grafana
|
||||
- [@RedSparr0w's monitor][monitor] which posts [notifications][] to a private
|
||||
[#monitor chat room][monitor discord]
|
||||
|
||||
[metrics]: https://metrics.shields.io/
|
||||
[status]: https://stats.uptimerobot.com/PjXogHB5p
|
||||
[status]: https://nodeping.com/reports/status/YBISBQB254
|
||||
[server metrics]: https://metrics.shields.io/
|
||||
[uptimerobot]: https://uptimerobot.com/
|
||||
[monitor]: https://shields.redsparr0w.com/1568/
|
||||
[notifications]: http://shields.redsparr0w.com/discord_notification
|
||||
[monitor discord]: https://discordapp.com/channels/308323056592486420/470700909182320646
|
||||
|
||||
@@ -290,6 +290,17 @@ Create an account, sign in and obtain generate a key on your
|
||||
`PYPI_URL` can be used to optionally send all the PyPI requests to a Self-hosted Pypi registry,
|
||||
users can also override this by query parameter `pypiBaseUrl`.
|
||||
|
||||
### Reddit
|
||||
|
||||
Using a token for Reddit is optional but will allow higher API rates.
|
||||
|
||||
- `REDDIT_CLIENT_ID` (yml: `private.reddit_client_id`)
|
||||
- `REDDIT_CLIENT_SECRET` (yml: `private.reddit_client_secret`)
|
||||
|
||||
Register to use the API using [this form](https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=14868593862164)
|
||||
and create an app in the [Reddit preferences page](https://www.reddit.com/prefs/apps)
|
||||
in order to obtain a client id and a client secret for making Reddit API calls.
|
||||
|
||||
### SymfonyInsight (formerly Sensiolabs)
|
||||
|
||||
- `SL_INSIGHT_USER_UUID` (yml: `private.sl_insight_userUuid`)
|
||||
|
||||
@@ -77,6 +77,7 @@ const config = {
|
||||
label: 'Documentation',
|
||||
position: 'left',
|
||||
},
|
||||
{ to: '/donate', label: 'Donate', position: 'left' },
|
||||
{ to: '/community', label: 'Community', position: 'left' },
|
||||
{ to: '/blog', label: 'Blog', position: 'left' },
|
||||
{
|
||||
@@ -114,8 +115,12 @@ const config = {
|
||||
title: 'Stats',
|
||||
items: [
|
||||
{
|
||||
label: 'Service Status',
|
||||
href: 'https://stats.uptimerobot.com/PjXogHB5p',
|
||||
label: 'Service Status (Upptime)',
|
||||
href: 'https://badges.github.io/uptime-monitoring/',
|
||||
},
|
||||
{
|
||||
label: 'Service Status (NodePing)',
|
||||
href: 'https://nodeping.com/reports/status/YBISBQB254',
|
||||
},
|
||||
{
|
||||
label: 'Metrics dashboard',
|
||||
|
||||
@@ -59,8 +59,6 @@ const FeatureList = [
|
||||
>
|
||||
docker image
|
||||
</a>
|
||||
<br />
|
||||
<code>docker pull shieldsio/shields</code>
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -68,15 +66,7 @@ const FeatureList = [
|
||||
title: 'Love Shields?',
|
||||
description: (
|
||||
<>
|
||||
Please consider{' '}
|
||||
<a
|
||||
href="https://opencollective.com/shields"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
donating
|
||||
</a>{' '}
|
||||
to sustain our activities
|
||||
Please consider <a href="/donate">donating</a> to sustain our activities
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -30,3 +30,13 @@ html[data-theme="dark"] .docusaurus-highlight-code-line {
|
||||
.opencollective-image {
|
||||
color-scheme: initial;
|
||||
}
|
||||
|
||||
.flex-column-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.align-bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,4 @@ Shields.io is possible thanks to the people and companies who donate money, serv
|
||||
<li>
|
||||
<a href="https://github.com/">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptimerobot.com/">Uptime Robot</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
68
frontend/src/pages/donate.md
Normal file
68
frontend/src/pages/donate.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Donate
|
||||
|
||||
You can donate to Shields.io via [OpenCollective](https://opencollective.com/shields).
|
||||
|
||||
## How the money is spent
|
||||
|
||||
Shields.io is a non-profit project run by unpaid volunteers. We use your donations to pay for our hosting costs.
|
||||
|
||||
Shields badges are everywhere. Shields badges appear on GitHub, NPM, PyPI, Ruby Gems, Rust Crates... If people build software there, shields badges are on it. Our userbase scales with the size of the software development community as a whole. This means we serve a lot of traffic. While the majority of image impressions are served from downstream proxies, we serve over 1.6 billion requests per month from our own infrastructure and transfer over 3Tb of outbound bandwidth each month.
|
||||
|
||||
Those are big numbers, and servers cost money. So does bandwidth. We cover our hosting costs with donations from the community.
|
||||
|
||||
## Donation tiers
|
||||
|
||||
While we accept donations of any size, we do have some suggested tiers.
|
||||
|
||||
<section>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col col--6">
|
||||
<div className="padding-horiz--md padding-vert--lg flex-column-container">
|
||||
<h3>Sponsor</h3>
|
||||
<p>Recommended for **companies**: With a monthly donation of $35, you can help to sustain our activities. Your company logo and a link to your website will feature at the top of our [community page](https://shields.io/community).</p>
|
||||
<p class="align-bottom"><a href="https://opencollective.com/shields/contribute/sponsor-2412/checkout" class="button button--primary button--medium">Become a Sponsor</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col col--6">
|
||||
<div className="padding-horiz--md padding-vert--lg flex-column-container">
|
||||
<h3>Monthly Backer</h3>
|
||||
<p>Recommended for **individuals**: With a monthly donation of $3, you can help to sustain our activities on an ongoing basis.</p>
|
||||
<p class="align-bottom"><a href="https://opencollective.com/shields/contribute/monthly-backer-2988/checkout" class="button button--primary button--medium">Become a Monthly Backer</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col col--6">
|
||||
<div className="padding-horiz--md padding-vert--lg flex-column-container">
|
||||
<h3>Backer</h3>
|
||||
<p>If you would prefer not to commit to a monthly donation, but you think shields.io has provided some value [over the last 10+ years](https://github.com/badges/shields/discussions/8867), consider making a one-time donation of $10.</p>
|
||||
<p class="align-bottom"><a href="https://opencollective.com/shields/contribute/backer-2411/checkout" class="button button--secondary button--medium">Become a Backer</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col col--6">
|
||||
<div className="padding-horiz--md padding-vert--lg flex-column-container">
|
||||
<h3>Something Else</h3>
|
||||
<p>Make a custom one-time or recurring donation of any amount.</p>
|
||||
<p class="align-bottom"><a href="https://opencollective.com/shields/donate" class="button button--secondary button--medium">Make a custom Donation</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
## FAQ
|
||||
|
||||
### Can I donate using another platform?
|
||||
|
||||
Currently we only accept donations via [OpenCollective](https://opencollective.com/shields). OpenCollective should be convenient for most users as it allows you to donate using credit card, bank transfer, or PayPal and is available in most countries.
|
||||
|
||||
### I donated as a sponsor. How do I change my company logo or URL?
|
||||
|
||||
We pull the logo and URL from your Open Collective profile. You can update these at any time from within Open Collective and those changes will be reflected on the community page within 24 hours.
|
||||
|
||||
### Can I see exactly how the money is being used?
|
||||
|
||||
Using OpenCollective means our finances are completely transparent. All transactions are publicly visible on https://opencollective.com/shields
|
||||
8472
package-lock.json
generated
8472
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
72
package.json
72
package.json
@@ -21,11 +21,11 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@renovatebot/pep440": "^4.0.1",
|
||||
"@renovatebot/pep440": "^4.1.0",
|
||||
"@renovatebot/ruby-semver": "^4.0.0",
|
||||
"@sentry/node": "^8.47.0",
|
||||
"@sentry/node": "^9.2.0",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"@xmldom/xmldom": "0.9.6",
|
||||
"@xmldom/xmldom": "0.9.7",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"byte-size": "^9.0.1",
|
||||
"bytes": "^3.1.2",
|
||||
@@ -37,33 +37,33 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"emojic": "^1.1.18",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.5.1",
|
||||
"glob": "^11.0.0",
|
||||
"fast-xml-parser": "^5.0.8",
|
||||
"glob": "^11.0.1",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.5",
|
||||
"got": "^14.4.6",
|
||||
"graphql": "16.10.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"joi": "17.13.3",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^10.2.0",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"lodash.countby": "^4.6.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"matcher": "^5.0.0",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"node-pg-migrate": "^7.8.0",
|
||||
"node-pg-migrate": "^7.9.1",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.3.0",
|
||||
"pg": "^8.13.1",
|
||||
"pg": "^8.13.3",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^15.1.3",
|
||||
"qs": "^6.13.1",
|
||||
"qs": "^6.14.0",
|
||||
"query-string": "^9.1.1",
|
||||
"semver": "~7.6.3",
|
||||
"simple-icons": "14.0.0",
|
||||
"semver": "~7.7.1",
|
||||
"simple-icons": "14.8.0",
|
||||
"smol-toml": "1.3.1",
|
||||
"svg-path-bbox": "^2.1.0",
|
||||
"svgpath": "^2.6.0",
|
||||
@@ -88,7 +88,7 @@
|
||||
"danger": "danger",
|
||||
"test:e2e": "cypress run",
|
||||
"test:core": "cross-env TZ='UTC' NODE_CONFIG_ENV=test mocha \"core/**/*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
|
||||
"test:package": "mocha \"badge-maker/**/*.spec.js\"",
|
||||
"test:package": "mocha \"badge-maker/**/*.spec.@(mjs|js)\"",
|
||||
"test:entrypoint": "cross-env NODE_CONFIG_ENV=test mocha entrypoint.spec.js",
|
||||
"test:integration": "cross-env NODE_CONFIG_ENV=test mocha \"core/**/*.integration.js\" \"services/**/*.integration.js\"",
|
||||
"test:services": "cross-env NODE_CONFIG_ENV=test mocha core/service-test-runner/cli.js",
|
||||
@@ -146,57 +146,57 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/core": "^3.5.2",
|
||||
"@docusaurus/preset-classic": "^3.5.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.46.1",
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/preset-classic": "^3.7.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.48.5",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"c8": "^10.1.3",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "5.1.2",
|
||||
"chai": "5.2.0",
|
||||
"chai-as-promised": "^8.0.1",
|
||||
"chai-datetime": "^1.8.1",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.1.1",
|
||||
"cypress": "^13.17.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"cypress": "^14.1.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^12.3.3",
|
||||
"danger": "^12.3.4",
|
||||
"deepmerge": "^4.3.1",
|
||||
"docusaurus-preset-openapi": "0.7.5",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"docusaurus-preset-openapi": "0.7.6",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-plugin-chai-friendly": "1.0.1",
|
||||
"eslint-plugin-cypress": "4.1.0",
|
||||
"eslint-plugin-icedfrisby": "0.2.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jsdoc": "50.6.1",
|
||||
"eslint-plugin-jsdoc": "50.6.3",
|
||||
"eslint-plugin-mocha": "10.5.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"eslint-plugin-sort-class-members": "1.21.0",
|
||||
"form-data": "^4.0.1",
|
||||
"globals": "15.14.0",
|
||||
"form-data": "^4.0.2",
|
||||
"globals": "16.0.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^5.1.0",
|
||||
"jsdoc": "^4.0.4",
|
||||
"lint-staged": "^15.2.11",
|
||||
"lint-staged": "^15.4.3",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^11.0.1",
|
||||
"mocha": "^11.1.0",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"neostandard": "0.12.0",
|
||||
"neostandard": "0.12.1",
|
||||
"nock": "13.5.6",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^8.0.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "3.4.2",
|
||||
"portfinder": "^1.0.33",
|
||||
"prettier": "3.5.2",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -207,13 +207,13 @@
|
||||
"sinon": "^19.0.2",
|
||||
"sinon-chai": "4.0.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "2.0.9",
|
||||
"start-server-and-test": "2.0.10",
|
||||
"tsd": "^0.31.2",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.10.0",
|
||||
"npm": "^9.0.0 || ^10.0.0"
|
||||
"npm": "^9 || ^10 || ^11"
|
||||
},
|
||||
"type": "module",
|
||||
"collective": {
|
||||
|
||||
@@ -22,8 +22,8 @@ export default class ChromeWebStoreSize extends BaseChromeWebStoreService {
|
||||
color: 'blue',
|
||||
}
|
||||
|
||||
transform(sizeStr) {
|
||||
const match = sizeStr.match(/^(\d+)([a-zA-Z]+)$/)
|
||||
static transform(sizeStr) {
|
||||
const match = sizeStr.match(/^(\d+(?:\.\d+)?)([a-zA-Z]+)$/)
|
||||
if (!match) {
|
||||
throw new InvalidResponse({
|
||||
prettyMessage: 'size does not match expected format',
|
||||
@@ -41,6 +41,6 @@ export default class ChromeWebStoreSize extends BaseChromeWebStoreService {
|
||||
throw new NotFound({ prettyMessage: 'not found' })
|
||||
}
|
||||
|
||||
return { message: this.transform(size) }
|
||||
return { message: this.constructor.transform(size) }
|
||||
}
|
||||
}
|
||||
|
||||
28
services/chrome-web-store/chrome-web-store-size.spec.js
Normal file
28
services/chrome-web-store/chrome-web-store-size.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import ChromeWebStoreSize from './chrome-web-store-size.service.js'
|
||||
|
||||
describe('transform function', function () {
|
||||
it('formats size correctly', function () {
|
||||
test(ChromeWebStoreSize.transform, () => {
|
||||
given('0.55KiB').expect('0.55 KiB')
|
||||
given('19.86KiB').expect('19.86 KiB')
|
||||
given('432KiB').expect('432 KiB')
|
||||
})
|
||||
})
|
||||
|
||||
it('throws when the format is unexpected', function () {
|
||||
expect(() => ChromeWebStoreSize.transform('432 KiB')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
expect(() => ChromeWebStoreSize.transform('432')).to.throw(InvalidResponse)
|
||||
expect(() => ChromeWebStoreSize.transform('KiB')).to.throw(InvalidResponse)
|
||||
expect(() => ChromeWebStoreSize.transform('foobar')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
expect(() => ChromeWebStoreSize.transform('4.4.4 KiB')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -113,7 +113,7 @@ export default class CodeclimateAnalysis extends BaseJsonService {
|
||||
},
|
||||
'/codeclimate/tech-debt/{user}/{repo}': {
|
||||
get: {
|
||||
summary: 'Code Climate issues',
|
||||
summary: 'Code Climate technical debt',
|
||||
parameters: pathParams(
|
||||
{ name: 'user', example: 'tensorflow' },
|
||||
{ name: 'repo', example: 'models' },
|
||||
@@ -122,7 +122,7 @@ export default class CodeclimateAnalysis extends BaseJsonService {
|
||||
},
|
||||
'/codeclimate/issues/{user}/{repo}': {
|
||||
get: {
|
||||
summary: 'Code Climate technical debt',
|
||||
summary: 'Code Climate issues',
|
||||
parameters: pathParams(
|
||||
{ name: 'user', example: 'tensorflow' },
|
||||
{ name: 'repo', example: 'models' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, pathParams } from '../index.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
reviews: Joi.number().required(),
|
||||
@@ -46,7 +47,7 @@ class CodeRabbitPullRequest extends BaseJsonService {
|
||||
|
||||
static render({ reviews }) {
|
||||
return {
|
||||
message: `${reviews}`,
|
||||
message: metric(reviews),
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
@@ -7,7 +7,7 @@ t.create('live CodeRabbitPullRequest')
|
||||
.get('/prs/github/coderabbitai/ast-grep-essentials.json')
|
||||
.expectBadge({
|
||||
label: 'coderabbit reviews',
|
||||
message: Joi.number().min(0),
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('live CodeRabbitPullRequest nonexistent org')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { nonNegativeInteger, optionalUrl } from '../validators.js'
|
||||
import { nonNegativeInteger, url } from '../validators.js'
|
||||
import { BaseJsonService, queryParams } from '../index.js'
|
||||
|
||||
const schemaSingular = Joi.object({
|
||||
@@ -20,7 +20,7 @@ const schemaPlural = Joi.object({
|
||||
const schema = Joi.alternatives(schemaSingular, schemaPlural)
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
server: optionalUrl.required(),
|
||||
server: url,
|
||||
}).required()
|
||||
|
||||
function singular(variant) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
url,
|
||||
query: Joi.string().required(),
|
||||
prefix: Joi.alternatives().try(Joi.string(), Joi.number()),
|
||||
suffix: Joi.alternatives().try(Joi.string(), Joi.number()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DOMParser } from '@xmldom/xmldom'
|
||||
import { DOMParser, MIME_TYPE } from '@xmldom/xmldom'
|
||||
import xpath from 'xpath'
|
||||
import { MetricNames } from '../../core/base-service/metric-helper.js'
|
||||
import { renderDynamicBadge, httpErrors } from '../dynamic-common.js'
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from '../index.js'
|
||||
import { createRoute } from './dynamic-helpers.js'
|
||||
|
||||
const MIME_TYPES = Object.values(MIME_TYPE)
|
||||
|
||||
const description = `
|
||||
The Dynamic XML Badge allows you to extract an arbitrary value from any
|
||||
XML Document using an XPath selector and show it on a badge.
|
||||
@@ -70,6 +72,10 @@ export default class DynamicXml extends BaseService {
|
||||
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
getmimeType(contentType) {
|
||||
return MIME_TYPES.find(mime => contentType.includes(mime)) ?? 'text/xml'
|
||||
}
|
||||
|
||||
transform({ pathExpression, buffer, contentType = 'text/xml' }) {
|
||||
// e.g. //book[2]/@id
|
||||
const pathIsAttr = (
|
||||
@@ -136,11 +142,8 @@ export default class DynamicXml extends BaseService {
|
||||
})
|
||||
|
||||
let contentType = 'text/xml'
|
||||
if (
|
||||
res.headers['content-type'] &&
|
||||
res.headers['content-type'].includes('text/html')
|
||||
) {
|
||||
contentType = 'text/html'
|
||||
if (res.headers['content-type']) {
|
||||
contentType = this.getmimeType(res.headers['content-type'])
|
||||
}
|
||||
|
||||
const { values: value } = this.transform({
|
||||
|
||||
@@ -156,5 +156,30 @@ describe('DynamicXml', function () {
|
||||
}).expect({
|
||||
values: ['Herman Melville - Moby-Dick'],
|
||||
})
|
||||
|
||||
// lowercase doctype
|
||||
// https://github.com/badges/shields/issues/10827
|
||||
given({
|
||||
pathExpression: '//h1[1]',
|
||||
buffer: exampleHtml.replace('<!DOCTYPE html>', '<!doctype html>'),
|
||||
contentType: 'text/html',
|
||||
}).expect({
|
||||
values: ['Herman Melville - Moby-Dick'],
|
||||
})
|
||||
})
|
||||
|
||||
test(DynamicXml.prototype.getmimeType, () => {
|
||||
// known types
|
||||
given('text/html').expect('text/html')
|
||||
given('application/xml').expect('application/xml')
|
||||
given('application/xhtml+xml').expect('application/xhtml+xml')
|
||||
given('image/svg+xml').expect('image/svg+xml')
|
||||
|
||||
// with character set
|
||||
given('text/html; charset=utf-8').expect('text/html')
|
||||
|
||||
// should fall back to text/xml if mime type is not one of the known types
|
||||
given('text/csv').expect('text/xml')
|
||||
given('foobar').expect('text/xml')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { URL } from 'url'
|
||||
import Joi from 'joi'
|
||||
import { httpErrors } from '../dynamic-common.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { fetchEndpointData } from '../endpoint-common.js'
|
||||
import { BaseJsonService, InvalidParameter, queryParams } from '../index.js'
|
||||
|
||||
const blockedDomains = ['github.com', 'shields.io']
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
url,
|
||||
}).required()
|
||||
|
||||
const description = `
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseJsonService, pathParam, queryParam } from '../index.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
baseUrl: optionalUrl.required(),
|
||||
baseUrl: url,
|
||||
}).required()
|
||||
|
||||
const schema = Joi.object({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseJsonService, pathParam, queryParam } from '../index.js'
|
||||
import { authConfig } from './jira-common.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
baseUrl: optionalUrl.required(),
|
||||
baseUrl: url,
|
||||
}).required()
|
||||
|
||||
const schema = Joi.object({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseJsonService, pathParam, queryParam } from '../index.js'
|
||||
import { authConfig } from './jira-common.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
baseUrl: optionalUrl.required(),
|
||||
baseUrl: url,
|
||||
}).required()
|
||||
|
||||
const schema = Joi.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { optionalUrl, nonNegativeInteger } from '../validators.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
@@ -9,15 +9,11 @@ const schema = Joi.object({
|
||||
})
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
domain: optionalUrl,
|
||||
domain: Joi.string().optional(),
|
||||
}).required()
|
||||
|
||||
const description = `
|
||||
To find your user id, you can use [this tool](https://prouser123.me/misc/mastodon-userid-lookup.html).
|
||||
|
||||
Alternatively you can make a request to \`https://your.mastodon.server/.well-known/webfinger?resource=acct:<user>@<domain>\`
|
||||
|
||||
Failing that, you can also visit your profile page, where your user ID will be in the header in a tag like this: \`<link href='https://your.mastodon.server/api/salmon/<your-user-id>' rel='salmon'>\`
|
||||
To find your user id, you can make a request to \`https://your.mastodon.server/api/v1/accounts/lookup?acct=yourusername\`.
|
||||
`
|
||||
|
||||
export default class MastodonFollow extends BaseJsonService {
|
||||
@@ -41,7 +37,7 @@ export default class MastodonFollow extends BaseJsonService {
|
||||
}),
|
||||
queryParam({
|
||||
name: 'domain',
|
||||
example: 'https://mastodon.social',
|
||||
example: 'mastodon.social',
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -58,8 +54,8 @@ export default class MastodonFollow extends BaseJsonService {
|
||||
message: metric(followers),
|
||||
style: 'social',
|
||||
link: [
|
||||
`${domain}/users/${username}`,
|
||||
`${domain}/users/${username}/followers`,
|
||||
`https://${domain}/users/${username}`,
|
||||
`https://${domain}/users/${username}/followers`,
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -67,13 +63,14 @@ export default class MastodonFollow extends BaseJsonService {
|
||||
async fetch({ id, domain }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `${domain}/api/v1/accounts/${id}/`,
|
||||
url: `https://${domain}/api/v1/accounts/${id}/`,
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ id }, { domain = 'https://mastodon.social' }) {
|
||||
async handle({ id }, { domain = 'mastodon.social' }) {
|
||||
if (isNaN(id))
|
||||
throw new NotFound({ prettyMessage: 'invalid user id format' })
|
||||
domain = domain.replace(/^https?:\/\//, '')
|
||||
const data = await this.fetch({ id, domain })
|
||||
return this.constructor.render({
|
||||
username: data.username,
|
||||
|
||||
@@ -28,6 +28,17 @@ t.create('Followers - default domain - invalid user ID (id not in use)')
|
||||
})
|
||||
|
||||
t.create('Followers - alternate domain')
|
||||
.get('/2214.json?domain=mastodon.xyz')
|
||||
.expectBadge({
|
||||
label: 'follow @PhotonQyv',
|
||||
message: isMetric,
|
||||
link: [
|
||||
'https://mastodon.xyz/users/PhotonQyv',
|
||||
'https://mastodon.xyz/users/PhotonQyv/followers',
|
||||
],
|
||||
})
|
||||
|
||||
t.create('Followers - alternate domain legacy')
|
||||
.get('/2214.json?domain=https%3A%2F%2Fmastodon.xyz')
|
||||
.expectBadge({
|
||||
label: 'follow @PhotonQyv',
|
||||
|
||||
@@ -5,9 +5,15 @@ import {
|
||||
pathParam,
|
||||
queryParam,
|
||||
} from '../index.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
|
||||
const fetchModeEnum = ['guest', 'summary']
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
server_fqdn: Joi.string().hostname(),
|
||||
fetchMode: Joi.string()
|
||||
.valid(...fetchModeEnum)
|
||||
.default('guest'),
|
||||
}).required()
|
||||
|
||||
const matrixRegisterSchema = Joi.object({
|
||||
@@ -31,9 +37,16 @@ const matrixStateSchema = Joi.array()
|
||||
)
|
||||
.required()
|
||||
|
||||
const matrixSummarySchema = Joi.object({
|
||||
num_joined_members: nonNegativeInteger,
|
||||
}).required()
|
||||
|
||||
const description = `
|
||||
In order for this badge to work, the host of your room must allow guest accounts or dummy accounts to register, and the room must be world readable (chat history visible to anyone).
|
||||
|
||||
Alternatively access via the experimental <code>summary</code> endpoint ([MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)) can be configured with the query parameter <code>fetchMode</code> for less server load and better performance, if supported by the homeserver<br/>
|
||||
For the <code>matrix.org</code> homeserver <code>fetchMode</code> is hard-coded to <code>summary</code>.
|
||||
|
||||
The following steps will show you how to setup the badge URL using the Element Matrix client.
|
||||
|
||||
<ul>
|
||||
@@ -76,12 +89,21 @@ export default class Matrix extends BaseJsonService {
|
||||
name: 'server_fqdn',
|
||||
example: 'matrix.org',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'fetchMode',
|
||||
example: 'guest',
|
||||
description: `<code>guest</code> configures guest authentication while <code>summary</code> configures usage of the experimental "summary" endpoint ([MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)). If not specified, the default fetch mode is <code>guest</code> (except for matrix.org).`,
|
||||
schema: {
|
||||
type: 'string',
|
||||
enum: fetchModeEnum,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static _cacheLength = 30
|
||||
static _cacheLength = 14400
|
||||
|
||||
static defaultBadgeData = { label: 'chat' }
|
||||
|
||||
@@ -147,27 +169,27 @@ export default class Matrix extends BaseJsonService {
|
||||
})
|
||||
}
|
||||
|
||||
async fetch({ roomAlias, serverFQDN }) {
|
||||
let host
|
||||
if (serverFQDN === undefined) {
|
||||
const splitAlias = roomAlias.split(':')
|
||||
// A room alias can either be in the form #localpart:server or
|
||||
// #localpart:server:port.
|
||||
switch (splitAlias.length) {
|
||||
case 2:
|
||||
host = splitAlias[1]
|
||||
break
|
||||
case 3:
|
||||
host = `${splitAlias[1]}:${splitAlias[2]}`
|
||||
break
|
||||
default:
|
||||
throw new InvalidParameter({ prettyMessage: 'invalid alias' })
|
||||
}
|
||||
} else {
|
||||
host = serverFQDN
|
||||
}
|
||||
async fetchSummary({ host, roomAlias }) {
|
||||
const data = await this._requestJson({
|
||||
url: `https://${host}/_matrix/client/unstable/im.nheko.summary/rooms/%23${encodeURIComponent(
|
||||
roomAlias,
|
||||
)}/summary`,
|
||||
schema: matrixSummarySchema,
|
||||
httpErrors: {
|
||||
400: 'unknown request',
|
||||
404: 'room or endpoint not found',
|
||||
},
|
||||
})
|
||||
return data.num_joined_members
|
||||
}
|
||||
|
||||
async fetchGuest({ host, roomAlias }) {
|
||||
const accessToken = await this.retrieveAccessToken({ host })
|
||||
const lookup = await this.lookupRoomAlias({ host, roomAlias, accessToken })
|
||||
const lookup = await this.lookupRoomAlias({
|
||||
host,
|
||||
roomAlias,
|
||||
accessToken,
|
||||
})
|
||||
const data = await this._requestJson({
|
||||
url: `https://${host}/_matrix/client/r0/rooms/${encodeURIComponent(
|
||||
lookup.room_id,
|
||||
@@ -194,8 +216,36 @@ export default class Matrix extends BaseJsonService {
|
||||
: 0
|
||||
}
|
||||
|
||||
async handle({ roomAlias }, { server_fqdn: serverFQDN }) {
|
||||
const members = await this.fetch({ roomAlias, serverFQDN })
|
||||
async fetch({ roomAlias, serverFQDN, fetchMode }) {
|
||||
let host
|
||||
if (serverFQDN === undefined) {
|
||||
const splitAlias = roomAlias.split(':')
|
||||
// A room alias can either be in the form #localpart:server or
|
||||
// #localpart:server:port.
|
||||
switch (splitAlias.length) {
|
||||
case 2:
|
||||
host = splitAlias[1]
|
||||
break
|
||||
case 3:
|
||||
host = `${splitAlias[1]}:${splitAlias[2]}`
|
||||
break
|
||||
default:
|
||||
throw new InvalidParameter({ prettyMessage: 'invalid alias' })
|
||||
}
|
||||
} else {
|
||||
host = serverFQDN
|
||||
}
|
||||
if (host.toLowerCase() === 'matrix.org' || fetchMode === 'summary') {
|
||||
// summary endpoint (default for matrix.org)
|
||||
return await this.fetchSummary({ host, roomAlias })
|
||||
} else {
|
||||
// guest access
|
||||
return await this.fetchGuest({ host, roomAlias })
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ roomAlias }, { server_fqdn: serverFQDN, fetchMode }) {
|
||||
const members = await this.fetch({ roomAlias, serverFQDN, fetchMode })
|
||||
return this.constructor.render({ members })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,26 @@ t.create('get room state as member (backup method)')
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('get room summary')
|
||||
.get('/ALIAS:DUMMY.dumb.json?fetchMode=summary')
|
||||
.intercept(nock =>
|
||||
nock('https://DUMMY.dumb/')
|
||||
.get(
|
||||
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
JSON.stringify({
|
||||
num_joined_members: 4,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: '4 users',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('bad server or connection')
|
||||
.get('/ALIAS:DUMMY.dumb.json')
|
||||
.networkOff()
|
||||
@@ -263,6 +283,27 @@ t.create('unknown request')
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
t.create('unknown summary request')
|
||||
.get('/ALIAS:DUMMY.dumb.json?fetchMode=summary')
|
||||
.intercept(nock =>
|
||||
nock('https://DUMMY.dumb/')
|
||||
.get(
|
||||
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
|
||||
)
|
||||
.reply(
|
||||
400,
|
||||
JSON.stringify({
|
||||
errcode: 'M_UNRECOGNIZED',
|
||||
error: 'Unrecognized request',
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: 'unknown request',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
t.create('unknown alias')
|
||||
.get('/ALIAS:DUMMY.dumb.json')
|
||||
.intercept(nock =>
|
||||
@@ -291,6 +332,27 @@ t.create('unknown alias')
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('unknown summary alias')
|
||||
.get('/ALIAS:DUMMY.dumb.json?fetchMode=summary')
|
||||
.intercept(nock =>
|
||||
nock('https://DUMMY.dumb/')
|
||||
.get(
|
||||
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
|
||||
)
|
||||
.reply(
|
||||
404,
|
||||
JSON.stringify({
|
||||
errcode: 'M_NOT_FOUND',
|
||||
error: 'Room alias #ALIAS%3ADUMMY.dumb not found.',
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: 'room or endpoint not found',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('invalid alias').get('/ALIASDUMMY.dumb.json').expectBadge({
|
||||
label: 'chat',
|
||||
message: 'invalid alias',
|
||||
@@ -368,6 +430,26 @@ t.create('server uses a custom port')
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('server uses a custom port for summary')
|
||||
.get('/ALIAS:DUMMY.dumb:5555.json?fetchMode=summary')
|
||||
.intercept(nock =>
|
||||
nock('https://DUMMY.dumb:5555/')
|
||||
.get(
|
||||
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb%3A5555/summary',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
JSON.stringify({
|
||||
num_joined_members: 4,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: '4 users',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('specify the homeserver fqdn')
|
||||
.get('/ALIAS:DUMMY.dumb.json?server_fqdn=matrix.DUMMY.dumb')
|
||||
.intercept(nock =>
|
||||
@@ -439,9 +521,56 @@ t.create('specify the homeserver fqdn')
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('test on real matrix room for API compliance')
|
||||
.get('/twim:matrix.org.json')
|
||||
.timeout(10000)
|
||||
t.create('specify the homeserver fqdn for summary')
|
||||
.get('/ALIAS:DUMMY.dumb.json?server_fqdn=matrix.DUMMY.dumb&fetchMode=summary')
|
||||
.intercept(nock =>
|
||||
nock('https://matrix.DUMMY.dumb/')
|
||||
.get(
|
||||
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
JSON.stringify({
|
||||
num_joined_members: 4,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: '4 users',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('test fetchMode=guest is ignored for matrix.org')
|
||||
.get('/ALIAS:DUMMY.dumb.json?server_fqdn=matrix.org&fetchMode=guest')
|
||||
.intercept(nock =>
|
||||
nock('https://matrix.org/')
|
||||
.get(
|
||||
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
JSON.stringify({
|
||||
num_joined_members: 4,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: '4 users',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('test on real matrix room for guest API compliance')
|
||||
.get('/ndcube:openastronomy.org.json?server_fqdn=openastronomy.modular.im')
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: Joi.string().regex(/^[0-9]+ users$/),
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('test on real matrix room for summary API compliance')
|
||||
.get('/twim:matrix.org.json')
|
||||
.expectBadge({
|
||||
label: 'chat',
|
||||
message: Joi.string().regex(/^[0-9]+ users$/),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { BaseXmlService, NotFound, queryParams } from '../index.js'
|
||||
import { description } from './maven-metadata.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
metadataUrl: optionalUrl.required(),
|
||||
metadataUrl: url,
|
||||
versionPrefix: Joi.string().optional(),
|
||||
versionSuffix: Joi.string().optional(),
|
||||
}).required()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import {
|
||||
optionalUrl,
|
||||
url,
|
||||
optionalDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../validators.js'
|
||||
import {
|
||||
@@ -49,7 +49,7 @@ const nexus2ResolveApiSchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
server: optionalUrl.required(),
|
||||
server: url,
|
||||
queryOpt: Joi.string()
|
||||
.regex(/(:[\w.]+=[^:]*)+/i)
|
||||
.optional(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseService, InvalidResponse, queryParam } from '../index.js'
|
||||
|
||||
const description = `
|
||||
@@ -12,7 +12,7 @@ can be viewed on the [OSS Tracker repository](https://github.com/Netflix/osstrac
|
||||
`
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
file_url: optionalUrl.required(),
|
||||
file_url: url,
|
||||
}).required()
|
||||
|
||||
export default class OssTracker extends BaseService {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class PypiTypes extends PypiBase {
|
||||
get: {
|
||||
summary: 'PyPI - Types',
|
||||
description:
|
||||
'Whether the package provides type information, as indicated by the presence of the Typing :: Typed classifier in the package metadata',
|
||||
'Type information provided by the package, as indicated by the presence of the `Typing :: Typed` and `Typing :: Stubs Only` classifiers in the package metadata',
|
||||
parameters: pypiGeneralParams,
|
||||
},
|
||||
},
|
||||
@@ -18,12 +18,17 @@ export default class PypiTypes extends PypiBase {
|
||||
|
||||
static defaultBadgeData = { label: 'types' }
|
||||
|
||||
static render({ isTyped }) {
|
||||
static render({ isTyped, isStubsOnly }) {
|
||||
if (isTyped) {
|
||||
return {
|
||||
message: 'typed',
|
||||
color: 'brightgreen',
|
||||
}
|
||||
} else if (isStubsOnly) {
|
||||
return {
|
||||
message: 'stubs',
|
||||
color: 'brightgreen',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
message: 'untyped',
|
||||
@@ -35,6 +40,9 @@ export default class PypiTypes extends PypiBase {
|
||||
async handle({ egg }, { pypiBaseUrl }) {
|
||||
const packageData = await this.fetch({ egg, pypiBaseUrl })
|
||||
const isTyped = packageData.info.classifiers.includes('Typing :: Typed')
|
||||
return this.constructor.render({ isTyped })
|
||||
const isStubsOnly = packageData.info.classifiers.includes(
|
||||
'Typing :: Stubs Only',
|
||||
)
|
||||
return this.constructor.render({ isTyped, isStubsOnly })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ t.create('types (no)')
|
||||
.get('/z3-solver.json')
|
||||
.expectBadge({ label: 'types', message: 'untyped' })
|
||||
|
||||
t.create('types (stubs)')
|
||||
.get('/types-requests.json')
|
||||
.expectBadge({ label: 'types', message: 'stubs' })
|
||||
|
||||
t.create('types (invalid)')
|
||||
.get('/not-a-package.json')
|
||||
.expectBadge({ label: 'types', message: 'package or version not found' })
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Joi from 'joi'
|
||||
import BaseTomlService from '../../core/base-service/base-toml.js'
|
||||
import { queryParams } from '../index.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
tomlFilePath: optionalUrl.required(),
|
||||
tomlFilePath: url,
|
||||
}).required()
|
||||
|
||||
const schema = Joi.object({
|
||||
|
||||
89
services/reddit/reddit-base.js
Normal file
89
services/reddit/reddit-base.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const tokenSchema = Joi.object({
|
||||
access_token: Joi.string().required(),
|
||||
expires_in: Joi.number(),
|
||||
})
|
||||
|
||||
// Abstract class for Reddit badges
|
||||
// Authorization flow based on https://github.com/reddit-archive/reddit/wiki/OAuth2#application-only-oauth.
|
||||
export default class RedditBase extends BaseJsonService {
|
||||
static category = 'social'
|
||||
|
||||
static auth = {
|
||||
userKey: 'reddit_client_id',
|
||||
passKey: 'reddit_client_secret',
|
||||
authorizedOrigins: ['https://www.reddit.com'],
|
||||
isRequired: false,
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
if (!RedditBase._redditToken && this.authHelper.isConfigured) {
|
||||
RedditBase._redditToken = this._getNewToken()
|
||||
}
|
||||
}
|
||||
|
||||
async _getNewToken() {
|
||||
const tokenRes = await super._requestJson(
|
||||
this.authHelper.withBasicAuth({
|
||||
schema: tokenSchema,
|
||||
url: 'https://www.reddit.com/api/v1/access_token',
|
||||
options: {
|
||||
method: 'POST',
|
||||
body: 'grant_type=client_credentials',
|
||||
},
|
||||
httpErrors: {
|
||||
401: 'invalid token',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// replace the token when we are 80% near the expire time
|
||||
// 2147483647 is the max 32-bit value that is accepted by setTimeout(), it's about 24.9 days
|
||||
const replaceTokenMs = Math.min(
|
||||
tokenRes.expires_in * 1000 * 0.8,
|
||||
2147483647,
|
||||
)
|
||||
const timeout = setTimeout(() => {
|
||||
RedditBase._redditToken = this._getNewToken()
|
||||
}, replaceTokenMs)
|
||||
|
||||
// do not block program exit
|
||||
timeout.unref()
|
||||
|
||||
return tokenRes.access_token
|
||||
}
|
||||
|
||||
async _requestJson(request) {
|
||||
if (!this.authHelper.isConfigured) {
|
||||
return super._requestJson(request)
|
||||
}
|
||||
|
||||
request = await this.addBearerAuthHeader(request)
|
||||
try {
|
||||
return await super._requestJson(request)
|
||||
} catch (err) {
|
||||
if (err.response && err.response.statusCode === 401) {
|
||||
// if the token is expired or has been revoked, retry once
|
||||
RedditBase._redditToken = this._getNewToken()
|
||||
request = await this.addBearerAuthHeader(request)
|
||||
return super._requestJson(request)
|
||||
}
|
||||
// cannot recover
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async addBearerAuthHeader(request) {
|
||||
return {
|
||||
...request,
|
||||
options: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${await RedditBase._redditToken}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalNonNegativeInteger } from '../validators.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { BaseJsonService, NotFound, pathParams } from '../index.js'
|
||||
import { NotFound, pathParams } from '../index.js'
|
||||
import RedditBase from './reddit-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
@@ -9,9 +10,7 @@ const schema = Joi.object({
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class RedditSubredditSubscribers extends BaseJsonService {
|
||||
static category = 'social'
|
||||
|
||||
export default class RedditSubredditSubscribers extends RedditBase {
|
||||
static route = {
|
||||
base: 'reddit/subreddit-subscribers',
|
||||
pattern: ':subreddit',
|
||||
@@ -29,8 +28,6 @@ export default class RedditSubredditSubscribers extends BaseJsonService {
|
||||
},
|
||||
}
|
||||
|
||||
static _cacheLength = 7200
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'reddit',
|
||||
namedLogo: 'reddit',
|
||||
@@ -49,7 +46,10 @@ export default class RedditSubredditSubscribers extends BaseJsonService {
|
||||
async fetch({ subreddit }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://www.reddit.com/r/${subreddit}/about.json`,
|
||||
// API requests with a bearer token should be made to https://oauth.reddit.com, NOT www.reddit.com.
|
||||
url: this.authHelper.isConfigured
|
||||
? `https://oauth.reddit.com/r/${subreddit}/about.json`
|
||||
: `https://www.reddit.com/r/${subreddit}/about.json`,
|
||||
httpErrors: {
|
||||
404: 'subreddit not found',
|
||||
403: 'subreddit is private',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { noToken } from '../test-helpers.js'
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import _serviceClass from './subreddit-subscribers.service.js'
|
||||
export const t = await createServiceTester()
|
||||
const noRedditToken = noToken(_serviceClass)
|
||||
const hasRedditToken = () => !noRedditToken()
|
||||
|
||||
t.create('subreddit-subscribers (valid subreddit)')
|
||||
.get('/drums.json')
|
||||
@@ -30,7 +34,8 @@ t.create('subreddit-subscribers (private sub)')
|
||||
message: 'subreddit is private',
|
||||
})
|
||||
|
||||
t.create('subreddit-subscribers (private sub)')
|
||||
t.create('subreddit-subscribers (private sub, without token)')
|
||||
.skipWhen(hasRedditToken)
|
||||
.get('/centuryclub.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.reddit.com/r')
|
||||
@@ -41,3 +46,17 @@ t.create('subreddit-subscribers (private sub)')
|
||||
label: 'reddit',
|
||||
message: 'subreddit not found',
|
||||
})
|
||||
|
||||
t.create('subreddit-subscribers (private sub, with token)')
|
||||
.skipWhen(noRedditToken)
|
||||
.get('/centuryclub.json')
|
||||
.intercept(nock =>
|
||||
nock('https://oauth.reddit.com/r')
|
||||
.get('/centuryclub/about.json')
|
||||
.reply(200, { kind: 't5', data: {} }),
|
||||
)
|
||||
.networkOn() // API /access_token may or may not be called depending on whether another test ran before and cached the token. Rather than conditionally intercepting it, let it go through and only mock the API call we're validating specific behaviour against.
|
||||
.expectBadge({
|
||||
label: 'reddit',
|
||||
message: 'subreddit not found',
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import { anyInteger } from '../validators.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { BaseJsonService, pathParams } from '../index.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import RedditBase from './reddit-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
@@ -10,9 +11,7 @@ const schema = Joi.object({
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class RedditUserKarma extends BaseJsonService {
|
||||
static category = 'social'
|
||||
|
||||
export default class RedditUserKarma extends RedditBase {
|
||||
static route = {
|
||||
base: 'reddit/user-karma',
|
||||
pattern: ':variant(link|comment|combined)/:user',
|
||||
@@ -37,8 +36,6 @@ export default class RedditUserKarma extends BaseJsonService {
|
||||
},
|
||||
}
|
||||
|
||||
static _cacheLength = 7200
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'reddit karma',
|
||||
namedLogo: 'reddit',
|
||||
@@ -61,7 +58,10 @@ export default class RedditUserKarma extends BaseJsonService {
|
||||
async fetch({ user }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://www.reddit.com/u/${user}/about.json`,
|
||||
// API requests with a bearer token should be made to https://oauth.reddit.com, NOT www.reddit.com.
|
||||
url: this.authHelper.isConfigured
|
||||
? `https://oauth.reddit.com/u/${user}/about.json`
|
||||
: `https://www.reddit.com/u/${user}/about.json`,
|
||||
httpErrors: {
|
||||
404: 'user not found',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { noToken } from '../test-helpers.js'
|
||||
import { isMetricAllowNegative } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import _serviceClass from './subreddit-subscribers.service.js'
|
||||
export const t = await createServiceTester()
|
||||
const noRedditToken = noToken(_serviceClass)
|
||||
const hasRedditToken = () => !noRedditToken()
|
||||
|
||||
t.create('user-karma (valid - link)')
|
||||
.get('/link/user_simulator.json')
|
||||
@@ -30,7 +34,8 @@ t.create('user-karma (non-existing user)')
|
||||
message: 'user not found',
|
||||
})
|
||||
|
||||
t.create('user-karma (link - math check)')
|
||||
t.create('user-karma (link - math check, without token)')
|
||||
.skipWhen(hasRedditToken)
|
||||
.get('/link/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.reddit.com/u')
|
||||
@@ -42,7 +47,22 @@ t.create('user-karma (link - math check)')
|
||||
message: '20',
|
||||
})
|
||||
|
||||
t.create('user-karma (comment - math check)')
|
||||
t.create('user-karma (link - math check, with token)')
|
||||
.skipWhen(noRedditToken)
|
||||
.get('/link/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://oauth.reddit.com/u')
|
||||
.get('/user_simulator/about.json')
|
||||
.reply(200, { kind: 't2', data: { link_karma: 20, comment_karma: 80 } }),
|
||||
)
|
||||
.networkOn() // API /access_token may or may not be called depending on whether another test ran before and cached the token. Rather than conditionally intercepting it, let it go through and only mock the API call we're validating specific behaviour against.
|
||||
.expectBadge({
|
||||
label: 'u/user_simulator karma (link)',
|
||||
message: '20',
|
||||
})
|
||||
|
||||
t.create('user-karma (comment - math check, without token)')
|
||||
.skipWhen(hasRedditToken)
|
||||
.get('/comment/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.reddit.com/u')
|
||||
@@ -54,7 +74,22 @@ t.create('user-karma (comment - math check)')
|
||||
message: '80',
|
||||
})
|
||||
|
||||
t.create('user-karma (combined - math check)')
|
||||
t.create('user-karma (comment - math check, with token)')
|
||||
.skipWhen(noRedditToken)
|
||||
.get('/comment/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://oauth.reddit.com/u')
|
||||
.get('/user_simulator/about.json')
|
||||
.reply(200, { kind: 't2', data: { link_karma: 20, comment_karma: 80 } }),
|
||||
)
|
||||
.networkOn() // API /access_token may or may not be called depending on whether another test ran before and cached the token. Rather than conditionally intercepting it, let it go through and only mock the API call we're validating specific behaviour against.
|
||||
.expectBadge({
|
||||
label: 'u/user_simulator karma (comment)',
|
||||
message: '80',
|
||||
})
|
||||
|
||||
t.create('user-karma (combined - math check, without token)')
|
||||
.skipWhen(hasRedditToken)
|
||||
.get('/combined/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.reddit.com/u')
|
||||
@@ -66,7 +101,22 @@ t.create('user-karma (combined - math check)')
|
||||
message: '100',
|
||||
})
|
||||
|
||||
t.create('user-karma (combined - missing data)')
|
||||
t.create('user-karma (combined - math check, with token)')
|
||||
.skipWhen(noRedditToken)
|
||||
.get('/combined/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://oauth.reddit.com/u')
|
||||
.get('/user_simulator/about.json')
|
||||
.reply(200, { kind: 't2', data: { link_karma: 20, comment_karma: 80 } }),
|
||||
)
|
||||
.networkOn() // API /access_token may or may not be called depending on whether another test ran before and cached the token. Rather than conditionally intercepting it, let it go through and only mock the API call we're validating specific behaviour against.
|
||||
.expectBadge({
|
||||
label: 'u/user_simulator karma',
|
||||
message: '100',
|
||||
})
|
||||
|
||||
t.create('user-karma (combined - missing data, without token)')
|
||||
.skipWhen(hasRedditToken)
|
||||
.get('/combined/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.reddit.com/u')
|
||||
@@ -77,3 +127,17 @@ t.create('user-karma (combined - missing data)')
|
||||
label: 'reddit karma',
|
||||
message: 'invalid response data',
|
||||
})
|
||||
|
||||
t.create('user-karma (combined - missing data, with token)')
|
||||
.skipWhen(noRedditToken)
|
||||
.get('/combined/user_simulator.json')
|
||||
.intercept(nock =>
|
||||
nock('https://oauth.reddit.com/u')
|
||||
.get('/user_simulator/about.json')
|
||||
.reply(200, { kind: 't2', data: { link_karma: 20 } }),
|
||||
)
|
||||
.networkOn() // API /access_token may or may not be called depending on whether another test ran before and cached the token. Rather than conditionally intercepting it, let it go through and only mock the API call we're validating specific behaviour against.
|
||||
.expectBadge({
|
||||
label: 'reddit karma',
|
||||
message: 'invalid response data',
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseService, NotFound, queryParams } from '../index.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
url,
|
||||
ignoreRedirects: Joi.equal(''),
|
||||
}).required()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { queryParams } from '../index.js'
|
||||
import { colorScale } from '../color-formatters.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
|
||||
const ratingPercentageScaleSteps = [10, 20, 50, 100]
|
||||
const ratingScaleColors = [
|
||||
@@ -38,7 +38,7 @@ const sonarVersionSchema = Joi.alternatives(
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
sonarVersion: sonarVersionSchema,
|
||||
server: optionalUrl.required(),
|
||||
server: url,
|
||||
}).required()
|
||||
|
||||
const openApiQueryParams = queryParams(
|
||||
@@ -48,7 +48,7 @@ const openApiQueryParams = queryParams(
|
||||
|
||||
const queryParamWithFormatSchema = Joi.object({
|
||||
sonarVersion: sonarVersionSchema,
|
||||
server: optionalUrl.required(),
|
||||
server: url,
|
||||
format: Joi.string().allow('short', 'long').optional(),
|
||||
}).required()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseJsonService, NotFound, queryParams } from '../index.js'
|
||||
|
||||
const schema = Joi.object()
|
||||
@@ -14,7 +14,7 @@ const schema = Joi.object()
|
||||
.required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
specUrl: optionalUrl.required(),
|
||||
specUrl: url,
|
||||
}).required()
|
||||
|
||||
export default class SwaggerValidatorService extends BaseJsonService {
|
||||
|
||||
50
services/terraform/terraform-base.js
Normal file
50
services/terraform/terraform-base.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const description =
|
||||
'[Terraform Registry](https://registry.terraform.io) is an interactive resource for discovering a wide selection of integrations (providers), configuration packages (modules), and security rules (policies) for use with Terraform.'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
attributes: Joi.object({
|
||||
month: nonNegativeInteger,
|
||||
total: nonNegativeInteger,
|
||||
week: nonNegativeInteger,
|
||||
year: nonNegativeInteger,
|
||||
}).required(),
|
||||
}),
|
||||
})
|
||||
|
||||
const intervalMap = {
|
||||
dw: {
|
||||
transform: json => json.data.attributes.week,
|
||||
interval: 'week',
|
||||
},
|
||||
dm: {
|
||||
transform: json => json.data.attributes.month,
|
||||
interval: 'month',
|
||||
},
|
||||
dy: {
|
||||
transform: json => json.data.attributes.year,
|
||||
interval: 'year',
|
||||
},
|
||||
dt: {
|
||||
transform: json => json.data.attributes.total,
|
||||
interval: '',
|
||||
},
|
||||
}
|
||||
|
||||
class BaseTerraformService extends BaseJsonService {
|
||||
static _cacheLength = 3600
|
||||
|
||||
async fetch({ kind, object }) {
|
||||
const url = `https://registry.terraform.io/v2/${kind}/${object}/downloads/summary`
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseTerraformService, intervalMap, description }
|
||||
60
services/terraform/terraform-module-downloads.service.js
Normal file
60
services/terraform/terraform-module-downloads.service.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import {
|
||||
BaseTerraformService,
|
||||
description,
|
||||
intervalMap,
|
||||
} from './terraform-base.js'
|
||||
|
||||
export default class TerraformModuleDownloads extends BaseTerraformService {
|
||||
static category = 'downloads'
|
||||
|
||||
static route = {
|
||||
base: 'terraform/module',
|
||||
pattern: ':interval(dw|dm|dy|dt)/:namespace/:name/:provider',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/terraform/module/{interval}/{namespace}/{name}/{provider}': {
|
||||
get: {
|
||||
summary: 'Terraform Module Downloads',
|
||||
description,
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'interval',
|
||||
example: 'dy',
|
||||
schema: { type: 'string', enum: this.getEnum('interval') },
|
||||
description: 'Weekly, Monthly, Yearly or Total downloads',
|
||||
},
|
||||
{
|
||||
name: 'namespace',
|
||||
example: 'hashicorp',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
example: 'consul',
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
example: 'aws',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'downloads' }
|
||||
|
||||
async handle({ interval, namespace, name, provider }) {
|
||||
const { transform } = intervalMap[interval]
|
||||
const json = await this.fetch({
|
||||
kind: 'modules',
|
||||
object: `${namespace}/${name}/${provider}`,
|
||||
})
|
||||
|
||||
return renderDownloadsBadge({
|
||||
downloads: transform(json),
|
||||
interval: intervalMap[interval].interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
36
services/terraform/terraform-module-downloads.tester.js
Normal file
36
services/terraform/terraform-module-downloads.tester.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric, isMetricOverTimePeriod } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('weekly downloads (valid)')
|
||||
.get('/dw/hashicorp/consul/aws.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('monthly downloads (valid)')
|
||||
.get('/dm/hashicorp/consul/aws.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('yearly downloads (valid)')
|
||||
.get('/dy/hashicorp/consul/aws.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('total downloads (valid)')
|
||||
.get('/dt/hashicorp/consul/aws.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetric })
|
||||
|
||||
t.create('weekly downloads (not found)')
|
||||
.get('/dw/not/real/module.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('monthly downloads (not found)')
|
||||
.get('/dm/not/real/module.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('yearly downloads (not found)')
|
||||
.get('/dy/not/real/module.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('total downloads (not found)')
|
||||
.get('/dt/not/real/module.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
54
services/terraform/terraform-provider-downloads.service.js
Normal file
54
services/terraform/terraform-provider-downloads.service.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import {
|
||||
BaseTerraformService,
|
||||
description,
|
||||
intervalMap,
|
||||
} from './terraform-base.js'
|
||||
|
||||
export default class TerraformProviderDownloads extends BaseTerraformService {
|
||||
static category = 'downloads'
|
||||
|
||||
static route = {
|
||||
base: 'terraform/provider',
|
||||
pattern: ':interval(dw|dm|dy|dt)/:providerId',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/terraform/provider/{interval}/{providerId}': {
|
||||
get: {
|
||||
summary: 'Terraform Provider Downloads',
|
||||
description,
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'interval',
|
||||
example: 'dy',
|
||||
schema: { type: 'string', enum: this.getEnum('interval') },
|
||||
description: 'Weekly, Monthly, Yearly or Total downloads',
|
||||
},
|
||||
{
|
||||
name: 'providerId',
|
||||
example: '323',
|
||||
description:
|
||||
'The provider ID can be found using `https://registry.terraform.io/v2/providers/{namespace}/{name}`',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'downloads' }
|
||||
|
||||
async handle({ interval, providerId }) {
|
||||
const { transform } = intervalMap[interval]
|
||||
const json = await this.fetch({
|
||||
kind: 'providers',
|
||||
object: providerId,
|
||||
})
|
||||
|
||||
return renderDownloadsBadge({
|
||||
downloads: transform(json),
|
||||
interval: intervalMap[interval].interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
36
services/terraform/terraform-provider-downloads.tester.js
Normal file
36
services/terraform/terraform-provider-downloads.tester.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric, isMetricOverTimePeriod } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('weekly downloads (valid)')
|
||||
.get('/dw/323.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('monthly downloads (valid)')
|
||||
.get('/dm/323.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('yearly downloads (valid)')
|
||||
.get('/dy/323.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('total downloads (valid)')
|
||||
.get('/dt/323.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetric })
|
||||
|
||||
t.create('weekly downloads (not found)')
|
||||
.get('/dw/not-valid.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('monthly downloads (not found)')
|
||||
.get('/dm/not-valid.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('yearly downloads (not found)')
|
||||
.get('/dy/not-valid.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('total downloads (not found)')
|
||||
.get('/dt/not-valid.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
@@ -1,9 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseService, pathParams, queryParams } from '../index.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
url,
|
||||
}).required()
|
||||
|
||||
class TwitterUrl extends BaseService {
|
||||
|
||||
@@ -70,6 +70,13 @@ export const optionalDottedVersionNClausesWithOptionalSuffix =
|
||||
*/
|
||||
export const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
|
||||
|
||||
/**
|
||||
* Joi validator that checks if a value is a URL and the value must be present.
|
||||
*
|
||||
* @type {Joi}
|
||||
*/
|
||||
export const url = optionalUrl.required()
|
||||
|
||||
/**
|
||||
* Joi validator for a file size we are going to pass to bytes.parse
|
||||
* see https://github.com/visionmedia/bytes.js#bytesparsestringnumber-value-numbernull
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { latest, renderVersionBadge } from '../version.js'
|
||||
import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
repository_url: optionalUrl.required(),
|
||||
repository_url: url,
|
||||
include_prereleases: Joi.equal(''),
|
||||
}).required()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
|
||||
import {
|
||||
description,
|
||||
@@ -25,7 +25,7 @@ const schema = Joi.object({
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
targetUrl: optionalUrl.required(),
|
||||
targetUrl: url,
|
||||
preset: Joi.string().regex(presetRegex).allow(''),
|
||||
}).required()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import emojic from 'emojic'
|
||||
import Joi from 'joi'
|
||||
import trace from '../../core/base-service/trace.js'
|
||||
import { BaseService, queryParams } from '../index.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { url } from '../validators.js'
|
||||
import {
|
||||
queryParamSchema,
|
||||
renderWebsiteStatus,
|
||||
@@ -20,7 +20,7 @@ A site will be classified as "down" if it fails to respond within 3.5 seconds.
|
||||
`
|
||||
|
||||
const urlQueryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
url,
|
||||
}).required()
|
||||
|
||||
export default class Website extends BaseService {
|
||||
|
||||
Reference in New Issue
Block a user