diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 619bb9c..ced69af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: steps: # Initialization - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - run: npm install - name: Run Tests timeout-minutes: 5 diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index fab273b..a4b57b8 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -13,8 +13,8 @@ jobs: steps: # Initialization - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - run: npm install - name: Run Tests timeout-minutes: 5 diff --git a/.github/workflows/generate-sqlite-base.yml b/.github/workflows/generate-sqlite-base.yml index c320e58..2714dc7 100644 --- a/.github/workflows/generate-sqlite-base.yml +++ b/.github/workflows/generate-sqlite-base.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - run: npm install - name: Set config run: | @@ -23,7 +23,7 @@ jobs: - name: Run Server timeout-minutes: 10 run: npm start - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: SponsorTimesDB.db path: databases/sponsorTimes.db diff --git a/.github/workflows/postgres-redis-ci.yml b/.github/workflows/postgres-redis-ci.yml index 44500f8..31ec50c 100644 --- a/.github/workflows/postgres-redis-ci.yml +++ b/.github/workflows/postgres-redis-ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build the docker-compose stack env: PG_USER: ci_db_user @@ -20,10 +20,12 @@ jobs: run: docker-compose -f docker/docker-compose-ci.yml up -d - name: Check running containers run: docker ps - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 - run: npm install - name: Run Tests env: TEST_POSTGRES: true timeout-minutes: 5 - run: npm test \ No newline at end of file + run: npx nyc --silent npm test + - name: Generate coverage report + run: npm run cover:report \ No newline at end of file diff --git a/.gitignore b/.gitignore index 25260b5..79d8a36 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ working /dist/ # nyc coverage output -.nyc_output/ \ No newline at end of file +.nyc_output/ +coverage/ \ No newline at end of file diff --git a/.nycrc.json b/.nycrc.json index 76027a5..ae148eb 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,5 +1,14 @@ { + "extends": "@istanbuljs/nyc-config-typescript", + "check-coverage": false, + "ski-full": true, + "reporter": ["text", "html"], + "include": [ + "src/**/*.ts" + ], "exclude": [ - "src/routes/addUnlitedVideo.ts" + "src/routes/addUnlistedVideo.ts", + "src/cronjob/downvoteSegmentArchiveJob.ts", + "src/databases/*" ] } \ No newline at end of file diff --git a/ci.json b/ci.json index 30e2f41..5a3ee2f 100644 --- a/ci.json +++ b/ci.json @@ -4,11 +4,12 @@ "globalSalt": "testSalt", "adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b", "newLeafURLs": ["placeholder"], - "discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook", - "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook", - "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook", - "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook", + "discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel", + "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions", + "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport", + "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject", "neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock", + "userCounterURL": "http://127.0.0.1:8081/UserCounter", "behindProxy": true, "postgres": { "user": "ci_db_user", @@ -70,5 +71,10 @@ "statusCode": 200 } }, + "patreon": { + "clientId": "testClientID", + "clientSecret": "testClientSecret", + "redirectUri": "http://127.0.0.1/fake/callback" + }, "minReputationToSubmitFiller": -1 } diff --git a/package-lock.json b/package-lock.json index 00e507e..a03b338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "sync-mysql": "^3.0.1" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/better-sqlite3": "^7.5.0", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", @@ -30,8 +31,10 @@ "@types/mocha": "^9.1.1", "@types/node": "^18.0.3", "@types/pg": "^8.6.5", + "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", + "axios-mock-adapter": "^1.21.2", "eslint": "^8.19.0", "mocha": "^10.0.0", "nodemon": "^2.0.19", @@ -630,6 +633,21 @@ "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": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -963,6 +981,21 @@ "@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": { "version": "5.30.6", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", @@ -1339,6 +1372,19 @@ "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": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -3094,6 +3140,29 @@ "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": { "version": "2.1.1", "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": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -6453,6 +6531,21 @@ "@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": { "version": "5.30.6", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", @@ -6698,6 +6791,16 @@ "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": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -7975,6 +8078,12 @@ "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", diff --git a/package.json b/package.json index 511aa19..1c8077c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "src/index.ts", "scripts": { "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: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", @@ -32,6 +33,7 @@ "sync-mysql": "^3.0.1" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/better-sqlite3": "^7.5.0", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", @@ -39,8 +41,10 @@ "@types/mocha": "^9.1.1", "@types/node": "^18.0.3", "@types/pg": "^8.6.5", + "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", + "axios-mock-adapter": "^1.21.2", "eslint": "^8.19.0", "mocha": "^10.0.0", "nodemon": "^2.0.19", diff --git a/src/app.ts b/src/app.ts index 87ea50a..49d1224 100644 --- a/src/app.ts +++ b/src/app.ts @@ -45,6 +45,8 @@ import { youtubeApiProxy } from "./routes/youtubeApiProxy"; import { getChapterNames } from "./routes/getChapterNames"; import { getTopCategoryUsers } from "./routes/getTopCategoryUsers"; import { addUserAsTempVIP } from "./routes/addUserAsTempVIP"; +import { endpoint as getVideoLabels } from "./routes/getVideoLabel"; +import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash"; import { addFeature } from "./routes/addFeature"; import { generateTokenRequest } from "./routes/generateToken"; import { verifyTokenRequest } from "./routes/verifyToken"; @@ -200,6 +202,11 @@ function setupRoutes(router: Router) { router.get("/api/generateToken/:type", generateTokenRequest); router.get("/api/verifyToken", verifyTokenRequest); + // labels + router.get("/api/videoLabels", getVideoLabels); + router.get("/api/videoLabels/:prefix", getVideoLabelsByHash); + + /* istanbul ignore next */ if (config.postgres?.enabled) { router.get("/database", (req, res) => dumpDatabase(req, res, true)); router.get("/database.json", (req, res) => dumpDatabase(req, res, false)); @@ -211,4 +218,4 @@ function setupRoutes(router: Router) { }); } } -/* eslint-enable @typescript-eslint/no-misused-promises */ \ No newline at end of file +/* eslint-enable @typescript-eslint/no-misused-promises */ diff --git a/src/routes/deleteLockCategories.ts b/src/routes/deleteLockCategories.ts index 10a3ec9..3340f10 100644 --- a/src/routes/deleteLockCategories.ts +++ b/src/routes/deleteLockCategories.ts @@ -35,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ || !categories || !Array.isArray(categories) || categories.length === 0 + || actionTypes && !Array.isArray(actionTypes) || actionTypes.length === 0 ) { return res.status(400).json({ @@ -48,7 +49,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ if (!userIsVIP) { return res.status(403).json({ - message: "Must be a VIP to mark videos.", + message: "Must be a VIP to lock videos.", }); } diff --git a/src/routes/generateToken.ts b/src/routes/generateToken.ts index 294a948..d617ecc 100644 --- a/src/routes/generateToken.ts +++ b/src/routes/generateToken.ts @@ -24,6 +24,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo if (type === TokenType.patreon || (type === TokenType.local && adminUserIDHash === config.adminUserID)) { const licenseKey = await createAndSaveToken(type, code); + /* istanbul ignore else */ if (licenseKey) { return res.status(200).send(`

@@ -45,5 +46,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo

`); } + } else { + return res.sendStatus(403); } } \ No newline at end of file diff --git a/src/routes/getDaysSavedFormatted.ts b/src/routes/getDaysSavedFormatted.ts index 61046e3..bd906a6 100644 --- a/src/routes/getDaysSavedFormatted.ts +++ b/src/routes/getDaysSavedFormatted.ts @@ -7,7 +7,11 @@ export async function getDaysSavedFormatted(req: Request, res: Response): Promis if (row !== undefined) { //send this result return res.send({ - daysSaved: row.daysSaved.toFixed(2), + daysSaved: row.daysSaved?.toFixed(2) ?? "0", + }); + } else { + return res.send({ + daysSaved: 0 }); } } diff --git a/src/routes/getIsUserVIP.ts b/src/routes/getIsUserVIP.ts index 09f4347..a9a3f3e 100644 --- a/src/routes/getIsUserVIP.ts +++ b/src/routes/getIsUserVIP.ts @@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise { let hashPrefix = req.params.prefix as VideoIDHash; - const actionTypes: ActionType[] = req.query.actionTypes - ? JSON.parse(req.query.actionTypes as string) - : req.query.actionType - ? Array.isArray(req.query.actionType) - ? req.query.actionType - : [req.query.actionType] - : [ActionType.Skip, ActionType.Mute]; + let actionTypes: ActionType[] = []; + try { + actionTypes = req.query.actionTypes + ? JSON.parse(req.query.actionTypes as string) + : req.query.actionType + ? Array.isArray(req.query.actionType) + ? 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)) { + return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix } 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); // merge all locks return res.send(mergeLocks(lockedRows, actionTypes)); - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return res.sendStatus(500); } diff --git a/src/routes/getLockReason.ts b/src/routes/getLockReason.ts index ef4e5a3..59ab528 100644 --- a/src/routes/getLockReason.ts +++ b/src/routes/getLockReason.ts @@ -32,18 +32,24 @@ export async function getLockReason(req: Request, res: Response): Promise possibleCategories.includes(x)); - if (!videoID || !Array.isArray(actionTypes)) { - //invalid request - return res.sendStatus(400); - } - try { // 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 }[]; @@ -115,7 +116,7 @@ export async function getLockReason(req: Request, res: Response): Promise, pag ); if (sortBy !== SortableFields.timeSubmitted) { + /* istanbul ignore next */ filteredSegments.sort((a,b) => { const key = sortDir === "desc" ? 1 : -1; if (a[sortBy] < b[sortBy]) { @@ -187,6 +183,7 @@ async function endpoint(req: Request, res: Response): Promise { return res.send(segmentResponse); } } catch (err) { + /* istanbul ignore next */ if (err instanceof SyntaxError) { return res.status(400).send("Invalid array in parameters"); } else return res.sendStatus(500); diff --git a/src/routes/getSegmentInfo.ts b/src/routes/getSegmentInfo.ts index d789336..d4df1aa 100644 --- a/src/routes/getSegmentInfo.ts +++ b/src/routes/getSegmentInfo.ts @@ -7,7 +7,7 @@ const isValidSegmentUUID = (str: string): boolean => /^([a-f0-9]{64}|[a-f0-9]{8} async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise { try { return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]); - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -62,7 +62,7 @@ async function endpoint(req: Request, res: Response): Promise { //send result return res.send(DBSegments); } - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { // catch JSON.parse error return res.status(400).send("UUIDs parameter does not match format requirements."); } else return res.sendStatus(500); diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 7da485e..6c93822 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -107,7 +107,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: } return processedSegments; - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err) { Logger.error(err as string); return null; @@ -169,7 +169,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, })); return segments; - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return null; } @@ -465,7 +465,7 @@ async function endpoint(req: Request, res: Response): Promise { //send result return res.send(segments); } - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { return res.status(400).send("Categories parameter does not match format requirements."); } else return res.sendStatus(500); diff --git a/src/routes/getSkipSegmentsByHash.ts b/src/routes/getSkipSegmentsByHash.ts index 663fd6d..454f232 100644 --- a/src/routes/getSkipSegmentsByHash.ts +++ b/src/routes/getSkipSegmentsByHash.ts @@ -67,8 +67,6 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis // Get all video id's that match hash prefix 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]) => ({ videoID, hash: data.hash, diff --git a/src/routes/getStatus.ts b/src/routes/getStatus.ts index 3f50fa1..6315fdf 100644 --- a/src/routes/getStatus.ts +++ b/src/routes/getStatus.ts @@ -18,7 +18,7 @@ export async function getStatus(req: Request, res: Response): Promise processTime = Date.now() - dbStartTime; return e.value; }) - .catch(e => { + .catch(e => /* istanbul ignore next */ { Logger.error(`status: SQL query timed out: ${e}`); return -1; }); @@ -28,7 +28,7 @@ export async function getStatus(req: Request, res: Response): Promise .then(e => { redisProcessTime = Date.now() - redisStartTime; return e; - }).catch(e => { + }).catch(e => /* istanbul ignore next */ { Logger.error(`status: redis increment timed out ${e}`); return [-1]; }); @@ -36,7 +36,7 @@ export async function getStatus(req: Request, res: Response): Promise const statusValues: Record = { uptime: process.uptime(), - commit: (global as any).HEADCOMMIT || "unknown", + commit: (global as any)?.HEADCOMMIT ?? "unknown", db: Number(dbVersion), startTime, processTime, @@ -48,7 +48,7 @@ export async function getStatus(req: Request, res: Response): Promise activeRedisRequests: getRedisActiveRequests(), }; return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues); - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return res.sendStatus(500); } diff --git a/src/routes/getTopUsers.ts b/src/routes/getTopUsers.ts index a40981e..552aaf9 100644 --- a/src/routes/getTopUsers.ts +++ b/src/routes/getTopUsers.ts @@ -75,11 +75,6 @@ export async function getTopUsers(req: Request, res: Response): Promise { try { return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]); - } catch (err) { + } catch (err) /* istanbul ignore next */{ return null; } } @@ -42,6 +42,7 @@ export async function getUserID(req: Request, res: Response): Promise : await getFuzzyUserID(userName); if (results === undefined || results === null) { + /* istanbul ignore next */ return res.sendStatus(500); } else if (results.length === 0) { return res.sendStatus(404); diff --git a/src/routes/getUserInfo.ts b/src/routes/getUserInfo.ts index 62ac131..70f3464 100644 --- a/src/routes/getUserInfo.ts +++ b/src/routes/getUserInfo.ts @@ -28,7 +28,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min segmentCount: 0, }; } - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -37,7 +37,7 @@ async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise { try { 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; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -46,7 +46,7 @@ async function dbGetUsername(userID: HashedUserID) { try { const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]); return row?.userName ?? userID; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -55,7 +55,7 @@ async function dbGetViewsForUser(userID: HashedUserID) { 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 }); return row?.viewCount ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -64,7 +64,7 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) { 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 }); return row?.ignoredViewCount ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -73,7 +73,7 @@ async function dbGetWarningsForUser(userID: HashedUserID): Promise { try { const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID], { useReplica: true }); return row?.total ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(`Couldn't get warnings for user ${userID}. returning 0`); return 0; } @@ -83,7 +83,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise { try { const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true }); return row?.userCount > 0 ?? false; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -194,7 +194,7 @@ async function getUserInfo(req: Request, res: Response): Promise { export async function endpoint(req: Request, res: Response): Promise { try { return await getUserInfo(req, res); - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { // catch JSON.parse error return res.status(400).send("Invalid values JSON"); } else return res.sendStatus(500); diff --git a/src/routes/getUserStats.ts b/src/routes/getUserStats.ts index 38c8764..b93cc17 100644 --- a/src/routes/getUserStats.ts +++ b/src/routes/getUserStats.ts @@ -75,7 +75,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea }; } return result; - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return null; } @@ -85,7 +85,7 @@ async function dbGetUsername(userID: HashedUserID) { try { const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]); return row?.userName ?? userID; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } diff --git a/src/routes/getUsername.ts b/src/routes/getUsername.ts index 28098fe..b3a2a7b 100644 --- a/src/routes/getUsername.ts +++ b/src/routes/getUsername.ts @@ -27,7 +27,7 @@ export async function getUsername(req: Request, res: Response): Promise ({ + category: chosenSegment.category, + actionType: chosenSegment.actionType, + segment: [chosenSegment.startTime, chosenSegment.endTime], + UUID: chosenSegment.UUID, + locked: chosenSegment.locked, + votes: chosenSegment.votes, + videoDuration: chosenSegment.videoDuration, + userID: chosenSegment.userID, + description: chosenSegment.description + })); +} + +async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise { + try { + const segments: DBSegment[] = await getSegmentsFromDBByVideoID(videoID, service); + return chooseSegment(segments); + } catch (err) { + if (err) { + Logger.error(err as string); + return null; + } + } +} + +async function getLabelsByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise> { + const segments: SBRecord = {}; + + try { + type SegmentWithHashPerVideoID = SBRecord; + + const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service)) + .reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => { + acc[segment.videoID] = acc[segment.videoID] || { + hash: segment.hashedVideoID, + segments: [] + }; + + acc[segment.videoID].segments ??= []; + acc[segment.videoID].segments.push(segment); + + return acc; + }, {}); + + for (const [videoID, videoData] of Object.entries(segmentPerVideoID)) { + const data: VideoData = { + hash: videoData.hash, + segments: chooseSegment(videoData.segments), + }; + + if (data.segments.length > 0) { + segments[videoID] = data; + } + } + + return segments; + } catch (err) { + Logger.error(err as string); + return null; + } +} + +async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise { + const fetchFromDB = () => db + .prepare( + "all", + `SELECT "startTime", "endTime", "videoID", "votes", "locked", "UUID", "userID", "category", "actionType", "hashedVideoID", "description" FROM "sponsorTimes" + WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`, + [`${hashedVideoIDPrefix}%`, service] + ) as Promise; + + if (hashedVideoIDPrefix.length === 4) { + return await QueryCacher.get(fetchFromDB, videoLabelsHashKey(hashedVideoIDPrefix, service)); + } + + return await fetchFromDB(); +} + +async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise { + const fetchFromDB = () => db + .prepare( + "all", + `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "description" FROM "sponsorTimes" + WHERE "videoID" = ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`, + [videoID, service] + ) as Promise; + + return await QueryCacher.get(fetchFromDB, videoLabelsKey(videoID, service)); +} + +function chooseSegment(choices: T[]): Segment[] { + // filter out -2 segments + choices = choices.filter((segment) => segment.votes > -2); + const results = []; + // trivial decisions + if (choices.length === 0) { + return []; + } else if (choices.length === 1) { + return transformDBSegments(choices); + } + // if locked, only choose from locked + const locked = choices.filter((segment) => segment.locked); + if (locked.length > 0) { + choices = locked; + } + //no need to filter, just one label + if (choices.length === 1) { + return transformDBSegments(choices); + } + // sponsor > exclusive > selfpromo + const findCategory = (category: string) => choices.find((segment) => segment.category === category); + + const categoryResult = findCategory("sponsor") ?? findCategory("exclusive_access") ?? findCategory("selfpromo"); + if (categoryResult) results.push(categoryResult); + + return transformDBSegments(results); +} + +async function handleGetLabel(req: Request, res: Response): Promise { + const videoID = req.query.videoID as VideoID; + if (!videoID) { + res.status(400).send("videoID not specified"); + return false; + } + + const service = getService(req.query.service, req.body.service); + const segments = await getLabelsByVideoID(videoID, service); + + if (!segments || segments.length === 0) { + res.sendStatus(404); + return false; + } + + return segments; +} + +async function endpoint(req: Request, res: Response): Promise { + try { + const segments = await handleGetLabel(req, res); + + // If false, res.send has already been called + if (segments) { + //send result + return res.send(segments); + } + } catch (err) { + if (err instanceof SyntaxError) { + return res.status(400).send("Categories parameter does not match format requirements."); + } else return res.sendStatus(500); + } +} + +export { + getLabelsByVideoID, + getLabelsByHash, + endpoint +}; diff --git a/src/routes/getVideoLabelByHash.ts b/src/routes/getVideoLabelByHash.ts new file mode 100644 index 0000000..e3a3615 --- /dev/null +++ b/src/routes/getVideoLabelByHash.ts @@ -0,0 +1,27 @@ +import { hashPrefixTester } from "../utils/hashPrefixTester"; +import { getLabelsByHash } from "./getVideoLabel"; +import { Request, Response } from "express"; +import { VideoIDHash, Service } from "../types/segments.model"; +import { getService } from "../utils/getService"; + +export async function getVideoLabelsByHash(req: Request, res: Response): Promise { + let hashPrefix = req.params.prefix as VideoIDHash; + if (!req.params.prefix || !hashPrefixTester(req.params.prefix)) { + return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix + } + hashPrefix = hashPrefix.toLowerCase() as VideoIDHash; + + const service: Service = getService(req.query.service, req.body.service); + + // Get all video id's that match hash prefix + const segments = await getLabelsByHash(hashPrefix, service); + + if (!segments) return res.status(404).json([]); + + const output = Object.entries(segments).map(([videoID, data]) => ({ + videoID, + hash: data.hash, + segments: data.segments, + })); + return res.status(output.length === 0 ? 404 : 200).json(output); +} diff --git a/src/routes/getViewsForUser.ts b/src/routes/getViewsForUser.ts index 7d7a7b2..4488c13 100644 --- a/src/routes/getViewsForUser.ts +++ b/src/routes/getViewsForUser.ts @@ -25,7 +25,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise + new RegExp(/[A-Za-z0-9]{40}|[A-Za-z0-9-]{35}/).test(token); + export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise { const { query: { licenseKey } } = req; if (!licenseKey) { return res.status(400).send("Invalid request"); - } - const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/); - if (!licenseRegex.test(licenseKey)) { + } else if (!validatelicenseKeyRegex(licenseKey)) { + // fast check for invalid licence key return res.status(200).send({ allowed: false }); @@ -34,6 +35,7 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response) refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error); } + /* istanbul ignore else */ if (identity) { const membership = identity.included?.[0]?.attributes; 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 { for (const link of config.gumroad.productPermalinks) { try { - const formData = new FormData(); - formData.append("product_permalink", link); - 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 result = await axios.post("https://api.gumroad.com/v2/licenses/verify", { + params: { product_permalink: link, license_key: licenseKey } }); const allowed = result.status === 200 && result.data?.success; if (allowed) return allowed; - } catch (e) { + } catch (e) /* istanbul ignore next */ { Logger.error(`Gumroad fetch for ${link} failed: ${e}`); } } diff --git a/src/types/segments.model.ts b/src/types/segments.model.ts index 841bb99..47be636 100644 --- a/src/types/segments.model.ts +++ b/src/types/segments.model.ts @@ -5,7 +5,7 @@ import { HashedUserID, UserID } from "./user.model"; export type SegmentUUID = string & { __segmentUUIDBrand: unknown }; export type VideoID = string & { __videoIDBrand: unknown }; export type VideoDuration = number & { __videoDurationBrand: unknown }; -export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "filler" | "poi_highlight" | "chapter") & { __categoryBrand: unknown }; +export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight" | "chapter" | "filler" | "exclusive_access") & { __categoryBrand: unknown }; export type VideoIDHash = VideoID & HashedValue; export type IPAddress = string & { __ipAddressBrand: unknown }; export type HashedIP = IPAddress & HashedValue; diff --git a/src/utils/getIP.ts b/src/utils/getIP.ts index 0d2dd90..85a7192 100644 --- a/src/utils/getIP.ts +++ b/src/utils/getIP.ts @@ -3,6 +3,9 @@ import { Request } from "express"; import { IPAddress } from "../types/segments.model"; 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") { config.behindProxy = "X-Forwarded-For"; } @@ -15,6 +18,6 @@ export function getIP(req: Request): IPAddress { case "X-Real-IP": return req.headers["x-real-ip"] as IPAddress; default: - return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress; + return req.socket?.remoteAddress as IPAddress; } } \ No newline at end of file diff --git a/src/utils/innerTubeAPI.ts b/src/utils/innerTubeAPI.ts index c330e92..475cda9 100644 --- a/src/utils/innerTubeAPI.ts +++ b/src/utils/innerTubeAPI.ts @@ -18,6 +18,7 @@ async function getFromITube (videoID: string): Promise { const result = await axios.post(url, data, { timeout: 3500 }); + /* istanbul ignore else */ if (result.status === 200) { return result.data.videoDetails; } else { @@ -39,6 +40,7 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom return data as innerTubeVideoDetails; } } catch (err) { + /* istanbul ignore next */ return Promise.reject(err); } } diff --git a/src/utils/queryCacher.ts b/src/utils/queryCacher.ts index cb1faeb..1b3f6a1 100644 --- a/src/utils/queryCacher.ts +++ b/src/utils/queryCacher.ts @@ -1,6 +1,6 @@ import redis from "../utils/redis"; import { Logger } from "../utils/logger"; -import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey } from "./redisKeys"; +import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsKey, videoLabelsHashKey } from "./redisKeys"; import { Service, VideoID, VideoIDHash } from "../types/segments.model"; import { Feature, HashedUserID, UserID } from "../types/user.model"; import { config } from "../config"; @@ -81,6 +81,8 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID redis.del(skipSegmentsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentGroupsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); + redis.del(videoLabelsKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); + redis.del(videoLabelsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); if (videoInfo.userID) redis.del(reputationKey(videoInfo.userID)).catch((err) => Logger.error(err)); } } diff --git a/src/utils/redisKeys.ts b/src/utils/redisKeys.ts index 86cfcef..e482e10 100644 --- a/src/utils/redisKeys.ts +++ b/src/utils/redisKeys.ts @@ -38,6 +38,16 @@ export function shaHashKey(singleIter: HashedValue): string { export const tempVIPKey = (userID: HashedUserID): string => `vip.temp.${userID}`; +export const videoLabelsKey = (videoID: VideoID, service: Service): string => + `labels.v1.${service}.videoID.${videoID}`; + +export function videoLabelsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string { + hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash; + if (hashedVideoIDPrefix.length !== 4) Logger.warn(`Redis skip segment hash-prefix key is not length 4! ${hashedVideoIDPrefix}`); + + return `labels.v1.${service}.${hashedVideoIDPrefix}`; +} + export function userFeatureKey (userID: HashedUserID, feature: Feature): string { return `user.${userID}.feature.${feature}`; } \ No newline at end of file diff --git a/src/utils/tokenUtils.ts b/src/utils/tokenUtils.ts index 7c5ad3c..e936b45 100644 --- a/src/utils/tokenUtils.ts +++ b/src/utils/tokenUtils.ts @@ -58,12 +58,11 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis return licenseKey; } - } catch (e) { + break; + } catch (e) /* istanbul ignore next */ { Logger.error(`token creation: ${e}`); return null; } - - break; } case TokenType.local: { const licenseKey = generateToken(); @@ -74,7 +73,6 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis return licenseKey; } } - return null; } @@ -102,15 +100,12 @@ export async function refreshToken(type: TokenType, licenseKey: string, refreshT return true; } - } catch (e) { + } catch (e) /* istanbul ignore next */ { Logger.error(`token refresh: ${e}`); return false; } - - break; } } - return false; } @@ -136,9 +131,8 @@ export async function getPatreonIdentity(accessToken: string): Promise 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)); + }); +}); \ No newline at end of file diff --git a/test/cases/generateVerifyToken.ts b/test/cases/generateVerifyToken.ts new file mode 100644 index 0000000..ef02a5f --- /dev/null +++ b/test/cases/generateVerifyToken.ts @@ -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)); + }); +}); diff --git a/test/cases/getDaysSavedFormatted.ts b/test/cases/getDaysSavedFormatted.ts new file mode 100644 index 0000000..2b2ae23 --- /dev/null +++ b/test/cases/getDaysSavedFormatted.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/test/cases/getIP.ts b/test/cases/getIP.ts new file mode 100644 index 0000000..f761e01 --- /dev/null +++ b/test/cases/getIP.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/test/cases/getLockCategoriesByHash.ts b/test/cases/getLockCategoriesByHash.ts index 9695731..f6b2757 100644 --- a/test/cases/getLockCategoriesByHash.ts +++ b/test/cases/getLockCategoriesByHash.ts @@ -166,17 +166,77 @@ describe("getLockCategoriesByHash", () => { .catch(err => done(err)); }); - it("Should be able to get by actionType", (done) => { - getLockCategories(fakeHash.substring(0,5), [ActionType.Full]) + it("should return 400 if invalid actionTypes", (done) => { + 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 => { assert.strictEqual(res.status, 200); const expected = [{ - videoID: "fakehash-2", - hash: fakeHash, + videoID, + hash, 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); done(); diff --git a/test/cases/getLockReason.ts b/test/cases/getLockReason.ts index 9bbb35b..1b3c36b 100644 --- a/test/cases/getLockReason.ts +++ b/test/cases/getLockReason.ts @@ -55,6 +55,45 @@ describe("getLockReason", () => { .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) => { client.get(endpoint, { params: { videoID: "getLockReason", category: "intro" } }) .then(res => { @@ -118,8 +157,10 @@ describe("getLockReason", () => { }) .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) .then(res => { assert.strictEqual(res.status, 400); @@ -128,15 +169,37 @@ describe("getLockReason", () => { .catch(err => done(err)); }); - it("should be able to get by actionType", (done) => { - client.get(endpoint, { params: { videoID: "getLockReason", actionType: "full" } }) + it("Should return 400 with invalid actionTypes ", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: 3 } }) .then(res => { - assert.strictEqual(res.status, 200); - const expected = [ - { category: "selfpromo", locked: 1, reason: "sponsor-reason", userID: vipUserID2, userName: vipUserName2 }, - { category: "sponsor", locked: 0, reason: "", userID: "", userName: "" } - ]; - partialDeepEquals(res.data, expected); + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + 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(); }) .catch(err => done(err)); diff --git a/test/cases/getSavedTimeForUser.ts b/test/cases/getSavedTimeForUser.ts index 7ed809f..b7e538a 100644 --- a/test/cases/getSavedTimeForUser.ts +++ b/test/cases/getSavedTimeForUser.ts @@ -2,22 +2,31 @@ import { db } from "../../src/databases/databases"; import { getHash } from "../../src/utils/getHash"; import { deepStrictEqual } from "assert"; import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers const endpoint = "/api/getSavedTimeForUser"; +const getSavedTimeForUser = (userID: string) => client({ + url: endpoint, + params: { userID } +}); describe("getSavedTimeForUser", () => { - const user1 = "getSavedTimeForUserUser"; + const user1 = "getSavedTimeForUser1"; + const user2 = "getSavedTimeforUser2"; + const [ start, end, views ] = [1, 11, 50]; + before(async () => { const startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", "views", "shadowHidden") VALUES'; 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; }); - - it("Should be able to get a 200", (done) => { - client.get(endpoint, { params: { userID: user1 } }) + it("Should be able to get a saved time", (done) => { + getSavedTimeForUser(user1) .then(res => { // (end-start)*minute * views - const savedMinutes = ((11-1)/60) * 50; + const savedMinutes = ((end-start)/60) * views; const expected = { timeSaved: savedMinutes }; @@ -26,4 +35,20 @@ describe("getSavedTimeForUser", () => { }) .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)); + }); }); diff --git a/test/cases/getSearchSegments.ts b/test/cases/getSearchSegments.ts index 4e78604..6dbef4f 100644 --- a/test/cases/getSearchSegments.ts +++ b/test/cases/getSearchSegments.ts @@ -80,6 +80,67 @@ describe("getSearchSegments", () => { .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) => { client.get(endpoint, { params: { videoID: "searchTest0", locked: false } }) .then(res => { diff --git a/test/cases/getSearchSegments4xx.ts b/test/cases/getSearchSegments4xx.ts new file mode 100644 index 0000000..708a0b0 --- /dev/null +++ b/test/cases/getSearchSegments4xx.ts @@ -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)); + }); +}); diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index 098f9c0..3129f91 100644 --- a/test/cases/getSkipSegmentsByHash.ts +++ b/test/cases/getSkipSegmentsByHash.ts @@ -3,7 +3,7 @@ import { partialDeepEquals, arrayPartialDeepEquals } from "../utils/partialDeepE import { getHash } from "../../src/utils/getHash"; import { ImportMock, } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; @@ -581,4 +581,78 @@ describe("getSkipSegmentsByHash", () => { }) .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).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)); + }); }); diff --git a/test/cases/getStatus.ts b/test/cases/getStatus.ts index 7f4ffaf..f471404 100644 --- a/test/cases/getStatus.ts +++ b/test/cases/getStatus.ts @@ -2,6 +2,7 @@ import assert from "assert"; import { db } from "../../src/databases/databases"; import { client } from "../utils/httpClient"; import { config } from "../../src/config"; +import sinon from "sinon"; let dbVersion: number; describe("getStatus", () => { @@ -122,4 +123,16 @@ describe("getStatus", () => { }) .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(); + }); }); diff --git a/test/cases/getTopUsers.ts b/test/cases/getTopUsers.ts index 8808910..07ef80c 100644 --- a/test/cases/getTopUsers.ts +++ b/test/cases/getTopUsers.ts @@ -38,6 +38,15 @@ describe("getTopUsers", () => { .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) => { client.get(endpoint, { params: { sortType: 0 } })// minutesSaved .then(res => { diff --git a/test/cases/getTotalStats.ts b/test/cases/getTotalStats.ts new file mode 100644 index 0000000..f95c292 --- /dev/null +++ b/test/cases/getTotalStats.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/test/cases/getUserInfo.ts b/test/cases/getUserInfo.ts index d4676c7..9bcf043 100644 --- a/test/cases/getUserInfo.ts +++ b/test/cases/getUserInfo.ts @@ -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, ["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, ["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 (?, ?, ?, ?, ?)'; @@ -264,6 +265,15 @@ describe("getUserInfo", () => { .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) => { client.get(endpoint, { params: { userID: "notused-userid" } }) .then(res => { @@ -309,6 +319,30 @@ describe("getUserInfo", () => { .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) => { client.get(endpoint, { params: { userID: "getuserinfo_user_04" } }) .then(res => { diff --git a/test/cases/getUserInfoFree.ts b/test/cases/getUserInfoFree.ts new file mode 100644 index 0000000..847a59a --- /dev/null +++ b/test/cases/getUserInfoFree.ts @@ -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)); + }); +}); diff --git a/test/cases/getUsername.ts b/test/cases/getUsername.ts new file mode 100644 index 0000000..5394434 --- /dev/null +++ b/test/cases/getUsername.ts @@ -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)); + }); +}); \ No newline at end of file diff --git a/test/cases/getVideoLabelByHash.ts b/test/cases/getVideoLabelByHash.ts new file mode 100644 index 0000000..650867e --- /dev/null +++ b/test/cases/getVideoLabelByHash.ts @@ -0,0 +1,184 @@ +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { client } from "../utils/httpClient"; +import { getHash } from "../../src/utils/getHash"; + +describe("getVideoLabelHash", () => { + const endpoint = "/api/videoLabels"; + before(async () => { + const query = 'INSERT INTO "sponsorTimes" ("videoID", "hashedVideoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)'; + await db.prepare("run", query, ["getLabelHashSponsor" , getHash("getLabelHashSponsor", 1) , 2, 0, "labelhash01", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashEA" , getHash("getLabelHashEA", 1) , 2, 0, "labelhash02", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashSelfpromo" , getHash("getLabelHashSelfpromo", 1) , 2, 0, "labelhash03", "labeluser", 0, "selfpromo", "full", 0, 0]); + // priority override + await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash04", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash05", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash06", "labeluser", 0, "selfpromo", "full", 0, 0]); + // locked only + await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 0, "labelhash07", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 0, "labelhash08", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 1, "labelhash09", "labeluser", 0, "selfpromo", "full", 0, 0]); + // hidden segments + await db.prepare("run", query, ["getLabelHashDownvote" , getHash("getLabelHashDownvote", 1) , -2, 0, "labelhash10", "labeluser", 0, "selfpromo", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashHidden" , getHash("getLabelHashHidden", 1) , 2, 0, "labelhash11", "labeluser", 0, "selfpromo", "full", 1, 0]); + await db.prepare("run", query, ["getLabelHashShHidden" , getHash("getLabelHashShHidden", 1) , 2, 0, "labelhash12", "labeluser", 0, "selfpromo", "full", 0, 1]); + // priority override2 + await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , -2, 0, "labelhash13", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , 2, 0, "labelhash14", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , 2, 0, "labelhash15", "labeluser", 0, "selfpromo", "full", 0, 0]); + + return; + }); + + function validateLabel(data: any, videoID: string) { + assert.strictEqual(data[0].videoID, videoID); + assert.strictEqual(data[0].segments.length, 1); + assert.strictEqual(data[0].segments[0].segment[0], 0); + assert.strictEqual(data[0].segments[0].segment[1], 0); + assert.strictEqual(data[0].segments[0].actionType, "full"); + assert.strictEqual(data[0].segments[0].userID, "labeluser"); + } + + const get = (videoID: string) => client.get(`${endpoint}/${getHash(videoID, 1).substring(0, 4)}`); + + it("Should be able to get sponsor only label", (done) => { + const videoID = "getLabelHashSponsor"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "sponsor"); + assert.strictEqual(result.UUID, "labelhash01"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get exclusive access only label", (done) => { + const videoID = "getLabelHashEA"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "exclusive_access"); + assert.strictEqual(result.UUID, "labelhash02"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get selfpromo only label", (done) => { + const videoID = "getLabelHashSelfpromo"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "selfpromo"); + assert.strictEqual(result.UUID, "labelhash03"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get only sponsor if multiple segments exist", (done) => { + const videoID = "getLabelHashPriority"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "sponsor"); + assert.strictEqual(result.UUID, "labelhash04"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should override priority if locked", (done) => { + const videoID = "getLabelHashLocked"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "selfpromo"); + assert.strictEqual(result.UUID, "labelhash09"); + assert.strictEqual(result.locked, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get highest priority category", (done) => { + const videoID = "getLabelHashPriority2"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "exclusive_access"); + assert.strictEqual(result.UUID, "labelhash14"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are downvoted", (done) => { + get("getLabelHashDownvote") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are hidden", (done) => { + get("getLabelHashHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are shadowhidden", (done) => { + get("getLabelHashShHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if no segment found", (done) => { + get("notarealvideo") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get 400 if no videoID passed in", (done) => { + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); diff --git a/test/cases/getVideoLabels.ts b/test/cases/getVideoLabels.ts new file mode 100644 index 0000000..7202d2c --- /dev/null +++ b/test/cases/getVideoLabels.ts @@ -0,0 +1,170 @@ +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { client } from "../utils/httpClient"; + +describe("getVideoLabels", () => { + const endpoint = "/api/videoLabels"; + before(async () => { + const query = 'INSERT INTO "sponsorTimes" ("videoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)'; + await db.prepare("run", query, ["getLabelSponsor" , 2, 0, "label01", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelEA" , 2, 0, "label02", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelSelfpromo" , 2, 0, "label03", "labeluser", 0, "selfpromo", "full", 0, 0]); + // priority override + await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label04", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label05", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label06", "labeluser", 0, "selfpromo", "full", 0, 0]); + // locked only + await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label07", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label08", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelLocked" , 2, 1, "label09", "labeluser", 0, "selfpromo", "full", 0, 0]); + // hidden segments + await db.prepare("run", query, ["getLabelDownvote" ,-2, 0, "label10", "labeluser", 0, "selfpromo", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHidden" ,2, 0, "label11", "labeluser", 0, "selfpromo", "full", 1, 0]); + await db.prepare("run", query, ["getLabelShadowHidden",2, 0, "label12", "labeluser", 0, "selfpromo", "full", 0, 1]); + // priority override2 + await db.prepare("run", query, ["getLabelPriority2" , -2, 0, "label13", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label14", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label15", "labeluser", 0, "selfpromo", "full", 0, 0]); + + return; + }); + + function validateLabel(result: any) { + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].segment[0], 0); + assert.strictEqual(result[0].segment[1], 0); + assert.strictEqual(result[0].actionType, "full"); + assert.strictEqual(result[0].userID, "labeluser"); + } + + const get = (videoID: string) => client.get(endpoint, { params: { videoID } }); + + it("Should be able to get sponsor only label", (done) => { + get("getLabelSponsor") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "sponsor"); + assert.strictEqual(data[0].UUID, "label01"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get exclusive access only label", (done) => { + get("getLabelEA") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "exclusive_access"); + assert.strictEqual(data[0].UUID, "label02"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get selfpromo only label", (done) => { + get("getLabelSelfpromo") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "selfpromo"); + assert.strictEqual(data[0].UUID, "label03"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get only sponsor if multiple segments exist", (done) => { + get("getLabelPriority") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "sponsor"); + assert.strictEqual(data[0].UUID, "label04"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should override priority if locked", (done) => { + get("getLabelLocked") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "selfpromo"); + assert.strictEqual(data[0].UUID, "label09"); + assert.strictEqual(data[0].locked, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get highest priority category", (done) => { + get("getLabelPriority2") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "exclusive_access"); + assert.strictEqual(data[0].UUID, "label14"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are downvoted", (done) => { + get("getLabelDownvote") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are hidden", (done) => { + get("getLabelHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are shadowhidden", (done) => { + get("getLabelShadowHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if no segment found", (done) => { + get("notarealvideo") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get 400 if no videoID passed in", (done) => { + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); diff --git a/test/cases/getViewsForUser.ts b/test/cases/getViewsForUser.ts new file mode 100644 index 0000000..9471e18 --- /dev/null +++ b/test/cases/getViewsForUser.ts @@ -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)); + }); +}); \ No newline at end of file diff --git a/test/cases/lockCategoriesHttp.ts b/test/cases/lockCategoriesHttp.ts new file mode 100644 index 0000000..8525d46 --- /dev/null +++ b/test/cases/lockCategoriesHttp.ts @@ -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 => 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)); + }); + } + } +}); \ No newline at end of file diff --git a/test/cases/lockCategoriesRecords.ts b/test/cases/lockCategoriesRecords.ts index 7d6bd4a..ebfc421 100644 --- a/test/cases/lockCategoriesRecords.ts +++ b/test/cases/lockCategoriesRecords.ts @@ -266,106 +266,6 @@ describe("lockCategoriesRecords", () => { .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) => { const videoID = "delete-record"; const json = { diff --git a/test/cases/postSkipSegments.ts b/test/cases/postSkipSegments.ts index 17729dd..57ebeb8 100644 --- a/test/cases/postSkipSegments.ts +++ b/test/cases/postSkipSegments.ts @@ -4,7 +4,7 @@ import { partialDeepEquals, arrayDeepEquals } from "../utils/partialDeepEquals"; import { db } from "../../src/databases/databases"; import { ImportMock } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; import { Feature } from "../../src/types/user.model"; diff --git a/test/cases/shadowBanUser.ts b/test/cases/shadowBanUser.ts index 834382c..a0b321c 100644 --- a/test/cases/shadowBanUser.ts +++ b/test/cases/shadowBanUser.ts @@ -187,10 +187,34 @@ describe("shadowBanUser", () => { }) .then(async res => { assert.strictEqual(res.status, 200); - const videoRow = await getShadowBanSegmentCategory(userID, 1); + 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 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"); done(); }) diff --git a/test/cases/shadowBanUser4xx.ts b/test/cases/shadowBanUser4xx.ts new file mode 100644 index 0000000..c9cdda9 --- /dev/null +++ b/test/cases/shadowBanUser4xx.ts @@ -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) => 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)); + }); +}); diff --git a/test/cases/testUtils.ts b/test/cases/testUtils.ts index 78818c8..29bfe60 100644 --- a/test/cases/testUtils.ts +++ b/test/cases/testUtils.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import { partialDeepEquals } from "../utils/partialDeepEquals"; +import { partialDeepEquals, mixedDeepEquals } from "../utils/partialDeepEquals"; describe("Test utils ", () => { it("objectContain", () => { @@ -135,4 +135,45 @@ describe("Test utils ", () => { } ), "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 + })); + }); }); \ No newline at end of file diff --git a/test/cases/tokenUtils.ts b/test/cases/tokenUtils.ts new file mode 100644 index 0000000..4b3890e --- /dev/null +++ b/test/cases/tokenUtils.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/test/cases/userCounter.ts b/test/cases/userCounter.ts index 26a1adb..8215ce1 100644 --- a/test/cases/userCounter.ts +++ b/test/cases/userCounter.ts @@ -3,10 +3,9 @@ import assert from "assert"; import { config } from "../../src/config"; import { getHash } from "../../src/utils/getHash"; - describe("userCounter", () => { - it("Should return 200", (done) => { - if (!config.userCounterURL) return done(); // skip if no userCounterURL is set + it("Should return 200", function (done) { + if (!config.userCounterURL) this.skip(); // skip if no userCounterURL is set axios.request({ method: "POST", baseURL: config.userCounterURL, diff --git a/test/cases/voteOnSponsorTime.ts b/test/cases/voteOnSponsorTime.ts index 11126b3..755735f 100644 --- a/test/cases/voteOnSponsorTime.ts +++ b/test/cases/voteOnSponsorTime.ts @@ -3,7 +3,7 @@ import { db, privateDB } from "../../src/databases/databases"; import { getHash } from "../../src/utils/getHash"; import { ImportMock } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; import { arrayDeepEquals } from "../utils/partialDeepEquals"; diff --git a/test/mocks.ts b/test/mocks.ts index a5c578e..d6673bf 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -1,23 +1,24 @@ import express from "express"; import { config } from "../src/config"; import { Server } from "http"; +import { UserCounter } from "./mocks/UserCounter"; const app = express(); -app.post("/ReportChannelWebhook", (req, res) => { +app.post("/webhook/ReportChannel", (req, res) => { res.sendStatus(200); }); -app.post("/FirstTimeSubmissionsWebhook", (req, res) => { +app.post("/webhook/FirstTimeSubmissions", (req, res) => { res.sendStatus(200); }); -app.post("/CompletelyIncorrectReportWebhook", (req, res) => { +app.post("/webhook/CompletelyIncorrectReport", (req, res) => { res.sendStatus(200); }); // Testing NeuralBlock -app.post("/NeuralBlockRejectWebhook", (req, res) => { +app.post("/webhook/NeuralBlockReject", (req, res) => { res.sendStatus(200); }); @@ -47,6 +48,9 @@ app.post("/CustomWebhook", (req, res) => { res.sendStatus(200); }); +// mocks +app.use("/UserCounter", UserCounter); + export function createMockServer(callback: () => void): Server { return app.listen(config.mockPort, callback); } diff --git a/test/mocks/UserCounter.ts b/test/mocks/UserCounter.ts new file mode 100644 index 0000000..d4ba32a --- /dev/null +++ b/test/mocks/UserCounter.ts @@ -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 + }); +}); \ No newline at end of file diff --git a/test/mocks/gumroadMock.ts b/test/mocks/gumroadMock.ts new file mode 100644 index 0000000..00ae831 --- /dev/null +++ b/test/mocks/gumroadMock.ts @@ -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()}`; diff --git a/test/mocks/mockExpressRequest.ts b/test/mocks/mockExpressRequest.ts new file mode 100644 index 0000000..7ad4ae0 --- /dev/null +++ b/test/mocks/mockExpressRequest.ts @@ -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 +}); diff --git a/test/mocks/patreonMock.ts b/test/mocks/patreonMock.ts new file mode 100644 index 0000000..aafe26e --- /dev/null +++ b/test/mocks/patreonMock.ts @@ -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, +}; \ No newline at end of file diff --git a/test/youtubeMock.ts b/test/mocks/youtubeMock.ts similarity index 97% rename from test/youtubeMock.ts rename to test/mocks/youtubeMock.ts index bb489af..95f3d20 100644 --- a/test/youtubeMock.ts +++ b/test/mocks/youtubeMock.ts @@ -1,4 +1,4 @@ -import { APIVideoData, APIVideoInfo } from "../src/types/youtubeApi.model"; +import { APIVideoData, APIVideoInfo } from "../../src/types/youtubeApi.model"; export class YouTubeApiMock { // eslint-disable-next-line require-await