mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-10 13:37:01 +03:00
Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into fullVideoLabels
This commit is contained in:
4
.github/workflows/postgres-redis-ci.yml
vendored
4
.github/workflows/postgres-redis-ci.yml
vendored
@@ -26,4 +26,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TEST_POSTGRES: true
|
TEST_POSTGRES: true
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
run: npm test
|
run: npx nyc --silent npm test
|
||||||
|
- name: Generate coverage report
|
||||||
|
run: npm run cover:report
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -47,4 +47,5 @@ working
|
|||||||
/dist/
|
/dist/
|
||||||
|
|
||||||
# nyc coverage output
|
# nyc coverage output
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
|
coverage/
|
||||||
11
.nycrc.json
11
.nycrc.json
@@ -1,5 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "@istanbuljs/nyc-config-typescript",
|
||||||
|
"check-coverage": false,
|
||||||
|
"ski-full": true,
|
||||||
|
"reporter": ["text", "html"],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"src/routes/addUnlitedVideo.ts"
|
"src/routes/addUnlistedVideo.ts",
|
||||||
|
"src/cronjob/downvoteSegmentArchiveJob.ts",
|
||||||
|
"src/databases/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,11 @@ RUN npm ci && npm run tsc
|
|||||||
|
|
||||||
FROM node:16-alpine as app
|
FROM node:16-alpine as app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
RUN apk add git postgresql-client
|
RUN apk add --no-cache git postgresql-client
|
||||||
COPY --from=builder ./node_modules ./node_modules
|
COPY --from=builder ./node_modules ./node_modules
|
||||||
COPY --from=builder ./dist ./dist
|
COPY --from=builder ./dist ./dist
|
||||||
COPY ./.git ./.git
|
COPY ./.git ./.git
|
||||||
COPY entrypoint.sh .
|
COPY entrypoint.sh .
|
||||||
COPY databases/*.sql databases/
|
COPY databases/*.sql databases/
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD ./entrypoint.sh
|
CMD ./entrypoint.sh
|
||||||
|
|||||||
14
ci.json
14
ci.json
@@ -4,11 +4,12 @@
|
|||||||
"globalSalt": "testSalt",
|
"globalSalt": "testSalt",
|
||||||
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
|
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
|
||||||
"newLeafURLs": ["placeholder"],
|
"newLeafURLs": ["placeholder"],
|
||||||
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook",
|
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel",
|
||||||
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook",
|
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions",
|
||||||
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook",
|
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport",
|
||||||
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook",
|
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject",
|
||||||
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
|
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
|
||||||
|
"userCounterURL": "http://127.0.0.1:8081/UserCounter",
|
||||||
"behindProxy": true,
|
"behindProxy": true,
|
||||||
"postgres": {
|
"postgres": {
|
||||||
"user": "ci_db_user",
|
"user": "ci_db_user",
|
||||||
@@ -70,5 +71,10 @@
|
|||||||
"statusCode": 200
|
"statusCode": 200
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"patreon": {
|
||||||
|
"clientId": "testClientID",
|
||||||
|
"clientSecret": "testClientSecret",
|
||||||
|
"redirectUri": "http://127.0.0.1/fake/callback"
|
||||||
|
},
|
||||||
"minReputationToSubmitFiller": -1
|
"minReputationToSubmitFiller": -1
|
||||||
}
|
}
|
||||||
|
|||||||
109
package-lock.json
generated
109
package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"sync-mysql": "^3.0.1"
|
"sync-mysql": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@types/better-sqlite3": "^7.5.0",
|
"@types/better-sqlite3": "^7.5.0",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
@@ -30,8 +31,10 @@
|
|||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
"@types/node": "^18.0.3",
|
"@types/node": "^18.0.3",
|
||||||
"@types/pg": "^8.6.5",
|
"@types/pg": "^8.6.5",
|
||||||
|
"@types/sinon": "^10.0.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||||
"@typescript-eslint/parser": "^5.30.6",
|
"@typescript-eslint/parser": "^5.30.6",
|
||||||
|
"axios-mock-adapter": "^1.21.2",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"mocha": "^10.0.0",
|
"mocha": "^10.0.0",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
@@ -630,6 +633,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@istanbuljs/nyc-config-typescript": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@istanbuljs/schema": "^0.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"nyc": ">=15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@istanbuljs/schema": {
|
"node_modules/@istanbuljs/schema": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||||
@@ -963,6 +981,21 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sinon": {
|
||||||
|
"version": "10.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz",
|
||||||
|
"integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/sinonjs__fake-timers": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sinonjs__fake-timers": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
|
||||||
|
"integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.30.6",
|
"version": "5.30.6",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
|
||||||
@@ -1339,6 +1372,19 @@
|
|||||||
"form-data": "^4.0.0"
|
"form-data": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios-mock-adapter": {
|
||||||
|
"version": "1.21.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz",
|
||||||
|
"integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"axios": ">= 0.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-runtime": {
|
"node_modules/babel-runtime": {
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
@@ -3094,6 +3140,29 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-buffer": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -6154,6 +6223,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@istanbuljs/nyc-config-typescript": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@istanbuljs/schema": "^0.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@istanbuljs/schema": {
|
"@istanbuljs/schema": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||||
@@ -6453,6 +6531,21 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/sinon": {
|
||||||
|
"version": "10.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz",
|
||||||
|
"integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/sinonjs__fake-timers": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/sinonjs__fake-timers": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
|
||||||
|
"integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.30.6",
|
"version": "5.30.6",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
|
||||||
@@ -6698,6 +6791,16 @@
|
|||||||
"form-data": "^4.0.0"
|
"form-data": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"axios-mock-adapter": {
|
||||||
|
"version": "1.21.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz",
|
||||||
|
"integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"babel-runtime": {
|
"babel-runtime": {
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
@@ -7975,6 +8078,12 @@
|
|||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-buffer": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"is-extglob": {
|
"is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run tsc && ts-node test/test.ts",
|
"test": "npm run tsc && ts-node test/test.ts",
|
||||||
"test:coverage": "nyc npm run test",
|
"cover": "nyc npm test",
|
||||||
|
"cover:report": "nyc report",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"dev:bash": "nodemon -x 'npm test ; npm start'",
|
"dev:bash": "nodemon -x 'npm test ; npm start'",
|
||||||
"postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:alpine",
|
"postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:alpine",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"sync-mysql": "^3.0.1"
|
"sync-mysql": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@types/better-sqlite3": "^7.5.0",
|
"@types/better-sqlite3": "^7.5.0",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
@@ -39,8 +41,10 @@
|
|||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
"@types/node": "^18.0.3",
|
"@types/node": "^18.0.3",
|
||||||
"@types/pg": "^8.6.5",
|
"@types/pg": "^8.6.5",
|
||||||
|
"@types/sinon": "^10.0.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||||
"@typescript-eslint/parser": "^5.30.6",
|
"@typescript-eslint/parser": "^5.30.6",
|
||||||
|
"axios-mock-adapter": "^1.21.2",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"mocha": "^10.0.0",
|
"mocha": "^10.0.0",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function setupRoutes(router: Router) {
|
|||||||
// Rate limit endpoint lists
|
// Rate limit endpoint lists
|
||||||
const voteEndpoints: RequestHandler[] = [voteOnSponsorTime];
|
const voteEndpoints: RequestHandler[] = [voteOnSponsorTime];
|
||||||
const viewEndpoints: RequestHandler[] = [viewedVideoSponsorTime];
|
const viewEndpoints: RequestHandler[] = [viewedVideoSponsorTime];
|
||||||
if (config.rateLimit) {
|
if (config.rateLimit && config.redisRateLimit) {
|
||||||
if (config.rateLimit.vote) voteEndpoints.unshift(rateLimitMiddleware(config.rateLimit.vote, voteGetUserID));
|
if (config.rateLimit.vote) voteEndpoints.unshift(rateLimitMiddleware(config.rateLimit.vote, voteGetUserID));
|
||||||
if (config.rateLimit.view) viewEndpoints.unshift(rateLimitMiddleware(config.rateLimit.view));
|
if (config.rateLimit.view) viewEndpoints.unshift(rateLimitMiddleware(config.rateLimit.view));
|
||||||
}
|
}
|
||||||
@@ -206,6 +206,7 @@ function setupRoutes(router: Router) {
|
|||||||
router.get("/api/videoLabels", getVideoLabels);
|
router.get("/api/videoLabels", getVideoLabels);
|
||||||
router.get("/api/videoLabels/:prefix", getVideoLabelsByHash);
|
router.get("/api/videoLabels/:prefix", getVideoLabelsByHash);
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
if (config.postgres?.enabled) {
|
if (config.postgres?.enabled) {
|
||||||
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
||||||
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ addDefaults(config, {
|
|||||||
port: 5432,
|
port: 5432,
|
||||||
max: 10,
|
max: 10,
|
||||||
idleTimeoutMillis: 10000,
|
idleTimeoutMillis: 10000,
|
||||||
maxTries: 3,
|
maxTries: 3
|
||||||
maxConcurrentRequests: 3500
|
|
||||||
},
|
},
|
||||||
postgresReadOnly: {
|
postgresReadOnly: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -91,7 +90,7 @@ addDefaults(config, {
|
|||||||
idleTimeoutMillis: 10000,
|
idleTimeoutMillis: 10000,
|
||||||
maxTries: 3,
|
maxTries: 3,
|
||||||
fallbackOnFail: true,
|
fallbackOnFail: true,
|
||||||
maxConcurrentRequests: 3500
|
stopRetryThreshold: 800
|
||||||
},
|
},
|
||||||
dumpDatabase: {
|
dumpDatabase: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -139,6 +138,16 @@ addDefaults(config, {
|
|||||||
expiryTime: 24 * 60 * 60,
|
expiryTime: 24 * 60 * 60,
|
||||||
getTimeout: 40
|
getTimeout: 40
|
||||||
},
|
},
|
||||||
|
redisRead: {
|
||||||
|
enabled: false,
|
||||||
|
socket: {
|
||||||
|
host: "",
|
||||||
|
port: 0
|
||||||
|
},
|
||||||
|
disableOfflineQueue: true,
|
||||||
|
weight: 1
|
||||||
|
},
|
||||||
|
redisRateLimit: true,
|
||||||
patreon: {
|
patreon: {
|
||||||
clientId: "",
|
clientId: "",
|
||||||
clientSecret: "",
|
clientSecret: "",
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ export class Postgres implements IDatabase {
|
|||||||
private poolRead: Pool;
|
private poolRead: Pool;
|
||||||
private lastPoolReadFail = 0;
|
private lastPoolReadFail = 0;
|
||||||
|
|
||||||
private concurrentRequests = 0;
|
activePostgresRequests = 0;
|
||||||
private concurrentReadRequests = 0;
|
|
||||||
|
|
||||||
constructor(private config: DatabaseConfig) {}
|
constructor(private config: DatabaseConfig) {}
|
||||||
|
|
||||||
@@ -54,19 +53,23 @@ export class Postgres implements IDatabase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.config.postgresReadOnly && this.config.postgresReadOnly.enabled) {
|
if (this.config.postgresReadOnly && this.config.postgresReadOnly.enabled) {
|
||||||
this.poolRead = new Pool({
|
try {
|
||||||
...this.config.postgresReadOnly
|
this.poolRead = new Pool({
|
||||||
});
|
...this.config.postgresReadOnly
|
||||||
this.poolRead.on("error", (err, client) => {
|
});
|
||||||
Logger.error(err.stack);
|
this.poolRead.on("error", (err, client) => {
|
||||||
this.lastPoolReadFail = Date.now();
|
Logger.error(err.stack);
|
||||||
|
this.lastPoolReadFail = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.release(true);
|
client.release(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`poolRead (postgres): ${err}`);
|
Logger.error(`poolRead (postgres): ${err}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(`poolRead (postgres): ${e}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.readOnly) {
|
if (!this.config.readOnly) {
|
||||||
@@ -102,22 +105,6 @@ export class Postgres implements IDatabase {
|
|||||||
|
|
||||||
Logger.debug(`prepare (postgres): type: ${type}, query: ${query}, params: ${params}`);
|
Logger.debug(`prepare (postgres): type: ${type}, query: ${query}, params: ${params}`);
|
||||||
|
|
||||||
if (this.config.readOnly) {
|
|
||||||
if (this.concurrentReadRequests > this.config.postgresReadOnly?.maxConcurrentRequests) {
|
|
||||||
Logger.error(`prepare (postgres): cancelling read query because too many concurrent requests, query: ${query}`);
|
|
||||||
throw new Error("Too many concurrent requests");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.concurrentReadRequests++;
|
|
||||||
} else {
|
|
||||||
if (this.concurrentRequests > this.config.postgres.maxConcurrentRequests) {
|
|
||||||
Logger.error(`prepare (postgres): cancelling query because too many concurrent requests, query: ${query}`);
|
|
||||||
throw new Error("Too many concurrent requests");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.concurrentRequests++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingQueries: PromiseWithState<QueryResult<any>>[] = [];
|
const pendingQueries: PromiseWithState<QueryResult<any>>[] = [];
|
||||||
let tries = 0;
|
let tries = 0;
|
||||||
let lastPool: Pool = null;
|
let lastPool: Pool = null;
|
||||||
@@ -127,6 +114,7 @@ export class Postgres implements IDatabase {
|
|||||||
tries++;
|
tries++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.activePostgresRequests++;
|
||||||
lastPool = this.getPool(type, options);
|
lastPool = this.getPool(type, options);
|
||||||
|
|
||||||
pendingQueries.push(savePromiseState(lastPool.query({ text: query, values: params })));
|
pendingQueries.push(savePromiseState(lastPool.query({ text: query, values: params })));
|
||||||
@@ -134,12 +122,7 @@ export class Postgres implements IDatabase {
|
|||||||
if (options.useReplica && maxTries() - tries > 1) currentPromises.push(savePromiseState(timeoutPomise(this.config.postgresReadOnly.readTimeout)));
|
if (options.useReplica && maxTries() - tries > 1) currentPromises.push(savePromiseState(timeoutPomise(this.config.postgresReadOnly.readTimeout)));
|
||||||
const queryResult = await nextFulfilment(currentPromises);
|
const queryResult = await nextFulfilment(currentPromises);
|
||||||
|
|
||||||
if (this.config.readOnly) {
|
this.activePostgresRequests--;
|
||||||
this.concurrentReadRequests--;
|
|
||||||
} else {
|
|
||||||
this.concurrentRequests--;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "get": {
|
case "get": {
|
||||||
const value = queryResult.rows[0];
|
const value = queryResult.rows[0];
|
||||||
@@ -159,30 +142,30 @@ export class Postgres implements IDatabase {
|
|||||||
if (lastPool === this.pool) {
|
if (lastPool === this.pool) {
|
||||||
// Only applies if it is get or all request
|
// Only applies if it is get or all request
|
||||||
options.forceReplica = true;
|
options.forceReplica = true;
|
||||||
} else if (lastPool === this.poolRead && maxTries() - tries <= 1) {
|
} else if (lastPool === this.poolRead) {
|
||||||
options.useReplica = false;
|
this.lastPoolReadFail = Date.now();
|
||||||
|
|
||||||
|
if (maxTries() - tries <= 1) {
|
||||||
|
options.useReplica = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.error(`prepare (postgres) try ${tries}: ${err}`);
|
Logger.error(`prepare (postgres) try ${tries}: ${err}`);
|
||||||
}
|
}
|
||||||
} while (this.isReadQuery(type) && tries < maxTries());
|
} while (this.isReadQuery(type) && tries < maxTries()
|
||||||
|
&& this.activePostgresRequests < this.config.postgresReadOnly.stopRetryThreshold);
|
||||||
if (this.config.readOnly) {
|
|
||||||
this.concurrentReadRequests--;
|
|
||||||
} else {
|
|
||||||
this.concurrentRequests--;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.activePostgresRequests--;
|
||||||
throw new Error(`prepare (postgres): ${type} ${query} failed after ${tries} tries`);
|
throw new Error(`prepare (postgres): ${type} ${query} failed after ${tries} tries`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPool(type: string, options: QueryOption): Pool {
|
private getPool(type: string, options: QueryOption): Pool {
|
||||||
const readAvailable = this.poolRead && options.useReplica && this.isReadQuery(type);
|
const readAvailable = this.poolRead && options.useReplica && this.isReadQuery(type);
|
||||||
const ignroreReadDueToFailure = this.config.postgresReadOnly.fallbackOnFail
|
const ignoreReadDueToFailure = this.config.postgresReadOnly.fallbackOnFail
|
||||||
&& this.lastPoolReadFail > Date.now() - 1000 * 30;
|
&& this.lastPoolReadFail > Date.now() - 1000 * 30;
|
||||||
const readDueToFailure = this.config.postgresReadOnly.fallbackOnFail
|
const readDueToFailure = this.config.postgresReadOnly.fallbackOnFail
|
||||||
&& this.lastPoolFail > Date.now() - 1000 * 30;
|
&& this.lastPoolFail > Date.now() - 1000 * 30;
|
||||||
if (readAvailable && !ignroreReadDueToFailure && (options.forceReplica || readDueToFailure ||
|
if (readAvailable && !ignoreReadDueToFailure && (options.forceReplica || readDueToFailure ||
|
||||||
Math.random() > 1 / (this.config.postgresReadOnly.weight + 1))) {
|
Math.random() > 1 / (this.config.postgresReadOnly.weight + 1))) {
|
||||||
return this.poolRead;
|
return this.poolRead;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
10
src/index.ts
10
src/index.ts
@@ -12,7 +12,13 @@ async function init() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
await initDb();
|
try {
|
||||||
|
await initDb();
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(`Init Db: ${e}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// edge case clause for creating compatible .db files, do not enable
|
// edge case clause for creating compatible .db files, do not enable
|
||||||
if (config.mode === "init-db-and-exit") process.exit(0);
|
if (config.mode === "init-db-and-exit") process.exit(0);
|
||||||
// do not enable init-db-only mode for usage.
|
// do not enable init-db-only mode for usage.
|
||||||
@@ -27,4 +33,4 @@ async function init() {
|
|||||||
}).setTimeout(15000);
|
}).setTimeout(15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
init().catch((err) => Logger.error(err));
|
init().catch((err) => Logger.error(`Index.js: ${err}`));
|
||||||
@@ -35,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ
|
|||||||
|| !categories
|
|| !categories
|
||||||
|| !Array.isArray(categories)
|
|| !Array.isArray(categories)
|
||||||
|| categories.length === 0
|
|| categories.length === 0
|
||||||
|
|| actionTypes && !Array.isArray(actionTypes)
|
||||||
|| actionTypes.length === 0
|
|| actionTypes.length === 0
|
||||||
) {
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -48,7 +49,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ
|
|||||||
|
|
||||||
if (!userIsVIP) {
|
if (!userIsVIP) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
message: "Must be a VIP to mark videos.",
|
message: "Must be a VIP to lock videos.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
import { createAndSaveToken, TokenType } from "../utils/tokenUtils";
|
import { createAndSaveToken, TokenType } from "../utils/tokenUtils";
|
||||||
|
import { getHashCache } from "../utils/getHashCache";
|
||||||
|
|
||||||
interface GenerateTokenRequest extends Request {
|
interface GenerateTokenRequest extends Request {
|
||||||
query: {
|
query: {
|
||||||
@@ -15,14 +15,16 @@ interface GenerateTokenRequest extends Request {
|
|||||||
|
|
||||||
export async function generateTokenRequest(req: GenerateTokenRequest, res: Response): Promise<Response> {
|
export async function generateTokenRequest(req: GenerateTokenRequest, res: Response): Promise<Response> {
|
||||||
const { query: { code, adminUserID }, params: { type } } = req;
|
const { query: { code, adminUserID }, params: { type } } = req;
|
||||||
|
const adminUserIDHash = adminUserID ? (await getHashCache(adminUserID)) : null;
|
||||||
|
|
||||||
if (!code || !type) {
|
if (!code || !type) {
|
||||||
return res.status(400).send("Invalid request");
|
return res.status(400).send("Invalid request");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === TokenType.patreon || (type === TokenType.local && adminUserID === config.adminUserID)) {
|
if (type === TokenType.patreon || (type === TokenType.local && adminUserIDHash === config.adminUserID)) {
|
||||||
const licenseKey = await createAndSaveToken(type, code);
|
const licenseKey = await createAndSaveToken(type, code);
|
||||||
|
|
||||||
|
/* istanbul ignore else */
|
||||||
if (licenseKey) {
|
if (licenseKey) {
|
||||||
return res.status(200).send(`
|
return res.status(200).send(`
|
||||||
<h1>
|
<h1>
|
||||||
@@ -44,5 +46,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo
|
|||||||
</h1>
|
</h1>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,11 @@ export async function getDaysSavedFormatted(req: Request, res: Response): Promis
|
|||||||
if (row !== undefined) {
|
if (row !== undefined) {
|
||||||
//send this result
|
//send this result
|
||||||
return res.send({
|
return res.send({
|
||||||
daysSaved: row.daysSaved.toFixed(2),
|
daysSaved: row.daysSaved?.toFixed(2) ?? "0",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.send({
|
||||||
|
daysSaved: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise<Respons
|
|||||||
hashedUserID: hashedUserID,
|
hashedUserID: hashedUserID,
|
||||||
vip: vipState,
|
vip: vipState,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export async function getLockCategories(req: Request, res: Response): Promise<Re
|
|||||||
categories,
|
categories,
|
||||||
actionTypes
|
actionTypes
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */{
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,14 +44,25 @@ const mergeLocks = (source: DBLock[], actionTypes: ActionType[]): LockResultByHa
|
|||||||
|
|
||||||
export async function getLockCategoriesByHash(req: Request, res: Response): Promise<Response> {
|
export async function getLockCategoriesByHash(req: Request, res: Response): Promise<Response> {
|
||||||
let hashPrefix = req.params.prefix as VideoIDHash;
|
let hashPrefix = req.params.prefix as VideoIDHash;
|
||||||
const actionTypes: ActionType[] = req.query.actionTypes
|
let actionTypes: ActionType[] = [];
|
||||||
? JSON.parse(req.query.actionTypes as string)
|
try {
|
||||||
: req.query.actionType
|
actionTypes = req.query.actionTypes
|
||||||
? Array.isArray(req.query.actionType)
|
? JSON.parse(req.query.actionTypes as string)
|
||||||
? req.query.actionType
|
: req.query.actionType
|
||||||
: [req.query.actionType]
|
? Array.isArray(req.query.actionType)
|
||||||
: [ActionType.Skip, ActionType.Mute];
|
? req.query.actionType
|
||||||
|
: [req.query.actionType]
|
||||||
|
: [ActionType.Skip, ActionType.Mute];
|
||||||
|
if (!Array.isArray(actionTypes)) {
|
||||||
|
//invalid request
|
||||||
|
return res.sendStatus(400);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
//invalid request
|
||||||
|
return res.status(400).send("Invalid request: JSON parse error (actionTypes)");
|
||||||
|
}
|
||||||
if (!hashPrefixTester(req.params.prefix)) {
|
if (!hashPrefixTester(req.params.prefix)) {
|
||||||
|
|
||||||
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
|
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
|
||||||
}
|
}
|
||||||
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
|
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
|
||||||
@@ -62,7 +73,7 @@ export async function getLockCategoriesByHash(req: Request, res: Response): Prom
|
|||||||
if (lockedRows.length === 0 || !lockedRows[0]) return res.sendStatus(404);
|
if (lockedRows.length === 0 || !lockedRows[0]) return res.sendStatus(404);
|
||||||
// merge all locks
|
// merge all locks
|
||||||
return res.send(mergeLocks(lockedRows, actionTypes));
|
return res.send(mergeLocks(lockedRows, actionTypes));
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,18 +32,24 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
|
|||||||
return res.status(400).send("No videoID provided");
|
return res.status(400).send("No videoID provided");
|
||||||
}
|
}
|
||||||
let categories: Category[] = [];
|
let categories: Category[] = [];
|
||||||
const actionTypes: ActionType[] = req.query.actionTypes
|
let actionTypes: ActionType[] = [];
|
||||||
? JSON.parse(req.query.actionTypes as string)
|
try {
|
||||||
: req.query.actionType
|
actionTypes = req.query.actionTypes
|
||||||
? Array.isArray(req.query.actionType)
|
? JSON.parse(req.query.actionTypes as string)
|
||||||
? req.query.actionType
|
: req.query.actionType
|
||||||
: [req.query.actionType]
|
? Array.isArray(req.query.actionType)
|
||||||
: [ActionType.Skip, ActionType.Mute];
|
? req.query.actionType
|
||||||
const possibleCategories = filterActionType(actionTypes);
|
: [req.query.actionType]
|
||||||
if (!Array.isArray(actionTypes)) {
|
: [ActionType.Skip, ActionType.Mute];
|
||||||
//invalid request
|
if (!Array.isArray(actionTypes)) {
|
||||||
return res.status(400).send("actionTypes parameter does not match format requirements");
|
//invalid request
|
||||||
|
return res.status(400).send("actionTypes parameter does not match format requirements");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).send("Bad parameter: actionTypes (invalid JSON)");
|
||||||
}
|
}
|
||||||
|
const possibleCategories = filterActionType(actionTypes);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
categories = req.query.categories
|
categories = req.query.categories
|
||||||
? JSON.parse(req.query.categories as string)
|
? JSON.parse(req.query.categories as string)
|
||||||
@@ -64,11 +70,6 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
|
|||||||
: categories.filter(x =>
|
: categories.filter(x =>
|
||||||
possibleCategories.includes(x));
|
possibleCategories.includes(x));
|
||||||
|
|
||||||
if (!videoID || !Array.isArray(actionTypes)) {
|
|
||||||
//invalid request
|
|
||||||
return res.sendStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get existing lock categories markers
|
// Get existing lock categories markers
|
||||||
const row = await db.prepare("all", 'SELECT "category", "reason", "actionType", "userID" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category, reason: string, actionType: ActionType, userID: string }[];
|
const row = await db.prepare("all", 'SELECT "category", "reason", "actionType", "userID" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category, reason: string, actionType: ActionType, userID: string }[];
|
||||||
@@ -115,7 +116,7 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.send(results);
|
return res.send(results);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getSavedTimeForUser(req: Request, res: Response): Promise<
|
|||||||
} else {
|
} else {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(`getSavedTimeForUser ${err}`);
|
Logger.error(`getSavedTimeForUser ${err}`);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type searchSegmentResponse = {
|
|||||||
function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
|
function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
|
||||||
return db.prepare(
|
return db.prepare(
|
||||||
"all",
|
"all",
|
||||||
`SELECT "UUID", "timeSubmitted", "startTime", "endTime", "category", "actionType", "votes", "views", "locked", "hidden", "shadowHidden", "userID" FROM "sponsorTimes"
|
`SELECT "UUID", "timeSubmitted", "startTime", "endTime", "category", "actionType", "votes", "views", "locked", "hidden", "shadowHidden", "userID", "description" FROM "sponsorTimes"
|
||||||
WHERE "videoID" = ? AND "service" = ? ORDER BY "timeSubmitted"`,
|
WHERE "videoID" = ? AND "service" = ? ORDER BY "timeSubmitted"`,
|
||||||
[videoID, service]
|
[videoID, service]
|
||||||
) as Promise<DBSegment[]>;
|
) as Promise<DBSegment[]>;
|
||||||
@@ -128,12 +128,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<searchSeg
|
|||||||
|
|
||||||
const segments = await getSegmentsFromDBByVideoID(videoID, service);
|
const segments = await getSegmentsFromDBByVideoID(videoID, service);
|
||||||
|
|
||||||
if (segments === null || segments === undefined) {
|
if (!segments?.length) {
|
||||||
res.sendStatus(500);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segments.length === 0) {
|
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -155,6 +150,7 @@ function filterSegments(segments: DBSegment[], filters: Record<string, any>, pag
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (sortBy !== SortableFields.timeSubmitted) {
|
if (sortBy !== SortableFields.timeSubmitted) {
|
||||||
|
/* istanbul ignore next */
|
||||||
filteredSegments.sort((a,b) => {
|
filteredSegments.sort((a,b) => {
|
||||||
const key = sortDir === "desc" ? 1 : -1;
|
const key = sortDir === "desc" ? 1 : -1;
|
||||||
if (a[sortBy] < b[sortBy]) {
|
if (a[sortBy] < b[sortBy]) {
|
||||||
@@ -187,6 +183,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
|
|||||||
return res.send(segmentResponse);
|
return res.send(segmentResponse);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
/* istanbul ignore next */
|
||||||
if (err instanceof SyntaxError) {
|
if (err instanceof SyntaxError) {
|
||||||
return res.status(400).send("Invalid array in parameters");
|
return res.status(400).send("Invalid array in parameters");
|
||||||
} else return res.sendStatus(500);
|
} else return res.sendStatus(500);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const isValidSegmentUUID = (str: string): boolean => /^([a-f0-9]{64}|[a-f0-9]{8}
|
|||||||
async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise<DBSegment> {
|
async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise<DBSegment> {
|
||||||
try {
|
try {
|
||||||
return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
|
|||||||
//send result
|
//send result
|
||||||
return res.send(DBSegments);
|
return res.send(DBSegments);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
if (err instanceof SyntaxError) { // catch JSON.parse error
|
if (err instanceof SyntaxError) { // catch JSON.parse error
|
||||||
return res.status(400).send("UUIDs parameter does not match format requirements.");
|
return res.status(400).send("UUIDs parameter does not match format requirements.");
|
||||||
} else return res.sendStatus(500);
|
} else return res.sendStatus(500);
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return processedSegments;
|
return processedSegments;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
if (err) {
|
if (err) {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return null;
|
return null;
|
||||||
@@ -169,7 +169,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -465,7 +465,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
|
|||||||
//send result
|
//send result
|
||||||
return res.send(segments);
|
return res.send(segments);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
if (err instanceof SyntaxError) {
|
if (err instanceof SyntaxError) {
|
||||||
return res.status(400).send("Categories parameter does not match format requirements.");
|
return res.status(400).send("Categories parameter does not match format requirements.");
|
||||||
} else return res.sendStatus(500);
|
} else return res.sendStatus(500);
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
|
|||||||
// Get all video id's that match hash prefix
|
// Get all video id's that match hash prefix
|
||||||
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);
|
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);
|
||||||
|
|
||||||
if (!segments) return res.status(404).json([]);
|
|
||||||
|
|
||||||
const output = Object.entries(segments).map(([videoID, data]) => ({
|
const output = Object.entries(segments).map(([videoID, data]) => ({
|
||||||
videoID,
|
videoID,
|
||||||
hash: data.hash,
|
hash: data.hash,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { db } from "../databases/databases";
|
|||||||
import { Logger } from "../utils/logger";
|
import { Logger } from "../utils/logger";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import redis from "../utils/redis";
|
import redis, { getRedisActiveRequests } from "../utils/redis";
|
||||||
import { promiseOrTimeout } from "../utils/promise";
|
import { promiseOrTimeout } from "../utils/promise";
|
||||||
|
import { Postgres } from "../databases/Postgres";
|
||||||
|
|
||||||
export async function getStatus(req: Request, res: Response): Promise<Response> {
|
export async function getStatus(req: Request, res: Response): Promise<Response> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -11,21 +12,23 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
|
|||||||
value = Array.isArray(value) ? value[0] : value;
|
value = Array.isArray(value) ? value[0] : value;
|
||||||
let processTime, redisProcessTime = -1;
|
let processTime, redisProcessTime = -1;
|
||||||
try {
|
try {
|
||||||
|
const dbStartTime = Date.now();
|
||||||
const dbVersion = await promiseOrTimeout(db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"]), 5000)
|
const dbVersion = await promiseOrTimeout(db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"]), 5000)
|
||||||
.then(e => {
|
.then(e => {
|
||||||
processTime = Date.now() - startTime;
|
processTime = Date.now() - dbStartTime;
|
||||||
return e.value;
|
return e.value;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => /* istanbul ignore next */ {
|
||||||
Logger.error(`status: SQL query timed out: ${e}`);
|
Logger.error(`status: SQL query timed out: ${e}`);
|
||||||
return -1;
|
return -1;
|
||||||
});
|
});
|
||||||
let statusRequests: unknown = 0;
|
let statusRequests: unknown = 0;
|
||||||
|
const redisStartTime = Date.now();
|
||||||
const numberRequests = await promiseOrTimeout(redis.increment("statusRequest"), 5000)
|
const numberRequests = await promiseOrTimeout(redis.increment("statusRequest"), 5000)
|
||||||
.then(e => {
|
.then(e => {
|
||||||
redisProcessTime = Date.now() - startTime;
|
redisProcessTime = Date.now() - redisStartTime;
|
||||||
return e;
|
return e;
|
||||||
}).catch(e => {
|
}).catch(e => /* istanbul ignore next */ {
|
||||||
Logger.error(`status: redis increment timed out ${e}`);
|
Logger.error(`status: redis increment timed out ${e}`);
|
||||||
return [-1];
|
return [-1];
|
||||||
});
|
});
|
||||||
@@ -33,17 +36,19 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
|
|||||||
|
|
||||||
const statusValues: Record<string, any> = {
|
const statusValues: Record<string, any> = {
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
commit: (global as any).HEADCOMMIT || "unknown",
|
commit: (global as any)?.HEADCOMMIT ?? "unknown",
|
||||||
db: Number(dbVersion),
|
db: Number(dbVersion),
|
||||||
startTime,
|
startTime,
|
||||||
processTime,
|
processTime,
|
||||||
redisProcessTime,
|
redisProcessTime,
|
||||||
loadavg: os.loadavg().slice(1), // only return 5 & 15 minute load average
|
loadavg: os.loadavg().slice(1), // only return 5 & 15 minute load average
|
||||||
statusRequests,
|
statusRequests,
|
||||||
hostname: os.hostname()
|
hostname: os.hostname(),
|
||||||
|
activePostgresRequests: (db as Postgres)?.activePostgresRequests,
|
||||||
|
activeRedisRequests: getRedisActiveRequests(),
|
||||||
};
|
};
|
||||||
return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues);
|
return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async function generateTopCategoryUsersStats(sortBy: string, category: string) {
|
|||||||
SUM("votes") as "userVotes", COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
|
SUM("votes") as "userVotes", COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
|
||||||
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
|
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
|
||||||
WHERE "sponsorTimes"."category" = ? AND "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "shadowBannedUsers"."userID" IS NULL
|
WHERE "sponsorTimes"."category" = ? AND "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "shadowBannedUsers"."userID" IS NULL
|
||||||
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 20
|
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 2
|
||||||
ORDER BY "${sortBy}" DESC LIMIT 100`, [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, category]);
|
ORDER BY "${sortBy}" DESC LIMIT 100`, [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, category]);
|
||||||
|
|
||||||
if (rows) {
|
if (rows) {
|
||||||
|
|||||||
@@ -28,14 +28,15 @@ async function generateTopUsersStats(sortBy: string, categoryStatsEnabled = fals
|
|||||||
SUM(CASE WHEN category = 'poi_highlight' THEN 1 ELSE 0 END) as "categorySumHighlight",
|
SUM(CASE WHEN category = 'poi_highlight' THEN 1 ELSE 0 END) as "categorySumHighlight",
|
||||||
SUM(CASE WHEN category = 'filler' THEN 1 ELSE 0 END) as "categorySumFiller",
|
SUM(CASE WHEN category = 'filler' THEN 1 ELSE 0 END) as "categorySumFiller",
|
||||||
SUM(CASE WHEN category = 'exclusive_access' THEN 1 ELSE 0 END) as "categorySumExclusiveAccess",
|
SUM(CASE WHEN category = 'exclusive_access' THEN 1 ELSE 0 END) as "categorySumExclusiveAccess",
|
||||||
|
SUM(CASE WHEN category = 'chapter' THEN 1 ELSE 0 END) as "categorySumChapter",
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await db.prepare("all", `SELECT COUNT(*) as "totalSubmissions", SUM(views) as "viewCount",
|
const rows = await db.prepare("all", `SELECT COUNT(*) as "totalSubmissions", SUM(views) as "viewCount",
|
||||||
SUM(((CASE WHEN "sponsorTimes"."endTime" - "sponsorTimes"."startTime" > ? THEN ? ELSE "sponsorTimes"."endTime" - "sponsorTimes"."startTime" END) / 60) * "sponsorTimes"."views") as "minutesSaved",
|
SUM(CASE WHEN "sponsorTimes"."actionType" = 'chapter' THEN 0 ELSE ((CASE WHEN "sponsorTimes"."endTime" - "sponsorTimes"."startTime" > ? THEN ? ELSE "sponsorTimes"."endTime" - "sponsorTimes"."startTime" END) / 60) * "sponsorTimes"."views" END) as "minutesSaved",
|
||||||
SUM("votes") as "userVotes", ${additionalFields} COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
|
SUM("votes") as "userVotes", ${additionalFields} COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
|
||||||
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
|
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
|
||||||
WHERE "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "sponsorTimes"."actionType" != 'chapter' AND "shadowBannedUsers"."userID" IS NULL
|
WHERE "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "shadowBannedUsers"."userID" IS NULL
|
||||||
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 20
|
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 20
|
||||||
ORDER BY "${sortBy}" DESC LIMIT 100`, [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds]);
|
ORDER BY "${sortBy}" DESC LIMIT 100`, [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds]);
|
||||||
|
|
||||||
@@ -55,7 +56,8 @@ async function generateTopUsersStats(sortBy: string, categoryStatsEnabled = fals
|
|||||||
row.categorySumPreview,
|
row.categorySumPreview,
|
||||||
row.categorySumHighlight,
|
row.categorySumHighlight,
|
||||||
row.categorySumFiller,
|
row.categorySumFiller,
|
||||||
row.categorySumExclusiveAccess
|
row.categorySumExclusiveAccess,
|
||||||
|
row.categorySumChapter
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,11 +75,6 @@ export async function getTopUsers(req: Request, res: Response): Promise<Response
|
|||||||
const sortType = parseInt(req.query.sortType as string);
|
const sortType = parseInt(req.query.sortType as string);
|
||||||
const categoryStatsEnabled = req.query.categoryStats;
|
const categoryStatsEnabled = req.query.categoryStats;
|
||||||
|
|
||||||
if (sortType == undefined) {
|
|
||||||
//invalid request
|
|
||||||
return res.sendStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
//setup which sort type to use
|
//setup which sort type to use
|
||||||
let sortBy = "";
|
let sortBy = "";
|
||||||
if (sortType == 0) {
|
if (sortType == 0) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
|
|||||||
try {
|
try {
|
||||||
return db.prepare("all", `SELECT "userName", "userID" FROM "userNames" WHERE "userName"
|
return db.prepare("all", `SELECT "userName", "userID" FROM "userNames" WHERE "userName"
|
||||||
LIKE ? ESCAPE '\\' LIMIT 10`, [userName]);
|
LIKE ? ESCAPE '\\' LIMIT 10`, [userName]);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
|
|||||||
function getExactUserID(userName: string): Promise<{userName: string, userID: UserID }[]> {
|
function getExactUserID(userName: string): Promise<{userName: string, userID: UserID }[]> {
|
||||||
try {
|
try {
|
||||||
return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]);
|
return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@ export async function getUserID(req: Request, res: Response): Promise<Response>
|
|||||||
: await getFuzzyUserID(userName);
|
: await getFuzzyUserID(userName);
|
||||||
|
|
||||||
if (results === undefined || results === null) {
|
if (results === undefined || results === null) {
|
||||||
|
/* istanbul ignore next */
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
} else if (results.length === 0) {
|
} else if (results.length === 0) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
|
|||||||
segmentCount: 0,
|
segmentCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise<number> {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
|
||||||
return row?.ignoredSegmentCount ?? 0;
|
return row?.ignoredSegmentCount ?? 0;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ async function dbGetUsername(userID: HashedUserID) {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||||
return row?.userName ?? userID;
|
return row?.userName ?? userID;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ async function dbGetViewsForUser(userID: HashedUserID) {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID], { useReplica: true });
|
||||||
return row?.viewCount ?? 0;
|
return row?.viewCount ?? 0;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
|
||||||
return row?.ignoredViewCount ?? 0;
|
return row?.ignoredViewCount ?? 0;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID], { useReplica: true });
|
||||||
return row?.total ?? 0;
|
return row?.total ?? 0;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
|
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUI
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID], { useReplica: true });
|
||||||
return row?.UUID ?? null;
|
return row?.UUID ?? null;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<st
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID], { useReplica: true });
|
||||||
return row?.reason ?? "";
|
return row?.reason ?? "";
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
|
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true });
|
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true });
|
||||||
return row?.userCount > 0 ?? false;
|
return row?.userCount > 0 ?? false;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,8 +118,7 @@ async function getPermissions(userID: HashedUserID): Promise<Record<string, bool
|
|||||||
|
|
||||||
async function getFreeChaptersAccess(userID: HashedUserID): Promise<boolean> {
|
async function getFreeChaptersAccess(userID: HashedUserID): Promise<boolean> {
|
||||||
return await oneOf([isUserVIP(userID),
|
return await oneOf([isUserVIP(userID),
|
||||||
(async () => !!(await db.prepare("get", `SELECT "timeSubmitted" FROM "sponsorTimes" WHERE "reputation" > 0 AND "timeSubmitted" < 1663872563000 AND "userID" = ? LIMIT 1`, [userID], { useReplica: true })))(),
|
(async () => !!(await db.prepare("get", `SELECT "timeSubmitted" FROM "sponsorTimes" WHERE "timeSubmitted" < 1666126187000 AND "userID" = ? LIMIT 1`, [userID], { useReplica: true })))()
|
||||||
(async () => !!(await db.prepare("get", `SELECT "timeSubmitted" FROM "sponsorTimes" WHERE "timeSubmitted" < 1590969600000 AND "userID" = ? LIMIT 1`, [userID], { useReplica: true })))()
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +194,7 @@ async function getUserInfo(req: Request, res: Response): Promise<Response> {
|
|||||||
export async function endpoint(req: Request, res: Response): Promise<Response> {
|
export async function endpoint(req: Request, res: Response): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
return await getUserInfo(req, res);
|
return await getUserInfo(req, res);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
if (err instanceof SyntaxError) { // catch JSON.parse error
|
if (err instanceof SyntaxError) { // catch JSON.parse error
|
||||||
return res.status(400).send("Invalid values JSON");
|
return res.status(400).send("Invalid values JSON");
|
||||||
} else return res.sendStatus(500);
|
} else return res.sendStatus(500);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ async function dbGetUsername(userID: HashedUserID) {
|
|||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||||
return row?.userName ?? userID;
|
return row?.userName ?? userID;
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getUsername(req: Request, res: Response): Promise<Response
|
|||||||
userName: userID,
|
userName: userID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise<Resp
|
|||||||
} else {
|
} else {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
|
|||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: `Cache cleared on video ${videoID}`
|
message: `Cache cleared on video ${videoID}`
|
||||||
});
|
});
|
||||||
} catch(err) {
|
} catch(err) /* istanbul ignore next */ {
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
|||||||
|
|
||||||
if (!userIsVIP) {
|
if (!userIsVIP) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
message: "Must be a VIP to mark videos.",
|
message: "Must be a VIP to lock videos.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
|||||||
for (const lock of locksToApply) {
|
for (const lock of locksToApply) {
|
||||||
try {
|
try {
|
||||||
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "actionType", "category", "hashedVideoID", "reason", "service") VALUES(?, ?, ?, ?, ?, ?, ?)`, [videoID, userID, lock.actionType, lock.category, hashedVideoID, reason, service]);
|
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "actionType", "category", "hashedVideoID", "reason", "service") VALUES(?, ?, ?, ?, ?, ?, ?)`, [videoID, userID, lock.actionType, lock.category, hashedVideoID, reason, service]);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
|
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -82,7 +82,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
|||||||
await db.prepare("run",
|
await db.prepare("run",
|
||||||
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "actionType" = ? AND "category" = ? AND "service" = ?',
|
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "actionType" = ? AND "category" = ? AND "service" = ?',
|
||||||
[reason, userID, videoID, lock.actionType, lock.category, service]);
|
[reason, userID, videoID, lock.actionType, lock.category, service]);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
|
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function postPurgeAllSegments(req: Request, res: Response): Promise
|
|||||||
service
|
service
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export async function postSegmentShift(req: Request, res: Response): Promise<Res
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ async function checkByAutoModerator(videoID: any, userID: any, segments: Array<a
|
|||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
errorCode: 403,
|
errorCode: 403,
|
||||||
errorMessage: `Request rejected by auto moderator: ${autoModerateResult} If this is an issue, send a message on Discord.`
|
errorMessage: `Hi, currently there are server issues and you might have not recieved segments even though they exist. Sorry about this, I'm working on it. Request rejected by auto moderator: ${autoModerateResult} If this is an issue, send a message on Discord.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
|||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) /* istanbul ignore next */ {
|
||||||
Logger.error(error as string);
|
Logger.error(error as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
|||||||
await logUserNameChange(userID, userName, oldUserName, adminUserIDInput !== undefined);
|
await logUserNameChange(userID, userName, oldUserName, adminUserIDInput !== undefined);
|
||||||
|
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
} catch (err) {
|
} catch (err) /* istanbul ignore next */ {
|
||||||
Logger.error(err as string);
|
Logger.error(err as string);
|
||||||
return res.sendStatus(500);
|
return res.sendStatus(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { config } from "../config";
|
|||||||
import { privateDB } from "../databases/databases";
|
import { privateDB } from "../databases/databases";
|
||||||
import { Logger } from "../utils/logger";
|
import { Logger } from "../utils/logger";
|
||||||
import { getPatreonIdentity, PatronStatus, refreshToken, TokenType } from "../utils/tokenUtils";
|
import { getPatreonIdentity, PatronStatus, refreshToken, TokenType } from "../utils/tokenUtils";
|
||||||
import FormData from "form-data";
|
|
||||||
|
|
||||||
interface VerifyTokenRequest extends Request {
|
interface VerifyTokenRequest extends Request {
|
||||||
query: {
|
query: {
|
||||||
@@ -12,14 +11,16 @@ interface VerifyTokenRequest extends Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validatelicenseKeyRegex = (token: string) =>
|
||||||
|
new RegExp(/[A-Za-z0-9]{40}|[A-Za-z0-9-]{35}/).test(token);
|
||||||
|
|
||||||
export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise<Response> {
|
export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise<Response> {
|
||||||
const { query: { licenseKey } } = req;
|
const { query: { licenseKey } } = req;
|
||||||
|
|
||||||
if (!licenseKey) {
|
if (!licenseKey) {
|
||||||
return res.status(400).send("Invalid request");
|
return res.status(400).send("Invalid request");
|
||||||
}
|
} else if (!validatelicenseKeyRegex(licenseKey)) {
|
||||||
const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/);
|
// fast check for invalid licence key
|
||||||
if (!licenseRegex.test(licenseKey)) {
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
allowed: false
|
allowed: false
|
||||||
});
|
});
|
||||||
@@ -34,6 +35,7 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
|
|||||||
refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error);
|
refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore else */
|
||||||
if (identity) {
|
if (identity) {
|
||||||
const membership = identity.included?.[0]?.attributes;
|
const membership = identity.included?.[0]?.attributes;
|
||||||
const allowed = !!membership && ((membership.patron_status === PatronStatus.active && membership.currently_entitled_amount_cents > 0)
|
const allowed = !!membership && ((membership.patron_status === PatronStatus.active && membership.currently_entitled_amount_cents > 0)
|
||||||
@@ -65,20 +67,13 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
|
|||||||
async function checkAllGumroadProducts(licenseKey: string): Promise<boolean> {
|
async function checkAllGumroadProducts(licenseKey: string): Promise<boolean> {
|
||||||
for (const link of config.gumroad.productPermalinks) {
|
for (const link of config.gumroad.productPermalinks) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const result = await axios.post("https://api.gumroad.com/v2/licenses/verify", {
|
||||||
formData.append("product_permalink", link);
|
params: { product_permalink: link, license_key: licenseKey }
|
||||||
formData.append("license_key", licenseKey);
|
|
||||||
|
|
||||||
const result = await axios.request({
|
|
||||||
url: "https://api.gumroad.com/v2/licenses/verify",
|
|
||||||
data: formData,
|
|
||||||
method: "POST",
|
|
||||||
headers: formData.getHeaders()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const allowed = result.status === 200 && result.data?.success;
|
const allowed = result.status === 200 && result.data?.success;
|
||||||
if (allowed) return allowed;
|
if (allowed) return allowed;
|
||||||
} catch (e) {
|
} catch (e) /* istanbul ignore next */ {
|
||||||
Logger.error(`Gumroad fetch for ${link} failed: ${e}`);
|
Logger.error(`Gumroad fetch for ${link} failed: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID
|
|||||||
import { QueryCacher } from "../utils/queryCacher";
|
import { QueryCacher } from "../utils/queryCacher";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
|
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
|
||||||
|
import { deleteLockCategories } from "./deleteLockCategories";
|
||||||
|
|
||||||
const voteTypes = {
|
const voteTypes = {
|
||||||
normal: 0,
|
normal: 0,
|
||||||
@@ -59,7 +60,7 @@ async function updateSegmentVideoDuration(UUID: SegmentUUID) {
|
|||||||
let apiVideoDetails: videoDetails = null;
|
let apiVideoDetails: videoDetails = null;
|
||||||
if (service == Service.YouTube) {
|
if (service == Service.YouTube) {
|
||||||
// don't use cache since we have no information about the video length
|
// don't use cache since we have no information about the video length
|
||||||
apiVideoDetails = await getVideoDetails(videoID);
|
apiVideoDetails = await getVideoDetails(videoID, true);
|
||||||
}
|
}
|
||||||
const apiVideoDuration = apiVideoDetails?.duration as VideoDuration;
|
const apiVideoDuration = apiVideoDetails?.duration as VideoDuration;
|
||||||
if (videoDurationChanged(videoDuration, apiVideoDuration)) {
|
if (videoDurationChanged(videoDuration, apiVideoDuration)) {
|
||||||
@@ -95,6 +96,7 @@ async function checkVideoDuration(UUID: SegmentUUID) {
|
|||||||
AND "hidden" = 0 AND "shadowHidden" = 0 AND
|
AND "hidden" = 0 AND "shadowHidden" = 0 AND
|
||||||
"actionType" != 'full' AND "votes" > -2`,
|
"actionType" != 'full' AND "votes" > -2`,
|
||||||
[videoID, service, latestSubmission.timeSubmitted]);
|
[videoID, service, latestSubmission.timeSubmitted]);
|
||||||
|
deleteLockCategories(videoID, null, null, service).catch(Logger.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +221,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||||||
[UUID], { useReplica: true })) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
|
[UUID], { useReplica: true })) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
|
||||||
|
|
||||||
if (!config.categorySupport[category]?.includes(segmentInfo.actionType) || segmentInfo.actionType === ActionType.Full) {
|
if (!config.categorySupport[category]?.includes(segmentInfo.actionType) || segmentInfo.actionType === ActionType.Full) {
|
||||||
return { status: 400, message: `Not allowed to change to ${category} when for segment of type ${segmentInfo.actionType}`};
|
return { status: 400, message: `Not allowed to change to ${category} when for segment of type ${segmentInfo.actionType}` };
|
||||||
}
|
}
|
||||||
if (!config.categoryList.includes(category)) {
|
if (!config.categoryList.includes(category)) {
|
||||||
return { status: 400, message: "Category doesn't exist." };
|
return { status: 400, message: "Category doesn't exist." };
|
||||||
|
|||||||
@@ -7,16 +7,21 @@ interface RedisConfig extends redis.RedisClientOptions {
|
|||||||
getTimeout: number;
|
getTimeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RedisReadOnlyConfig extends redis.RedisClientOptions {
|
||||||
|
enabled: boolean;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomPostgresConfig extends PoolConfig {
|
export interface CustomPostgresConfig extends PoolConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
maxTries: number;
|
maxTries: number;
|
||||||
maxConcurrentRequests: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomPostgresReadOnlyConfig extends CustomPostgresConfig {
|
export interface CustomPostgresReadOnlyConfig extends CustomPostgresConfig {
|
||||||
weight: number;
|
weight: number;
|
||||||
readTimeout: number;
|
readTimeout: number;
|
||||||
fallbackOnFail: boolean;
|
fallbackOnFail: boolean;
|
||||||
|
stopRetryThreshold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SBSConfig {
|
export interface SBSConfig {
|
||||||
@@ -61,6 +66,8 @@ export interface SBSConfig {
|
|||||||
minimumPrefix?: string;
|
minimumPrefix?: string;
|
||||||
maximumPrefix?: string;
|
maximumPrefix?: string;
|
||||||
redis?: RedisConfig;
|
redis?: RedisConfig;
|
||||||
|
redisRead?: RedisReadOnlyConfig;
|
||||||
|
redisRateLimit: boolean;
|
||||||
maxRewardTimePerSegmentInSeconds?: number;
|
maxRewardTimePerSegmentInSeconds?: number;
|
||||||
postgres?: CustomPostgresConfig;
|
postgres?: CustomPostgresConfig;
|
||||||
postgresReadOnly?: CustomPostgresReadOnlyConfig;
|
postgresReadOnly?: CustomPostgresReadOnlyConfig;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { Request } from "express";
|
|||||||
import { IPAddress } from "../types/segments.model";
|
import { IPAddress } from "../types/segments.model";
|
||||||
|
|
||||||
export function getIP(req: Request): IPAddress {
|
export function getIP(req: Request): IPAddress {
|
||||||
|
// if in testing mode, return immediately
|
||||||
|
if (config.mode === "test") return "127.0.0.1" as IPAddress;
|
||||||
|
|
||||||
if (config.behindProxy === true || config.behindProxy === "true") {
|
if (config.behindProxy === true || config.behindProxy === "true") {
|
||||||
config.behindProxy = "X-Forwarded-For";
|
config.behindProxy = "X-Forwarded-For";
|
||||||
}
|
}
|
||||||
@@ -15,6 +18,6 @@ export function getIP(req: Request): IPAddress {
|
|||||||
case "X-Real-IP":
|
case "X-Real-IP":
|
||||||
return req.headers["x-real-ip"] as IPAddress;
|
return req.headers["x-real-ip"] as IPAddress;
|
||||||
default:
|
default:
|
||||||
return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress;
|
return req.socket?.remoteAddress as IPAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ async function getFromITube (videoID: string): Promise<innerTubeVideoDetails> {
|
|||||||
const result = await axios.post(url, data, {
|
const result = await axios.post(url, data, {
|
||||||
timeout: 3500
|
timeout: 3500
|
||||||
});
|
});
|
||||||
|
/* istanbul ignore else */
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
return result.data.videoDetails;
|
return result.data.videoDetails;
|
||||||
} else {
|
} else {
|
||||||
@@ -39,6 +40,7 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom
|
|||||||
return data as innerTubeVideoDetails;
|
return data as innerTubeVideoDetails;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
/* istanbul ignore next */
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { getReputation } from "./reputation";
|
|||||||
|
|
||||||
interface CanSubmitResult {
|
interface CanSubmitResult {
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
reason?: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function lowDownvotes(userID: HashedUserID): Promise<boolean> {
|
async function lowDownvotes(userID: HashedUserID): Promise<boolean> {
|
||||||
@@ -27,11 +27,13 @@ export async function canSubmit(userID: HashedUserID, category: Category): Promi
|
|||||||
lowDownvotes(userID),
|
lowDownvotes(userID),
|
||||||
(async () => (await getReputation(userID)) > config.minReputationToSubmitChapter)(),
|
(async () => (await getReputation(userID)) > config.minReputationToSubmitChapter)(),
|
||||||
hasFeature(userID, Feature.ChapterSubmitter)
|
hasFeature(userID, Feature.ChapterSubmitter)
|
||||||
])
|
]),
|
||||||
|
reason: "Submitting chapters requires a minimum reputation. You can ask on Discord/Matrix to get permission with less reputation."
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
canSubmit: true
|
canSubmit: true,
|
||||||
|
reason: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,19 +25,40 @@ let exportClient: RedisSB = {
|
|||||||
quit: () => new Promise((resolve) => resolve(null)),
|
quit: () => new Promise((resolve) => resolve(null)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lastClientFail = 0;
|
||||||
|
let lastReadFail = 0;
|
||||||
|
let activeRequests = 0;
|
||||||
|
|
||||||
if (config.redis?.enabled) {
|
if (config.redis?.enabled) {
|
||||||
Logger.info("Connected to redis");
|
Logger.info("Connected to redis");
|
||||||
const client = createClient(config.redis);
|
const client = createClient(config.redis);
|
||||||
|
const readClient = config.redisRead?.enabled ? createClient(config.redisRead) : null;
|
||||||
void client.connect(); // void as we don't care about the promise
|
void client.connect(); // void as we don't care about the promise
|
||||||
|
void readClient?.connect();
|
||||||
exportClient = client as RedisSB;
|
exportClient = client as RedisSB;
|
||||||
|
|
||||||
|
|
||||||
const get = client.get.bind(client);
|
const get = client.get.bind(client);
|
||||||
|
const getRead = readClient?.get?.bind(readClient);
|
||||||
exportClient.get = (key) => new Promise((resolve, reject) => {
|
exportClient.get = (key) => new Promise((resolve, reject) => {
|
||||||
|
activeRequests++;
|
||||||
const timeout = config.redis.getTimeout ? setTimeout(() => reject(), config.redis.getTimeout) : null;
|
const timeout = config.redis.getTimeout ? setTimeout(() => reject(), config.redis.getTimeout) : null;
|
||||||
get(key).then((reply) => {
|
const chosenGet = pickChoice(get, getRead);
|
||||||
|
chosenGet(key).then((reply) => {
|
||||||
if (timeout !== null) clearTimeout(timeout);
|
if (timeout !== null) clearTimeout(timeout);
|
||||||
|
|
||||||
|
activeRequests--;
|
||||||
resolve(reply);
|
resolve(reply);
|
||||||
}).catch((err) => reject(err));
|
}).catch((err) => {
|
||||||
|
if (chosenGet === get) {
|
||||||
|
lastClientFail = Date.now();
|
||||||
|
} else {
|
||||||
|
lastReadFail = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRequests--;
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
exportClient.increment = (key) => new Promise((resolve, reject) =>
|
exportClient.increment = (key) => new Promise((resolve, reject) =>
|
||||||
void client.multi()
|
void client.multi()
|
||||||
@@ -48,11 +69,35 @@ if (config.redis?.enabled) {
|
|||||||
.catch((err) => reject(err))
|
.catch((err) => reject(err))
|
||||||
);
|
);
|
||||||
client.on("error", function(error) {
|
client.on("error", function(error) {
|
||||||
|
lastClientFail = Date.now();
|
||||||
Logger.error(`Redis Error: ${error}`);
|
Logger.error(`Redis Error: ${error}`);
|
||||||
});
|
});
|
||||||
client.on("reconnect", () => {
|
client.on("reconnect", () => {
|
||||||
Logger.info("Redis: trying to reconnect");
|
Logger.info("Redis: trying to reconnect");
|
||||||
});
|
});
|
||||||
|
readClient?.on("error", function(error) {
|
||||||
|
lastReadFail = Date.now();
|
||||||
|
Logger.error(`Redis Read-Only Error: ${error}`);
|
||||||
|
});
|
||||||
|
readClient?.on("reconnect", () => {
|
||||||
|
Logger.info("Redis Read-Only: trying to reconnect");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickChoice<T>(client: T, readClient: T): T {
|
||||||
|
const readAvailable = !!readClient;
|
||||||
|
const ignoreReadDueToFailure = lastReadFail > Date.now() - 1000 * 30;
|
||||||
|
const readDueToFailure = lastClientFail > Date.now() - 1000 * 30;
|
||||||
|
if (readAvailable && !ignoreReadDueToFailure && (readDueToFailure ||
|
||||||
|
Math.random() > 1 / (config.redisRead?.weight + 1))) {
|
||||||
|
return readClient;
|
||||||
|
} else {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRedisActiveRequests(): number {
|
||||||
|
return activeRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default exportClient;
|
export default exportClient;
|
||||||
|
|||||||
@@ -58,12 +58,11 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis
|
|||||||
|
|
||||||
return licenseKey;
|
return licenseKey;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
break;
|
||||||
|
} catch (e) /* istanbul ignore next */ {
|
||||||
Logger.error(`token creation: ${e}`);
|
Logger.error(`token creation: ${e}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case TokenType.local: {
|
case TokenType.local: {
|
||||||
const licenseKey = generateToken();
|
const licenseKey = generateToken();
|
||||||
@@ -74,7 +73,6 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis
|
|||||||
return licenseKey;
|
return licenseKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,15 +100,12 @@ export async function refreshToken(type: TokenType, licenseKey: string, refreshT
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) /* istanbul ignore next */ {
|
||||||
Logger.error(`token refresh: ${e}`);
|
Logger.error(`token refresh: ${e}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +131,8 @@ export async function getPatreonIdentity(accessToken: string): Promise<PatreonId
|
|||||||
if (identityRequest.status === 200) {
|
if (identityRequest.status === 200) {
|
||||||
return identityRequest.data;
|
return identityRequest.data;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) /* istanbul ignore next */ {
|
||||||
Logger.error(`identity request: ${e}`);
|
Logger.error(`identity request: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
14
test.json
14
test.json
@@ -4,11 +4,12 @@
|
|||||||
"globalSalt": "testSalt",
|
"globalSalt": "testSalt",
|
||||||
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
|
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
|
||||||
"newLeafURLs": ["placeholder"],
|
"newLeafURLs": ["placeholder"],
|
||||||
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook",
|
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel",
|
||||||
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook",
|
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions",
|
||||||
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook",
|
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport",
|
||||||
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook",
|
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject",
|
||||||
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
|
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
|
||||||
|
"userCounterURL": "http://127.0.0.1:8081/UserCounter",
|
||||||
"behindProxy": true,
|
"behindProxy": true,
|
||||||
"db": ":memory:",
|
"db": ":memory:",
|
||||||
"privateDB": ":memory:",
|
"privateDB": ":memory:",
|
||||||
@@ -58,5 +59,10 @@
|
|||||||
"statusCode": 200
|
"statusCode": 200
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"patreon": {
|
||||||
|
"clientId": "testClientID",
|
||||||
|
"clientSecret": "testClientSecret",
|
||||||
|
"redirectUri": "http://127.0.0.1/fake/callback"
|
||||||
|
},
|
||||||
"minReputationToSubmitFiller": -1
|
"minReputationToSubmitFiller": -1
|
||||||
}
|
}
|
||||||
|
|||||||
141
test/cases/addUserAsVIP.ts
Normal file
141
test/cases/addUserAsVIP.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
import { HashedUserID } from "../../src/types/user.model";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
const checkUserVIP = (publicID: string) => db.prepare("get", `SELECT "userID" FROM "vipUsers" WHERE "userID" = ?`, [publicID]);
|
||||||
|
|
||||||
|
const adminPrivateUserID = "testUserId";
|
||||||
|
const permVIP1 = "addVIP_permaVIPOne";
|
||||||
|
const publicPermVIP1 = getHash(permVIP1) as HashedUserID;
|
||||||
|
const permVIP2 = "addVIP_permaVIPTwo";
|
||||||
|
const publicPermVIP2 = getHash(permVIP2) as HashedUserID;
|
||||||
|
const permVIP3 = "addVIP_permaVIPThree";
|
||||||
|
const publicPermVIP3 = getHash(permVIP3) as HashedUserID;
|
||||||
|
|
||||||
|
const endpoint = "/api/addUserAsVIP";
|
||||||
|
const addUserAsVIP = (userID: string, enabled: boolean, adminUserID = adminPrivateUserID) => client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
userID,
|
||||||
|
adminUserID,
|
||||||
|
enabled: String(enabled)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addVIP test", function() {
|
||||||
|
it("User should not already be VIP", (done) => {
|
||||||
|
checkUserVIP(publicPermVIP1)
|
||||||
|
.then(result => {
|
||||||
|
assert.ok(!result);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should be able to add user as VIP", (done) => {
|
||||||
|
addUserAsVIP(publicPermVIP1, true)
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const row = await checkUserVIP(publicPermVIP1);
|
||||||
|
assert.ok(row);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should be able to add second user as VIP", (done) => {
|
||||||
|
addUserAsVIP(publicPermVIP2, true)
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const row = await checkUserVIP(publicPermVIP2);
|
||||||
|
assert.ok(row);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should return 403 with invalid adminID", (done) => {
|
||||||
|
addUserAsVIP(publicPermVIP1, true, "Invalid_Admin_User_ID")
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should return 400 with missing adminID", (done) => {
|
||||||
|
client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
userID: publicPermVIP1,
|
||||||
|
enabled: String(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should return 400 with missing userID", (done) => {
|
||||||
|
client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
enabled: String(true),
|
||||||
|
adminUserID: adminPrivateUserID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should be able to remove VIP", (done) => {
|
||||||
|
addUserAsVIP(publicPermVIP1, false)
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const row = await checkUserVIP(publicPermVIP1);
|
||||||
|
assert.ok(!row);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should remove VIP if enabled is false", (done) => {
|
||||||
|
client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
userID: publicPermVIP2,
|
||||||
|
adminUserID: adminPrivateUserID,
|
||||||
|
enabled: "invalid-text"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const row = await checkUserVIP(publicPermVIP2);
|
||||||
|
assert.ok(!row);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should remove VIP if enabled is missing", (done) => {
|
||||||
|
client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
userID: publicPermVIP3,
|
||||||
|
adminUserID: adminPrivateUserID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const row = await checkUserVIP(publicPermVIP3);
|
||||||
|
assert.ok(!row);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
189
test/cases/generateVerifyToken.ts
Normal file
189
test/cases/generateVerifyToken.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { config } from "../../src/config";
|
||||||
|
import axios from "axios";
|
||||||
|
import { createAndSaveToken, TokenType } from "../../src/utils/tokenUtils";
|
||||||
|
import MockAdapter from "axios-mock-adapter";
|
||||||
|
let mock: MockAdapter;
|
||||||
|
import * as patreon from "../mocks/patreonMock";
|
||||||
|
import * as gumroad from "../mocks/gumroadMock";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import { validatelicenseKeyRegex } from "../../src/routes/verifyToken";
|
||||||
|
|
||||||
|
const generateEndpoint = "/api/generateToken";
|
||||||
|
const getGenerateToken = (type: string, code: string | null, adminUserID: string | null) => client({
|
||||||
|
url: `${generateEndpoint}/${type}`,
|
||||||
|
params: { code, adminUserID }
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyEndpoint = "/api/verifyToken";
|
||||||
|
const getVerifyToken = (licenseKey: string | null) => client({
|
||||||
|
url: verifyEndpoint,
|
||||||
|
params: { licenseKey }
|
||||||
|
});
|
||||||
|
|
||||||
|
let patreonLicense: string;
|
||||||
|
let localLicense: string;
|
||||||
|
const gumroadLicense = gumroad.generateLicense();
|
||||||
|
|
||||||
|
const extractLicenseKey = (data: string) => {
|
||||||
|
const regex = /([A-Za-z0-9]{40})/;
|
||||||
|
const match = data.match(regex);
|
||||||
|
if (!match) throw new Error("Failed to extract license key");
|
||||||
|
return match[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("generateToken test", function() {
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
|
||||||
|
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to create patreon token for active patron", function (done) {
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
getGenerateToken("patreon", "patreon_code", "").then(res => {
|
||||||
|
patreonLicense = extractLicenseKey(res.data);
|
||||||
|
assert.ok(validatelicenseKeyRegex(patreonLicense));
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should create patreon token for invalid patron", function (done) {
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.formerIdentityFail);
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
getGenerateToken("patreon", "patreon_code", "").then(res => {
|
||||||
|
patreonLicense = extractLicenseKey(res.data);
|
||||||
|
assert.ok(validatelicenseKeyRegex(patreonLicense));
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to create new local token", function (done) {
|
||||||
|
createAndSaveToken(TokenType.local).then((licenseKey) => {
|
||||||
|
assert.ok(validatelicenseKeyRegex(licenseKey));
|
||||||
|
localLicense = licenseKey;
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if missing code parameter", function (done) {
|
||||||
|
getGenerateToken("patreon", null, "").then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 403 if missing adminuserID parameter", function (done) {
|
||||||
|
getGenerateToken("local", "fake-code", null).then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 403 for invalid adminuserID parameter", function (done) {
|
||||||
|
getGenerateToken("local", "fake-code", "fakeAdminID").then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("verifyToken static tests", function() {
|
||||||
|
it("Should fast reject invalid token", function (done) {
|
||||||
|
getVerifyToken("00000").then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(!res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if missing code token", function (done) {
|
||||||
|
getVerifyToken(null).then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("verifyToken mock tests", function() {
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
|
||||||
|
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should accept current patron", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
|
||||||
|
getVerifyToken(patreonLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should reject nonexistent patron", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.invalidIdentity);
|
||||||
|
getVerifyToken(patreonLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(!res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should accept qualitying former patron", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.formerIdentitySucceed);
|
||||||
|
getVerifyToken(patreonLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should reject unqualitifed former patron", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.formerIdentityFail);
|
||||||
|
getVerifyToken(patreonLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(!res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should accept real gumroad key", function (done) {
|
||||||
|
mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseSuccess);
|
||||||
|
getVerifyToken(gumroadLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should reject fake gumroad key", function (done) {
|
||||||
|
mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseFail);
|
||||||
|
getVerifyToken(gumroadLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(!res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should validate local license", function (done) {
|
||||||
|
getVerifyToken(localLicense).then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.ok(res.data.allowed);
|
||||||
|
done();
|
||||||
|
}).catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
27
test/cases/getDaysSavedFormatted.ts
Normal file
27
test/cases/getDaysSavedFormatted.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import sinon from "sinon";
|
||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
|
||||||
|
const endpoint = "/api/getDaysSavedFormatted";
|
||||||
|
|
||||||
|
describe("getDaysSavedFormatted", () => {
|
||||||
|
it("can get days saved", async () => {
|
||||||
|
const result = await client({ url: endpoint });
|
||||||
|
assert.ok(result.data.daysSaved >= 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 days saved if no segments", async () => {
|
||||||
|
const stub = sinon.stub(db, "prepare").resolves(undefined);
|
||||||
|
const result = await client({ url: endpoint });
|
||||||
|
assert.ok(result.data.daysSaved >= 0);
|
||||||
|
stub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns days saved to 2 fixed points", async () => {
|
||||||
|
const stub = sinon.stub(db, "prepare").resolves({ daysSaved: 1.23456789 });
|
||||||
|
const result = await client({ url: endpoint });
|
||||||
|
assert.strictEqual(result.data.daysSaved, "1.23");
|
||||||
|
stub.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
109
test/cases/getIP.ts
Normal file
109
test/cases/getIP.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import sinon from "sinon";
|
||||||
|
import { config } from "../../src/config";
|
||||||
|
import assert from "assert";
|
||||||
|
const mode = "production";
|
||||||
|
let stub: sinon.SinonStub;
|
||||||
|
let stub2: sinon.SinonStub;
|
||||||
|
import { createRequest } from "../mocks/mockExpressRequest";
|
||||||
|
import { getIP } from "../../src/utils/getIP";
|
||||||
|
|
||||||
|
const v4RequestOptions = {
|
||||||
|
headers: {
|
||||||
|
"x-forwarded-for": "127.0.1.1",
|
||||||
|
"cf-connecting-ip": "127.0.1.2",
|
||||||
|
"x-real-ip": "127.0.1.3",
|
||||||
|
},
|
||||||
|
ip: "127.0.1.5",
|
||||||
|
socket: {
|
||||||
|
remoteAddress: "127.0.1.4"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const v6RequestOptions = {
|
||||||
|
headers: {
|
||||||
|
"x-forwarded-for": "[100::1]",
|
||||||
|
"cf-connecting-ip": "[100::2]",
|
||||||
|
"x-real-ip": "[100::3]",
|
||||||
|
},
|
||||||
|
ip: "[100::5]",
|
||||||
|
socket: {
|
||||||
|
remoteAddress: "[100::4]"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const v4MockRequest = createRequest(v4RequestOptions);
|
||||||
|
const v6MockRequest = createRequest(v6RequestOptions);
|
||||||
|
|
||||||
|
const expectedIP4 = {
|
||||||
|
"X-Forwarded-For": "127.0.1.1",
|
||||||
|
"Cloudflare": "127.0.1.2",
|
||||||
|
"X-Real-IP": "127.0.1.3",
|
||||||
|
"default": "127.0.1.4",
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedIP6 = {
|
||||||
|
"X-Forwarded-For": "[100::1]",
|
||||||
|
"Cloudflare": "[100::2]",
|
||||||
|
"X-Real-IP": "[100::3]",
|
||||||
|
"default": "[100::4]",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("getIP stubs", () => {
|
||||||
|
before(() => stub = sinon.stub(config, "mode").value(mode));
|
||||||
|
after(() => stub.restore());
|
||||||
|
|
||||||
|
it("Should return production mode if stub worked", (done) => {
|
||||||
|
assert.strictEqual(config.mode, mode);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getIP array tests", () => {
|
||||||
|
beforeEach(() => stub = sinon.stub(config, "mode").value(mode));
|
||||||
|
afterEach(() => {
|
||||||
|
stub.restore();
|
||||||
|
stub2.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(expectedIP4)) {
|
||||||
|
it(`Should return correct IPv4 from ${key}`, (done) => {
|
||||||
|
stub2 = sinon.stub(config, "behindProxy").value(key);
|
||||||
|
const ip = getIP(v4MockRequest);
|
||||||
|
assert.strictEqual(config.behindProxy, key);
|
||||||
|
assert.strictEqual(ip, value);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(expectedIP6)) {
|
||||||
|
it(`Should return correct IPv6 from ${key}`, (done) => {
|
||||||
|
stub2 = sinon.stub(config, "behindProxy").value(key);
|
||||||
|
const ip = getIP(v6MockRequest);
|
||||||
|
assert.strictEqual(config.behindProxy, key);
|
||||||
|
assert.strictEqual(ip, value);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getIP true tests", () => {
|
||||||
|
before(() => stub = sinon.stub(config, "mode").value(mode));
|
||||||
|
after(() => {
|
||||||
|
stub.restore();
|
||||||
|
stub2.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`Should return correct IPv4 from with bool true`, (done) => {
|
||||||
|
stub2 = sinon.stub(config, "behindProxy").value(true);
|
||||||
|
const ip = getIP(v4MockRequest);
|
||||||
|
assert.strictEqual(config.behindProxy, "X-Forwarded-For");
|
||||||
|
assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`Should return correct IPv4 from with string true`, (done) => {
|
||||||
|
stub2 = sinon.stub(config, "behindProxy").value("true");
|
||||||
|
const ip = getIP(v4MockRequest);
|
||||||
|
assert.strictEqual(config.behindProxy, "X-Forwarded-For");
|
||||||
|
assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -166,17 +166,77 @@ describe("getLockCategoriesByHash", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should be able to get by actionType", (done) => {
|
it("should return 400 if invalid actionTypes", (done) => {
|
||||||
getLockCategories(fakeHash.substring(0,5), [ActionType.Full])
|
client.get(`${endpoint}/aaaa`, { params: { actionTypes: 3 } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 if invalid actionTypes JSON", (done) => {
|
||||||
|
client.get(`${endpoint}/aaaa`, { params: { actionTypes: "{3}" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get single lock", (done) => {
|
||||||
|
const videoID = "getLockHash2";
|
||||||
|
const hash = getHash(videoID, 1);
|
||||||
|
getLockCategories(hash.substring(0,6))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const expected = [{
|
const expected = [{
|
||||||
videoID: "fakehash-2",
|
videoID,
|
||||||
hash: fakeHash,
|
hash,
|
||||||
categories: [
|
categories: [
|
||||||
"sponsor"
|
"preview"
|
||||||
],
|
],
|
||||||
reason: "fake2-notshown"
|
reason: "2-reason"
|
||||||
|
}];
|
||||||
|
assert.deepStrictEqual(res.data, expected);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get by actionType not in array", (done) => {
|
||||||
|
const videoID = "getLockHash2";
|
||||||
|
const hash = getHash(videoID, 1);
|
||||||
|
client.get(`${endpoint}/${hash.substring(0,6)}`, { params: { actionType: ActionType.Skip } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const expected = [{
|
||||||
|
videoID,
|
||||||
|
hash,
|
||||||
|
categories: [
|
||||||
|
"preview"
|
||||||
|
],
|
||||||
|
reason: "2-reason"
|
||||||
|
}];
|
||||||
|
assert.deepStrictEqual(res.data, expected);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get by no actionType", (done) => {
|
||||||
|
const videoID = "getLockHash2";
|
||||||
|
const hash = getHash(videoID, 1);
|
||||||
|
client.get(`${endpoint}/${hash.substring(0,6)}`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const expected = [{
|
||||||
|
videoID,
|
||||||
|
hash,
|
||||||
|
categories: [
|
||||||
|
"preview"
|
||||||
|
],
|
||||||
|
reason: "2-reason"
|
||||||
}];
|
}];
|
||||||
assert.deepStrictEqual(res.data, expected);
|
assert.deepStrictEqual(res.data, expected);
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -55,6 +55,45 @@ describe("getLockReason", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should be able to get with actionTypes array", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionTypes: '["full"]' } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const expected = [
|
||||||
|
{ category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 }
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual(res.data, expected);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get with actionType", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: "full" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const expected = [
|
||||||
|
{ category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 }
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual(res.data, expected);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to get with actionType array", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: ["full"] } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const expected = [
|
||||||
|
{ category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 }
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual(res.data, expected);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
it("Should be able to get empty locks", (done) => {
|
it("Should be able to get empty locks", (done) => {
|
||||||
client.get(endpoint, { params: { videoID: "getLockReason", category: "intro" } })
|
client.get(endpoint, { params: { videoID: "getLockReason", category: "intro" } })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
@@ -118,8 +157,10 @@ describe("getLockReason", () => {
|
|||||||
})
|
})
|
||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should return 400 if no videoID specified", (done) => {
|
describe("getLockReason 400", () => {
|
||||||
|
it("Should return 400 with missing videoID", (done) => {
|
||||||
client.get(endpoint)
|
client.get(endpoint)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
@@ -128,15 +169,37 @@ describe("getLockReason", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to get by actionType", (done) => {
|
it("Should return 400 with invalid actionTypes ", (done) => {
|
||||||
client.get(endpoint, { params: { videoID: "getLockReason", actionType: "full" } })
|
client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: 3 } })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 400);
|
||||||
const expected = [
|
done();
|
||||||
{ category: "selfpromo", locked: 1, reason: "sponsor-reason", userID: vipUserID2, userName: vipUserName2 },
|
})
|
||||||
{ category: "sponsor", locked: 0, reason: "", userID: "", userName: "" }
|
.catch(err => done(err));
|
||||||
];
|
});
|
||||||
partialDeepEquals(res.data, expected);
|
|
||||||
|
it("Should return 400 with invalid actionTypes JSON ", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: "{3}" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 with invalid categories", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "valid-videoid", categories: 3 } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 with invalid categories JSON", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "valid-videoid", categories: "{3}" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
|
|||||||
@@ -2,22 +2,31 @@ import { db } from "../../src/databases/databases";
|
|||||||
import { getHash } from "../../src/utils/getHash";
|
import { getHash } from "../../src/utils/getHash";
|
||||||
import { deepStrictEqual } from "assert";
|
import { deepStrictEqual } from "assert";
|
||||||
import { client } from "../utils/httpClient";
|
import { client } from "../utils/httpClient";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
// helpers
|
||||||
const endpoint = "/api/getSavedTimeForUser";
|
const endpoint = "/api/getSavedTimeForUser";
|
||||||
|
const getSavedTimeForUser = (userID: string) => client({
|
||||||
|
url: endpoint,
|
||||||
|
params: { userID }
|
||||||
|
});
|
||||||
|
|
||||||
describe("getSavedTimeForUser", () => {
|
describe("getSavedTimeForUser", () => {
|
||||||
const user1 = "getSavedTimeForUserUser";
|
const user1 = "getSavedTimeForUser1";
|
||||||
|
const user2 = "getSavedTimeforUser2";
|
||||||
|
const [ start, end, views ] = [1, 11, 50];
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
const startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", "views", "shadowHidden") VALUES';
|
const startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", "views", "shadowHidden") VALUES';
|
||||||
await db.prepare("run", `${startOfQuery}(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
await db.prepare("run", `${startOfQuery}(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
["getSavedTimeForUser", 1, 11, 2, "gstfu0", getHash(user1), 0, 50, 0]);
|
["getSavedTimeForUser", start, end, 2, "getSavedTimeUUID0", getHash(user1), 0, views, 0]);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
it("Should be able to get a saved time", (done) => {
|
||||||
it("Should be able to get a 200", (done) => {
|
getSavedTimeForUser(user1)
|
||||||
client.get(endpoint, { params: { userID: user1 } })
|
|
||||||
.then(res => {
|
.then(res => {
|
||||||
// (end-start)*minute * views
|
// (end-start)*minute * views
|
||||||
const savedMinutes = ((11-1)/60) * 50;
|
const savedMinutes = ((end-start)/60) * views;
|
||||||
const expected = {
|
const expected = {
|
||||||
timeSaved: savedMinutes
|
timeSaved: savedMinutes
|
||||||
};
|
};
|
||||||
@@ -26,4 +35,20 @@ describe("getSavedTimeForUser", () => {
|
|||||||
})
|
})
|
||||||
.catch((err) => done(err));
|
.catch((err) => done(err));
|
||||||
});
|
});
|
||||||
|
it("Should return 404 if no submissions", (done) => {
|
||||||
|
getSavedTimeForUser(user2)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((err) => done(err));
|
||||||
|
});
|
||||||
|
it("Should return 400 if no userID", (done) => {
|
||||||
|
client({ url: endpoint })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((err) => done(err));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,6 +80,67 @@ describe("getSearchSegments", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should be able to filter by category with categories string", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "searchTest0", categories: `["selfpromo"]` } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
const segments = data.segments;
|
||||||
|
assert.strictEqual(data.segmentCount, 1);
|
||||||
|
assert.strictEqual(data.page, 0);
|
||||||
|
assert.strictEqual(segments[0].UUID, "search-downvote");
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to filter by category with categories array", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "searchTest0", category: ["selfpromo"] } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
const segments = data.segments;
|
||||||
|
assert.strictEqual(data.segmentCount, 1);
|
||||||
|
assert.strictEqual(data.page, 0);
|
||||||
|
assert.strictEqual(segments[0].UUID, "search-downvote");
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to filter by category with actionTypes JSON", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "searchTest5", actionTypes: `["mute"]` } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
assert.strictEqual(data.segmentCount, 1);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to filter by category with actionType array", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "searchTest5", actionType: ["mute"] } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
assert.strictEqual(data.segmentCount, 1);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to filter by category with actionType string", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "searchTest5", actionType: "mute" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = res.data;
|
||||||
|
assert.strictEqual(data.segmentCount, 1);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
it("Should be able to filter by lock status", (done) => {
|
it("Should be able to filter by lock status", (done) => {
|
||||||
client.get(endpoint, { params: { videoID: "searchTest0", locked: false } })
|
client.get(endpoint, { params: { videoID: "searchTest0", locked: false } })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
@@ -274,7 +335,8 @@ describe("getSearchSegments", () => {
|
|||||||
locked: 1,
|
locked: 1,
|
||||||
hidden: 0,
|
hidden: 0,
|
||||||
shadowHidden: 0,
|
shadowHidden: 0,
|
||||||
userID: "searchTestUser"
|
userID: "searchTestUser",
|
||||||
|
description: ""
|
||||||
};
|
};
|
||||||
assert.deepStrictEqual(segment0, expected);
|
assert.deepStrictEqual(segment0, expected);
|
||||||
done();
|
done();
|
||||||
|
|||||||
48
test/cases/getSearchSegments4xx.ts
Normal file
48
test/cases/getSearchSegments4xx.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
describe("getSearchSegments 4xx", () => {
|
||||||
|
const endpoint = "/api/searchSegments";
|
||||||
|
|
||||||
|
it("Should return 400 if no videoID", (done) => {
|
||||||
|
client.get(endpoint, { params: {} })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
const data = res.data;
|
||||||
|
assert.strictEqual(data, "videoID not specified");
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if invalid categories", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "nullVideo", categories: 3 } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
const data = res.data;
|
||||||
|
assert.strictEqual(data, "Categories parameter does not match format requirements.");
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if invalid actionTypes", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "nullVideo", actionTypes: 3 } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
const data = res.data;
|
||||||
|
assert.strictEqual(data, "actionTypes parameter does not match format requirements.");
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 404 if no segments", (done) => {
|
||||||
|
client.get(endpoint, { params: { videoID: "nullVideo", actionType: "chapter" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { partialDeepEquals, arrayPartialDeepEquals } from "../utils/partialDeepE
|
|||||||
import { getHash } from "../../src/utils/getHash";
|
import { getHash } from "../../src/utils/getHash";
|
||||||
import { ImportMock, } from "ts-mock-imports";
|
import { ImportMock, } from "ts-mock-imports";
|
||||||
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
|
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
|
||||||
import { YouTubeApiMock } from "../youtubeMock";
|
import { YouTubeApiMock } from "../mocks/youtubeMock";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { client } from "../utils/httpClient";
|
import { client } from "../utils/httpClient";
|
||||||
|
|
||||||
@@ -581,4 +581,78 @@ describe("getSkipSegmentsByHash", () => {
|
|||||||
})
|
})
|
||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should be able to get single segment with requiredSegments", (done) => {
|
||||||
|
const requiredSegment1 = "fbf0af454059733c8822f6a4ac8ec568e0787f8c0a5ee915dd5b05e0d7a9a388";
|
||||||
|
client.get(`${endpoint}/17bf?requiredSegment=${requiredSegment1}`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const data = (res.data as Array<any>).sort((a, b) => a.videoID.localeCompare(b.videoID));
|
||||||
|
assert.strictEqual(data.length, 1);
|
||||||
|
const expected = [{
|
||||||
|
segments: [{
|
||||||
|
UUID: requiredSegment1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
assert.ok(partialDeepEquals(data, expected));
|
||||||
|
assert.strictEqual(data[0].segments.length, 1);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if categories are is number", (done) => {
|
||||||
|
client.get(`${endpoint}/17bf?categories=3`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if actionTypes is number", (done) => {
|
||||||
|
client.get(`${endpoint}/17bf?actionTypes=3`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if actionTypes are invalid json", (done) => {
|
||||||
|
client.get(`${endpoint}/17bf?actionTypes={test}`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if requiredSegments is number", (done) => {
|
||||||
|
client.get(`${endpoint}/17bf?requiredSegments=3`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("Should return 404 if requiredSegments is invalid json", (done) => {
|
||||||
|
client.get(`${endpoint}/17bf?requiredSegments={test}`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if requiredSegments is not present", (done) => {
|
||||||
|
client.get(`${endpoint}/17bf?requiredSegment=${fullCategoryVidHash}`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import assert from "assert";
|
|||||||
import { db } from "../../src/databases/databases";
|
import { db } from "../../src/databases/databases";
|
||||||
import { client } from "../utils/httpClient";
|
import { client } from "../utils/httpClient";
|
||||||
import { config } from "../../src/config";
|
import { config } from "../../src/config";
|
||||||
|
import sinon from "sinon";
|
||||||
let dbVersion: number;
|
let dbVersion: number;
|
||||||
|
|
||||||
describe("getStatus", () => {
|
describe("getStatus", () => {
|
||||||
@@ -122,4 +123,16 @@ describe("getStatus", () => {
|
|||||||
})
|
})
|
||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should return commit unkown if not present", (done) => {
|
||||||
|
sinon.stub((global as any), "HEADCOMMIT").value(undefined);
|
||||||
|
client.get(`${endpoint}/commit`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.data, "test"); // commit should be test
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ describe("getTopUsers", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if undefined sortType provided", (done) => {
|
||||||
|
client.get(endpoint)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
it("Should be able to get by all sortTypes", (done) => {
|
it("Should be able to get by all sortTypes", (done) => {
|
||||||
client.get(endpoint, { params: { sortType: 0 } })// minutesSaved
|
client.get(endpoint, { params: { sortType: 0 } })// minutesSaved
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|||||||
17
test/cases/getTotalStats.ts
Normal file
17
test/cases/getTotalStats.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
|
||||||
|
const endpoint = "/api/getTotalStats";
|
||||||
|
|
||||||
|
describe("getTotalStats", () => {
|
||||||
|
it("Can get total stats", async () => {
|
||||||
|
const result = await client({ url: endpoint });
|
||||||
|
const data = result.data;
|
||||||
|
assert.ok(data?.userCount ?? true);
|
||||||
|
assert.ok(data.activeUsers >= 0);
|
||||||
|
assert.ok(data.apiUsers >= 0);
|
||||||
|
assert.ok(data.viewCount >= 0);
|
||||||
|
assert.ok(data.totalSubmissions >= 0);
|
||||||
|
assert.ok(data.minutesSaved >= 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ describe("getUserInfo", () => {
|
|||||||
await db.prepare("run", sponsorTimesQuery, ["getUserInfo0", 0, 36000, 2,"uuid000009", getHash("getuserinfo_user_03"), 8, 10, "sponsor", "skip", 0]);
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfo0", 0, 36000, 2,"uuid000009", getHash("getuserinfo_user_03"), 8, 10, "sponsor", "skip", 0]);
|
||||||
await db.prepare("run", sponsorTimesQuery, ["getUserInfo3", 1, 11, 2, "uuid000006", getHash("getuserinfo_user_02"), 6, 10, "sponsor", "skip", 0]);
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfo3", 1, 11, 2, "uuid000006", getHash("getuserinfo_user_02"), 6, 10, "sponsor", "skip", 0]);
|
||||||
await db.prepare("run", sponsorTimesQuery, ["getUserInfo4", 1, 11, 2, "uuid000010", getHash("getuserinfo_user_04"), 9, 10, "chapter", "chapter", 0]);
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfo4", 1, 11, 2, "uuid000010", getHash("getuserinfo_user_04"), 9, 10, "chapter", "chapter", 0]);
|
||||||
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfo5", 1, 11, 2, "uuid000011", getHash("getuserinfo_user_05"), 9, 10, "sponsor", "skip", 0]);
|
||||||
|
|
||||||
|
|
||||||
const insertWarningQuery = 'INSERT INTO warnings ("userID", "issueTime", "issuerUserID", "enabled", "reason") VALUES (?, ?, ?, ?, ?)';
|
const insertWarningQuery = 'INSERT INTO warnings ("userID", "issueTime", "issuerUserID", "enabled", "reason") VALUES (?, ?, ?, ?, ?)';
|
||||||
@@ -264,6 +265,15 @@ describe("getUserInfo", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should throw 400 with invalid array", (done) => {
|
||||||
|
client.get(endpoint, { params: { userID: "x", values: 123 } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done(); // pass
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
it("Should return 200 on userID not found", (done) => {
|
it("Should return 200 on userID not found", (done) => {
|
||||||
client.get(endpoint, { params: { userID: "notused-userid" } })
|
client.get(endpoint, { params: { userID: "notused-userid" } })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
@@ -309,6 +319,30 @@ describe("getUserInfo", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should be able to get permissions", (done) => {
|
||||||
|
client.get(endpoint, { params: { userID: "getuserinfo_user_01", value: "permissions" } })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const expected = {
|
||||||
|
permissions: {
|
||||||
|
sponsor: true,
|
||||||
|
selfpromo: true,
|
||||||
|
exclusive_access: true,
|
||||||
|
interaction: true,
|
||||||
|
intro: true,
|
||||||
|
outro: true,
|
||||||
|
preview: true,
|
||||||
|
music_offtopic: true,
|
||||||
|
filler: true,
|
||||||
|
poi_highlight: true,
|
||||||
|
chapter: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.ok(partialDeepEquals(res.data, expected));
|
||||||
|
done(); // pass
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("Should ignore chapters for saved time calculations", (done) => {
|
it("Should ignore chapters for saved time calculations", (done) => {
|
||||||
client.get(endpoint, { params: { userID: "getuserinfo_user_04" } })
|
client.get(endpoint, { params: { userID: "getuserinfo_user_04" } })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|||||||
76
test/cases/getUserInfoFree.ts
Normal file
76
test/cases/getUserInfoFree.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
import assert from "assert";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
|
||||||
|
describe("getUserInfo Free Chapters", () => {
|
||||||
|
const endpoint = "/api/userInfo";
|
||||||
|
|
||||||
|
const newQualifyUserID = "getUserInfo-Free-newQualify";
|
||||||
|
const vipQualifyUserID = "getUserInfo-Free-VIP";
|
||||||
|
const repQualifyUserID = "getUserInfo-Free-RepQualify";
|
||||||
|
const oldQualifyUserID = "getUserInfo-Free-OldQualify";
|
||||||
|
const newNoQualityUserID = "getUserInfo-Free-newNoQualify";
|
||||||
|
const postOldQualify = 1600000000000;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const sponsorTimesQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "reputation", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-0", getHash(repQualifyUserID), postOldQualify, 0, "sponsor", "skip", 20, 0]);
|
||||||
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-1", getHash(oldQualifyUserID), 0, 0, "sponsor", "skip", 0, 0]); // submit at epoch
|
||||||
|
await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-2", getHash(newQualifyUserID), postOldQualify, 0, "sponsor", "skip", 0, 0]);
|
||||||
|
|
||||||
|
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(vipQualifyUserID)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserInfo = (userID: string) => client.get(endpoint, { params: { userID, value: "freeChaptersAccess" } });
|
||||||
|
|
||||||
|
it("Should not get free access under new rule (newNoQualify)", (done) => {
|
||||||
|
getUserInfo(newNoQualityUserID)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.data.freeChaptersAccess, false);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get free access under new rule (newQualify)", (done) => {
|
||||||
|
getUserInfo(newQualifyUserID)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.data.freeChaptersAccess, true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get free access (VIP)", (done) => {
|
||||||
|
getUserInfo(vipQualifyUserID)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.data.freeChaptersAccess, true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get free access (rep)", (done) => {
|
||||||
|
getUserInfo(repQualifyUserID)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.data.freeChaptersAccess, true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should get free access (old)", (done) => {
|
||||||
|
getUserInfo(oldQualifyUserID)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.data.freeChaptersAccess, true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
53
test/cases/getUsername.ts
Normal file
53
test/cases/getUsername.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
const getUsername = (userID: string) => client({
|
||||||
|
url: "/api/getUsername",
|
||||||
|
params: { userID }
|
||||||
|
});
|
||||||
|
|
||||||
|
const postSetUserName = (userID: string, username: string) => client({
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/setUsername",
|
||||||
|
params: {
|
||||||
|
userID,
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const userOnePrivate = "getUsername_0";
|
||||||
|
const userOnePublic = getHash(userOnePrivate);
|
||||||
|
const userOneUsername = "getUsername_username";
|
||||||
|
|
||||||
|
describe("getUsername test", function() {
|
||||||
|
it("Should get back publicUserID if not set", (done) => {
|
||||||
|
getUsername(userOnePrivate)
|
||||||
|
.then(result => {
|
||||||
|
assert.strictEqual(result.data.userName, userOnePublic);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should be able to get username after setting", (done) => {
|
||||||
|
postSetUserName(userOnePrivate, userOneUsername)
|
||||||
|
.then(async () => {
|
||||||
|
const result = await getUsername(userOnePrivate);
|
||||||
|
const actual = result.data.userName;
|
||||||
|
assert.strictEqual(actual, userOneUsername);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should return 400 if no userID provided", (done) => {
|
||||||
|
client({
|
||||||
|
url: "/api/getUsername"
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
62
test/cases/getViewsForUser.ts
Normal file
62
test/cases/getViewsForUser.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
const endpoint = "/api/getViewsForUser";
|
||||||
|
const getViewsForUser = (userID: string) => client({
|
||||||
|
url: endpoint,
|
||||||
|
params: { userID }
|
||||||
|
});
|
||||||
|
|
||||||
|
const getViewUserOne = "getViewUser1";
|
||||||
|
const userOneViewsFirst = 30;
|
||||||
|
const userOneViewsSecond = 0;
|
||||||
|
|
||||||
|
const getViewUserTwo = "getViewUser2";
|
||||||
|
const userTwoViews = 0;
|
||||||
|
|
||||||
|
const getViewUserThree = "getViewUser3";
|
||||||
|
|
||||||
|
|
||||||
|
describe("getViewsForUser", function() {
|
||||||
|
before(() => {
|
||||||
|
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo0", getHash(getViewUserOne), 0, userOneViewsFirst, "sponsor", "skip", 0, 0, "getViewUserVideo"]);
|
||||||
|
db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo1", getHash(getViewUserOne), 0, userOneViewsSecond, "sponsor", "skip", 0, 0, "getViewUserVideo"]);
|
||||||
|
db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo2", getHash(getViewUserTwo), 0, userTwoViews, "sponsor", "skip", 0, 0, "getViewUserVideo"]);
|
||||||
|
});
|
||||||
|
it("Should get back views for user one", (done) => {
|
||||||
|
getViewsForUser(getViewUserOne)
|
||||||
|
.then(result => {
|
||||||
|
assert.strictEqual(result.data.viewCount, userOneViewsFirst + userOneViewsSecond);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should get back views for user two", (done) => {
|
||||||
|
getViewsForUser(getViewUserTwo)
|
||||||
|
.then(result => {
|
||||||
|
assert.strictEqual(result.data.viewCount, userTwoViews);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should get 404 if no submissions", (done) => {
|
||||||
|
getViewsForUser(getViewUserThree)
|
||||||
|
.then(result => {
|
||||||
|
assert.strictEqual(result.status, 404);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("Should return 400 if no userID provided", (done) => {
|
||||||
|
client({ url: endpoint })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
252
test/cases/lockCategoriesHttp.ts
Normal file
252
test/cases/lockCategoriesHttp.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
import { UserID } from "../../src/types/user.model";
|
||||||
|
import { Category, VideoID } from "../../src/types/segments.model";
|
||||||
|
|
||||||
|
interface LockCategory {
|
||||||
|
category: Category,
|
||||||
|
reason: string,
|
||||||
|
videoID: VideoID,
|
||||||
|
userID: UserID
|
||||||
|
}
|
||||||
|
const lockVIPUser = "lockCategoriesHttpVIPUser";
|
||||||
|
const lockVIPUserHash = getHash(lockVIPUser);
|
||||||
|
const endpoint = "/api/lockCategories";
|
||||||
|
const checkLockCategories = (videoID: string): Promise<LockCategory[]> => db.prepare("all", 'SELECT * FROM "lockCategories" WHERE "videoID" = ?', [videoID]);
|
||||||
|
|
||||||
|
|
||||||
|
const goodResponse = (): any => ({
|
||||||
|
videoID: "test-videoid",
|
||||||
|
userID: "not-vip-test-userid",
|
||||||
|
categories: ["sponsor"],
|
||||||
|
actionTypes: ["skip"]
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST lockCategories HTTP submitting", () => {
|
||||||
|
before(async () => {
|
||||||
|
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
|
||||||
|
await db.prepare("run", insertVipUserQuery, [lockVIPUserHash]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should update the database version when starting the application", async () => {
|
||||||
|
const version = (await db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"])).value;
|
||||||
|
assert.ok(version > 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to add poi type category by type skip", (done) => {
|
||||||
|
const videoID = "add-record-poi";
|
||||||
|
client.post(endpoint, {
|
||||||
|
videoID,
|
||||||
|
userID: lockVIPUser,
|
||||||
|
categories: ["poi_highlight"],
|
||||||
|
actionTypes: ["skip"]
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
checkLockCategories(videoID)
|
||||||
|
.then(result => {
|
||||||
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.strictEqual(result[0], "poi_highlight");
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not add lock of invalid type", (done) => {
|
||||||
|
const videoID = "add_invalid_type";
|
||||||
|
client.post(endpoint, {
|
||||||
|
videoID,
|
||||||
|
userID: lockVIPUser,
|
||||||
|
categories: ["future_unused_invalid_type"],
|
||||||
|
actionTypes: ["skip"]
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
checkLockCategories(videoID)
|
||||||
|
.then(result => {
|
||||||
|
assert.strictEqual(result.length, 0);
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DELETE lockCategories 403/400 tests", () => {
|
||||||
|
it(" Should return 400 for no data", (done) => {
|
||||||
|
client.delete(endpoint, {})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 for no categories", (done) => {
|
||||||
|
const json: any = {
|
||||||
|
videoID: "test",
|
||||||
|
userID: "test",
|
||||||
|
categories: [],
|
||||||
|
};
|
||||||
|
client.delete(endpoint, json)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 for no userID", (done) => {
|
||||||
|
const json: any = {
|
||||||
|
videoID: "test",
|
||||||
|
userID: null,
|
||||||
|
categories: ["sponsor"],
|
||||||
|
};
|
||||||
|
|
||||||
|
client.post(endpoint, json)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 for no videoID", (done) => {
|
||||||
|
const json: any = {
|
||||||
|
videoID: null,
|
||||||
|
userID: "test",
|
||||||
|
categories: ["sponsor"],
|
||||||
|
};
|
||||||
|
|
||||||
|
client.post(endpoint, json)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 for invalid category array", (done) => {
|
||||||
|
const json = {
|
||||||
|
videoID: "test",
|
||||||
|
userID: "test",
|
||||||
|
categories: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
client.post(endpoint, json)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 for bad format categories", (done) => {
|
||||||
|
const json = {
|
||||||
|
videoID: "test",
|
||||||
|
userID: "test",
|
||||||
|
categories: "sponsor",
|
||||||
|
};
|
||||||
|
|
||||||
|
client.post(endpoint, json)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 403 if user is not VIP", (done) => {
|
||||||
|
const json = {
|
||||||
|
videoID: "test",
|
||||||
|
userID: "test",
|
||||||
|
categories: [
|
||||||
|
"sponsor",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
client.post(endpoint, json)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("manual DELETE/POST lockCategories 400 tests", () => {
|
||||||
|
it("DELETE Should return 400 for no data", (done) => {
|
||||||
|
client.delete(endpoint, { data: {} })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("POST Should return 400 for no data", (done) => {
|
||||||
|
client.post(endpoint, {})
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("DELETE Should return 400 for bad format categories", (done) => {
|
||||||
|
const data = goodResponse();
|
||||||
|
data.categories = "sponsor";
|
||||||
|
client.delete(endpoint, { data })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("POST Should return 400 for bad format categories", (done) => {
|
||||||
|
const data = goodResponse();
|
||||||
|
data.categories = "sponsor";
|
||||||
|
client.post(endpoint, data)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("DELETE Should return 403 if user is not VIP", (done) => {
|
||||||
|
const data = goodResponse();
|
||||||
|
client.delete(endpoint, { data })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
it("POST Should return 403 if user is not VIP", (done) => {
|
||||||
|
const data = goodResponse();
|
||||||
|
client.post(endpoint, data)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("array of DELETE/POST lockCategories 400 tests", () => {
|
||||||
|
for (const key of [ "videoID", "userID", "categories" ]) {
|
||||||
|
for (const method of ["DELETE", "POST"]) {
|
||||||
|
it(`${method} - Should return 400 for invalid ${key}`, (done) => {
|
||||||
|
const data = goodResponse();
|
||||||
|
data[key] = null;
|
||||||
|
client(endpoint, { data, method })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -266,106 +266,6 @@ describe("lockCategoriesRecords", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return 400 for missing params", (done) => {
|
|
||||||
client.post(endpoint, {})
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 400);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return 400 for no categories", (done) => {
|
|
||||||
const json: any = {
|
|
||||||
videoID: "test",
|
|
||||||
userID: "test",
|
|
||||||
categories: [],
|
|
||||||
};
|
|
||||||
client.post(endpoint, json)
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 400);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return 400 for no userID", (done) => {
|
|
||||||
const json: any = {
|
|
||||||
videoID: "test",
|
|
||||||
userID: null,
|
|
||||||
categories: ["sponsor"],
|
|
||||||
};
|
|
||||||
|
|
||||||
client.post(endpoint, json)
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 400);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return 400 for no videoID", (done) => {
|
|
||||||
const json: any = {
|
|
||||||
videoID: null,
|
|
||||||
userID: "test",
|
|
||||||
categories: ["sponsor"],
|
|
||||||
};
|
|
||||||
|
|
||||||
client.post(endpoint, json)
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 400);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return 400 object categories", (done) => {
|
|
||||||
const json = {
|
|
||||||
videoID: "test",
|
|
||||||
userID: "test",
|
|
||||||
categories: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
client.post(endpoint, json)
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 400);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return 400 bad format categories", (done) => {
|
|
||||||
const json = {
|
|
||||||
videoID: "test",
|
|
||||||
userID: "test",
|
|
||||||
categories: "sponsor",
|
|
||||||
};
|
|
||||||
|
|
||||||
client.post(endpoint, json)
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 400);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return 403 if user is not VIP", (done) => {
|
|
||||||
const json = {
|
|
||||||
videoID: "test",
|
|
||||||
userID: "test",
|
|
||||||
categories: [
|
|
||||||
"sponsor",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
client.post(endpoint, json)
|
|
||||||
.then(res => {
|
|
||||||
assert.strictEqual(res.status, 403);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(err => done(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should be able to delete a lockCategories record", (done) => {
|
it("Should be able to delete a lockCategories record", (done) => {
|
||||||
const videoID = "delete-record";
|
const videoID = "delete-record";
|
||||||
const json = {
|
const json = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { partialDeepEquals, arrayDeepEquals } from "../utils/partialDeepEquals";
|
|||||||
import { db } from "../../src/databases/databases";
|
import { db } from "../../src/databases/databases";
|
||||||
import { ImportMock } from "ts-mock-imports";
|
import { ImportMock } from "ts-mock-imports";
|
||||||
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
|
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
|
||||||
import { YouTubeApiMock } from "../youtubeMock";
|
import { YouTubeApiMock } from "../mocks/youtubeMock";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { client } from "../utils/httpClient";
|
import { client } from "../utils/httpClient";
|
||||||
import { Feature } from "../../src/types/user.model";
|
import { Feature } from "../../src/types/user.model";
|
||||||
|
|||||||
@@ -187,10 +187,34 @@ describe("shadowBanUser", () => {
|
|||||||
})
|
})
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
const videoRow = await getShadowBanSegmentCategory(userID, 1);
|
const videoRow = await getShadowBanSegmentCategory(userID, 0);
|
||||||
const shadowRow = await getShadowBan(userID);
|
const shadowRow = await getShadowBan(userID);
|
||||||
assert.ok(shadowRow); // ban still exists
|
assert.ok(shadowRow); // ban still exists
|
||||||
assert.strictEqual(videoRow.length, 1); // videos should be hidden
|
assert.strictEqual(videoRow.length, 0); // videos should be hidden
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to un-shadowban user to restore old submissions", (done) => {
|
||||||
|
const userID = "shadowBanned4";
|
||||||
|
client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
userID,
|
||||||
|
adminUserID: VIPuserID,
|
||||||
|
enabled: false,
|
||||||
|
categories: `["sponsor"]`,
|
||||||
|
unHideOldSubmissions: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const videoRow = await getShadowBanSegmentCategory(userID, 0);
|
||||||
|
const shadowRow = await getShadowBan(userID);
|
||||||
|
assert.ok(!shadowRow); // ban still exists
|
||||||
|
assert.strictEqual(videoRow.length, 1); // videos should be visible
|
||||||
assert.strictEqual(videoRow[0].category, "sponsor");
|
assert.strictEqual(videoRow[0].category, "sponsor");
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
|
|||||||
48
test/cases/shadowBanUser4xx.ts
Normal file
48
test/cases/shadowBanUser4xx.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { db } from "../../src/databases/databases";
|
||||||
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
import assert from "assert";
|
||||||
|
import { client } from "../utils/httpClient";
|
||||||
|
|
||||||
|
const endpoint = "/api/shadowBanUser";
|
||||||
|
|
||||||
|
const postShadowBan = (params: Record<string, string>) => client({
|
||||||
|
method: "POST",
|
||||||
|
url: endpoint,
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shadowBanUser 4xx", () => {
|
||||||
|
const VIPuserID = "shadow-ban-4xx-vip";
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES(?)`, [getHash(VIPuserID)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if no adminUserID", (done) => {
|
||||||
|
const userID = "shadowBanned";
|
||||||
|
postShadowBan({ userID })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 400 if no userID", (done) => {
|
||||||
|
postShadowBan({ adminUserID: VIPuserID })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return 403 if not authorized", (done) => {
|
||||||
|
postShadowBan({ adminUserID: "notVIPUserID", userID: "shadowBanned" })
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { partialDeepEquals } from "../utils/partialDeepEquals";
|
import { partialDeepEquals, mixedDeepEquals } from "../utils/partialDeepEquals";
|
||||||
|
|
||||||
describe("Test utils ", () => {
|
describe("Test utils ", () => {
|
||||||
it("objectContain", () => {
|
it("objectContain", () => {
|
||||||
@@ -135,4 +135,45 @@ describe("Test utils ", () => {
|
|||||||
}
|
}
|
||||||
), "Did not match partial child array");
|
), "Did not match partial child array");
|
||||||
});
|
});
|
||||||
|
it("mixedDeepEquals exists", () => {
|
||||||
|
assert(!mixedDeepEquals({
|
||||||
|
name: "lorem",
|
||||||
|
values: [{
|
||||||
|
name: "ipsum",
|
||||||
|
}],
|
||||||
|
child: {
|
||||||
|
name: "dolor",
|
||||||
|
},
|
||||||
|
ignore: true
|
||||||
|
}, {
|
||||||
|
name: "lorem",
|
||||||
|
values: [{
|
||||||
|
name: "ipsum",
|
||||||
|
}],
|
||||||
|
child: {
|
||||||
|
name: "dolor",
|
||||||
|
},
|
||||||
|
ignore: false
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
it("mixedDeepEquals noProperty", () => {
|
||||||
|
assert(!mixedDeepEquals({
|
||||||
|
name: "lorem",
|
||||||
|
values: [{
|
||||||
|
name: "ipsum",
|
||||||
|
}],
|
||||||
|
child: {
|
||||||
|
name: "dolor",
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
name: "lorem",
|
||||||
|
values: [{
|
||||||
|
name: "ipsum",
|
||||||
|
}],
|
||||||
|
child: {
|
||||||
|
name: "dolor",
|
||||||
|
},
|
||||||
|
ignore: false
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
50
test/cases/tokenUtils.ts
Normal file
50
test/cases/tokenUtils.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { config } from "../../src/config";
|
||||||
|
import axios from "axios";
|
||||||
|
import * as tokenUtils from "../../src/utils/tokenUtils";
|
||||||
|
import MockAdapter from "axios-mock-adapter";
|
||||||
|
import { validatelicenseKeyRegex } from "../../src/routes/verifyToken";
|
||||||
|
let mock: MockAdapter;
|
||||||
|
import * as patreon from "../mocks/patreonMock";
|
||||||
|
|
||||||
|
const validateToken = validatelicenseKeyRegex;
|
||||||
|
|
||||||
|
describe("tokenUtils test", function() {
|
||||||
|
before(function() {
|
||||||
|
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
|
||||||
|
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
|
||||||
|
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to create patreon token", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code").then((licenseKey) => {
|
||||||
|
assert.ok(validateToken(licenseKey));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Should be able to create local token", (done) => {
|
||||||
|
tokenUtils.createAndSaveToken(tokenUtils.TokenType.local).then((licenseKey) => {
|
||||||
|
assert.ok(validateToken(licenseKey));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Should be able to get patreon identity", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
tokenUtils.getPatreonIdentity("fake_access_token").then((result) => {
|
||||||
|
assert.deepEqual(result, patreon.activeIdentity);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Should be able to refresh token", function (done) {
|
||||||
|
if (!config?.patreon) this.skip();
|
||||||
|
tokenUtils.refreshToken(tokenUtils.TokenType.patreon, "fake-licence-Key", "fake_refresh_token").then((result) => {
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,10 +3,9 @@ import assert from "assert";
|
|||||||
import { config } from "../../src/config";
|
import { config } from "../../src/config";
|
||||||
import { getHash } from "../../src/utils/getHash";
|
import { getHash } from "../../src/utils/getHash";
|
||||||
|
|
||||||
|
|
||||||
describe("userCounter", () => {
|
describe("userCounter", () => {
|
||||||
it("Should return 200", (done) => {
|
it("Should return 200", function (done) {
|
||||||
if (!config.userCounterURL) return done(); // skip if no userCounterURL is set
|
if (!config.userCounterURL) this.skip(); // skip if no userCounterURL is set
|
||||||
axios.request({
|
axios.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
baseURL: config.userCounterURL,
|
baseURL: config.userCounterURL,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { db, privateDB } from "../../src/databases/databases";
|
|||||||
import { getHash } from "../../src/utils/getHash";
|
import { getHash } from "../../src/utils/getHash";
|
||||||
import { ImportMock } from "ts-mock-imports";
|
import { ImportMock } from "ts-mock-imports";
|
||||||
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
|
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
|
||||||
import { YouTubeApiMock } from "../youtubeMock";
|
import { YouTubeApiMock } from "../mocks/youtubeMock";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { client } from "../utils/httpClient";
|
import { client } from "../utils/httpClient";
|
||||||
import { arrayDeepEquals } from "../utils/partialDeepEquals";
|
import { arrayDeepEquals } from "../utils/partialDeepEquals";
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { config } from "../src/config";
|
import { config } from "../src/config";
|
||||||
import { Server } from "http";
|
import { Server } from "http";
|
||||||
|
import { UserCounter } from "./mocks/UserCounter";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.post("/ReportChannelWebhook", (req, res) => {
|
app.post("/webhook/ReportChannel", (req, res) => {
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/FirstTimeSubmissionsWebhook", (req, res) => {
|
app.post("/webhook/FirstTimeSubmissions", (req, res) => {
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/CompletelyIncorrectReportWebhook", (req, res) => {
|
app.post("/webhook/CompletelyIncorrectReport", (req, res) => {
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Testing NeuralBlock
|
// Testing NeuralBlock
|
||||||
app.post("/NeuralBlockRejectWebhook", (req, res) => {
|
app.post("/webhook/NeuralBlockReject", (req, res) => {
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,6 +48,9 @@ app.post("/CustomWebhook", (req, res) => {
|
|||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// mocks
|
||||||
|
app.use("/UserCounter", UserCounter);
|
||||||
|
|
||||||
export function createMockServer(callback: () => void): Server {
|
export function createMockServer(callback: () => void): Server {
|
||||||
return app.listen(config.mockPort, callback);
|
return app.listen(config.mockPort, callback);
|
||||||
}
|
}
|
||||||
|
|||||||
11
test/mocks/UserCounter.ts
Normal file
11
test/mocks/UserCounter.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
export const UserCounter = Router();
|
||||||
|
|
||||||
|
UserCounter.post("/api/v1/addIP", (req, res) => {
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
UserCounter.get("/api/v1/userCount", (req, res) => {
|
||||||
|
res.send({
|
||||||
|
userCount: 100
|
||||||
|
});
|
||||||
|
});
|
||||||
22
test/mocks/gumroadMock.ts
Normal file
22
test/mocks/gumroadMock.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const licenseSuccess = {
|
||||||
|
success: true,
|
||||||
|
uses: 4,
|
||||||
|
purchase: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const licenseFail = {
|
||||||
|
success: false,
|
||||||
|
message: "That license does not exist for the provided product."
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const subCode = (length = 8) => {
|
||||||
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters[(Math.floor(Math.random() * characters.length))];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateLicense = (): string => `${subCode()}-${subCode()}-${subCode()}-${subCode()}`;
|
||||||
33
test/mocks/mockExpressRequest.ts
Normal file
33
test/mocks/mockExpressRequest.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const nullStub = (): any => null;
|
||||||
|
|
||||||
|
export const createRequest = (options: any) => ({
|
||||||
|
app: {},
|
||||||
|
baseUrl: "",
|
||||||
|
body: {},
|
||||||
|
cookies: {},
|
||||||
|
fresh: true,
|
||||||
|
headers: {},
|
||||||
|
hostname: "example.com",
|
||||||
|
ip: "",
|
||||||
|
ips: [],
|
||||||
|
method: "GET",
|
||||||
|
originalUrl: "/",
|
||||||
|
params: {},
|
||||||
|
path: "/",
|
||||||
|
protocol: "https",
|
||||||
|
query: {},
|
||||||
|
route: {},
|
||||||
|
secure: true,
|
||||||
|
signedCookies: {},
|
||||||
|
stale: false,
|
||||||
|
subdomains: [],
|
||||||
|
xhr: true,
|
||||||
|
accepts: nullStub(),
|
||||||
|
acceptsCharsets: nullStub(),
|
||||||
|
acceptsEncodings: nullStub(),
|
||||||
|
acceptsLanguages: nullStub(),
|
||||||
|
get: nullStub(),
|
||||||
|
is: nullStub(),
|
||||||
|
range: nullStub(),
|
||||||
|
...options
|
||||||
|
});
|
||||||
59
test/mocks/patreonMock.ts
Normal file
59
test/mocks/patreonMock.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
export const activeIdentity = {
|
||||||
|
data: {},
|
||||||
|
links: {},
|
||||||
|
included: [
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
is_monthly: true,
|
||||||
|
currently_entitled_amount_cents: 100,
|
||||||
|
patron_status: "active_patron",
|
||||||
|
},
|
||||||
|
id: "id",
|
||||||
|
type: "campaign"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const invalidIdentity = {
|
||||||
|
data: {},
|
||||||
|
links: {},
|
||||||
|
included: [{}],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formerIdentitySucceed = {
|
||||||
|
data: {},
|
||||||
|
links: {},
|
||||||
|
included: [
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
is_monthly: true,
|
||||||
|
campaign_lifetime_support_cents: 500,
|
||||||
|
patron_status: "former_patron",
|
||||||
|
},
|
||||||
|
id: "id",
|
||||||
|
type: "campaign"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formerIdentityFail = {
|
||||||
|
data: {},
|
||||||
|
links: {},
|
||||||
|
included: [
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
is_monthly: true,
|
||||||
|
campaign_lifetime_support_cents: 1,
|
||||||
|
patron_status: "former_patron",
|
||||||
|
},
|
||||||
|
id: "id",
|
||||||
|
type: "campaign"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fakeOauth = {
|
||||||
|
access_token: "test_access_token",
|
||||||
|
refresh_token: "test_refresh_token",
|
||||||
|
expires_in: 3600,
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { APIVideoData, APIVideoInfo } from "../src/types/youtubeApi.model";
|
import { APIVideoData, APIVideoInfo } from "../../src/types/youtubeApi.model";
|
||||||
|
|
||||||
export class YouTubeApiMock {
|
export class YouTubeApiMock {
|
||||||
// eslint-disable-next-line require-await
|
// eslint-disable-next-line require-await
|
||||||
Reference in New Issue
Block a user