diff --git a/ci.json b/ci.json index 1637b41..c18b0d5 100644 --- a/ci.json +++ b/ci.json @@ -56,7 +56,6 @@ ] } ], - "maxNumberOfActiveWarnings": 3, "hoursAfterWarningExpires": 24, "rateLimit": { "vote": { diff --git a/package.json b/package.json index c47f349..32fd685 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "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:14-alpine", - "redis:docker": "docker run --rm -p 6379:6379 redis:7-alpine --save '' --appendonly no", + "postgres:docker": "docker run --init -it --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:14-alpine", + "redis:docker": "docker run --init -it --rm -p 6379:6379 redis:7-alpine --save '' --appendonly no", "start": "ts-node src/index.ts", "tsc": "tsc -p tsconfig.json", "lint": "eslint src test", diff --git a/src/middleware/userCounter.ts b/src/middleware/userCounter.ts index dff3ab5..eda60a3 100644 --- a/src/middleware/userCounter.ts +++ b/src/middleware/userCounter.ts @@ -14,7 +14,7 @@ export function userCounter(req: Request, res: Response, next: NextFunction): vo method: "post", url: `${config.userCounterURL}/api/v1/addIP?hashedIP=${getIP(req)}`, httpAgent - }).catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`)); + }).catch(() => /* instanbul skip next */ Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`)); } } diff --git a/src/routes/getSegmentInfo.ts b/src/routes/getSegmentInfo.ts index d4df1aa..f6625af 100644 --- a/src/routes/getSegmentInfo.ts +++ b/src/routes/getSegmentInfo.ts @@ -34,11 +34,11 @@ async function handleGetSegmentInfo(req: Request, res: Response): Promise 10) UUIDs = UUIDs.slice(0, 10); - if (!Array.isArray(UUIDs) || !UUIDs) { + if (!Array.isArray(UUIDs) || !UUIDs?.length) { res.status(400).send("UUIDs parameter does not match format requirements."); return; } + if (UUIDs.length > 10) UUIDs = UUIDs.slice(0, 10); const DBSegments = await getSegmentsByUUID(UUIDs); // all uuids failed lookup if (!DBSegments?.length) { diff --git a/src/routes/getSkipSegmentsByHash.ts b/src/routes/getSkipSegmentsByHash.ts index eaf7f97..d5b89bd 100644 --- a/src/routes/getSkipSegmentsByHash.ts +++ b/src/routes/getSkipSegmentsByHash.ts @@ -25,13 +25,13 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis try { await getEtag("skipSegmentsHash", hashPrefix, service) .then(etag => res.set("ETag", etag)) - .catch(() => null); + .catch(/* istanbul ignore next */ () => null); const output = Object.entries(segments).map(([videoID, data]) => ({ videoID, segments: data.segments, })); return res.status(output.length === 0 ? 404 : 200).json(output); - } catch(e) { + } catch (e) /* istanbul ignore next */ { Logger.error(`skip segments by hash error: ${e}`); return res.status(500).send("Internal server error"); diff --git a/src/routes/getTopCategoryUsers.ts b/src/routes/getTopCategoryUsers.ts index 74853b3..c8ce0d0 100644 --- a/src/routes/getTopCategoryUsers.ts +++ b/src/routes/getTopCategoryUsers.ts @@ -2,10 +2,12 @@ import { db } from "../databases/databases"; import { createMemoryCache } from "../utils/createMemoryCache"; import { config } from "../config"; import { Request, Response } from "express"; +import { validateCategories } from "../utils/parseParams"; const MILLISECONDS_IN_MINUTE = 60000; // eslint-disable-next-line @typescript-eslint/no-misused-promises const getTopCategoryUsersWithCache = createMemoryCache(generateTopCategoryUsersStats, config.getTopUsersCacheTimeMinutes * MILLISECONDS_IN_MINUTE); +/* istanbul ignore next */ const maxRewardTimePerSegmentInSeconds = config.maxRewardTimePerSegmentInSeconds ?? 86400; interface DBSegment { @@ -38,7 +40,6 @@ async function generateTopCategoryUsersStats(sortBy: string, category: string) { } } - return { userNames, viewCounts, @@ -51,7 +52,7 @@ export async function getTopCategoryUsers(req: Request, res: Response): Promise< const sortType = parseInt(req.query.sortType as string); const category = req.query.category as string; - if (sortType == undefined || !config.categoryList.includes(category) ) { + if (sortType == undefined || !validateCategories([category]) ) { //invalid request return res.sendStatus(400); } diff --git a/src/routes/getTotalStats.ts b/src/routes/getTotalStats.ts index 187a59d..c0fb7bf 100644 --- a/src/routes/getTotalStats.ts +++ b/src/routes/getTotalStats.ts @@ -3,6 +3,7 @@ import { config } from "../config"; import { Request, Response } from "express"; import axios from "axios"; import { Logger } from "../utils/logger"; +import { getCWSUsers } from "../utils/getCWSUsers"; // A cache of the number of chrome web store users let chromeUsersCache = 0; @@ -29,30 +30,30 @@ let lastFetch: DBStatsData = { updateExtensionUsers(); export async function getTotalStats(req: Request, res: Response): Promise { - - const row = await getStats(!!req.query.countContributingUsers); + const countContributingUsers = Boolean(req.query?.countContributingUsers == "true"); + const row = await getStats(countContributingUsers); lastFetch = row; - if (row !== undefined) { - const extensionUsers = chromeUsersCache + firefoxUsersCache; + /* istanbul ignore if */ + if (!row) res.sendStatus(500); + const extensionUsers = chromeUsersCache + firefoxUsersCache; - //send this result - res.send({ - userCount: row.userCount, - activeUsers: extensionUsers, - apiUsers: Math.max(apiUsersCache, extensionUsers), - viewCount: row.viewCount, - totalSubmissions: row.totalSubmissions, - minutesSaved: row.minutesSaved, - }); + //send this result + res.send({ + userCount: row.userCount ?? 0, + activeUsers: extensionUsers, + apiUsers: Math.max(apiUsersCache, extensionUsers), + viewCount: row.viewCount, + totalSubmissions: row.totalSubmissions, + minutesSaved: row.minutesSaved, + }); - // Check if the cache should be updated (every ~14 hours) - const now = Date.now(); - if (now - lastUserCountCheck > 5000000) { - lastUserCountCheck = now; + // Check if the cache should be updated (every ~14 hours) + const now = Date.now(); + if (now - lastUserCountCheck > 5000000) { + lastUserCountCheck = now; - updateExtensionUsers(); - } + updateExtensionUsers(); } } @@ -67,42 +68,53 @@ function getStats(countContributingUsers: boolean): Promise { } } - function updateExtensionUsers() { + /* istanbul ignore else */ if (config.userCounterURL) { axios.get(`${config.userCounterURL}/api/v1/userCount`) - .then(res => { - apiUsersCache = Math.max(apiUsersCache, res.data.userCount); - }) - .catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`)); + .then(res => apiUsersCache = Math.max(apiUsersCache, res.data.userCount)) + .catch( /* istanbul ignore next */ () => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`)); } const mozillaAddonsUrl = "https://addons.mozilla.org/api/v3/addons/addon/sponsorblock/"; const chromeExtensionUrl = "https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone"; + const chromeExtId = "mnjggcdmjocbbbhaepdhchncahnbgone"; axios.get(mozillaAddonsUrl) - .then(res => { - firefoxUsersCache = res.data.average_daily_users; - axios.get(chromeExtensionUrl) - .then(res => { - const body = res.data; - // 2021-01-05 - // [...]= 0) { - const closingQuoteIndex = body.indexOf('"', userDownloadsStartIndex + matchingStringLen); - const userDownloadsStr = body.substr(userDownloadsStartIndex + matchingStringLen, closingQuoteIndex - userDownloadsStartIndex).replace(",", "").replace(".", ""); - chromeUsersCache = parseInt(userDownloadsStr); - } - else { - lastUserCountCheck = 0; - } - }) - .catch(() => Logger.debug(`Failing to connect to ${chromeExtensionUrl}`)); - }) - .catch(() => { + .then(res => firefoxUsersCache = res.data.average_daily_users ) + .catch( /* istanbul ignore next */ () => { Logger.debug(`Failing to connect to ${mozillaAddonsUrl}`); + return 0; }); + getCWSUsers(chromeExtId) + .then(res => chromeUsersCache = res) + .catch(/* istanbul ignore next */ () => + getChromeUsers(chromeExtensionUrl) + .then(res => chromeUsersCache = res) + ); } + +/* istanbul ignore next */ +function getChromeUsers(chromeExtensionUrl: string): Promise { + return axios.get(chromeExtensionUrl) + .then(res => { + const body = res.data; + // 2021-01-05 + // [...]= 0) { + const closingQuoteIndex = body.indexOf('"', userDownloadsStartIndex + matchingStringLen); + const userDownloadsStr = body.substr(userDownloadsStartIndex + matchingStringLen, closingQuoteIndex - userDownloadsStartIndex).replace(",", "").replace(".", ""); + return parseInt(userDownloadsStr); + } else { + lastUserCountCheck = 0; + } + }) + .catch(/* istanbul ignore next */ () => { + Logger.debug(`Failing to connect to ${chromeExtensionUrl}`); + return 0; + }); +} \ No newline at end of file diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index dcae914..d574de9 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -120,7 +120,7 @@ async function sendWebhooks(apiVideoDetails: videoDetails, userID: string, video // false for a pass - it was confusing and lead to this bug - any use of this function in // the future could have the same problem. async function autoModerateSubmission(apiVideoDetails: videoDetails, - submission: { videoID: VideoID; userID: UserID; segments: IncomingSegment[], service: Service, videoDuration: number }) { + submission: { videoID: VideoID; userID: HashedUserID; segments: IncomingSegment[], service: Service, videoDuration: number }) { // get duration from API const apiDuration = apiVideoDetails.duration; // if API fail or returns 0, get duration from client @@ -156,7 +156,7 @@ async function autoModerateSubmission(apiVideoDetails: videoDetails, return false; } -async function checkUserActiveWarning(userID: string): Promise { +async function checkUserActiveWarning(userID: HashedUserID): Promise { const MILLISECONDS_IN_HOUR = 3600000; const now = Date.now(); const warnings = (await db.prepare("all", @@ -337,10 +337,10 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user return CHECK_PASS; } -async function checkByAutoModerator(videoID: any, userID: any, segments: Array, service:string, apiVideoDetails: videoDetails, videoDuration: number): Promise { +async function checkByAutoModerator(videoID: VideoID, userID: HashedUserID, segments: IncomingSegment[], service: Service, apiVideoDetails: videoDetails, videoDuration: number): Promise { // Auto moderator check if (service == Service.YouTube) { - const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { userID, videoID, segments, service, videoDuration }); + const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { videoID, userID, segments, service, videoDuration }); if (autoModerateResult) { return { pass: false, @@ -492,7 +492,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise void, cacheTimeMs: number): any { + /* istanbul ignore if */ if (isNaN(cacheTimeMs)) cacheTimeMs = 0; // holds the promise results diff --git a/src/utils/getCWSUsers.ts b/src/utils/getCWSUsers.ts new file mode 100644 index 0000000..35aeaf0 --- /dev/null +++ b/src/utils/getCWSUsers.ts @@ -0,0 +1,13 @@ +import axios from "axios"; +import { Logger } from "../utils/logger"; + +export const getCWSUsers = (extID: string): Promise => + axios.post(`https://chrome.google.com/webstore/ajax/detail?pv=20210820&id=${extID}`) + .then(res => res.data.split("\n")[2]) + .then(data => JSON.parse(data)) + .then(data => (data[1][1][0][23]).replaceAll(/,|\+/g,"")) + .then(data => parseInt(data)) + .catch((err) => { + Logger.error(`Error getting chrome users - ${err}`); + return 0; + }); \ No newline at end of file diff --git a/src/utils/getHashCache.ts b/src/utils/getHashCache.ts index 0c15cc0..7004880 100644 --- a/src/utils/getHashCache.ts +++ b/src/utils/getHashCache.ts @@ -28,7 +28,7 @@ async function getFromRedis(key: HashedValue): Promise(key: HashedValue): Promise Logger.error(err)); + redis.set(redisKey, data).catch(/* istanbul ignore next */ (err) => Logger.error(err)); } return data as T & HashedValue; diff --git a/src/utils/innerTubeAPI.ts b/src/utils/innerTubeAPI.ts index dbdd728..2fe0b46 100644 --- a/src/utils/innerTubeAPI.ts +++ b/src/utils/innerTubeAPI.ts @@ -70,15 +70,16 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom } try { const data = await getFromITube(videoID) - .catch(err => { + .catch(/* istanbul ignore next */ err => { Logger.warn(`InnerTube API Error for ${videoID}: ${err}`); return Promise.reject(err); }); DiskCache.set(cacheKey, data) .then(() => Logger.debug(`InnerTube API: video information cache set for: ${videoID}`)) - .catch((err: any) => Logger.warn(err)); + .catch(/* istanbul ignore next */ (err: any) => Logger.warn(err)); return data; } catch (err) { + /* istanbul ignore next */ return Promise.reject(err); } } \ No newline at end of file diff --git a/src/utils/isUserTempVIP.ts b/src/utils/isUserTempVIP.ts index 44bc649..3afeff1 100644 --- a/src/utils/isUserTempVIP.ts +++ b/src/utils/isUserTempVIP.ts @@ -11,7 +11,7 @@ export const isUserTempVIP = async (hashedUserID: HashedUserID, videoID: VideoID try { const reply = await redis.get(tempVIPKey(hashedUserID)); return reply && reply == channelID; - } catch (e) { + } catch (e) /* istanbul ignore next */ { Logger.error(e as string); return false; } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 8507a46..a637c66 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -45,6 +45,7 @@ class Logger { }; constructor() { + /* istanbul ignore if */ if (config.mode === "development") { this._settings.INFO = true; this._settings.DEBUG = true; @@ -73,9 +74,11 @@ class Logger { let color = colors.Bright; if (level === LogLevel.ERROR) color = colors.FgRed; + /* istanbul ignore if */ if (level === LogLevel.WARN) color = colors.FgYellow; let levelStr = level.toString(); + /* istanbul ignore if */ if (levelStr.length === 4) { levelStr += " "; // ensure logs are aligned } diff --git a/src/utils/parseParams.ts b/src/utils/parseParams.ts index 258b78a..cb0c93d 100644 --- a/src/utils/parseParams.ts +++ b/src/utils/parseParams.ts @@ -73,3 +73,6 @@ export const parseActionTypes = (req: Request, fallback: ActionType[]): ActionTy export const parseRequiredSegments = (req: Request): SegmentUUID[] | undefined => syntaxErrorWrapper(getRequiredSegments, req, []); // never fall back + +export const validateCategories = (categories: string[]): boolean => + categories.every((category: string) => config.categoryList.includes(category)); \ No newline at end of file diff --git a/src/utils/redis.ts b/src/utils/redis.ts index f8be2c7..8ee2a1b 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -135,17 +135,21 @@ if (config.redis?.enabled) { .then((reply) => resolve(reply)) .catch((err) => reject(err)) ); + /* istanbul ignore next */ client.on("error", function(error) { lastClientFail = Date.now(); Logger.error(`Redis Error: ${error}`); }); + /* istanbul ignore next */ client.on("reconnect", () => { Logger.info("Redis: trying to reconnect"); }); + /* istanbul ignore next */ readClient?.on("error", function(error) { lastReadFail = Date.now(); Logger.error(`Redis Read-Only Error: ${error}`); }); + /* istanbul ignore next */ readClient?.on("reconnect", () => { Logger.info("Redis Read-Only: trying to reconnect"); }); diff --git a/test.json b/test.json index 0d38463..8a4a045 100644 --- a/test.json +++ b/test.json @@ -50,7 +50,6 @@ ] } ], - "maxNumberOfActiveWarnings": 3, "hoursAfterWarningExpires": 24, "rateLimit": { "vote": { diff --git a/test/cases/eTag.ts b/test/cases/eTag.ts new file mode 100644 index 0000000..162b205 --- /dev/null +++ b/test/cases/eTag.ts @@ -0,0 +1,65 @@ +import assert from "assert"; +import { client } from "../utils/httpClient"; +import redis from "../../src/utils/redis"; +import { config } from "../../src/config"; +import { genRandom } from "../utils/getRandom"; + +const validateEtag = (expected: string, actual: string): boolean => { + const [actualHashType, actualHashKey, actualService] = actual.split(";"); + const [expectedHashType, expectedHashKey, expectedService] = expected.split(";"); + return (actualHashType === expectedHashType) && (actualHashKey === expectedHashKey) && (actualService === expectedService); +}; + +describe("eTag", () => { + before(function() { + if (!config.redis?.enabled) this.skip(); + }); + + const endpoint = "/etag"; + it("Should reject weak etag", (done) => { + const etagKey = `W/test-etag-${genRandom()}`; + client.get(endpoint, { headers: { "If-None-Match": etagKey } }) + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("304 etag validation", () => { + before(function() { + if (!config.redis?.enabled) this.skip(); + }); + + const endpoint = "/etag"; + for (const hashType of ["skipSegments", "skipSegmentsHash", "videoLabel", "videoLabelHash"]) { + it(`${hashType} etag should return 304`, (done) => { + const etagKey = `${hashType};${genRandom};YouTube;${Date.now()}`; + redis.setEx(etagKey, 8400, "test").then(() => + client.get(endpoint, { headers: { "If-None-Match": etagKey } }).then(res => { + assert.strictEqual(res.status, 304); + const etag = res.headers?.etag ?? ""; + assert.ok(validateEtag(etagKey, etag)); + done(); + }).catch(err => done(err)) + ); + }); + } + + it(`other etag type should not return 304`, (done) => { + const etagKey = `invalidHashType;${genRandom};YouTube;${Date.now()}`; + client.get(endpoint, { headers: { "If-None-Match": etagKey } }).then(res => { + assert.strictEqual(res.status, 404); + done(); + }).catch(err => done(err)); + }); + + it(`outdated etag type should not return 304`, (done) => { + const etagKey = `skipSegments;${genRandom};YouTube;5000`; + client.get(endpoint, { headers: { "If-None-Match": etagKey } }).then(res => { + assert.strictEqual(res.status, 404); + done(); + }).catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/getHashCache.ts b/test/cases/getHashCache.ts index 40434e6..ed5bf09 100644 --- a/test/cases/getHashCache.ts +++ b/test/cases/getHashCache.ts @@ -3,11 +3,9 @@ import { getHashCache } from "../../src/utils/getHashCache"; import { shaHashKey } from "../../src/utils/redisKeys"; import { getHash } from "../../src/utils/getHash"; import redis from "../../src/utils/redis"; -import crypto from "crypto"; import assert from "assert"; import { setTimeout } from "timers/promises"; - -const genRandom = (bytes=8) => crypto.pseudoRandomBytes(bytes).toString("hex"); +import { genRandom } from "../utils/getRandom"; const rand1Hash = genRandom(24); const rand1Hash_Key = getHash(rand1Hash, 1); diff --git a/test/cases/getSegmentInfo.ts b/test/cases/getSegmentInfo.ts index 87245cc..d9420c0 100644 --- a/test/cases/getSegmentInfo.ts +++ b/test/cases/getSegmentInfo.ts @@ -338,4 +338,13 @@ describe("getSegmentInfo", () => { }) .catch(err => done(err)); }); + + it("Should return 400 if no UUIDs not sent", (done) => { + client.get(endpoint) + .then(res => { + if (res.status !== 400) done(`non 400 response code: ${res.status}`); + else done(); // pass + }) + .catch(err => done(err)); + }); }); diff --git a/test/cases/getSkipSegments.ts b/test/cases/getSkipSegments.ts index f89b778..275f231 100644 --- a/test/cases/getSkipSegments.ts +++ b/test/cases/getSkipSegments.ts @@ -486,4 +486,13 @@ describe("getSkipSegments", () => { }) .catch(err => done(err)); }); + + it("Should get 400 for invalid category type", (done) => { + client.get(endpoint, { params: { videoID: "getSkipSegmentID0", category: 1 } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); }); diff --git a/test/cases/getTopCategoryUsers.ts b/test/cases/getTopCategoryUsers.ts index f321373..bd8adc4 100644 --- a/test/cases/getTopCategoryUsers.ts +++ b/test/cases/getTopCategoryUsers.ts @@ -29,7 +29,7 @@ describe("getTopCategoryUsers", () => { .catch(err => done(err)); }); - it("Should return 400 if invalid sortType provided", (done) => { + it("Should return 400 if invalid type of sortType provided", (done) => { client.get(endpoint, { params: { sortType: "a" } }) .then(res => { assert.strictEqual(res.status, 400); @@ -38,6 +38,15 @@ describe("getTopCategoryUsers", () => { .catch(err => done(err)); }); + it("Should return 400 if invalid sortType number provided", (done) => { + client.get(endpoint, { params: { sortType: 15, category: "sponsor" } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 if invalid category provided", (done) => { client.get(endpoint, { params: { sortType: 1, category: "never_valid_category" } }) .then(res => { @@ -121,4 +130,16 @@ describe("getTopCategoryUsers", () => { }) .catch(err => done(err)); }); + + it("Should return no time saved for chapters", (done) => { + client.get(endpoint, { params: { sortType: 2, category: "chapter" } }) + .then(res => { + assert.strictEqual(res.status, 200); + for (const timeSaved of res.data.minutesSaved) { + assert.strictEqual(timeSaved, 0, "Time saved should be 0"); + } + done(); + }) + .catch(err => done(err)); + }); }); diff --git a/test/cases/getTopUsers.ts b/test/cases/getTopUsers.ts index 07ef80c..c4f0633 100644 --- a/test/cases/getTopUsers.ts +++ b/test/cases/getTopUsers.ts @@ -81,4 +81,14 @@ describe("getTopUsers", () => { }) .catch(err => done(err)); }); + + it("Should be able to get cached result", (done) => { + client.get(endpoint, { params: { sortType: 0 } })// minutesSaved + .then(res => { + assert.strictEqual(res.status, 200); + assert.ok(res.data.userNames.indexOf(user1) < res.data.userNames.indexOf(user2), `Actual Order: ${res.data.userNames}`); + done(); + }) + .catch(err => done(err)); + }); }); diff --git a/test/cases/getTotalStats.ts b/test/cases/getTotalStats.ts index f95c292..775be29 100644 --- a/test/cases/getTotalStats.ts +++ b/test/cases/getTotalStats.ts @@ -7,7 +7,29 @@ describe("getTotalStats", () => { it("Can get total stats", async () => { const result = await client({ url: endpoint }); const data = result.data; - assert.ok(data?.userCount ?? true); + assert.strictEqual(data.userCount, 0, "User count should default false"); + 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); + }); + + it("Can get total stats without contributing users", async () => { + const result = await client({ url: `${endpoint}?countContributingUsers=false` }); + const data = result.data; + assert.strictEqual(data.userCount, 0); + 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); + }); + + it("Can get total stats with contributing users", async () => { + const result = await client({ url: `${endpoint}?countContributingUsers=true` }); + const data = result.data; + assert.ok(data.userCount >= 0); assert.ok(data.activeUsers >= 0); assert.ok(data.apiUsers >= 0); assert.ok(data.viewCount >= 0); diff --git a/test/cases/highLoad.ts b/test/cases/highLoad.ts new file mode 100644 index 0000000..c997f6e --- /dev/null +++ b/test/cases/highLoad.ts @@ -0,0 +1,36 @@ +import sinon from "sinon"; +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { client } from "../utils/httpClient"; +client.defaults.validateStatus = (status) => status < 600; + +describe("High load test", () => { + before(() => { + sinon.stub(db, "highLoad").returns(true); + }); + + after(() => { + sinon.restore(); + }); + + it("Should return 503 on getTopUsers", async () => { + await client.get("/api/getTopUsers?sortType=0") + .then(res => { + assert.strictEqual(res.status, 503); + }); + }); + + it("Should return 503 on getTopCategoryUsers", async () => { + await client.get("/api/getTopCategoryUsers?sortType=0&category=sponsor") + .then(res => { + assert.strictEqual(res.status, 503); + }); + }); + + it("Should return 0 on getTotalStats", async () => { + await client.get("/api/getTotalStats") + .then(res => { + assert.strictEqual(res.status, 200); + }); + }); +}); diff --git a/test/cases/postSkipSegments.ts b/test/cases/postSkipSegments.ts index 233f511..cf45bca 100644 --- a/test/cases/postSkipSegments.ts +++ b/test/cases/postSkipSegments.ts @@ -1,4 +1,3 @@ -import { config } from "../../src/config"; import { getHash } from "../../src/utils/getHash"; import { partialDeepEquals, arrayDeepEquals } from "../utils/partialDeepEquals"; import { db } from "../../src/databases/databases"; @@ -7,117 +6,60 @@ import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; -import { Feature } from "../../src/types/user.model"; + +export type Segment = { + segment: number[]; + category: string; + actionType?: string; + description?: string; +}; + +const endpoint = "/api/skipSegments"; +export const postSkipSegmentJSON = (data: Record) => client({ + method: "POST", + url: endpoint, + data +}); +export const postSkipSegmentParam = (params: Record) => client({ + method: "POST", + url: endpoint, + params +}); +export const convertMultipleToDBFormat = (segments: Segment[]) => + segments.map(segment => convertSingleToDBFormat(segment)); + +export const convertSingleToDBFormat = (segment: Segment) => ({ + startTime: segment.segment[0], + endTime: segment.segment[1], + category: segment.category, +}); const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, "YouTubeAPI"); const sinonStub = mockManager.mock("listVideos"); sinonStub.callsFake(YouTubeApiMock.listVideos); describe("postSkipSegments", () => { -// Constant and helpers + // Constant and helpers const submitUserOne = `PostSkipUser1${".".repeat(18)}`; - const submitUserOneHash = getHash(submitUserOne); const submitUserTwo = `PostSkipUser2${".".repeat(18)}`; const submitUserTwoHash = getHash(submitUserTwo); - const submitUserThree = `PostSkipUser3${".".repeat(18)}`; - - const warnUser01 = "warn-user01-qwertyuiopasdfghjklzxcvbnm"; - const warnUser01Hash = getHash(warnUser01); - const warnUser02 = "warn-user02-qwertyuiopasdfghjklzxcvbnm"; - const warnUser02Hash = getHash(warnUser02); - const warnUser03 = "warn-user03-qwertyuiopasdfghjklzxcvbnm"; - const warnUser03Hash = getHash(warnUser03); - const warnUser04 = "warn-user04-qwertyuiopasdfghjklzxcvbnm"; - const warnUser04Hash = getHash(warnUser04); - const banUser01 = "ban-user01-loremipsumdolorsitametconsectetur"; - const banUser01Hash = getHash(banUser01); const submitVIPuser = `VIPPostSkipUser${".".repeat(16)}`; - const warnVideoID = "postSkip2"; - const badInputVideoID = "dQw4w9WgXcQ"; - const shadowBanVideoID = "postSkipBan"; - const shadowBanVideoID2 = "postSkipBan2"; const queryDatabase = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "votes", "userID", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseActionType = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); - const queryDatabaseChapter = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType", "description" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); - const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]); - const endpoint = "/api/skipSegments"; - const postSkipSegmentJSON = (data: Record) => client({ - method: "POST", - url: endpoint, - data - }); - const postSkipSegmentParam = (params: Record) => client({ - method: "POST", - url: endpoint, - params - }); - before(() => { const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 1000, 0, "80percent-uuid-0", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]); - db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 1001, 1005, 0, "80percent-uuid-1", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]); - db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 5000, -2, "80percent-uuid-2", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]); - db.prepare("run", insertSponsorTimeQuery, ["full_video_segment", 0, 0, 0, "full-video-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 0, 0, "full_video_segment"]); - db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 0, 0, 0, "full-video-duration-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 123, 0, "full_video_duration_segment"]); - db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 25, 30, 0, "full-video-duration-uuid-1", submitUserTwoHash, 0, 0, "sponsor", "skip", 123, 0, "full_video_duration_segment"]); - - const reputationVideoID = "post_reputation_video"; - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-0", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-1", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-2", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-3", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-4", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 0,"post_reputation-5-uuid-6", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 0,"post_reputation-5-uuid-7", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]); - - const now = Date.now(); - const warnVip01Hash = getHash("warn-vip01-qwertyuiopasdfghjklzxcvbnm"); - const reason01 = "Reason01"; - const reason02 = ""; - const reason03 = "Reason03"; - const reason04 = ""; - const MILLISECONDS_IN_HOUR = 3600000; - const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires; - - const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)'; - // User 1 - db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, now]); - db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 1000)]); - db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 2000)]); - db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 3601000)]); - // User 2 - db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, now]); - db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, (now - (warningExpireTime + 1000))]); - db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, (now - (warningExpireTime + 2000))]); - // User 3 - db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 0, reason03, now]); - db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 0, reason03, (now - 1000)]); - db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - 2000)]); - db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - 3601000)]); - // User 4 - db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 0, reason04, now]); - db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 0, reason04, (now - 1000)]); - db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 1, reason04, (now - 2000)]); - db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 1, reason04, (now - 3601000)]); - const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)'; db.prepare("run", insertVipUserQuery, [getHash(submitVIPuser)]); - - // ban user - db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, [banUser01Hash]); - - // user feature - db.prepare("run", `INSERT INTO "userFeatures" ("userID", "feature", "issuerUserID", "timeSubmitted") VALUES(?, ?, ?, ?)`, [submitUserTwoHash, Feature.ChapterSubmitter, "some-user", 0]); }); it("Should be able to submit a single time (Params method)", (done) => { - const videoID = "postSkip1"; + const videoID = "postSkipParamSingle"; postSkipSegmentParam({ videoID, startTime: 2, @@ -150,7 +92,7 @@ describe("postSkipSegments", () => { }); it("Should be able to submit a single time (JSON method)", (done) => { - const videoID = "postSkip2"; + const videoID = "postSkipJSONSingle"; postSkipSegmentJSON({ userID: submitUserOne, videoID, @@ -175,7 +117,7 @@ describe("postSkipSegments", () => { }); it("Should be able to submit a single time with an action type (JSON method)", (done) => { - const videoID = "postSkip3"; + const videoID = "postSkipJSONSingleActionType"; postSkipSegmentJSON({ userID: submitUserOne, videoID, @@ -200,309 +142,6 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }); - it("Should be able to submit a single chapter due to reputation (JSON method)", (done) => { - const videoID = "postSkipChapter1"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [0, 10], - category: "chapter", - actionType: "chapter", - description: "This is a chapter" - }], - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await queryDatabaseChapter(videoID); - const expected = { - startTime: 0, - endTime: 10, - category: "chapter", - actionType: "chapter", - description: "This is a chapter" - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit a single chapter due to user feature (JSON method)", (done) => { - const videoID = "postSkipChapter2"; - postSkipSegmentJSON({ - userID: submitUserTwo, - videoID, - segments: [{ - segment: [0, 10], - category: "chapter", - actionType: "chapter", - description: "This is a chapter" - }], - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await queryDatabaseChapter(videoID); - const expected = { - startTime: 0, - endTime: 10, - category: "chapter", - actionType: "chapter", - description: "This is a chapter" - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not be able to submit an music_offtopic with mute action type (JSON method)", (done) => { - const videoID = "postSkip4"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [0, 10], - category: "music_offtopic", - actionType: "mute" - }], - }) - .then(async res => { - assert.strictEqual(res.status, 400); - const row = await queryDatabaseActionType(videoID); - assert.strictEqual(row, undefined); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not be able to submit a chapter without permission (JSON method)", (done) => { - const videoID = "postSkipChapter3"; - postSkipSegmentJSON({ - userID: submitUserThree, - videoID, - segments: [{ - segment: [0, 10], - category: "chapter", - actionType: "chapter", - description: "This is a chapter" - }], - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not be able to submit a chapter with skip action type (JSON method)", (done) => { - const videoID = "postSkipChapter4"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [0, 10], - category: "chapter", - actionType: "skip" - }], - }) - .then(async res => { - assert.strictEqual(res.status, 400); - const row = await queryDatabaseActionType(videoID); - assert.strictEqual(row, undefined); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not be able to submit a sponsor with a description (JSON method)", (done) => { - const videoID = "postSkipChapter5"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [0, 10], - category: "sponsor", - description: "This is a sponsor" - }], - }) - .then(async res => { - assert.strictEqual(res.status, 400); - const row = await queryDatabaseActionType(videoID); - assert.strictEqual(row, undefined); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit a single time with a duration from the YouTube API (JSON method)", (done) => { - const videoID = "postSkip5"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - videoDuration: 100, - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await queryDatabaseDuration(videoID); - const expected = { - startTime: 0, - endTime: 10, - category: "sponsor", - videoDuration: 4980, - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit a single time with a precise duration close to the one from the YouTube API (JSON method)", (done) => { - const videoID = "postSkip6"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - videoDuration: 4980.20, - segments: [{ - segment: [1, 10], - category: "sponsor", - }], - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await queryDatabaseDuration(videoID); - const expected = { - startTime: 1, - endTime: 10, - locked: 0, - category: "sponsor", - videoDuration: 4980.20, - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit a single time with a duration in the body (JSON method)", (done) => { - const videoID = "noDuration"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - videoDuration: 100, - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await queryDatabaseDuration(videoID); - const expected = { - startTime: 0, - endTime: 10, - locked: 0, - category: "sponsor", - videoDuration: 100, - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit with a new duration, and hide old submissions and remove segment locks", async () => { - const videoID = "noDuration"; - await db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category") - VALUES(?, ?, ?)`, [getHash("VIPUser-lockCategories"), videoID, "sponsor"]); - - try { - const res = await postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - videoDuration: 100, - segments: [{ - segment: [1, 10], - category: "sponsor", - }], - }); - assert.strictEqual(res.status, 200); - const lockCategoriesRow = await db.prepare("get", `SELECT * from "lockCategories" WHERE videoID = ?`, [videoID]); - const videoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" - FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 0`, [videoID]); - const hiddenVideoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" - FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 1`, [videoID]); - assert.ok(!lockCategoriesRow); - const expected = { - startTime: 1, - endTime: 10, - locked: 0, - category: "sponsor", - videoDuration: 100, - }; - assert.ok(partialDeepEquals(videoRows[0], expected)); - assert.strictEqual(videoRows.length, 1); - assert.strictEqual(hiddenVideoRows.length, 1); - } catch (e) { - return e; - } - }); - - it("Should still not be allowed if youtube thinks duration is 0", (done) => { - const videoID= "noDuration"; - postSkipSegmentJSON({ - userID: submitUserThree, - videoID, - videoDuration: 100, - segments: [{ - segment: [30, 10000], - category: "sponsor", - }], - }) - .then(res => { - assert.strictEqual(res.status, 403); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit with a new duration, and not hide full video segments", async () => { - const videoID = "full_video_duration_segment"; - const res = await postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - videoDuration: 100, - segments: [{ - segment: [20, 30], - category: "sponsor", - }], - }); - assert.strictEqual(res.status, 200); - const videoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "actionType", "videoDuration" - FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 0`, [videoID]); - const hiddenVideoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" - FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 1`, [videoID]); - assert.strictEqual(videoRows.length, 2); - const expected = { - startTime: 20, - endTime: 30, - locked: 0, - category: "sponsor", - videoDuration: 100 - }; - const fullExpected = { - category: "sponsor", - actionType: "full" - }; - assert.ok((partialDeepEquals(videoRows[0], fullExpected) && partialDeepEquals(videoRows[1], expected)) - || (partialDeepEquals(videoRows[1], fullExpected) && partialDeepEquals(videoRows[0], expected))); - assert.strictEqual(hiddenVideoRows.length, 1); - }); - it("Should be able to submit a single time under a different service (JSON method)", (done) => { const videoID = "postSkip7"; postSkipSegmentJSON({ @@ -556,7 +195,7 @@ describe("postSkipSegments", () => { }); it("Should be able to submit multiple times (JSON method)", (done) => { - const videoID = "postSkip11"; + const videoID = "postSkipJSONMultiple"; postSkipSegmentJSON({ userID: submitUserOne, videoID, @@ -586,117 +225,6 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }).timeout(5000); - it("Should allow multiple times if total is under 80% of video(JSON method)", (done) => { - const videoID = "postSkip9"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [3, 3000], - category: "sponsor", - }, { - segment: [3002, 3050], - category: "intro", - }, { - segment: [45, 100], - category: "interaction", - }, { - segment: [99, 170], - category: "sponsor", - }], - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const rows = await db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]); - const expected = [{ - startTime: 3, - endTime: 3000, - category: "sponsor" - }, { - startTime: 3002, - endTime: 3050, - category: "intro" - }, { - startTime: 45, - endTime: 100, - category: "interaction" - }, { - startTime: 99, - endTime: 170, - category: "sponsor" - }]; - assert.ok(arrayDeepEquals(rows, expected)); - done(); - }) - .catch(err => done(err)); - }).timeout(5000); - - it("Should reject multiple times if total is over 80% of video (JSON method)", (done) => { - const videoID = "n9rIGdXnSJc"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [0, 2000], - category: "interaction", - }, { - segment: [3000, 4000], - category: "sponsor", - }, { - segment: [1500, 2750], - category: "sponsor", - }, { - segment: [4050, 4750], - category: "intro", - }], - }) - .then(async res => { - assert.strictEqual(res.status, 403); - const rows = await db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]); - assert.deepStrictEqual(rows, []); - done(); - }) - .catch(err => done(err)); - }).timeout(5000); - - it("Should reject multiple times if total is over 80% of video including previosuly submitted times(JSON method)", (done) => { - const videoID = "80percent_video"; - postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [2000, 4000], - category: "sponsor", - }, { - segment: [1500, 2750], - category: "sponsor", - }, { - segment: [4050, 4750], - category: "sponsor", - }], - }) - .then(async res => { - assert.strictEqual(res.status, 403); - const expected = [{ - category: "interaction", - startTime: 0, - endTime: 1000 - }, { - category: "interaction", - startTime: 1001, - endTime: 1005 - }, { - category: "interaction", - startTime: 0, - endTime: 5000 - }]; - const rows = await db.prepare("all", `SELECT "category", "startTime", "endTime" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); - assert.ok(arrayDeepEquals(rows, expected)); - done(); - }) - .catch(err => done(err)); - }).timeout(5000); - it("Should be accepted if a non-sponsor is less than 1 second", (done) => { const videoID = "qqwerty"; postSkipSegmentParam({ @@ -713,22 +241,6 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }); - it("Should be rejected if segment starts and ends at the same time", (done) => { - const videoID = "qqwerty"; - postSkipSegmentParam({ - videoID, - startTime: 90, - endTime: 90, - userID: submitUserTwo, - category: "intro" - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - it("Should be accepted if highlight segment starts and ends at the same time", (done) => { const videoID = "qqwerty"; postSkipSegmentParam({ @@ -745,368 +257,6 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }); - it("Should be rejected if highlight segment doesn't start and end at the same time", (done) => { - const videoID = "qqwerty"; - postSkipSegmentParam({ - videoID, - startTime: 30, - endTime: 30.5, - userID: submitUserTwo, - category: "poi_highlight" - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be rejected if a sponsor is less than 1 second", (done) => { - const videoID = "qqwerty"; - postSkipSegmentParam({ - videoID, - startTime: 30, - endTime: 30.5, - userID: submitUserTwo - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be rejected if over 80% of the video", (done) => { - const videoID = "qqwerty"; - postSkipSegmentParam({ - videoID, - startTime: 30, - endTime: 1000000, - userID: submitUserTwo, - category: "sponsor" - }) - .then(res => { - assert.strictEqual(res.status, 403); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be rejected with custom message if user has to many active warnings", (done) => { - postSkipSegmentJSON({ - userID: warnUser01, - videoID: warnVideoID, - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - }) - .then(res => { - assert.strictEqual(res.status, 403); - const errorMessage = res.data; - const reason = "Reason01"; - const expected = "Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes" - + " that are not malicious, and we just want to clarify the rules. " - + "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? " - + `Your userID is ${warnUser01Hash}.\n\nWarning reason: '${reason}'`; - - assert.strictEqual(errorMessage, expected); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be accepted if user has some active warnings", (done) => { - postSkipSegmentJSON({ - userID: warnUser02, - videoID: warnVideoID, - segments: [{ - segment: [50, 60], - category: "sponsor", - }], - }) - .then(res => { - if (res.status === 200) { - done(); // success - } else { - done(`Status code was ${res.status} ${res.data}`); - } - }) - .catch(err => done(err)); - }); - - it("Should be accepted if user has some warnings removed", (done) => { - postSkipSegmentJSON({ - userID: warnUser03, - videoID: warnVideoID, - segments: [{ - segment: [53, 60], - category: "sponsor", - }], - }) - .then(res => { - if (res.status === 200) { - done(); // success - } else { - done(`Status code was ${res.status} ${res.data}`); - } - }) - .catch(err => done(err)); - }); - - it("Should return 400 for missing params (Params method)", (done) => { - postSkipSegmentParam({ - startTime: 9, - endTime: 10, - userID: submitUserOne - }) - .then(res => { - if (res.status === 400) done(); - else done(true); - }) - .catch(err => done(err)); - }); - - it("Should be rejected with default message if user has to many active warnings", (done) => { - postSkipSegmentJSON({ - userID: warnUser01, - videoID: warnVideoID, - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - }) - .then(res => { - assert.strictEqual(res.status, 403); - const errorMessage = res.data; - assert.notStrictEqual(errorMessage, ""); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for missing params (JSON method) 1", (done) => { - postSkipSegmentJSON({ - userID: submitUserOne, - segments: [{ - segment: [9, 10], - category: "sponsor", - }, { - segment: [31, 60], - category: "intro", - }], - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - it("Should return 400 for missing params (JSON method) 2", (done) => { - postSkipSegmentJSON({ - userID: submitUserOne, - videoID: badInputVideoID, - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - it("Should return 400 for missing params (JSON method) 3", (done) => { - postSkipSegmentJSON({ - userID: submitUserOne, - videoID: badInputVideoID, - segments: [{ - segment: [0], - category: "sponsor", - }, { - segment: [31, 60], - category: "intro", - }], - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for missing params (JSON method) 4", (done) => { - postSkipSegmentJSON({ - userID: submitUserOne, - videoID: badInputVideoID, - segments: [{ - segment: [9, 10], - }, { - segment: [31, 60], - category: "intro", - }], - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for missing params (JSON method) 5", (done) => { - postSkipSegmentJSON({ - userID: submitUserOne, - videoID: badInputVideoID, - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 403 and custom reason for submiting in lockedCategory", (done) => { - const videoID = "lockedVideo"; - db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") - VALUES(?, ?, ?, ?)`, [getHash("VIPUser-lockCategories"), videoID, "sponsor", "Custom Reason"]) - .then(() => postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [1, 10], - category: "sponsor", - }], - })) - .then(res => { - assert.strictEqual(res.status, 403); - assert.match(res.data, /Reason: /); - assert.match(res.data, /Custom Reason/); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return not be 403 when submitting with locked category but unlocked actionType", (done) => { - const videoID = "lockedVideo"; - db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") - VALUES(?, ?, ?, ?)`, [getHash("VIPUser-lockCategories"), videoID, "sponsor", "Custom Reason"]) - .then(() => postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [1, 10], - category: "sponsor", - actionType: "mute" - }], - })) - .then(res => { - assert.strictEqual(res.status, 200); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 403 for submiting in lockedCategory", (done) => { - const videoID = "lockedVideo1"; - db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") - VALUES(?, ?, ?, ?)`, [getHash("VIPUser-lockCategories"), videoID, "intro", ""]) - .then(() => postSkipSegmentJSON({ - userID: submitUserOne, - videoID, - segments: [{ - segment: [1, 10], - category: "intro", - }], - })) - .then(res => { - assert.strictEqual(res.status, 403); - assert.doesNotMatch(res.data, /Lock reason: /); - assert.doesNotMatch(res.data, /Custom Reason/); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit with custom user-agent 1", (done) => { - client({ - url: endpoint, - method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "com.google.android.youtube/5.0" - }, - data: { - userID: submitUserOne, - videoID: "userAgent-1", - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - } - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "userAgent" FROM "sponsorTimes" WHERE "videoID" = ?`, ["userAgent-1"]); - const expected = { - startTime: 0, - endTime: 10, - userAgent: "Vanced/5.0", - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit with empty user-agent", (done) => { - client({ - url: endpoint, - method: "POST", - data: { - userID: submitUserOne, - videoID: "userAgent-3", - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - userAgent: "", - } - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "userAgent" FROM "sponsorTimes" WHERE "videoID" = ?`, ["userAgent-3"]); - const expected = { - startTime: 0, - endTime: 10, - userAgent: "", - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit with custom userAgent in body", (done) => { - postSkipSegmentJSON({ - userID: submitUserOne, - videoID: "userAgent-4", - segments: [{ - segment: [0, 10], - category: "sponsor", - }], - userAgent: "MeaBot/5.0" - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "userAgent" FROM "sponsorTimes" WHERE "videoID" = ?`, ["userAgent-4"]); - const expected = { - startTime: 0, - endTime: 10, - userAgent: "MeaBot/5.0", - }; - assert.ok(partialDeepEquals(row, expected)); - done(); - }) - .catch(err => done(err)); - }); - it("Should be able to submit with commas in timestamps", (done) => { const videoID = "commas-1"; postSkipSegmentJSON({ @@ -1130,22 +280,6 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }); - it("Should be rejected if a POI is at less than 1 second", (done) => { - const videoID = "qqwerty"; - postSkipSegmentParam({ - videoID, - startTime: 0.5, - endTime: 0.5, - category: "poi_highlight", - userID: submitUserTwo - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - it("Should allow submitting full video sponsor", (done) => { const videoID = "qqwerth"; postSkipSegmentParam({ @@ -1200,23 +334,6 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }); - it("Should not allow submitting full video sponsor not at zero seconds", (done) => { - const videoID = "qqwerth"; - postSkipSegmentParam({ - videoID, - startTime: 0, - endTime: 1, - category: "sponsor", - actionType: "full", - userID: submitUserTwo - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - it("Should not be able to submit with colons in timestamps", (done) => { const videoID = "colon-1"; postSkipSegmentJSON({ @@ -1234,76 +351,25 @@ describe("postSkipSegments", () => { .catch(err => done(err)); }); - it("Should automatically shadowban segments if user is banned", (done) => { - const videoID = shadowBanVideoID; - postSkipSegmentParam({ - videoID, - startTime: 0, - endTime: 10, - category: "sponsor", - userID: banUser01 - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); - const expected = { - startTime: 0, - endTime: 10, - shadowHidden: 1, - userID: banUser01Hash - }; - assert.deepStrictEqual(row, expected); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not add full segments to database if user if shadowbanned", (done) => { - const videoID = shadowBanVideoID2; - postSkipSegmentParam({ - videoID, - startTime: 0, - endTime: 0, - category: "sponsor", - actionType: "full", - userID: banUser01 - }) - .then(async res => { - assert.strictEqual(res.status, 200); - const row = await db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); - assert.strictEqual(row, undefined); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 if videoID is empty", (done) => { - const videoID = null as unknown as string; - postSkipSegmentParam({ - videoID, - startTime: 1, - endTime: 5, - category: "sponsor", - userID: submitUserTwo - }) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should successfully submit if video is private", (done) => { + it("Should throw 409 on duplicate submission", (done) => { const videoID = "private-video"; postSkipSegmentParam({ videoID, - startTime: 1, - endTime: 5, + startTime: 5.555, + endTime: 8.888, category: "sponsor", userID: submitUserTwo }) + .then(res => assert.strictEqual(res.status, 200) ) + .then(() => postSkipSegmentParam({ + videoID, + startTime: 5.555, + endTime: 8.888, + category: "sponsor", + userID: submitUserTwo + })) .then(res => { - assert.strictEqual(res.status, 200); + assert.strictEqual(res.status, 409); done(); }) .catch(err => done(err)); diff --git a/test/cases/postSkipSegments400.ts b/test/cases/postSkipSegments400.ts new file mode 100644 index 0000000..ac3e351 --- /dev/null +++ b/test/cases/postSkipSegments400.ts @@ -0,0 +1,316 @@ +import assert from "assert"; +import { postSkipSegmentJSON, postSkipSegmentParam } from "./postSkipSegments"; + +const videoID = "postSkipSegments-404-video"; +const userID = "postSkipSegments-404-user"; + +describe("postSkipSegments 400 - missing params", () => { + it("Should return 400 for missing params (JSON method) 1", (done) => { + postSkipSegmentJSON({ + userID, + segments: [{ + segment: [9, 10], + category: "sponsor", + }, { + segment: [31, 60], + category: "intro", + }], + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for missing params (JSON method) 2", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for missing params (JSON method) 3", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [0], + category: "sponsor", + }, { + segment: [31, 60], + category: "intro", + }], + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for missing params (JSON method) 4", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [9, 10], + }, { + segment: [31, 60], + category: "intro", + }], + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for missing params (JSON method) 5", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for missing multiple params (Params method)", (done) => { + postSkipSegmentParam({ + startTime: 9, + endTime: 10, + userID + }) + .then(res => { + if (res.status === 400) done(); + else done(true); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if videoID is empty", (done) => { + const videoID = null as unknown as string; + postSkipSegmentParam({ + videoID, + startTime: 1, + endTime: 5, + category: "sponsor", + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if no segments provided", (done) => { + postSkipSegmentJSON({ + videoID, + segments: [], + category: "sponsor", + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("postSkipSegments 400 - Chapters", () => { + const actionType = "chapter"; + const category = actionType; + + it("Should not be able to submit a chapter name that is too long", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 1, + endTime: 5, + category, + actionType, + description: "a".repeat(256), + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("postSkipSegments 400 - POI", () => { + const category = "poi_highlight"; + it("Should be rejected if a POI is at less than 1 second", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 0.5, + endTime: 0.5, + category, + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be rejected if highlight segment doesn't start and end at the same time", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 30, + endTime: 30.5, + category, + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("postSkipSegments 400 - Automod", () => { + it("Should be rejected if over 80% of the video", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 30, + endTime: 1000000, + userID, + category: "sponsor" + }) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be rejected if a sponsor is less than 1 second", (done) => { + postSkipSegmentParam({ + videoID, + category: "sponsor", + startTime: 30, + endTime: 30.5, + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be rejected if non-POI segment starts and ends at the same time", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 90, + endTime: 90, + userID, + category: "intro" + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not allow submitting full video not at zero seconds", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 0, + endTime: 1, + category: "sponsor", + actionType: "full", + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not be able to submit an music_offtopic with mute action type (JSON method)", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [0, 10], + category: "music_offtopic", + actionType: "mute" + }], + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("postSkipSegments 400 - Mismatched Types", () => { + it("Should not be able to submit with a category that does not exist", (done) => { + postSkipSegmentParam({ + videoID, + startTime: 1, + endTime: 5, + category: "this-category-will-never-exist", + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not be able to submit a chapter with skip action type (JSON method)", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [0, 10], + category: "chapter", + actionType: "skip" + }], + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not be able to submit a sponsor with a description (JSON method)", (done) => { + const videoID = "postSkipChapter5"; + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [0, 10], + category: "sponsor", + description: "This is a sponsor" + }], + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/postSkipSegments400Stub.ts b/test/cases/postSkipSegments400Stub.ts new file mode 100644 index 0000000..53f2b4e --- /dev/null +++ b/test/cases/postSkipSegments400Stub.ts @@ -0,0 +1,32 @@ +import assert from "assert"; +import { postSkipSegmentParam } from "./postSkipSegments"; +import { config } from "../../src/config"; +import sinon from "sinon"; + +const videoID = "postSkipSegments-404-video"; + +describe("postSkipSegments 400 - stubbed config", () => { + const USERID_LIMIT = 30; + before(() => { + sinon.stub(config, "minUserIDLength").value(USERID_LIMIT); + }); + after(() => { + sinon.restore(); + }); + + it("Should return 400 if userID is too short", (done) => { + const userID = "a".repeat(USERID_LIMIT - 10); + postSkipSegmentParam({ + videoID, + startTime: 1, + endTime: 5, + category: "sponsor", + userID + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); diff --git a/test/cases/postSkipSegmentsAutomod.ts b/test/cases/postSkipSegmentsAutomod.ts new file mode 100644 index 0000000..b6ffde0 --- /dev/null +++ b/test/cases/postSkipSegmentsAutomod.ts @@ -0,0 +1,121 @@ +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { arrayDeepEquals } from "../utils/partialDeepEquals"; +import { postSkipSegmentJSON, convertMultipleToDBFormat } from "./postSkipSegments"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; +import { ImportMock } from "ts-mock-imports"; +import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; + +const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, "YouTubeAPI"); +const sinonStub = mockManager.mock("listVideos"); +sinonStub.callsFake(YouTubeApiMock.listVideos); + +describe("postSkipSegments - Automod 80%", () => { + const userID = "postSkipSegments-automodSubmit"; + const userIDHash = getHash(userID); + + const over80VideoID = "80percent_video"; + + const queryDatabaseCategory = (videoID: string) => db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]); + + before(() => { + const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 0, 1000, 0, "80percent-uuid-0", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]); + db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 1001, 1005, 0, "80percent-uuid-1", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]); + db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 0, 5000, -2, "80percent-uuid-2", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]); + }); + + it("Should allow multiple times if total is under 80% of video (JSON method)", (done) => { + const videoID = "postSkipSegments_80percent_video_blank1"; + const segments = [{ + segment: [3, 3000], + category: "sponsor", + }, { + segment: [3002, 3050], + category: "intro", + }, { + segment: [45, 100], + category: "interaction", + }, { + segment: [99, 170], + category: "sponsor", + }]; + postSkipSegmentJSON({ + userID, + videoID, + segments + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const rows = await queryDatabaseCategory(videoID); + const expected = convertMultipleToDBFormat(segments); + assert.ok(arrayDeepEquals(rows, expected)); + done(); + }) + .catch(err => done(err)); + }).timeout(5000); + + it("Should reject multiple times if total is over 80% of video (JSON method)", (done) => { + const videoID = "postSkipSegments_80percent_video_blank2"; + const segments = [{ + segment: [0, 2000], + category: "interaction", + }, { + segment: [3000, 4000], + category: "sponsor", + }, { + segment: [1500, 2750], + category: "sponsor", + }, { + segment: [4050, 4750], + category: "intro", + }]; + postSkipSegmentJSON({ + userID, + videoID, + segments + }) + .then(async res => { + assert.strictEqual(res.status, 403); + const rows = await queryDatabaseCategory(videoID); + assert.deepStrictEqual(rows, []); + done(); + }) + .catch(err => done(err)); + }).timeout(5000); + + it("Should reject multiple times if total is over 80% of video including previosuly submitted times (JSON method)", (done) => { + const segments = [{ + segment: [2000, 4000], // adds 2000 + category: "sponsor", + }, { + segment: [1500, 2750], // adds 500 + category: "sponsor", + }, { + segment: [4050, 4570], // adds 520 + category: "sponsor", + }]; + const expected = [{ + startTime: 0, + endTime: 1000, + category: "interaction" + }, { + startTime: 1001, + endTime: 1005, + category: "interaction" + }]; + postSkipSegmentJSON({ + userID, + videoID: over80VideoID, + segments: segments + }) + .then(async res => { + assert.strictEqual(res.status, 403); + const rows = await queryDatabaseCategory(over80VideoID); + assert.ok(arrayDeepEquals(rows, expected, true)); + done(); + }) + .catch(err => done(err)); + }).timeout(5000); +}); diff --git a/test/cases/postSkipSegmentsDuration.ts b/test/cases/postSkipSegmentsDuration.ts new file mode 100644 index 0000000..7277192 --- /dev/null +++ b/test/cases/postSkipSegmentsDuration.ts @@ -0,0 +1,205 @@ +import assert from "assert"; +import { postSkipSegmentJSON, postSkipSegmentParam } from "./postSkipSegments"; +import { getHash } from "../../src/utils/getHash"; +import { partialDeepEquals } 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 "../mocks/youtubeMock"; +import { convertSingleToDBFormat } from "./postSkipSegments"; + +const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, "YouTubeAPI"); +const sinonStub = mockManager.mock("listVideos"); +sinonStub.callsFake(YouTubeApiMock.listVideos); + +describe("postSkipSegments - duration", () => { + const userIDOne = "postSkip-DurationUserOne"; + const userIDTwo = "postSkip-DurationUserTwo"; + const videoID = "postSkip-DurationVideo"; + const noDurationVideoID = "noDuration"; + const userID = userIDOne; + + const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); + + before(() => { + const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 0, 0, 0, "full-video-duration-uuid-0", userIDTwo, 0, 0, "sponsor", "full", 123, 0, "full_video_duration_segment"]); + db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 25, 30, 0, "full-video-duration-uuid-1", userIDTwo, 0, 0, "sponsor", "skip", 123, 0, "full_video_duration_segment"]); + }); + + it("Should be able to submit a single time with a precise duration close to the one from the YouTube API (JSON method)", (done) => { + const segment = { + segment: [1, 10], + category: "sponsor", + }; + postSkipSegmentJSON({ + userID, + videoID, + videoDuration: 4980.20, + segments: [segment], + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseDuration(videoID); + const expected = { + ...convertSingleToDBFormat(segment), + locked: 0, + videoDuration: 4980.20, + }; + assert.ok(partialDeepEquals(row, expected)); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to submit a single time with a duration in the body (JSON method)", (done) => { + const videoID = "noDuration"; + const segment = { + segment: [0, 10], + category: "sponsor", + }; + postSkipSegmentJSON({ + userID, + videoID, + videoDuration: 100, + segments: [segment], + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseDuration(videoID); + const expected = { + ...convertSingleToDBFormat(segment), + locked: 0, + videoDuration: 100, + }; + assert.ok(partialDeepEquals(row, expected)); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to submit with a new duration, and hide old submissions and remove segment locks", async () => { + const videoID = "noDuration"; + const segment = { + segment: [1, 10], + category: "sponsor", + }; + await db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category") + VALUES(?, ?, ?)`, [getHash("generic-VIP"), videoID, "sponsor"]); + try { + const res = await postSkipSegmentJSON({ + userID, + videoID, + videoDuration: 100, + segments: [segment], + }); + assert.strictEqual(res.status, 200); + const lockCategoriesRow = await db.prepare("get", `SELECT * from "lockCategories" WHERE videoID = ?`, [videoID]); + const videoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" + FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 0`, [videoID]); + const hiddenVideoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" + FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 1`, [videoID]); + assert.ok(!lockCategoriesRow); + const expected = { + ...convertSingleToDBFormat(segment), + locked: 0, + videoDuration: 100, + }; + assert.ok(partialDeepEquals(videoRows[0], expected)); + assert.strictEqual(videoRows.length, 1); + assert.strictEqual(hiddenVideoRows.length, 1); + } catch (e) { + return e; + } + }); + + it("Should still not be allowed if youtube thinks duration is 0", (done) => { + postSkipSegmentJSON({ + userID, + videoID: noDurationVideoID, + videoDuration: 100, + segments: [{ + segment: [30, 10000], + category: "sponsor", + }], + }) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to submit with a new duration, and not hide full video segments", async () => { + const videoID = "full_video_duration_segment"; + const segment = { + segment: [20, 30], + category: "sponsor", + }; + const res = await postSkipSegmentJSON({ + userID, + videoID, + videoDuration: 100, + segments: [segment], + }); + assert.strictEqual(res.status, 200); + const videoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "actionType", "videoDuration" + FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 0`, [videoID]); + const hiddenVideoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" + FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 1`, [videoID]); + assert.strictEqual(videoRows.length, 2); + const expected = { + ...convertSingleToDBFormat(segment), + locked: 0, + videoDuration: 100 + }; + const fullExpected = { + category: "sponsor", + actionType: "full" + }; + assert.ok((partialDeepEquals(videoRows[0], fullExpected) && partialDeepEquals(videoRows[1], expected)) + || (partialDeepEquals(videoRows[1], fullExpected) && partialDeepEquals(videoRows[0], expected))); + assert.strictEqual(hiddenVideoRows.length, 1); + }); + + it("Should be able to submit a single time with a duration from the YouTube API (JSON method)", (done) => { + const segment = { + segment: [0, 10], + category: "sponsor", + }; + const videoID = "postDuration-ytjson"; + postSkipSegmentJSON({ + userID, + videoID, + videoDuration: 100, + segments: [segment], + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseDuration(videoID); + const expected = { + ...convertSingleToDBFormat(segment), + videoDuration: 4980, + }; + assert.ok(partialDeepEquals(row, expected)); + done(); + }) + .catch(err => done(err)); + }); + + it("Should successfully submit if video is private", (done) => { + const videoID = "private-video"; + postSkipSegmentParam({ + videoID, + startTime: 1, + endTime: 5, + category: "sponsor", + userID + }) + .then(res => { + assert.strictEqual(res.status, 200); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/postSkipSegmentsFeatures.ts b/test/cases/postSkipSegmentsFeatures.ts new file mode 100644 index 0000000..94ca27f --- /dev/null +++ b/test/cases/postSkipSegmentsFeatures.ts @@ -0,0 +1,84 @@ +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { partialDeepEquals } from "../utils/partialDeepEquals"; +import { genRandom } from "../utils/getRandom"; +import { Feature } from "../../src/types/user.model"; +import { Segment, postSkipSegmentJSON, convertSingleToDBFormat } from "./postSkipSegments"; + +describe("postSkipSegments Features - Chapters", () => { + const submitUser_noPermissions = "postSkipSegments-chapters-noperm"; + const submitUser_reputation = "postSkipSegments-chapters-reputation"; + const submitUser_feature = "postSkipSegments-chapters-feature"; + + const queryDatabaseChapter = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "category", "actionType", "description" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); + function createSegment(): Segment { + return { + segment: [0, 10], + category: "chapter", + actionType: "chapter", + description: genRandom() + }; + } + + before(() => { + const submitNumberOfTimes = 10; + const submitUser_reputationHash = getHash(submitUser_reputation); + const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "actionType", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + for (let i = 0; i < submitNumberOfTimes; i++) { + const uuid = `post_reputation_uuid-${i}`; + const videoID = `post_reputation_video-${i}`; + db.prepare("run", insertSponsorTimeQuery, [videoID, 1, 11, 5, 1, uuid, submitUser_reputationHash, 1597240000000, 50, "sponsor", "skip", 0]); + } + // user feature + db.prepare("run", `INSERT INTO "userFeatures" ("userID", "feature", "issuerUserID", "timeSubmitted") VALUES(?, ?, ?, ?)`, [getHash(submitUser_feature), Feature.ChapterSubmitter, "generic-VIP", 0]); + }); + + it("Should be able to submit a single chapter due to reputation (JSON method)", (done) => { + const segment = createSegment(); + const videoID = "postSkipSegments-chapter-reputation"; + postSkipSegmentJSON({ + userID: submitUser_reputation, + videoID, + segments: [segment] + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseChapter(videoID); + assert.ok(partialDeepEquals(row, convertSingleToDBFormat(segment))); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to submit a single chapter due to user feature (JSON method)", (done) => { + const segment = createSegment(); + const videoID = "postSkipSegments-chapter-feature"; + postSkipSegmentJSON({ + userID: submitUser_feature, + videoID, + segments: [segment] + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseChapter(videoID); + assert.ok(partialDeepEquals(row, convertSingleToDBFormat(segment))); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not be able to submit a chapter without permission (JSON method)", (done) => { + const videoID = "postSkipSegments-chapter-submit"; + postSkipSegmentJSON({ + userID: submitUser_noPermissions, + videoID, + segments: [createSegment()] + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); diff --git a/test/cases/postSkipSegmentsLocked.ts b/test/cases/postSkipSegmentsLocked.ts new file mode 100644 index 0000000..0fc0ab8 --- /dev/null +++ b/test/cases/postSkipSegmentsLocked.ts @@ -0,0 +1,70 @@ +import assert from "assert"; +import { postSkipSegmentJSON } from "./postSkipSegments"; +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; + +describe("postSkipSegments - LockedVideos", () => { + const userIDOne = "postSkip-DurationUserOne"; + const VIPLockUser = "VIPUser-lockCategories"; + const videoID = "lockedVideo"; + const userID = userIDOne; + + before(() => { + const insertLockCategoriesQuery = `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") VALUES(?, ?, ?, ?)`; + db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "sponsor", "Custom Reason"]); + db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "intro", ""]); + }); + + it("Should return 403 and custom reason for submiting in lockedCategory", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [1, 10], + category: "sponsor", + }], + }) + .then(res => { + assert.strictEqual(res.status, 403); + assert.match(res.data, /Reason: /); + assert.match(res.data, /Custom Reason/); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return not be 403 when submitting with locked category but unlocked actionType", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [1, 10], + category: "sponsor", + actionType: "mute" + }], + }) + .then(res => { + assert.strictEqual(res.status, 200); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 403 for submiting in lockedCategory", (done) => { + postSkipSegmentJSON({ + userID, + videoID, + segments: [{ + segment: [1, 10], + category: "intro", + }], + }) + .then(res => { + assert.strictEqual(res.status, 403); + assert.doesNotMatch(res.data, /Lock reason: /); + assert.doesNotMatch(res.data, /Custom Reason/); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/postSkipSegmentsShadowban.ts b/test/cases/postSkipSegmentsShadowban.ts new file mode 100644 index 0000000..38a0f76 --- /dev/null +++ b/test/cases/postSkipSegmentsShadowban.ts @@ -0,0 +1,68 @@ +import assert from "assert"; +import { postSkipSegmentParam } from "./postSkipSegments"; +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import { ImportMock } from "ts-mock-imports"; +import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; + +const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, "YouTubeAPI"); +const sinonStub = mockManager.mock("listVideos"); +sinonStub.callsFake(YouTubeApiMock.listVideos); + +describe("postSkipSegments - shadowban", () => { + const banUser01 = "postSkip-banUser01"; + const banUser01Hash = getHash(banUser01); + + const shadowBanVideoID1 = "postSkipBan1"; + const shadowBanVideoID2 = "postSkipBan2"; + + const queryDatabaseShadowhidden = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); + + before(() => { + db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, [banUser01Hash]); + }); + + it("Should automatically shadowban segments if user is banned", (done) => { + const videoID = shadowBanVideoID1; + postSkipSegmentParam({ + videoID, + startTime: 0, + endTime: 10, + category: "sponsor", + userID: banUser01 + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseShadowhidden(videoID); + const expected = { + startTime: 0, + endTime: 10, + shadowHidden: 1, + userID: banUser01Hash + }; + assert.deepStrictEqual(row, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not add full segments to database if user if shadowbanned", (done) => { + const videoID = shadowBanVideoID2; + postSkipSegmentParam({ + videoID, + startTime: 0, + endTime: 0, + category: "sponsor", + actionType: "full", + userID: banUser01 + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryDatabaseShadowhidden(videoID); + assert.strictEqual(row, undefined); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/postSkipSegmentsUserAgent.ts b/test/cases/postSkipSegmentsUserAgent.ts new file mode 100644 index 0000000..c5e68c9 --- /dev/null +++ b/test/cases/postSkipSegmentsUserAgent.ts @@ -0,0 +1,104 @@ +import assert from "assert"; +import { convertSingleToDBFormat } from "./postSkipSegments"; +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import { partialDeepEquals } from "../utils/partialDeepEquals"; +import { client } from "../utils/httpClient"; + +const endpoint = "/api/skipSegments"; + +const queryUseragent = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "userAgent" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); + +describe("postSkipSegments - userAgent", () => { + const userIDOne = "postSkip-DurationUserOne"; + const VIPLockUser = "VIPUser-lockCategories"; + const videoID = "lockedVideo"; + const userID = userIDOne; + + const segment = { + segment: [0, 10], + category: "sponsor", + }; + const dbFormatSegment = convertSingleToDBFormat(segment); + + before(() => { + const insertLockCategoriesQuery = `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") VALUES(?, ?, ?, ?)`; + db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "sponsor", "Custom Reason"]); + db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "intro", ""]); + }); + + it("Should be able to submit with empty user-agent", (done) => { + const videoID = "userAgent-3"; + client(endpoint, { + method: "POST", + data: { + userID, + videoID, + segments: [segment], + userAgent: "", + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryUseragent(videoID); + const expected = { + ...dbFormatSegment, + userAgent: "", + }; + assert.ok(partialDeepEquals(row, expected)); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to submit with custom userAgent in body", (done) => { + const videoID = "userAgent-4"; + client(endpoint, { + method: "POST", + data: { + userID, + videoID, + segments: [segment], + userAgent: "MeaBot/5.0" + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryUseragent(videoID); + const expected = { + ...dbFormatSegment, + userAgent: "MeaBot/5.0", + }; + assert.ok(partialDeepEquals(row, expected)); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to submit with custom user-agent 1", (done) => { + const videoID = "userAgent-1"; + client(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "com.google.android.youtube/5.0" + }, + data: { + userID, + videoID, + segments: [segment], + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await queryUseragent(videoID); + const expected = { + ...dbFormatSegment, + userAgent: "Vanced/5.0", + }; + assert.ok(partialDeepEquals(row, expected)); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/postSkipSegmentsWarnings.ts b/test/cases/postSkipSegmentsWarnings.ts new file mode 100644 index 0000000..9b1cd65 --- /dev/null +++ b/test/cases/postSkipSegmentsWarnings.ts @@ -0,0 +1,127 @@ +import { config } from "../../src/config"; +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { client } from "../utils/httpClient"; + +describe("postSkipSegments Warnings", () => { + // Constant and helpers + const warnUser01 = "warn-user01"; + const warnUser01Hash = getHash(warnUser01); + const warnUser02 = "warn-user02"; + const warnUser02Hash = getHash(warnUser02); + const warnUser03 = "warn-user03"; + const warnUser03Hash = getHash(warnUser03); + const warnUser04 = "warn-user04"; + const warnUser04Hash = getHash(warnUser04); + + const warnVideoID = "postSkipSegments-warn-video"; + + const endpoint = "/api/skipSegments"; + const postSkipSegmentJSON = (data: Record) => client({ + method: "POST", + url: endpoint, + data + }); + + before(() => { + const now = Date.now(); + + const warnVip01Hash = getHash("postSkipSegmentsWarnVIP"); + + const reason01 = "Reason01"; + const reason02 = ""; + const reason03 = "Reason03"; + + const MILLISECONDS_IN_HOUR = 3600000; + const WARNING_EXPIRATION_TIME = config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR; + + const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)'; + // User 1 | 1 active | custom reason + db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, now]); + // User 2 | 1 inactive | default reason + db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 0, reason02, now]); + // User 3 | 1 expired, active | custom reason + db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - WARNING_EXPIRATION_TIME - 1000)]); + // User 4 | 1 active | default reason + db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 1, reason02, now]); + }); + + it("Should be rejected with custom message if user has active warnings", (done) => { + postSkipSegmentJSON({ + userID: warnUser01, + videoID: warnVideoID, + segments: [{ + segment: [0, 10], + category: "sponsor", + }], + }) + .then(res => { + assert.strictEqual(res.status, 403); + const errorMessage = res.data; + const reason = "Reason01"; + const expected = "Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes" + + " that are not malicious, and we just want to clarify the rules. " + + "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? " + + `Your userID is ${warnUser01Hash}.\n\nWarning reason: '${reason}'`; + + assert.strictEqual(errorMessage, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be accepted if user has inactive warning", (done) => { + postSkipSegmentJSON({ + userID: warnUser02, + videoID: warnVideoID, + segments: [{ + segment: [50, 60], + category: "sponsor", + }], + }) + .then(res => { + assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be accepted if user has expired warning", (done) => { + postSkipSegmentJSON({ + userID: warnUser03, + videoID: warnVideoID, + segments: [{ + segment: [53, 60], + category: "sponsor", + }], + }) + .then(res => { + assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be rejected with default message if user has active warning", (done) => { + postSkipSegmentJSON({ + userID: warnUser04, + videoID: warnVideoID, + segments: [{ + segment: [0, 10], + category: "sponsor", + }], + }) + .then(res => { + assert.strictEqual(res.status, 403); + const errorMessage = res.data; + const expected = "Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes" + + " that are not malicious, and we just want to clarify the rules. " + + "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? " + + `Your userID is ${warnUser04Hash}.`; + assert.strictEqual(errorMessage, expected); + done(); + }) + .catch(err => done(err)); + }); +}); diff --git a/test/cases/postWarning.ts b/test/cases/postWarning.ts index 722dcbc..dbd707a 100644 --- a/test/cases/postWarning.ts +++ b/test/cases/postWarning.ts @@ -9,16 +9,21 @@ describe("postWarning", () => { const endpoint = "/api/warnUser"; const getWarning = (userID: string) => db.prepare("get", `SELECT "userID", "issueTime", "issuerUserID", enabled, "reason" FROM warnings WHERE "userID" = ?`, [userID]); - const warnedUser = getHash("warning-0"); + const warneduserID = "warning-0"; + const warnedUserPublicID = getHash(warneduserID); + const warningVipOne = "warning-vip-1"; + const warningVipTwo = "warning-vip-2"; + const nonVipUser = "warning-non-vip"; before(async () => { - await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash("warning-vip")]); + await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(warningVipOne)]); + await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(warningVipTwo)]); }); it("Should be able to create warning if vip (exp 200)", (done) => { const json = { - issuerUserID: "warning-vip", - userID: warnedUser, + issuerUserID: warningVipOne, + userID: warnedUserPublicID, reason: "warning-reason-0" }; client.post(endpoint, json) @@ -38,8 +43,8 @@ describe("postWarning", () => { it("Should be not be able to create a duplicate warning if vip", (done) => { const json = { - issuerUserID: "warning-vip", - userID: warnedUser, + issuerUserID: warningVipOne, + userID: warnedUserPublicID, }; client.post(endpoint, json) @@ -58,8 +63,8 @@ describe("postWarning", () => { it("Should be able to remove warning if vip", (done) => { const json = { - issuerUserID: "warning-vip", - userID: warnedUser, + issuerUserID: warningVipOne, + userID: warnedUserPublicID, enabled: false }; @@ -78,8 +83,8 @@ describe("postWarning", () => { it("Should not be able to create warning if not vip (exp 403)", (done) => { const json = { - issuerUserID: "warning-not-vip", - userID: "warning-1", + issuerUserID: nonVipUser, + userID: warnedUserPublicID, }; client.post(endpoint, json) @@ -101,8 +106,8 @@ describe("postWarning", () => { it("Should re-enable disabled warning", (done) => { const json = { - issuerUserID: "warning-vip", - userID: warnedUser, + issuerUserID: warningVipOne, + userID: warnedUserPublicID, enabled: true }; @@ -121,14 +126,14 @@ describe("postWarning", () => { it("Should be able to remove your own warning", (done) => { const json = { - userID: "warning-0", + userID: warneduserID, enabled: false }; client.post(endpoint, json) .then(async res => { assert.strictEqual(res.status, 200); - const data = await getWarning(warnedUser); + const data = await getWarning(warnedUserPublicID); const expected = { enabled: 0 }; @@ -138,15 +143,16 @@ describe("postWarning", () => { .catch(err => done(err)); }); - it("Should be able to add your own warning", (done) => { + it("Should not be able to add your own warning", (done) => { const json = { - userID: "warning-0" + userID: warneduserID, + enabled: true }; client.post(endpoint, json) .then(async res => { assert.strictEqual(res.status, 403); - const data = await getWarning(warnedUser); + const data = await getWarning(warnedUserPublicID); const expected = { enabled: 0 }; diff --git a/test/cases/redisTest.ts b/test/cases/redisTest.ts index d2f7be4..ba1dd0c 100644 --- a/test/cases/redisTest.ts +++ b/test/cases/redisTest.ts @@ -1,9 +1,7 @@ import { config } from "../../src/config"; import redis from "../../src/utils/redis"; -import crypto from "crypto"; import assert from "assert"; - -const genRandom = (bytes=8) => crypto.pseudoRandomBytes(bytes).toString("hex"); +import { genRandom } from "../utils/getRandom"; const randKey1 = genRandom(); const randValue1 = genRandom(); diff --git a/test/cases/shadowBanUser.ts b/test/cases/shadowBanUser.ts index 8e52918..7dfb024 100644 --- a/test/cases/shadowBanUser.ts +++ b/test/cases/shadowBanUser.ts @@ -410,6 +410,27 @@ describe("shadowBanUser", () => { .catch(err => done(err)); }); + it("Should be possible to ban self", (done) => { + const userID = VIPuserID; + const hashUserID = getHash(userID); + client({ + method: "POST", + url: endpoint, + params: { + enabled: true, + userID: hashUserID, + categories: `["sponsor"]`, + unHideOldSubmissions: true, + adminUserID: userID, + } + }) + .then(res => { + assert.strictEqual(res.status, 200); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to ban user by userID and other users who used that IP and hide specific category", (done) => { const hashedIP = "shadowBannedIP8"; const userID = "shadowBanned8"; diff --git a/test/cases/userCounter.ts b/test/cases/userCounter.ts index 8215ce1..1730d26 100644 --- a/test/cases/userCounter.ts +++ b/test/cases/userCounter.ts @@ -2,6 +2,7 @@ import axios from "axios"; import assert from "assert"; import { config } from "../../src/config"; import { getHash } from "../../src/utils/getHash"; +import { client } from "../utils/httpClient"; describe("userCounter", () => { it("Should return 200", function (done) { @@ -20,4 +21,13 @@ describe("userCounter", () => { }) .catch(err => done(err)); }); + it("Should not incremeent counter on OPTIONS", function (done) { + /* cannot spy test */ + if (!config.userCounterURL) this.skip(); // skip if no userCounterURL is set + //const spy = sinon.spy(UserCounter); + client({ method: "OPTIONS", url: "/api/status" }) + .then(() => client({ method: "GET", url: "/api/status" })); + //assert.strictEqual(spy.callCount, 1); + done(); + }); }); \ No newline at end of file diff --git a/test/test.ts b/test/test.ts index d91da93..edc8740 100644 --- a/test/test.ts +++ b/test/test.ts @@ -10,6 +10,7 @@ import { ImportMock } from "ts-mock-imports"; import * as rateLimitMiddlewareModule from "../src/middleware/requestRateLimit"; import rateLimit from "express-rate-limit"; import redis from "../src/utils/redis"; +import { resetRedis, resetPostgres } from "./utils/reset"; async function init() { ImportMock.mockFunction(rateLimitMiddlewareModule, "rateLimitMiddleware", rateLimit({ @@ -19,6 +20,8 @@ async function init() { // delete old test database if (fs.existsSync(config.db)) fs.unlinkSync(config.db); if (fs.existsSync(config.privateDB)) fs.unlinkSync(config.privateDB); + if (config?.redis?.enabled) await resetRedis(); + if (config?.postgres) await resetPostgres(); await initDb(); @@ -59,6 +62,7 @@ async function init() { server.close(); redis.quit(); process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures + process.exit(); }); }); }); diff --git a/test/utils/getRandom.ts b/test/utils/getRandom.ts new file mode 100644 index 0000000..6ac6437 --- /dev/null +++ b/test/utils/getRandom.ts @@ -0,0 +1,3 @@ +import crypto from "crypto"; + +export const genRandom = (bytes=8) => crypto.pseudoRandomBytes(bytes).toString("hex"); diff --git a/test/utils/reset.ts b/test/utils/reset.ts new file mode 100644 index 0000000..be025b7 --- /dev/null +++ b/test/utils/reset.ts @@ -0,0 +1,22 @@ +// drop postgres tables +// reset redis cache +import { config } from "../../src/config"; +import { createClient } from "redis"; +import { Pool } from "pg"; +import { Logger } from "../../src/utils/logger"; + +export async function resetRedis() { + if (config?.redis?.enabled && config.mode === "test") { + const client = createClient(config.redis); + await client.connect(); + await client.flushAll(); + } +} +export async function resetPostgres() { + if (process.env.TEST_POSTGRES && config.mode == "test" && config.postgres) { + const pool = new Pool({ ...config.postgres }); + await pool.query(`DROP DATABASE IF EXISTS "sponsorTimes"`); + await pool.query(`DROP DATABASE IF EXISTS "privateDB"`); + await pool.end().catch(err => Logger.error(`closing db (postgres): ${err}`)); + } +} \ No newline at end of file