From b652102c42370402c4a30413f26c80cd0c3fa416 Mon Sep 17 00:00:00 2001 From: Michael C Date: Tue, 20 Sep 2022 23:18:35 -0400 Subject: [PATCH 01/26] move mocks to subfolder --- test/cases/getSkipSegmentsByHash.ts | 2 +- test/cases/postSkipSegments.ts | 2 +- test/cases/voteOnSponsorTime.ts | 2 +- test/{ => mocks}/youtubeMock.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename test/{ => mocks}/youtubeMock.ts (97%) diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index 098f9c0e..0923fdb7 100644 --- a/test/cases/getSkipSegmentsByHash.ts +++ b/test/cases/getSkipSegmentsByHash.ts @@ -3,7 +3,7 @@ import { partialDeepEquals, arrayPartialDeepEquals } from "../utils/partialDeepE import { getHash } from "../../src/utils/getHash"; import { ImportMock, } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; diff --git a/test/cases/postSkipSegments.ts b/test/cases/postSkipSegments.ts index 1369282a..c0bb36cc 100644 --- a/test/cases/postSkipSegments.ts +++ b/test/cases/postSkipSegments.ts @@ -4,7 +4,7 @@ import { partialDeepEquals, arrayDeepEquals } from "../utils/partialDeepEquals"; import { db } from "../../src/databases/databases"; import { ImportMock } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; import { Feature } from "../../src/types/user.model"; diff --git a/test/cases/voteOnSponsorTime.ts b/test/cases/voteOnSponsorTime.ts index 11126b38..755735ff 100644 --- a/test/cases/voteOnSponsorTime.ts +++ b/test/cases/voteOnSponsorTime.ts @@ -3,7 +3,7 @@ import { db, privateDB } from "../../src/databases/databases"; import { getHash } from "../../src/utils/getHash"; import { ImportMock } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; import { arrayDeepEquals } from "../utils/partialDeepEquals"; diff --git a/test/youtubeMock.ts b/test/mocks/youtubeMock.ts similarity index 97% rename from test/youtubeMock.ts rename to test/mocks/youtubeMock.ts index f0b89d1b..0ed18b7f 100644 --- a/test/youtubeMock.ts +++ b/test/mocks/youtubeMock.ts @@ -1,4 +1,4 @@ -import { APIVideoData, APIVideoInfo } from "../src/types/youtubeApi.model"; +import { APIVideoData, APIVideoInfo } from "../../src/types/youtubeApi.model"; export class YouTubeApiMock { // eslint-disable-next-line require-await From f5bafa28682e725b3673742f2d775d3d4668ac8e Mon Sep 17 00:00:00 2001 From: Michael C Date: Sat, 24 Sep 2022 22:46:16 -0400 Subject: [PATCH 02/26] add nyc output to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 25260b5d..79d8a36f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ working /dist/ # nyc coverage output -.nyc_output/ \ No newline at end of file +.nyc_output/ +coverage/ \ No newline at end of file From 3f470a72f51572c5ee6491e3d1eb489879287063 Mon Sep 17 00:00:00 2001 From: Michael C Date: Tue, 20 Sep 2022 23:22:21 -0400 Subject: [PATCH 03/26] add additional/missing tests --- src/routes/getDaysSavedFormatted.ts | 6 +- test/cases/addUserAsVIP.ts | 92 +++++++++++++++++++++++++++++ test/cases/getDaysSavedFormatted.ts | 11 ++++ test/cases/getSavedTimeForUser.ts | 37 ++++++++++-- test/cases/getTotalStats.ts | 17 ++++++ test/cases/getUsername.ts | 53 +++++++++++++++++ test/cases/getViewsForUser.ts | 62 +++++++++++++++++++ 7 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 test/cases/addUserAsVIP.ts create mode 100644 test/cases/getDaysSavedFormatted.ts create mode 100644 test/cases/getTotalStats.ts create mode 100644 test/cases/getUsername.ts create mode 100644 test/cases/getViewsForUser.ts diff --git a/src/routes/getDaysSavedFormatted.ts b/src/routes/getDaysSavedFormatted.ts index 61046e38..bd906a66 100644 --- a/src/routes/getDaysSavedFormatted.ts +++ b/src/routes/getDaysSavedFormatted.ts @@ -7,7 +7,11 @@ export async function getDaysSavedFormatted(req: Request, res: Response): Promis if (row !== undefined) { //send this result return res.send({ - daysSaved: row.daysSaved.toFixed(2), + daysSaved: row.daysSaved?.toFixed(2) ?? "0", + }); + } else { + return res.send({ + daysSaved: 0 }); } } diff --git a/test/cases/addUserAsVIP.ts b/test/cases/addUserAsVIP.ts new file mode 100644 index 00000000..49aba60d --- /dev/null +++ b/test/cases/addUserAsVIP.ts @@ -0,0 +1,92 @@ +import { getHash } from "../../src/utils/getHash"; +import { HashedUserID } from "../../src/types/user.model"; +import { client } from "../utils/httpClient"; +import { db } from "../../src/databases/databases"; +import assert from "assert"; + +// helpers +const checkUserVIP = (publicID: string) => db.prepare("get", `SELECT "userID" FROM "vipUsers" WHERE "userID" = ?`, [publicID]); + +const adminPrivateUserID = "testUserId"; +const permVIP1 = "addVIP_permaVIPOne"; +const publicPermVIP1 = getHash(permVIP1) as HashedUserID; + +const endpoint = "/api/addUserAsVIP"; +const addUserAsVIP = (userID: string, enabled: boolean, adminUserID = adminPrivateUserID) => client({ + method: "POST", + url: endpoint, + params: { + userID, + adminUserID, + enabled: String(enabled) + } +}); + +describe("addVIP test", function() { + it("User should not already be VIP", (done) => { + checkUserVIP(publicPermVIP1) + .then(result => { + assert.ok(!result); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to add user as VIP", (done) => { + addUserAsVIP(publicPermVIP1, true) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await checkUserVIP(publicPermVIP1); + assert.ok(row); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 403 with invalid adminID", (done) => { + addUserAsVIP(publicPermVIP1, true, "Invalid_Admin_User_ID") + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 with missing adminID", (done) => { + client({ + method: "POST", + url: endpoint, + params: { + userID: publicPermVIP1, + enabled: String(true) + } + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 with missing userID", (done) => { + client({ + method: "POST", + url: endpoint, + params: { + enabled: String(true), + adminUserID: adminPrivateUserID + } + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to remove VIP", (done) => { + addUserAsVIP(publicPermVIP1, false) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await checkUserVIP(publicPermVIP1); + assert.ok(!row); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/getDaysSavedFormatted.ts b/test/cases/getDaysSavedFormatted.ts new file mode 100644 index 00000000..8414b73b --- /dev/null +++ b/test/cases/getDaysSavedFormatted.ts @@ -0,0 +1,11 @@ +import assert from "assert"; +import { client } from "../utils/httpClient"; + +const endpoint = "/api/getDaysSavedFormatted"; + +describe("getDaysSavedFormatted", () => { + it("can get days saved", async () => { + const result = await client({ url: endpoint }); + assert.ok(result.data.daysSaved >= 0); + }); +}); \ No newline at end of file diff --git a/test/cases/getSavedTimeForUser.ts b/test/cases/getSavedTimeForUser.ts index 7ed809fb..b7e538a9 100644 --- a/test/cases/getSavedTimeForUser.ts +++ b/test/cases/getSavedTimeForUser.ts @@ -2,22 +2,31 @@ import { db } from "../../src/databases/databases"; import { getHash } from "../../src/utils/getHash"; import { deepStrictEqual } from "assert"; import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers const endpoint = "/api/getSavedTimeForUser"; +const getSavedTimeForUser = (userID: string) => client({ + url: endpoint, + params: { userID } +}); describe("getSavedTimeForUser", () => { - const user1 = "getSavedTimeForUserUser"; + const user1 = "getSavedTimeForUser1"; + const user2 = "getSavedTimeforUser2"; + const [ start, end, views ] = [1, 11, 50]; + before(async () => { const startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", "views", "shadowHidden") VALUES'; await db.prepare("run", `${startOfQuery}(?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ["getSavedTimeForUser", 1, 11, 2, "gstfu0", getHash(user1), 0, 50, 0]); + ["getSavedTimeForUser", start, end, 2, "getSavedTimeUUID0", getHash(user1), 0, views, 0]); return; }); - - it("Should be able to get a 200", (done) => { - client.get(endpoint, { params: { userID: user1 } }) + it("Should be able to get a saved time", (done) => { + getSavedTimeForUser(user1) .then(res => { // (end-start)*minute * views - const savedMinutes = ((11-1)/60) * 50; + const savedMinutes = ((end-start)/60) * views; const expected = { timeSaved: savedMinutes }; @@ -26,4 +35,20 @@ describe("getSavedTimeForUser", () => { }) .catch((err) => done(err)); }); + it("Should return 404 if no submissions", (done) => { + getSavedTimeForUser(user2) + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch((err) => done(err)); + }); + it("Should return 400 if no userID", (done) => { + client({ url: endpoint }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch((err) => done(err)); + }); }); diff --git a/test/cases/getTotalStats.ts b/test/cases/getTotalStats.ts new file mode 100644 index 00000000..04eab62d --- /dev/null +++ b/test/cases/getTotalStats.ts @@ -0,0 +1,17 @@ +import assert from "assert"; +import { client } from "../utils/httpClient"; + +const endpoint = "/api/getTotalStats"; + +describe("getTotalStats", () => { + it("Can get total stats", async () => { + const result = await client({ url: endpoint }); + const data = result.data; + assert.ok(data.userCount >= 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); + }); +}); \ No newline at end of file diff --git a/test/cases/getUsername.ts b/test/cases/getUsername.ts new file mode 100644 index 00000000..5394434d --- /dev/null +++ b/test/cases/getUsername.ts @@ -0,0 +1,53 @@ +import { getHash } from "../../src/utils/getHash"; +import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers +const getUsername = (userID: string) => client({ + url: "/api/getUsername", + params: { userID } +}); + +const postSetUserName = (userID: string, username: string) => client({ + method: "POST", + url: "/api/setUsername", + params: { + userID, + username, + } +}); + +const userOnePrivate = "getUsername_0"; +const userOnePublic = getHash(userOnePrivate); +const userOneUsername = "getUsername_username"; + +describe("getUsername test", function() { + it("Should get back publicUserID if not set", (done) => { + getUsername(userOnePrivate) + .then(result => { + assert.strictEqual(result.data.userName, userOnePublic); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to get username after setting", (done) => { + postSetUserName(userOnePrivate, userOneUsername) + .then(async () => { + const result = await getUsername(userOnePrivate); + const actual = result.data.userName; + assert.strictEqual(actual, userOneUsername); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 if no userID provided", (done) => { + client({ + url: "/api/getUsername" + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/getViewsForUser.ts b/test/cases/getViewsForUser.ts new file mode 100644 index 00000000..9471e18c --- /dev/null +++ b/test/cases/getViewsForUser.ts @@ -0,0 +1,62 @@ +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers +const endpoint = "/api/getViewsForUser"; +const getViewsForUser = (userID: string) => client({ + url: endpoint, + params: { userID } +}); + +const getViewUserOne = "getViewUser1"; +const userOneViewsFirst = 30; +const userOneViewsSecond = 0; + +const getViewUserTwo = "getViewUser2"; +const userTwoViews = 0; + +const getViewUserThree = "getViewUser3"; + + +describe("getViewsForUser", function() { + before(() => { + const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo0", getHash(getViewUserOne), 0, userOneViewsFirst, "sponsor", "skip", 0, 0, "getViewUserVideo"]); + db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo1", getHash(getViewUserOne), 0, userOneViewsSecond, "sponsor", "skip", 0, 0, "getViewUserVideo"]); + db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo2", getHash(getViewUserTwo), 0, userTwoViews, "sponsor", "skip", 0, 0, "getViewUserVideo"]); + }); + it("Should get back views for user one", (done) => { + getViewsForUser(getViewUserOne) + .then(result => { + assert.strictEqual(result.data.viewCount, userOneViewsFirst + userOneViewsSecond); + done(); + }) + .catch(err => done(err)); + }); + it("Should get back views for user two", (done) => { + getViewsForUser(getViewUserTwo) + .then(result => { + assert.strictEqual(result.data.viewCount, userTwoViews); + done(); + }) + .catch(err => done(err)); + }); + it("Should get 404 if no submissions", (done) => { + getViewsForUser(getViewUserThree) + .then(result => { + assert.strictEqual(result.status, 404); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 if no userID provided", (done) => { + client({ url: endpoint }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file From f683ed4f2941cf8119131299052a719e0bfdbe1f Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 01:14:22 -0400 Subject: [PATCH 04/26] add userCounter mocks and rearrange webhook path --- ci.json | 9 +++++---- src/utils/getIP.ts | 5 ++++- test.json | 9 +++++---- test/cases/userCounter.ts | 4 ++-- test/mocks.ts | 12 ++++++++---- test/mocks/UserCounter.ts | 11 +++++++++++ 6 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 test/mocks/UserCounter.ts diff --git a/ci.json b/ci.json index 30e2f412..4c9267b1 100644 --- a/ci.json +++ b/ci.json @@ -4,11 +4,12 @@ "globalSalt": "testSalt", "adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b", "newLeafURLs": ["placeholder"], - "discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook", - "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook", - "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook", - "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook", + "discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel", + "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions", + "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport", + "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject", "neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock", + "userCounterURL": "https://127.0.0.1:8081/UserCounter", "behindProxy": true, "postgres": { "user": "ci_db_user", diff --git a/src/utils/getIP.ts b/src/utils/getIP.ts index 0d2dd908..85a71929 100644 --- a/src/utils/getIP.ts +++ b/src/utils/getIP.ts @@ -3,6 +3,9 @@ import { Request } from "express"; import { IPAddress } from "../types/segments.model"; export function getIP(req: Request): IPAddress { + // if in testing mode, return immediately + if (config.mode === "test") return "127.0.0.1" as IPAddress; + if (config.behindProxy === true || config.behindProxy === "true") { config.behindProxy = "X-Forwarded-For"; } @@ -15,6 +18,6 @@ export function getIP(req: Request): IPAddress { case "X-Real-IP": return req.headers["x-real-ip"] as IPAddress; default: - return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress; + return req.socket?.remoteAddress as IPAddress; } } \ No newline at end of file diff --git a/test.json b/test.json index 593977b3..5692e760 100644 --- a/test.json +++ b/test.json @@ -4,11 +4,12 @@ "globalSalt": "testSalt", "adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b", "newLeafURLs": ["placeholder"], - "discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook", - "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook", - "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook", - "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook", + "discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel", + "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions", + "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport", + "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject", "neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock", + "userCounterURL": "http://127.0.0.1:8081/UserCounter", "behindProxy": true, "db": ":memory:", "privateDB": ":memory:", diff --git a/test/cases/userCounter.ts b/test/cases/userCounter.ts index 26a1adbe..120e0e80 100644 --- a/test/cases/userCounter.ts +++ b/test/cases/userCounter.ts @@ -5,8 +5,8 @@ import { getHash } from "../../src/utils/getHash"; describe("userCounter", () => { - it("Should return 200", (done) => { - if (!config.userCounterURL) return done(); // skip if no userCounterURL is set + it("Should return 200", function (done) { + if (!config.userCounterURL) this.skip(); // skip if no userCounterURL is set axios.request({ method: "POST", baseURL: config.userCounterURL, diff --git a/test/mocks.ts b/test/mocks.ts index a5c578e9..d6673bfd 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -1,23 +1,24 @@ import express from "express"; import { config } from "../src/config"; import { Server } from "http"; +import { UserCounter } from "./mocks/UserCounter"; const app = express(); -app.post("/ReportChannelWebhook", (req, res) => { +app.post("/webhook/ReportChannel", (req, res) => { res.sendStatus(200); }); -app.post("/FirstTimeSubmissionsWebhook", (req, res) => { +app.post("/webhook/FirstTimeSubmissions", (req, res) => { res.sendStatus(200); }); -app.post("/CompletelyIncorrectReportWebhook", (req, res) => { +app.post("/webhook/CompletelyIncorrectReport", (req, res) => { res.sendStatus(200); }); // Testing NeuralBlock -app.post("/NeuralBlockRejectWebhook", (req, res) => { +app.post("/webhook/NeuralBlockReject", (req, res) => { res.sendStatus(200); }); @@ -47,6 +48,9 @@ app.post("/CustomWebhook", (req, res) => { res.sendStatus(200); }); +// mocks +app.use("/UserCounter", UserCounter); + export function createMockServer(callback: () => void): Server { return app.listen(config.mockPort, callback); } diff --git a/test/mocks/UserCounter.ts b/test/mocks/UserCounter.ts new file mode 100644 index 00000000..d4ba32a7 --- /dev/null +++ b/test/mocks/UserCounter.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +export const UserCounter = Router(); + +UserCounter.post("/api/v1/addIP", (req, res) => { + res.sendStatus(200); +}); +UserCounter.get("/api/v1/userCount", (req, res) => { + res.send({ + userCount: 100 + }); +}); \ No newline at end of file From dd7656d14341b6cfebd6aedf85f95d8386b66059 Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 02:27:48 -0400 Subject: [PATCH 05/26] add lockCategories tests, getUserInfo --- test/cases/getUserInfo.ts | 34 +++++++++++++++++++++++++ test/cases/lockCategoriesRecords.ts | 39 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/test/cases/getUserInfo.ts b/test/cases/getUserInfo.ts index 4f8d3aee..acc6ed19 100644 --- a/test/cases/getUserInfo.ts +++ b/test/cases/getUserInfo.ts @@ -263,6 +263,15 @@ describe("getUserInfo", () => { .catch(err => done(err)); }); + it("Should throw 400 with invalid array", (done) => { + client.get(endpoint, { params: { userID: "x", values: 123 } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); // pass + }) + .catch(err => done(err)); + }); + it("Should return 200 on userID not found", (done) => { client.get(endpoint, { params: { userID: "notused-userid" } }) .then(res => { @@ -307,4 +316,29 @@ describe("getUserInfo", () => { }) .catch(err => done(err)); }); + + it("Should be able to get permissions", (done) => { + client.get(endpoint, { params: { userID: "getuserinfo_user_01", value: "permissions" } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = { + permissions: { + sponsor: true, + selfpromo: true, + exclusive_access: true, + interaction: true, + intro: true, + outro: true, + preview: true, + music_offtopic: true, + filler: true, + poi_highlight: true, + chapter: false, + }, + }; + assert.ok(partialDeepEquals(res.data, expected)); + done(); // pass + }) + .catch(err => done(err)); + }); }); diff --git a/test/cases/lockCategoriesRecords.ts b/test/cases/lockCategoriesRecords.ts index 7d6bd4a3..49e06af6 100644 --- a/test/cases/lockCategoriesRecords.ts +++ b/test/cases/lockCategoriesRecords.ts @@ -560,4 +560,43 @@ describe("lockCategoriesRecords", () => { }) .catch(err => done(err)); }); + + it("should be able to add poi type category by type skip", (done) => { + const videoID = "add-record-poi"; + client.post(endpoint, { + videoID, + userID: lockVIPUser, + categories: ["poi_highlight"], + actionTypes: ["skip"] + }) + .then(res => { + assert.strictEqual(res.status, 200); + checkLockCategories(videoID) + .then(result => { + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0], "poi_highlight"); + }); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not add lock of invalid type", (done) => { + const videoID = "add_invalid_type"; + client.post(endpoint, { + videoID, + userID: lockVIPUser, + categories: ["future_unused_invalid_type"], + actionTypes: ["skip"] + }) + .then(res => { + assert.strictEqual(res.status, 200); + checkLockCategories(videoID) + .then(result => { + assert.strictEqual(result.length, 0); + }); + done(); + }) + .catch(err => done(err)); + }); }); From e0be4744bece18bf6956ff939d498c82666c59bc Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 03:04:16 -0400 Subject: [PATCH 06/26] fix tokenUtils tests, skip if not configured --- ci.json | 7 +++- package-lock.json | 84 +++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/routes/verifyToken.ts | 8 ++++ test.json | 5 +++ test/cases/tokenUtils.ts | 68 +++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 test/cases/tokenUtils.ts diff --git a/ci.json b/ci.json index 4c9267b1..5a3ee2fb 100644 --- a/ci.json +++ b/ci.json @@ -9,7 +9,7 @@ "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport", "discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject", "neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock", - "userCounterURL": "https://127.0.0.1:8081/UserCounter", + "userCounterURL": "http://127.0.0.1:8081/UserCounter", "behindProxy": true, "postgres": { "user": "ci_db_user", @@ -71,5 +71,10 @@ "statusCode": 200 } }, + "patreon": { + "clientId": "testClientID", + "clientSecret": "testClientSecret", + "redirectUri": "http://127.0.0.1/fake/callback" + }, "minReputationToSubmitFiller": -1 } diff --git a/package-lock.json b/package-lock.json index 00e507ec..1656aa32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,8 +30,10 @@ "@types/mocha": "^9.1.1", "@types/node": "^18.0.3", "@types/pg": "^8.6.5", + "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", + "axios-mock-adapter": "^1.21.2", "eslint": "^8.19.0", "mocha": "^10.0.0", "nodemon": "^2.0.19", @@ -963,6 +965,21 @@ "@types/node": "*" } }, + "node_modules/@types/sinon": { + "version": "10.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", + "integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.30.6", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", @@ -1339,6 +1356,19 @@ "form-data": "^4.0.0" } }, + "node_modules/axios-mock-adapter": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", + "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -3094,6 +3124,29 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6453,6 +6506,21 @@ "@types/node": "*" } }, + "@types/sinon": { + "version": "10.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", + "integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.30.6", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", @@ -6698,6 +6766,16 @@ "form-data": "^4.0.0" } }, + "axios-mock-adapter": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", + "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -7975,6 +8053,12 @@ "binary-extensions": "^2.0.0" } }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", diff --git a/package.json b/package.json index 511aa19e..0f505b0f 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,10 @@ "@types/mocha": "^9.1.1", "@types/node": "^18.0.3", "@types/pg": "^8.6.5", + "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", + "axios-mock-adapter": "^1.21.2", "eslint": "^8.19.0", "mocha": "^10.0.0", "nodemon": "^2.0.19", diff --git a/src/routes/verifyToken.ts b/src/routes/verifyToken.ts index 59910eff..2b18b5a3 100644 --- a/src/routes/verifyToken.ts +++ b/src/routes/verifyToken.ts @@ -12,11 +12,19 @@ interface VerifyTokenRequest extends Request { } } +export const validatelicenseKeyRegex = (token: string) => + new RegExp(/[A-Za-z0-9]{40}/).test(token); + export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise { const { query: { licenseKey } } = req; if (!licenseKey) { return res.status(400).send("Invalid request"); + } else if (!validatelicenseKeyRegex(licenseKey)) { + // fast check for invalid licence key + return res.status(200).send({ + allowed: false + }); } const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/); if (!licenseRegex.test(licenseKey)) { diff --git a/test.json b/test.json index 5692e760..16ace5c6 100644 --- a/test.json +++ b/test.json @@ -59,5 +59,10 @@ "statusCode": 200 } }, + "patreon": { + "clientId": "testClientID", + "clientSecret": "testClientSecret", + "redirectUri": "http://127.0.0.1/fake/callback" + }, "minReputationToSubmitFiller": -1 } diff --git a/test/cases/tokenUtils.ts b/test/cases/tokenUtils.ts new file mode 100644 index 00000000..bd0b90ac --- /dev/null +++ b/test/cases/tokenUtils.ts @@ -0,0 +1,68 @@ +import assert from "assert"; +import { config } from "../../src/config"; +import axios from "axios"; +import * as tokenUtils from "../../src/utils/tokenUtils"; +import MockAdapter from "axios-mock-adapter"; +import { validatelicenseKeyRegex } from "../../src/routes/verifyToken"; +let mock: MockAdapter; + +const validateToken = validatelicenseKeyRegex; +const fakePatreonIdentity = { + data: {}, + links: {}, + included: [ + { + attributes: { + is_monthly: true, + currently_entitled_amount_cents: 100, + patron_status: "active_patron", + }, + id: "id", + type: "campaign" + } + ], +}; + +describe("tokenUtils test", function() { + before(function() { + mock = new MockAdapter(axios, { onNoMatch: "throwException" }); + mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, { + access_token: "test_access_token", + refresh_token: "test_refresh_token", + expires_in: 3600, + }); + mock.onGet(/identity/).reply(200, fakePatreonIdentity); + }); + + it("Should be able to create patreon token", function (done) { + if (!config?.patreon) this.skip(); + tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code").then((licenseKey) => { + assert.ok(validateToken(licenseKey)); + done(); + }); + }); + it("Should be able to create local token", (done) => { + tokenUtils.createAndSaveToken(tokenUtils.TokenType.local).then((licenseKey) => { + assert.ok(validateToken(licenseKey)); + done(); + }); + }); + it("Should be able to get patreon identity", function (done) { + if (!config?.patreon) this.skip(); + tokenUtils.getPatreonIdentity("fake_access_token").then((result) => { + assert.deepEqual(result, fakePatreonIdentity); + done(); + }); + }); + it("Should be able to refresh token", function (done) { + if (!config?.patreon) this.skip(); + tokenUtils.refreshToken(tokenUtils.TokenType.patreon, "fake-licence-Key", "fake_refresh_token").then((result) => { + assert.strictEqual(result, true); + done(); + }); + }); + + after(function () { + mock.restore(); + }); +}); \ No newline at end of file From 0a102c15fd95b1839a3147710131da8acfa741d6 Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 15:11:10 -0400 Subject: [PATCH 07/26] add lockCategory tests and typo tweak --- src/routes/deleteLockCategories.ts | 3 +- src/routes/postLockCategories.ts | 2 +- test/cases/lockCategoriesHttp.ts | 252 ++++++++++++++++++++++++++++ test/cases/lockCategoriesRecords.ts | 139 --------------- 4 files changed, 255 insertions(+), 141 deletions(-) create mode 100644 test/cases/lockCategoriesHttp.ts diff --git a/src/routes/deleteLockCategories.ts b/src/routes/deleteLockCategories.ts index 10a3ec9c..3340f10a 100644 --- a/src/routes/deleteLockCategories.ts +++ b/src/routes/deleteLockCategories.ts @@ -35,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ || !categories || !Array.isArray(categories) || categories.length === 0 + || actionTypes && !Array.isArray(actionTypes) || actionTypes.length === 0 ) { return res.status(400).json({ @@ -48,7 +49,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ if (!userIsVIP) { return res.status(403).json({ - message: "Must be a VIP to mark videos.", + message: "Must be a VIP to lock videos.", }); } diff --git a/src/routes/postLockCategories.ts b/src/routes/postLockCategories.ts index 13f7f04f..88e9cf0b 100644 --- a/src/routes/postLockCategories.ts +++ b/src/routes/postLockCategories.ts @@ -37,7 +37,7 @@ export async function postLockCategories(req: Request, res: Response): Promise => db.prepare("all", 'SELECT * FROM "lockCategories" WHERE "videoID" = ?', [videoID]); + + +const goodResponse = (): any => ({ + videoID: "test-videoid", + userID: "not-vip-test-userid", + categories: ["sponsor"], + actionTypes: ["skip"] +}); + +describe("POST lockCategories HTTP submitting", () => { + before(async () => { + const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)'; + await db.prepare("run", insertVipUserQuery, [lockVIPUserHash]); + }); + + it("Should update the database version when starting the application", async () => { + const version = (await db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"])).value; + assert.ok(version > 1); + }); + + it("should be able to add poi type category by type skip", (done) => { + const videoID = "add-record-poi"; + client.post(endpoint, { + videoID, + userID: lockVIPUser, + categories: ["poi_highlight"], + actionTypes: ["skip"] + }) + .then(res => { + assert.strictEqual(res.status, 200); + checkLockCategories(videoID) + .then(result => { + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0], "poi_highlight"); + }); + done(); + }) + .catch(err => done(err)); + }); + + it("Should not add lock of invalid type", (done) => { + const videoID = "add_invalid_type"; + client.post(endpoint, { + videoID, + userID: lockVIPUser, + categories: ["future_unused_invalid_type"], + actionTypes: ["skip"] + }) + .then(res => { + assert.strictEqual(res.status, 200); + checkLockCategories(videoID) + .then(result => { + assert.strictEqual(result.length, 0); + }); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("DELETE lockCategories 403/400 tests", () => { + it(" Should return 400 for no data", (done) => { + client.delete(endpoint, {}) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for no categories", (done) => { + const json: any = { + videoID: "test", + userID: "test", + categories: [], + }; + client.delete(endpoint, json) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for no userID", (done) => { + const json: any = { + videoID: "test", + userID: null, + categories: ["sponsor"], + }; + + client.post(endpoint, json) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for no videoID", (done) => { + const json: any = { + videoID: null, + userID: "test", + categories: ["sponsor"], + }; + + client.post(endpoint, json) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for invalid category array", (done) => { + const json = { + videoID: "test", + userID: "test", + categories: {}, + }; + + client.post(endpoint, json) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 for bad format categories", (done) => { + const json = { + videoID: "test", + userID: "test", + categories: "sponsor", + }; + + client.post(endpoint, json) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 403 if user is not VIP", (done) => { + const json = { + videoID: "test", + userID: "test", + categories: [ + "sponsor", + ], + }; + + client.post(endpoint, json) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("manual DELETE/POST lockCategories 400 tests", () => { + it("DELETE Should return 400 for no data", (done) => { + client.delete(endpoint, { data: {} }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("POST Should return 400 for no data", (done) => { + client.post(endpoint, {}) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("DELETE Should return 400 for bad format categories", (done) => { + const data = goodResponse(); + data.categories = "sponsor"; + client.delete(endpoint, { data }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("POST Should return 400 for bad format categories", (done) => { + const data = goodResponse(); + data.categories = "sponsor"; + client.post(endpoint, data) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("DELETE Should return 403 if user is not VIP", (done) => { + const data = goodResponse(); + client.delete(endpoint, { data }) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); + it("POST Should return 403 if user is not VIP", (done) => { + const data = goodResponse(); + client.post(endpoint, data) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); +}); + +describe("array of DELETE/POST lockCategories 400 tests", () => { + for (const key of [ "videoID", "userID", "categories" ]) { + for (const method of ["DELETE", "POST"]) { + it(`${method} - Should return 400 for invalid ${key}`, (done) => { + const data = goodResponse(); + data[key] = null; + client(endpoint, { data, method }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + } + } +}); \ No newline at end of file diff --git a/test/cases/lockCategoriesRecords.ts b/test/cases/lockCategoriesRecords.ts index 49e06af6..ebfc4218 100644 --- a/test/cases/lockCategoriesRecords.ts +++ b/test/cases/lockCategoriesRecords.ts @@ -266,106 +266,6 @@ describe("lockCategoriesRecords", () => { .catch(err => done(err)); }); - it("Should return 400 for missing params", (done) => { - client.post(endpoint, {}) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for no categories", (done) => { - const json: any = { - videoID: "test", - userID: "test", - categories: [], - }; - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for no userID", (done) => { - const json: any = { - videoID: "test", - userID: null, - categories: ["sponsor"], - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for no videoID", (done) => { - const json: any = { - videoID: null, - userID: "test", - categories: ["sponsor"], - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 object categories", (done) => { - const json = { - videoID: "test", - userID: "test", - categories: {}, - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 bad format categories", (done) => { - const json = { - videoID: "test", - userID: "test", - categories: "sponsor", - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 403 if user is not VIP", (done) => { - const json = { - videoID: "test", - userID: "test", - categories: [ - "sponsor", - ], - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 403); - done(); - }) - .catch(err => done(err)); - }); - it("Should be able to delete a lockCategories record", (done) => { const videoID = "delete-record"; const json = { @@ -560,43 +460,4 @@ describe("lockCategoriesRecords", () => { }) .catch(err => done(err)); }); - - it("should be able to add poi type category by type skip", (done) => { - const videoID = "add-record-poi"; - client.post(endpoint, { - videoID, - userID: lockVIPUser, - categories: ["poi_highlight"], - actionTypes: ["skip"] - }) - .then(res => { - assert.strictEqual(res.status, 200); - checkLockCategories(videoID) - .then(result => { - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0], "poi_highlight"); - }); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not add lock of invalid type", (done) => { - const videoID = "add_invalid_type"; - client.post(endpoint, { - videoID, - userID: lockVIPUser, - categories: ["future_unused_invalid_type"], - actionTypes: ["skip"] - }) - .then(res => { - assert.strictEqual(res.status, 200); - checkLockCategories(videoID) - .then(result => { - assert.strictEqual(result.length, 0); - }); - done(); - }) - .catch(err => done(err)); - }); }); From 6499381b4fbb883a85f3acf1ec210690fae701f8 Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 15:57:20 -0400 Subject: [PATCH 08/26] add coverage reports to PostgreSQL tests --- .github/workflows/postgres-redis-ci.yml | 4 +++- .nycrc.json | 9 +++++++-- package-lock.json | 25 +++++++++++++++++++++++++ package.json | 4 +++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.github/workflows/postgres-redis-ci.yml b/.github/workflows/postgres-redis-ci.yml index 44500f8b..6bf05b08 100644 --- a/.github/workflows/postgres-redis-ci.yml +++ b/.github/workflows/postgres-redis-ci.yml @@ -26,4 +26,6 @@ jobs: env: TEST_POSTGRES: true timeout-minutes: 5 - run: npm test \ No newline at end of file + run: npx nyc --silent npm test + - name: Generate coverage report + run: npm run cover:report \ No newline at end of file diff --git a/.nycrc.json b/.nycrc.json index 76027a56..af6eab0b 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,5 +1,10 @@ { + "extends": "@istanbuljs/nyc-config-typescript", + "check-coverage": false, "exclude": [ - "src/routes/addUnlitedVideo.ts" - ] + "src/routes/addUnlistedVideo.ts", + "src/cronjob/downvoteSegmentArchiveJob.ts", + "src/databases/*" + ], + "reporter": ["text", "html"] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1656aa32..a03b338e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "sync-mysql": "^3.0.1" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/better-sqlite3": "^7.5.0", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", @@ -632,6 +633,21 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -6207,6 +6223,15 @@ } } }, + "@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2" + } + }, "@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", diff --git a/package.json b/package.json index 0f505b0f..1c8077cb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "npm run tsc && ts-node test/test.ts", - "test:coverage": "nyc npm run test", + "cover": "nyc npm test", + "cover:report": "nyc report", "dev": "nodemon", "dev:bash": "nodemon -x 'npm test ; npm start'", "postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:alpine", @@ -32,6 +33,7 @@ "sync-mysql": "^3.0.1" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/better-sqlite3": "^7.5.0", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", From a00048aaac8d11b3afac368ede45b59f47067aaa Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 19:39:03 -0400 Subject: [PATCH 09/26] add getIP test cases, misc others --- .nycrc.json | 8 ++- src/app.ts | 1 + test/cases/addUserAsVIP.ts | 49 ++++++++++++++ test/cases/getIP.ts | 109 +++++++++++++++++++++++++++++++ test/cases/testUtils.ts | 43 +++++++++++- test/cases/userCounter.ts | 1 - test/mocks/mockExpressRequest.ts | 33 ++++++++++ 7 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 test/cases/getIP.ts create mode 100644 test/mocks/mockExpressRequest.ts diff --git a/.nycrc.json b/.nycrc.json index af6eab0b..ae148ebc 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,10 +1,14 @@ { "extends": "@istanbuljs/nyc-config-typescript", "check-coverage": false, + "ski-full": true, + "reporter": ["text", "html"], + "include": [ + "src/**/*.ts" + ], "exclude": [ "src/routes/addUnlistedVideo.ts", "src/cronjob/downvoteSegmentArchiveJob.ts", "src/databases/*" - ], - "reporter": ["text", "html"] + ] } \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index f68507e6..e7ba2c11 100644 --- a/src/app.ts +++ b/src/app.ts @@ -200,6 +200,7 @@ function setupRoutes(router: Router) { router.get("/api/generateToken/:type", generateTokenRequest); router.get("/api/verifyToken", verifyTokenRequest); + /* instanbul ignore next */ if (config.postgres?.enabled) { router.get("/database", (req, res) => dumpDatabase(req, res, true)); router.get("/database.json", (req, res) => dumpDatabase(req, res, false)); diff --git a/test/cases/addUserAsVIP.ts b/test/cases/addUserAsVIP.ts index 49aba60d..ebbcd6fc 100644 --- a/test/cases/addUserAsVIP.ts +++ b/test/cases/addUserAsVIP.ts @@ -10,6 +10,10 @@ const checkUserVIP = (publicID: string) => db.prepare("get", `SELECT "userID" FR const adminPrivateUserID = "testUserId"; const permVIP1 = "addVIP_permaVIPOne"; const publicPermVIP1 = getHash(permVIP1) as HashedUserID; +const permVIP2 = "addVIP_permaVIPTwo"; +const publicPermVIP2 = getHash(permVIP2) as HashedUserID; +const permVIP3 = "addVIP_permaVIPThree"; +const publicPermVIP3 = getHash(permVIP3) as HashedUserID; const endpoint = "/api/addUserAsVIP"; const addUserAsVIP = (userID: string, enabled: boolean, adminUserID = adminPrivateUserID) => client({ @@ -41,6 +45,16 @@ describe("addVIP test", function() { }) .catch(err => done(err)); }); + it("Should be able to add second user as VIP", (done) => { + addUserAsVIP(publicPermVIP2, true) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await checkUserVIP(publicPermVIP2); + assert.ok(row); + done(); + }) + .catch(err => done(err)); + }); it("Should return 403 with invalid adminID", (done) => { addUserAsVIP(publicPermVIP1, true, "Invalid_Admin_User_ID") .then(res => { @@ -89,4 +103,39 @@ describe("addVIP test", function() { }) .catch(err => done(err)); }); + it("Should remove VIP if enabled is false", (done) => { + client({ + method: "POST", + url: endpoint, + params: { + userID: publicPermVIP2, + adminUserID: adminPrivateUserID, + enabled: "invalid-text" + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await checkUserVIP(publicPermVIP2); + assert.ok(!row); + done(); + }) + .catch(err => done(err)); + }); + it("Should remove VIP if enabled is missing", (done) => { + client({ + method: "POST", + url: endpoint, + params: { + userID: publicPermVIP3, + adminUserID: adminPrivateUserID + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await checkUserVIP(publicPermVIP3); + assert.ok(!row); + done(); + }) + .catch(err => done(err)); + }); }); \ No newline at end of file diff --git a/test/cases/getIP.ts b/test/cases/getIP.ts new file mode 100644 index 00000000..f761e01b --- /dev/null +++ b/test/cases/getIP.ts @@ -0,0 +1,109 @@ +import sinon from "sinon"; +import { config } from "../../src/config"; +import assert from "assert"; +const mode = "production"; +let stub: sinon.SinonStub; +let stub2: sinon.SinonStub; +import { createRequest } from "../mocks/mockExpressRequest"; +import { getIP } from "../../src/utils/getIP"; + +const v4RequestOptions = { + headers: { + "x-forwarded-for": "127.0.1.1", + "cf-connecting-ip": "127.0.1.2", + "x-real-ip": "127.0.1.3", + }, + ip: "127.0.1.5", + socket: { + remoteAddress: "127.0.1.4" + } +}; +const v6RequestOptions = { + headers: { + "x-forwarded-for": "[100::1]", + "cf-connecting-ip": "[100::2]", + "x-real-ip": "[100::3]", + }, + ip: "[100::5]", + socket: { + remoteAddress: "[100::4]" + } +}; +const v4MockRequest = createRequest(v4RequestOptions); +const v6MockRequest = createRequest(v6RequestOptions); + +const expectedIP4 = { + "X-Forwarded-For": "127.0.1.1", + "Cloudflare": "127.0.1.2", + "X-Real-IP": "127.0.1.3", + "default": "127.0.1.4", +}; + +const expectedIP6 = { + "X-Forwarded-For": "[100::1]", + "Cloudflare": "[100::2]", + "X-Real-IP": "[100::3]", + "default": "[100::4]", +}; + +describe("getIP stubs", () => { + before(() => stub = sinon.stub(config, "mode").value(mode)); + after(() => stub.restore()); + + it("Should return production mode if stub worked", (done) => { + assert.strictEqual(config.mode, mode); + done(); + }); +}); + +describe("getIP array tests", () => { + beforeEach(() => stub = sinon.stub(config, "mode").value(mode)); + afterEach(() => { + stub.restore(); + stub2.restore(); + }); + + for (const [key, value] of Object.entries(expectedIP4)) { + it(`Should return correct IPv4 from ${key}`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value(key); + const ip = getIP(v4MockRequest); + assert.strictEqual(config.behindProxy, key); + assert.strictEqual(ip, value); + done(); + }); + } + + for (const [key, value] of Object.entries(expectedIP6)) { + it(`Should return correct IPv6 from ${key}`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value(key); + const ip = getIP(v6MockRequest); + assert.strictEqual(config.behindProxy, key); + assert.strictEqual(ip, value); + done(); + }); + } +}); + +describe("getIP true tests", () => { + before(() => stub = sinon.stub(config, "mode").value(mode)); + after(() => { + stub.restore(); + stub2.restore(); + }); + + it(`Should return correct IPv4 from with bool true`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value(true); + const ip = getIP(v4MockRequest); + assert.strictEqual(config.behindProxy, "X-Forwarded-For"); + assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]); + done(); + }); + + it(`Should return correct IPv4 from with string true`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value("true"); + const ip = getIP(v4MockRequest); + assert.strictEqual(config.behindProxy, "X-Forwarded-For"); + assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]); + done(); + }); +}); \ No newline at end of file diff --git a/test/cases/testUtils.ts b/test/cases/testUtils.ts index 78818c87..29bfe60f 100644 --- a/test/cases/testUtils.ts +++ b/test/cases/testUtils.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import { partialDeepEquals } from "../utils/partialDeepEquals"; +import { partialDeepEquals, mixedDeepEquals } from "../utils/partialDeepEquals"; describe("Test utils ", () => { it("objectContain", () => { @@ -135,4 +135,45 @@ describe("Test utils ", () => { } ), "Did not match partial child array"); }); + it("mixedDeepEquals exists", () => { + assert(!mixedDeepEquals({ + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + }, + ignore: true + }, { + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + }, + ignore: false + })); + }); + it("mixedDeepEquals noProperty", () => { + assert(!mixedDeepEquals({ + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + } + }, { + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + }, + ignore: false + })); + }); }); \ No newline at end of file diff --git a/test/cases/userCounter.ts b/test/cases/userCounter.ts index 120e0e80..8215ce11 100644 --- a/test/cases/userCounter.ts +++ b/test/cases/userCounter.ts @@ -3,7 +3,6 @@ import assert from "assert"; import { config } from "../../src/config"; import { getHash } from "../../src/utils/getHash"; - describe("userCounter", () => { it("Should return 200", function (done) { if (!config.userCounterURL) this.skip(); // skip if no userCounterURL is set diff --git a/test/mocks/mockExpressRequest.ts b/test/mocks/mockExpressRequest.ts new file mode 100644 index 00000000..7ad4ae08 --- /dev/null +++ b/test/mocks/mockExpressRequest.ts @@ -0,0 +1,33 @@ +const nullStub = (): any => null; + +export const createRequest = (options: any) => ({ + app: {}, + baseUrl: "", + body: {}, + cookies: {}, + fresh: true, + headers: {}, + hostname: "example.com", + ip: "", + ips: [], + method: "GET", + originalUrl: "/", + params: {}, + path: "/", + protocol: "https", + query: {}, + route: {}, + secure: true, + signedCookies: {}, + stale: false, + subdomains: [], + xhr: true, + accepts: nullStub(), + acceptsCharsets: nullStub(), + acceptsEncodings: nullStub(), + acceptsLanguages: nullStub(), + get: nullStub(), + is: nullStub(), + range: nullStub(), + ...options +}); From 7457b51aa49dc0826c2d63665d86bf49452bf81b Mon Sep 17 00:00:00 2001 From: Michael C Date: Sat, 24 Sep 2022 21:43:52 -0400 Subject: [PATCH 10/26] add getByHash tests, remove redundant check - `{}` always returns true so the early exit is never taken --- src/routes/getSkipSegmentsByHash.ts | 2 - test/cases/getSkipSegmentsByHash.ts | 74 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/routes/getSkipSegmentsByHash.ts b/src/routes/getSkipSegmentsByHash.ts index 663fd6dc..454f2329 100644 --- a/src/routes/getSkipSegmentsByHash.ts +++ b/src/routes/getSkipSegmentsByHash.ts @@ -67,8 +67,6 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis // Get all video id's that match hash prefix const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service); - if (!segments) return res.status(404).json([]); - const output = Object.entries(segments).map(([videoID, data]) => ({ videoID, hash: data.hash, diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index 0923fdb7..3129f91f 100644 --- a/test/cases/getSkipSegmentsByHash.ts +++ b/test/cases/getSkipSegmentsByHash.ts @@ -581,4 +581,78 @@ describe("getSkipSegmentsByHash", () => { }) .catch(err => done(err)); }); + + it("Should be able to get single segment with requiredSegments", (done) => { + const requiredSegment1 = "fbf0af454059733c8822f6a4ac8ec568e0787f8c0a5ee915dd5b05e0d7a9a388"; + client.get(`${endpoint}/17bf?requiredSegment=${requiredSegment1}`) + .then(res => { + assert.strictEqual(res.status, 200); + const data = (res.data as Array).sort((a, b) => a.videoID.localeCompare(b.videoID)); + assert.strictEqual(data.length, 1); + const expected = [{ + segments: [{ + UUID: requiredSegment1 + }] + }]; + assert.ok(partialDeepEquals(data, expected)); + assert.strictEqual(data[0].segments.length, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if categories are is number", (done) => { + client.get(`${endpoint}/17bf?categories=3`) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if actionTypes is number", (done) => { + client.get(`${endpoint}/17bf?actionTypes=3`) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if actionTypes are invalid json", (done) => { + client.get(`${endpoint}/17bf?actionTypes={test}`) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if requiredSegments is number", (done) => { + client.get(`${endpoint}/17bf?requiredSegments=3`) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + + it("Should return 404 if requiredSegments is invalid json", (done) => { + client.get(`${endpoint}/17bf?requiredSegments={test}`) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if requiredSegments is not present", (done) => { + client.get(`${endpoint}/17bf?requiredSegment=${fullCategoryVidHash}`) + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); }); From 47616711cebed50ff02c9673992d7d5b6e4458da Mon Sep 17 00:00:00 2001 From: Michael C Date: Sat, 24 Sep 2022 22:10:29 -0400 Subject: [PATCH 11/26] move patreon mock --- test/cases/tokenUtils.ts | 11 ++++------- test/mocks/patreonMock.ts | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 test/mocks/patreonMock.ts diff --git a/test/cases/tokenUtils.ts b/test/cases/tokenUtils.ts index bd0b90ac..be360c5a 100644 --- a/test/cases/tokenUtils.ts +++ b/test/cases/tokenUtils.ts @@ -5,6 +5,7 @@ import * as tokenUtils from "../../src/utils/tokenUtils"; import MockAdapter from "axios-mock-adapter"; import { validatelicenseKeyRegex } from "../../src/routes/verifyToken"; let mock: MockAdapter; +import * as patreon from "../mocks/patreonMock"; const validateToken = validatelicenseKeyRegex; const fakePatreonIdentity = { @@ -26,12 +27,8 @@ const fakePatreonIdentity = { describe("tokenUtils test", function() { before(function() { mock = new MockAdapter(axios, { onNoMatch: "throwException" }); - mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, { - access_token: "test_access_token", - refresh_token: "test_refresh_token", - expires_in: 3600, - }); - mock.onGet(/identity/).reply(200, fakePatreonIdentity); + mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth); + mock.onGet(/identity/).reply(200, patreon.fakeIdentity); }); it("Should be able to create patreon token", function (done) { @@ -50,7 +47,7 @@ describe("tokenUtils test", function() { it("Should be able to get patreon identity", function (done) { if (!config?.patreon) this.skip(); tokenUtils.getPatreonIdentity("fake_access_token").then((result) => { - assert.deepEqual(result, fakePatreonIdentity); + assert.deepEqual(result, patreon.fakeIdentity); done(); }); }); diff --git a/test/mocks/patreonMock.ts b/test/mocks/patreonMock.ts new file mode 100644 index 00000000..3b5c9aea --- /dev/null +++ b/test/mocks/patreonMock.ts @@ -0,0 +1,21 @@ +export const fakeIdentity = { + data: {}, + links: {}, + included: [ + { + attributes: { + is_monthly: true, + currently_entitled_amount_cents: 100, + patron_status: "active_patron", + }, + id: "id", + type: "campaign" + } + ], +}; + +export const fakeOauth = { + access_token: "test_access_token", + refresh_token: "test_refresh_token", + expires_in: 3600, +}; \ No newline at end of file From c0952c15c86aaabf6961e58123ad6a53ed5c8077 Mon Sep 17 00:00:00 2001 From: Michael C Date: Sat, 24 Sep 2022 23:08:36 -0400 Subject: [PATCH 12/26] fix getTotalStats userCount --- test/cases/getTotalStats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cases/getTotalStats.ts b/test/cases/getTotalStats.ts index 04eab62d..f95c2927 100644 --- a/test/cases/getTotalStats.ts +++ b/test/cases/getTotalStats.ts @@ -7,7 +7,7 @@ describe("getTotalStats", () => { it("Can get total stats", async () => { const result = await client({ url: endpoint }); const data = result.data; - assert.ok(data.userCount >= 0); + assert.ok(data?.userCount ?? true); assert.ok(data.activeUsers >= 0); assert.ok(data.apiUsers >= 0); assert.ok(data.viewCount >= 0); From 9ca087206e9cc45359e23298120725e67caf5e4a Mon Sep 17 00:00:00 2001 From: Michael C Date: Sun, 25 Sep 2022 02:04:30 -0400 Subject: [PATCH 13/26] add token tests --- src/routes/generateToken.ts | 2 + src/routes/verifyToken.ts | 23 +--- test/cases/generateVerifyToken.ts | 178 ++++++++++++++++++++++++++++++ test/cases/tokenUtils.ts | 19 +--- test/mocks/gumroadMock.ts | 22 ++++ test/mocks/patreonMock.ts | 40 ++++++- 6 files changed, 248 insertions(+), 36 deletions(-) create mode 100644 test/cases/generateVerifyToken.ts create mode 100644 test/mocks/gumroadMock.ts diff --git a/src/routes/generateToken.ts b/src/routes/generateToken.ts index 4c0771e7..8b06612e 100644 --- a/src/routes/generateToken.ts +++ b/src/routes/generateToken.ts @@ -44,5 +44,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo `); } + } else { + return res.sendStatus(403); } } \ No newline at end of file diff --git a/src/routes/verifyToken.ts b/src/routes/verifyToken.ts index 2b18b5a3..dfbb0163 100644 --- a/src/routes/verifyToken.ts +++ b/src/routes/verifyToken.ts @@ -4,7 +4,6 @@ import { config } from "../config"; import { privateDB } from "../databases/databases"; import { Logger } from "../utils/logger"; import { getPatreonIdentity, PatronStatus, refreshToken, TokenType } from "../utils/tokenUtils"; -import FormData from "form-data"; interface VerifyTokenRequest extends Request { query: { @@ -13,7 +12,7 @@ interface VerifyTokenRequest extends Request { } export const validatelicenseKeyRegex = (token: string) => - new RegExp(/[A-Za-z0-9]{40}/).test(token); + new RegExp(/[A-Za-z0-9]{40}|[A-Za-z0-9-]{35}/).test(token); export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise { const { query: { licenseKey } } = req; @@ -26,12 +25,6 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response) allowed: false }); } - const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/); - if (!licenseRegex.test(licenseKey)) { - return res.status(200).send({ - allowed: false - }); - } const tokens = (await privateDB.prepare("get", `SELECT "accessToken", "refreshToken", "expiresIn" from "oauthLicenseKeys" WHERE "licenseKey" = ?` , [licenseKey])) as {accessToken: string, refreshToken: string, expiresIn: number}; @@ -42,6 +35,7 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response) refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error); } + /* istanbul ignore else */ if (identity) { const membership = identity.included?.[0]?.attributes; const allowed = !!membership && ((membership.patron_status === PatronStatus.active && membership.currently_entitled_amount_cents > 0) @@ -73,20 +67,13 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response) async function checkAllGumroadProducts(licenseKey: string): Promise { for (const link of config.gumroad.productPermalinks) { try { - const formData = new FormData(); - formData.append("product_permalink", link); - formData.append("license_key", licenseKey); - - const result = await axios.request({ - url: "https://api.gumroad.com/v2/licenses/verify", - data: formData, - method: "POST", - headers: formData.getHeaders() + const result = await axios.post("https://api.gumroad.com/v2/licenses/verify", { + params: { product_permalink: link, license_key: licenseKey } }); const allowed = result.status === 200 && result.data?.success; if (allowed) return allowed; - } catch (e) { + } catch (e) /* istanbul ignore next */ { Logger.error(`Gumroad fetch for ${link} failed: ${e}`); } } diff --git a/test/cases/generateVerifyToken.ts b/test/cases/generateVerifyToken.ts new file mode 100644 index 00000000..fc11cc52 --- /dev/null +++ b/test/cases/generateVerifyToken.ts @@ -0,0 +1,178 @@ +import assert from "assert"; +import { config } from "../../src/config"; +import axios from "axios"; +import { createAndSaveToken, TokenType } from "../../src/utils/tokenUtils"; +import MockAdapter from "axios-mock-adapter"; +let mock: MockAdapter; +import * as patreon from "../mocks/patreonMock"; +import * as gumroad from "../mocks/gumroadMock"; +import { client } from "../utils/httpClient"; +import { validatelicenseKeyRegex } from "../../src/routes/verifyToken"; + +const generateEndpoint = "/api/generateToken"; +const getGenerateToken = (type: string, code: string | null, adminUserID: string | null) => client({ + url: `${generateEndpoint}/${type}`, + params: { code, adminUserID } +}); + +const verifyEndpoint = "/api/verifyToken"; +const getVerifyToken = (licenseKey: string | null) => client({ + url: verifyEndpoint, + params: { licenseKey } +}); + +let patreonLicense: string; +let localLicense: string; +const gumroadLicense = gumroad.generateLicense(); + +const extractLicenseKey = (data: string) => { + const regex = /([A-Za-z0-9]{40})/; + const match = data.match(regex); + if (!match) throw new Error("Failed to extract license key"); + return match[1]; +}; + +describe("generateToken test", function() { + + before(function() { + mock = new MockAdapter(axios, { onNoMatch: "throwException" }); + mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth); + }); + + after(function () { + mock.restore(); + }); + + it("Should be able to create patreon token for active patron", function (done) { + mock.onGet(/identity/).reply(200, patreon.activeIdentity); + if (!config?.patreon) this.skip(); + getGenerateToken("patreon", "patreon_code", "").then(res => { + patreonLicense = extractLicenseKey(res.data); + assert.ok(validatelicenseKeyRegex(patreonLicense)); + done(); + }); + }); + + it("Should be able to create new local token", function (done) { + createAndSaveToken(TokenType.local).then((licenseKey) => { + assert.ok(validatelicenseKeyRegex(licenseKey)); + localLicense = licenseKey; + done(); + }); + }); + + it("Should return 400 if missing code parameter", function (done) { + getGenerateToken("patreon", null, "").then(res => { + assert.strictEqual(res.status, 400); + done(); + }); + }); + + it("Should return 403 if missing adminuserID parameter", function (done) { + getGenerateToken("local", "fake-code", null).then(res => { + assert.strictEqual(res.status, 403); + done(); + }); + }); + + it("Should return 403 for invalid adminuserID parameter", function (done) { + getGenerateToken("local", "fake-code", "fakeAdminID").then(res => { + assert.strictEqual(res.status, 403); + done(); + }); + }); +}); + +describe("verifyToken static tests", function() { + it("Should fast reject invalid token", function (done) { + getVerifyToken("00000").then(res => { + assert.strictEqual(res.status, 200); + assert.ok(!res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should return 400 if missing code token", function (done) { + getVerifyToken(null).then(res => { + assert.strictEqual(res.status, 400); + done(); + }); + }); +}); + +describe("verifyToken mock tests", function() { + + beforeEach(function() { + mock = new MockAdapter(axios, { onNoMatch: "throwException" }); + }); + + afterEach(function () { + mock.restore(); + }); + + it("Should accept current patron", function (done) { + if (!config?.patreon) this.skip(); + mock.onGet(/identity/).reply(200, patreon.activeIdentity); + getVerifyToken(patreonLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should reject nonexistent patron", function (done) { + if (!config?.patreon) this.skip(); + mock.onGet(/identity/).reply(200, patreon.invalidIdentity); + getVerifyToken(patreonLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(!res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should accept qualitying former patron", function (done) { + if (!config?.patreon) this.skip(); + mock.onGet(/identity/).reply(200, patreon.formerIdentitySucceed); + getVerifyToken(patreonLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should reject unqualitifed former patron", function (done) { + if (!config?.patreon) this.skip(); + mock.onGet(/identity/).reply(200, patreon.formerIdentityFail); + getVerifyToken(patreonLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(!res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should accept real gumroad key", function (done) { + mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseSuccess); + getVerifyToken(gumroadLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should reject fake gumroad key", function (done) { + mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseFail); + getVerifyToken(gumroadLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(!res.data.allowed); + done(); + }).catch(err => done(err)); + }); + + it("Should validate local license", function (done) { + getVerifyToken(localLicense).then(res => { + assert.strictEqual(res.status, 200); + assert.ok(res.data.allowed); + done(); + }).catch(err => done(err)); + }); +}); diff --git a/test/cases/tokenUtils.ts b/test/cases/tokenUtils.ts index be360c5a..4b3890eb 100644 --- a/test/cases/tokenUtils.ts +++ b/test/cases/tokenUtils.ts @@ -8,27 +8,12 @@ let mock: MockAdapter; import * as patreon from "../mocks/patreonMock"; const validateToken = validatelicenseKeyRegex; -const fakePatreonIdentity = { - data: {}, - links: {}, - included: [ - { - attributes: { - is_monthly: true, - currently_entitled_amount_cents: 100, - patron_status: "active_patron", - }, - id: "id", - type: "campaign" - } - ], -}; describe("tokenUtils test", function() { before(function() { mock = new MockAdapter(axios, { onNoMatch: "throwException" }); mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth); - mock.onGet(/identity/).reply(200, patreon.fakeIdentity); + mock.onGet(/identity/).reply(200, patreon.activeIdentity); }); it("Should be able to create patreon token", function (done) { @@ -47,7 +32,7 @@ describe("tokenUtils test", function() { it("Should be able to get patreon identity", function (done) { if (!config?.patreon) this.skip(); tokenUtils.getPatreonIdentity("fake_access_token").then((result) => { - assert.deepEqual(result, patreon.fakeIdentity); + assert.deepEqual(result, patreon.activeIdentity); done(); }); }); diff --git a/test/mocks/gumroadMock.ts b/test/mocks/gumroadMock.ts new file mode 100644 index 00000000..349e5c47 --- /dev/null +++ b/test/mocks/gumroadMock.ts @@ -0,0 +1,22 @@ +export const licenseSuccess = { + success: true, + uses: 4, + purchase: {} +}; + +export const licenseFail = { + success: false, + message: "That license does not exist for the provided product." +}; + + +const subCode = (length = 8) => { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += characters[(Math.floor(Math.random() * characters.length))]; + } + return result; +} + +export const generateLicense = (): string => `${subCode()}-${subCode()}-${subCode()}-${subCode()}`; diff --git a/test/mocks/patreonMock.ts b/test/mocks/patreonMock.ts index 3b5c9aea..aafe26e6 100644 --- a/test/mocks/patreonMock.ts +++ b/test/mocks/patreonMock.ts @@ -1,4 +1,4 @@ -export const fakeIdentity = { +export const activeIdentity = { data: {}, links: {}, included: [ @@ -14,6 +14,44 @@ export const fakeIdentity = { ], }; +export const invalidIdentity = { + data: {}, + links: {}, + included: [{}], +}; + +export const formerIdentitySucceed = { + data: {}, + links: {}, + included: [ + { + attributes: { + is_monthly: true, + campaign_lifetime_support_cents: 500, + patron_status: "former_patron", + }, + id: "id", + type: "campaign" + } + ], +}; + +export const formerIdentityFail = { + data: {}, + links: {}, + included: [ + { + attributes: { + is_monthly: true, + campaign_lifetime_support_cents: 1, + patron_status: "former_patron", + }, + id: "id", + type: "campaign" + } + ], +}; + export const fakeOauth = { access_token: "test_access_token", refresh_token: "test_refresh_token", From 005ae2c9fb0f7399fe2105b812e350ba3977f41e Mon Sep 17 00:00:00 2001 From: Michael C Date: Sun, 25 Sep 2022 02:04:51 -0400 Subject: [PATCH 14/26] add ignores for impossible cases --- src/routes/getIsUserVIP.ts | 2 +- src/routes/getUserStats.ts | 4 ++-- src/routes/getViewsForUser.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/getIsUserVIP.ts b/src/routes/getIsUserVIP.ts index 09f43473..c8d048df 100644 --- a/src/routes/getIsUserVIP.ts +++ b/src/routes/getIsUserVIP.ts @@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise Date: Sun, 25 Sep 2022 02:05:51 -0400 Subject: [PATCH 15/26] lint fix --- src/routes/voteOnSponsorTime.ts | 2 +- test/mocks/gumroadMock.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 5b00aacc..6a9ba245 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -222,7 +222,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i [UUID], { useReplica: true })) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number}; if (!config.categorySupport[category]?.includes(segmentInfo.actionType) || segmentInfo.actionType === ActionType.Full) { - return { status: 400, message: `Not allowed to change to ${category} when for segment of type ${segmentInfo.actionType}`}; + return { status: 400, message: `Not allowed to change to ${category} when for segment of type ${segmentInfo.actionType}` }; } if (!config.categoryList.includes(category)) { return { status: 400, message: "Category doesn't exist." }; diff --git a/test/mocks/gumroadMock.ts b/test/mocks/gumroadMock.ts index 349e5c47..00ae8311 100644 --- a/test/mocks/gumroadMock.ts +++ b/test/mocks/gumroadMock.ts @@ -17,6 +17,6 @@ const subCode = (length = 8) => { result += characters[(Math.floor(Math.random() * characters.length))]; } return result; -} +}; export const generateLicense = (): string => `${subCode()}-${subCode()}-${subCode()}-${subCode()}`; From 506b6570f3c5a5c651d53e76877ea06b9bac8162 Mon Sep 17 00:00:00 2001 From: Michael C Date: Sun, 25 Sep 2022 03:29:31 -0400 Subject: [PATCH 16/26] add ignore next to catch errors --- src/app.ts | 2 +- src/routes/getIsUserVIP.ts | 2 +- src/routes/getLockCategories.ts | 2 +- src/routes/getSavedTimeForUser.ts | 2 +- src/routes/getSegmentInfo.ts | 4 ++-- src/routes/getSkipSegments.ts | 6 +++--- src/routes/getStatus.ts | 2 +- src/routes/getUserID.ts | 5 +++-- src/routes/getUserInfo.ts | 20 ++++++++++---------- src/routes/getUserStats.ts | 4 ++-- src/routes/getUsername.ts | 2 +- src/routes/getViewsForUser.ts | 2 +- src/routes/postClearCache.ts | 2 +- src/routes/postLockCategories.ts | 4 ++-- src/routes/postPurgeAllSegments.ts | 2 +- src/routes/postSegmentShift.ts | 2 +- src/routes/setUsername.ts | 4 ++-- 17 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/app.ts b/src/app.ts index e7ba2c11..caee4fab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -200,7 +200,7 @@ function setupRoutes(router: Router) { router.get("/api/generateToken/:type", generateTokenRequest); router.get("/api/verifyToken", verifyTokenRequest); - /* instanbul ignore next */ + /* istanbul ignore next */ if (config.postgres?.enabled) { router.get("/database", (req, res) => dumpDatabase(req, res, true)); router.get("/database.json", (req, res) => dumpDatabase(req, res, false)); diff --git a/src/routes/getIsUserVIP.ts b/src/routes/getIsUserVIP.ts index c8d048df..a9a3f3e8 100644 --- a/src/routes/getIsUserVIP.ts +++ b/src/routes/getIsUserVIP.ts @@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise /^([a-f0-9]{64}|[a-f0-9]{8} async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise { try { return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]); - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -62,7 +62,7 @@ async function endpoint(req: Request, res: Response): Promise { //send result return res.send(DBSegments); } - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { // catch JSON.parse error return res.status(400).send("UUIDs parameter does not match format requirements."); } else return res.sendStatus(500); diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 7da485e6..6c938225 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -107,7 +107,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: } return processedSegments; - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err) { Logger.error(err as string); return null; @@ -169,7 +169,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, })); return segments; - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return null; } @@ -465,7 +465,7 @@ async function endpoint(req: Request, res: Response): Promise { //send result return res.send(segments); } - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { return res.status(400).send("Categories parameter does not match format requirements."); } else return res.sendStatus(500); diff --git a/src/routes/getStatus.ts b/src/routes/getStatus.ts index 7f764f53..1c56ceed 100644 --- a/src/routes/getStatus.ts +++ b/src/routes/getStatus.ts @@ -27,7 +27,7 @@ export async function getStatus(req: Request, res: Response): Promise hostname: os.hostname() }; return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues); - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return res.sendStatus(500); } diff --git a/src/routes/getUserID.ts b/src/routes/getUserID.ts index c93fa627..5acf659d 100644 --- a/src/routes/getUserID.ts +++ b/src/routes/getUserID.ts @@ -12,7 +12,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us try { return db.prepare("all", `SELECT "userName", "userID" FROM "userNames" WHERE "userName" LIKE ? ESCAPE '\\' LIMIT 10`, [userName]); - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -20,7 +20,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us function getExactUserID(userName: string): Promise<{userName: string, userID: UserID }[]> { try { return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]); - } catch (err) { + } catch (err) /* istanbul ignore next */{ return null; } } @@ -42,6 +42,7 @@ export async function getUserID(req: Request, res: Response): Promise : await getFuzzyUserID(userName); if (results === undefined || results === null) { + /* istanbul ignore next */ return res.sendStatus(500); } else if (results.length === 0) { return res.sendStatus(404); diff --git a/src/routes/getUserInfo.ts b/src/routes/getUserInfo.ts index c0b19556..6492fdd5 100644 --- a/src/routes/getUserInfo.ts +++ b/src/routes/getUserInfo.ts @@ -28,7 +28,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min segmentCount: 0, }; } - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -37,7 +37,7 @@ async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise { try { const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true }); return row?.ignoredSegmentCount ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -46,7 +46,7 @@ async function dbGetUsername(userID: HashedUserID) { try { const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]); return row?.userName ?? userID; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -55,7 +55,7 @@ async function dbGetViewsForUser(userID: HashedUserID) { try { const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID], { useReplica: true }); return row?.viewCount ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -64,7 +64,7 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) { try { const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true }); return row?.ignoredViewCount ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -73,7 +73,7 @@ async function dbGetWarningsForUser(userID: HashedUserID): Promise { try { const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID], { useReplica: true }); return row?.total ?? 0; - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(`Couldn't get warnings for user ${userID}. returning 0`); return 0; } @@ -83,7 +83,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise { try { const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true }); return row?.userCount > 0 ?? false; - } catch (err) { + } catch (err) /* istanbul ignore next */ { return false; } } @@ -195,7 +195,7 @@ async function getUserInfo(req: Request, res: Response): Promise { export async function endpoint(req: Request, res: Response): Promise { try { return await getUserInfo(req, res); - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { // catch JSON.parse error return res.status(400).send("Invalid values JSON"); } else return res.sendStatus(500); diff --git a/src/routes/getUserStats.ts b/src/routes/getUserStats.ts index 63185f4a..ea239d14 100644 --- a/src/routes/getUserStats.ts +++ b/src/routes/getUserStats.ts @@ -71,7 +71,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea }; } return result; - } catch (err) /* instanbul ignore next */ { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return null; } @@ -81,7 +81,7 @@ async function dbGetUsername(userID: HashedUserID) { try { const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]); return row?.userName ?? userID; - } catch (err) /* instanbul ignore next */ { + } catch (err) /* istanbul ignore next */ { return false; } } diff --git a/src/routes/getUsername.ts b/src/routes/getUsername.ts index 28098fea..b3a2a7b9 100644 --- a/src/routes/getUsername.ts +++ b/src/routes/getUsername.ts @@ -27,7 +27,7 @@ export async function getUsername(req: Request, res: Response): Promise Date: Sun, 25 Sep 2022 03:30:33 -0400 Subject: [PATCH 17/26] add uniform parsing and catching for arrays, remove redundant check --- src/routes/getLockCategoriesByHash.ts | 27 +++++++++++++++------ src/routes/getLockReason.ts | 35 ++++++++++++++------------- src/routes/getTopUsers.ts | 5 ---- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/routes/getLockCategoriesByHash.ts b/src/routes/getLockCategoriesByHash.ts index 227f68a2..1aee4410 100644 --- a/src/routes/getLockCategoriesByHash.ts +++ b/src/routes/getLockCategoriesByHash.ts @@ -44,14 +44,25 @@ const mergeLocks = (source: DBLock[], actionTypes: ActionType[]): LockResultByHa export async function getLockCategoriesByHash(req: Request, res: Response): Promise { let hashPrefix = req.params.prefix as VideoIDHash; - const actionTypes: ActionType[] = req.query.actionTypes - ? JSON.parse(req.query.actionTypes as string) - : req.query.actionType - ? Array.isArray(req.query.actionType) - ? req.query.actionType - : [req.query.actionType] - : [ActionType.Skip, ActionType.Mute]; + let actionTypes: ActionType[] = []; + try { + actionTypes = req.query.actionTypes + ? JSON.parse(req.query.actionTypes as string) + : req.query.actionType + ? Array.isArray(req.query.actionType) + ? req.query.actionType + : [req.query.actionType] + : [ActionType.Skip, ActionType.Mute]; + if (!Array.isArray(actionTypes)) { + //invalid request + return res.sendStatus(400); + } + } catch (err) { + //invalid request + return res.status(400).send("Invalid request: JSON parse error (actionTypes)"); + } if (!hashPrefixTester(req.params.prefix)) { + return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix } hashPrefix = hashPrefix.toLowerCase() as VideoIDHash; @@ -62,7 +73,7 @@ export async function getLockCategoriesByHash(req: Request, res: Response): Prom if (lockedRows.length === 0 || !lockedRows[0]) return res.sendStatus(404); // merge all locks return res.send(mergeLocks(lockedRows, actionTypes)); - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return res.sendStatus(500); } diff --git a/src/routes/getLockReason.ts b/src/routes/getLockReason.ts index ef4e5a33..59ab5287 100644 --- a/src/routes/getLockReason.ts +++ b/src/routes/getLockReason.ts @@ -32,18 +32,24 @@ export async function getLockReason(req: Request, res: Response): Promise possibleCategories.includes(x)); - if (!videoID || !Array.isArray(actionTypes)) { - //invalid request - return res.sendStatus(400); - } - try { // Get existing lock categories markers const row = await db.prepare("all", 'SELECT "category", "reason", "actionType", "userID" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category, reason: string, actionType: ActionType, userID: string }[]; @@ -115,7 +116,7 @@ export async function getLockReason(req: Request, res: Response): Promise Date: Sun, 25 Sep 2022 03:31:25 -0400 Subject: [PATCH 18/26] add 4xx tests --- test/cases/generateVerifyToken.ts | 1 + test/cases/getDaysSavedFormatted.ts | 16 ++++++ test/cases/getLockCategoriesByHash.ts | 72 ++++++++++++++++++++++-- test/cases/getLockReason.ts | 81 ++++++++++++++++++++++++--- test/cases/getTopUsers.ts | 9 +++ 5 files changed, 164 insertions(+), 15 deletions(-) diff --git a/test/cases/generateVerifyToken.ts b/test/cases/generateVerifyToken.ts index fc11cc52..b1f56300 100644 --- a/test/cases/generateVerifyToken.ts +++ b/test/cases/generateVerifyToken.ts @@ -104,6 +104,7 @@ describe("verifyToken mock tests", function() { beforeEach(function() { mock = new MockAdapter(axios, { onNoMatch: "throwException" }); + mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth); }); afterEach(function () { diff --git a/test/cases/getDaysSavedFormatted.ts b/test/cases/getDaysSavedFormatted.ts index 8414b73b..2b2ae230 100644 --- a/test/cases/getDaysSavedFormatted.ts +++ b/test/cases/getDaysSavedFormatted.ts @@ -1,5 +1,7 @@ import assert from "assert"; import { client } from "../utils/httpClient"; +import sinon from "sinon"; +import { db } from "../../src/databases/databases"; const endpoint = "/api/getDaysSavedFormatted"; @@ -8,4 +10,18 @@ describe("getDaysSavedFormatted", () => { const result = await client({ url: endpoint }); assert.ok(result.data.daysSaved >= 0); }); + + it("returns 0 days saved if no segments", async () => { + const stub = sinon.stub(db, "prepare").resolves(undefined); + const result = await client({ url: endpoint }); + assert.ok(result.data.daysSaved >= 0); + stub.restore(); + }); + + it("returns days saved to 2 fixed points", async () => { + const stub = sinon.stub(db, "prepare").resolves({ daysSaved: 1.23456789 }); + const result = await client({ url: endpoint }); + assert.strictEqual(result.data.daysSaved, "1.23"); + stub.restore(); + }); }); \ No newline at end of file diff --git a/test/cases/getLockCategoriesByHash.ts b/test/cases/getLockCategoriesByHash.ts index 9695731e..f6b2757a 100644 --- a/test/cases/getLockCategoriesByHash.ts +++ b/test/cases/getLockCategoriesByHash.ts @@ -166,17 +166,77 @@ describe("getLockCategoriesByHash", () => { .catch(err => done(err)); }); - it("Should be able to get by actionType", (done) => { - getLockCategories(fakeHash.substring(0,5), [ActionType.Full]) + it("should return 400 if invalid actionTypes", (done) => { + client.get(`${endpoint}/aaaa`, { params: { actionTypes: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("should return 400 if invalid actionTypes JSON", (done) => { + client.get(`${endpoint}/aaaa`, { params: { actionTypes: "{3}" } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get single lock", (done) => { + const videoID = "getLockHash2"; + const hash = getHash(videoID, 1); + getLockCategories(hash.substring(0,6)) .then(res => { assert.strictEqual(res.status, 200); const expected = [{ - videoID: "fakehash-2", - hash: fakeHash, + videoID, + hash, + categories: [ + "preview" + ], + reason: "2-reason" + }]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get by actionType not in array", (done) => { + const videoID = "getLockHash2"; + const hash = getHash(videoID, 1); + client.get(`${endpoint}/${hash.substring(0,6)}`, { params: { actionType: ActionType.Skip } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [{ + videoID, + hash, + categories: [ + "preview" + ], + reason: "2-reason" + }]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get by no actionType", (done) => { + const videoID = "getLockHash2"; + const hash = getHash(videoID, 1); + client.get(`${endpoint}/${hash.substring(0,6)}`) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [{ + videoID, + hash, categories: [ - "sponsor" + "preview" ], - reason: "fake2-notshown" + reason: "2-reason" }]; assert.deepStrictEqual(res.data, expected); done(); diff --git a/test/cases/getLockReason.ts b/test/cases/getLockReason.ts index 9bbb35bc..1b3c36ba 100644 --- a/test/cases/getLockReason.ts +++ b/test/cases/getLockReason.ts @@ -55,6 +55,45 @@ describe("getLockReason", () => { .catch(err => done(err)); }); + it("Should be able to get with actionTypes array", (done) => { + client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionTypes: '["full"]' } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [ + { category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 } + ]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get with actionType", (done) => { + client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: "full" } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [ + { category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 } + ]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get with actionType array", (done) => { + client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: ["full"] } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [ + { category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 } + ]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to get empty locks", (done) => { client.get(endpoint, { params: { videoID: "getLockReason", category: "intro" } }) .then(res => { @@ -118,8 +157,10 @@ describe("getLockReason", () => { }) .catch(err => done(err)); }); +}); - it("should return 400 if no videoID specified", (done) => { +describe("getLockReason 400", () => { + it("Should return 400 with missing videoID", (done) => { client.get(endpoint) .then(res => { assert.strictEqual(res.status, 400); @@ -128,15 +169,37 @@ describe("getLockReason", () => { .catch(err => done(err)); }); - it("should be able to get by actionType", (done) => { - client.get(endpoint, { params: { videoID: "getLockReason", actionType: "full" } }) + it("Should return 400 with invalid actionTypes ", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: 3 } }) .then(res => { - assert.strictEqual(res.status, 200); - const expected = [ - { category: "selfpromo", locked: 1, reason: "sponsor-reason", userID: vipUserID2, userName: vipUserName2 }, - { category: "sponsor", locked: 0, reason: "", userID: "", userName: "" } - ]; - partialDeepEquals(res.data, expected); + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 with invalid actionTypes JSON ", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: "{3}" } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 with invalid categories", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", categories: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 with invalid categories JSON", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", categories: "{3}" } }) + .then(res => { + assert.strictEqual(res.status, 400); done(); }) .catch(err => done(err)); diff --git a/test/cases/getTopUsers.ts b/test/cases/getTopUsers.ts index 8808910d..07ef80c6 100644 --- a/test/cases/getTopUsers.ts +++ b/test/cases/getTopUsers.ts @@ -38,6 +38,15 @@ describe("getTopUsers", () => { .catch(err => done(err)); }); + it("Should return 400 if undefined sortType provided", (done) => { + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to get by all sortTypes", (done) => { client.get(endpoint, { params: { sortType: 0 } })// minutesSaved .then(res => { From 9ef0eafac1e4c798c875e0c1e7c73ceafc813dae Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:56:59 -0400 Subject: [PATCH 19/26] add istanbul exclusions --- src/routes/generateToken.ts | 1 + src/utils/innerTubeAPI.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/routes/generateToken.ts b/src/routes/generateToken.ts index 8b06612e..cc889927 100644 --- a/src/routes/generateToken.ts +++ b/src/routes/generateToken.ts @@ -23,6 +23,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo if (type === TokenType.patreon || (type === TokenType.local && adminUserID === config.adminUserID)) { const licenseKey = await createAndSaveToken(type, code); + /* istanbul ignore else */ if (licenseKey) { return res.status(200).send(`

diff --git a/src/utils/innerTubeAPI.ts b/src/utils/innerTubeAPI.ts index c330e92c..475cda93 100644 --- a/src/utils/innerTubeAPI.ts +++ b/src/utils/innerTubeAPI.ts @@ -18,6 +18,7 @@ async function getFromITube (videoID: string): Promise { const result = await axios.post(url, data, { timeout: 3500 }); + /* istanbul ignore else */ if (result.status === 200) { return result.data.videoDetails; } else { @@ -39,6 +40,7 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom return data as innerTubeVideoDetails; } } catch (err) { + /* istanbul ignore next */ return Promise.reject(err); } } From 0b9e7029c5b5f8c8ec50cab0f0b4be413107af22 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:57:33 -0400 Subject: [PATCH 20/26] minor optimizations --- src/routes/getSearchSegments.ts | 9 +++------ src/routes/getStatus.ts | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/routes/getSearchSegments.ts b/src/routes/getSearchSegments.ts index d8fc8605..9dec9df7 100644 --- a/src/routes/getSearchSegments.ts +++ b/src/routes/getSearchSegments.ts @@ -128,12 +128,7 @@ async function handleGetSegments(req: Request, res: Response): Promise, pag ); if (sortBy !== SortableFields.timeSubmitted) { + /* istanbul ignore next */ filteredSegments.sort((a,b) => { const key = sortDir === "desc" ? 1 : -1; if (a[sortBy] < b[sortBy]) { @@ -187,6 +183,7 @@ async function endpoint(req: Request, res: Response): Promise { return res.send(segmentResponse); } } catch (err) { + /* istanbul ignore next */ if (err instanceof SyntaxError) { return res.status(400).send("Invalid array in parameters"); } else return res.sendStatus(500); diff --git a/src/routes/getStatus.ts b/src/routes/getStatus.ts index d453cc40..c7413ff9 100644 --- a/src/routes/getStatus.ts +++ b/src/routes/getStatus.ts @@ -16,7 +16,7 @@ export async function getStatus(req: Request, res: Response): Promise processTime = Date.now() - startTime; return e.value; }) - .catch(e => { + .catch(e => /* istanbul ignore next */ { Logger.error(`status: SQL query timed out: ${e}`); return -1; }); @@ -25,7 +25,7 @@ export async function getStatus(req: Request, res: Response): Promise .then(e => { redisProcessTime = Date.now() - startTime; return e; - }).catch(e => { + }).catch(e => /* istanbul ignore next */ { Logger.error(`status: redis increment timed out ${e}`); return [-1]; }); @@ -33,7 +33,7 @@ export async function getStatus(req: Request, res: Response): Promise const statusValues: Record = { uptime: process.uptime(), - commit: (global as any).HEADCOMMIT || "unknown", + commit: (global as any)?.HEADCOMMIT ?? "unknown", db: Number(dbVersion), startTime, processTime, From 715d41fbb2c2e55802c7ef926bcd621cfed16cb2 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:08 -0400 Subject: [PATCH 21/26] getStatus, token tests and mocks --- test/cases/generateVerifyToken.ts | 22 ++++++++++++++++------ test/cases/getStatus.ts | 13 +++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/test/cases/generateVerifyToken.ts b/test/cases/generateVerifyToken.ts index b1f56300..ef02a5fc 100644 --- a/test/cases/generateVerifyToken.ts +++ b/test/cases/generateVerifyToken.ts @@ -50,7 +50,17 @@ describe("generateToken test", function() { patreonLicense = extractLicenseKey(res.data); assert.ok(validatelicenseKeyRegex(patreonLicense)); done(); - }); + }).catch(err => done(err)); + }); + + it("Should create patreon token for invalid patron", function (done) { + mock.onGet(/identity/).reply(200, patreon.formerIdentityFail); + if (!config?.patreon) this.skip(); + getGenerateToken("patreon", "patreon_code", "").then(res => { + patreonLicense = extractLicenseKey(res.data); + assert.ok(validatelicenseKeyRegex(patreonLicense)); + done(); + }).catch(err => done(err)); }); it("Should be able to create new local token", function (done) { @@ -58,28 +68,28 @@ describe("generateToken test", function() { assert.ok(validatelicenseKeyRegex(licenseKey)); localLicense = licenseKey; done(); - }); + }).catch(err => done(err)); }); it("Should return 400 if missing code parameter", function (done) { getGenerateToken("patreon", null, "").then(res => { assert.strictEqual(res.status, 400); done(); - }); + }).catch(err => done(err)); }); it("Should return 403 if missing adminuserID parameter", function (done) { getGenerateToken("local", "fake-code", null).then(res => { assert.strictEqual(res.status, 403); done(); - }); + }).catch(err => done(err)); }); it("Should return 403 for invalid adminuserID parameter", function (done) { getGenerateToken("local", "fake-code", "fakeAdminID").then(res => { assert.strictEqual(res.status, 403); done(); - }); + }).catch(err => done(err)); }); }); @@ -96,7 +106,7 @@ describe("verifyToken static tests", function() { getVerifyToken(null).then(res => { assert.strictEqual(res.status, 400); done(); - }); + }).catch(err => done(err)); }); }); diff --git a/test/cases/getStatus.ts b/test/cases/getStatus.ts index 7f4ffaf4..f471404b 100644 --- a/test/cases/getStatus.ts +++ b/test/cases/getStatus.ts @@ -2,6 +2,7 @@ import assert from "assert"; import { db } from "../../src/databases/databases"; import { client } from "../utils/httpClient"; import { config } from "../../src/config"; +import sinon from "sinon"; let dbVersion: number; describe("getStatus", () => { @@ -122,4 +123,16 @@ describe("getStatus", () => { }) .catch(err => done(err)); }); + + it("Should return commit unkown if not present", (done) => { + sinon.stub((global as any), "HEADCOMMIT").value(undefined); + client.get(`${endpoint}/commit`) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data, "test"); // commit should be test + done(); + }) + .catch(err => done(err)); + sinon.restore(); + }); }); From d163b1d43627bc634052ecb10e217e19665426f4 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:20 -0400 Subject: [PATCH 22/26] shadowban tests --- test/cases/shadowBanUser.ts | 28 ++++++++++++++++++-- test/cases/shadowBanUser4xx.ts | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 test/cases/shadowBanUser4xx.ts diff --git a/test/cases/shadowBanUser.ts b/test/cases/shadowBanUser.ts index 834382cc..a0b321c6 100644 --- a/test/cases/shadowBanUser.ts +++ b/test/cases/shadowBanUser.ts @@ -187,10 +187,34 @@ describe("shadowBanUser", () => { }) .then(async res => { assert.strictEqual(res.status, 200); - const videoRow = await getShadowBanSegmentCategory(userID, 1); + const videoRow = await getShadowBanSegmentCategory(userID, 0); const shadowRow = await getShadowBan(userID); assert.ok(shadowRow); // ban still exists - assert.strictEqual(videoRow.length, 1); // videos should be hidden + assert.strictEqual(videoRow.length, 0); // videos should be hidden + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to un-shadowban user to restore old submissions", (done) => { + const userID = "shadowBanned4"; + client({ + method: "POST", + url: endpoint, + params: { + userID, + adminUserID: VIPuserID, + enabled: false, + categories: `["sponsor"]`, + unHideOldSubmissions: true + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const videoRow = await getShadowBanSegmentCategory(userID, 0); + const shadowRow = await getShadowBan(userID); + assert.ok(!shadowRow); // ban still exists + assert.strictEqual(videoRow.length, 1); // videos should be visible assert.strictEqual(videoRow[0].category, "sponsor"); done(); }) diff --git a/test/cases/shadowBanUser4xx.ts b/test/cases/shadowBanUser4xx.ts new file mode 100644 index 00000000..c9cdda9c --- /dev/null +++ b/test/cases/shadowBanUser4xx.ts @@ -0,0 +1,48 @@ +import { db } from "../../src/databases/databases"; +import { getHash } from "../../src/utils/getHash"; +import assert from "assert"; +import { client } from "../utils/httpClient"; + +const endpoint = "/api/shadowBanUser"; + +const postShadowBan = (params: Record) => client({ + method: "POST", + url: endpoint, + params +}); + +describe("shadowBanUser 4xx", () => { + const VIPuserID = "shadow-ban-4xx-vip"; + + before(async () => { + await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES(?)`, [getHash(VIPuserID)]); + }); + + it("Should return 400 if no adminUserID", (done) => { + const userID = "shadowBanned"; + postShadowBan({ userID }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if no userID", (done) => { + postShadowBan({ adminUserID: VIPuserID }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 403 if not authorized", (done) => { + postShadowBan({ adminUserID: "notVIPUserID", userID: "shadowBanned" }) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); +}); From 67eb165b53d793f3ffd5a824dbb5ff42ae88e5e7 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:37 -0400 Subject: [PATCH 23/26] getSearchSegments tests --- test/cases/getSearchSegments.ts | 61 ++++++++++++++++++++++++++++++ test/cases/getSearchSegments4xx.ts | 48 +++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 test/cases/getSearchSegments4xx.ts diff --git a/test/cases/getSearchSegments.ts b/test/cases/getSearchSegments.ts index 00842049..c18bef21 100644 --- a/test/cases/getSearchSegments.ts +++ b/test/cases/getSearchSegments.ts @@ -80,6 +80,67 @@ describe("getSearchSegments", () => { .catch(err => done(err)); }); + it("Should be able to filter by category with categories string", (done) => { + client.get(endpoint, { params: { videoID: "searchTest0", categories: `["selfpromo"]` } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + const segments = data.segments; + assert.strictEqual(data.segmentCount, 1); + assert.strictEqual(data.page, 0); + assert.strictEqual(segments[0].UUID, "search-downvote"); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with categories array", (done) => { + client.get(endpoint, { params: { videoID: "searchTest0", category: ["selfpromo"] } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + const segments = data.segments; + assert.strictEqual(data.segmentCount, 1); + assert.strictEqual(data.page, 0); + assert.strictEqual(segments[0].UUID, "search-downvote"); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with actionTypes JSON", (done) => { + client.get(endpoint, { params: { videoID: "searchTest5", actionTypes: `["mute"]` } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.segmentCount, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with actionType array", (done) => { + client.get(endpoint, { params: { videoID: "searchTest5", actionType: ["mute"] } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.segmentCount, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with actionType string", (done) => { + client.get(endpoint, { params: { videoID: "searchTest5", actionType: "mute" } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.segmentCount, 1); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to filter by lock status", (done) => { client.get(endpoint, { params: { videoID: "searchTest0", locked: false } }) .then(res => { diff --git a/test/cases/getSearchSegments4xx.ts b/test/cases/getSearchSegments4xx.ts new file mode 100644 index 00000000..708a0b0a --- /dev/null +++ b/test/cases/getSearchSegments4xx.ts @@ -0,0 +1,48 @@ +import { client } from "../utils/httpClient"; +import assert from "assert"; + +describe("getSearchSegments 4xx", () => { + const endpoint = "/api/searchSegments"; + + it("Should return 400 if no videoID", (done) => { + client.get(endpoint, { params: {} }) + .then(res => { + assert.strictEqual(res.status, 400); + const data = res.data; + assert.strictEqual(data, "videoID not specified"); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if invalid categories", (done) => { + client.get(endpoint, { params: { videoID: "nullVideo", categories: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + const data = res.data; + assert.strictEqual(data, "Categories parameter does not match format requirements."); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if invalid actionTypes", (done) => { + client.get(endpoint, { params: { videoID: "nullVideo", actionTypes: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + const data = res.data; + assert.strictEqual(data, "actionTypes parameter does not match format requirements."); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if no segments", (done) => { + client.get(endpoint, { params: { videoID: "nullVideo", actionType: "chapter" } }) + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); +}); From 95dd36a782ee57492e4adb14332dfeb2b80b6c61 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:49 -0400 Subject: [PATCH 24/26] getUserInfo and free chapters tests --- test/cases/getUserInfo.ts | 1 + test/cases/getUserInfoFree.ts | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/cases/getUserInfoFree.ts diff --git a/test/cases/getUserInfo.ts b/test/cases/getUserInfo.ts index 632774a9..9bcf0433 100644 --- a/test/cases/getUserInfo.ts +++ b/test/cases/getUserInfo.ts @@ -21,6 +21,7 @@ describe("getUserInfo", () => { await db.prepare("run", sponsorTimesQuery, ["getUserInfo0", 0, 36000, 2,"uuid000009", getHash("getuserinfo_user_03"), 8, 10, "sponsor", "skip", 0]); await db.prepare("run", sponsorTimesQuery, ["getUserInfo3", 1, 11, 2, "uuid000006", getHash("getuserinfo_user_02"), 6, 10, "sponsor", "skip", 0]); await db.prepare("run", sponsorTimesQuery, ["getUserInfo4", 1, 11, 2, "uuid000010", getHash("getuserinfo_user_04"), 9, 10, "chapter", "chapter", 0]); + await db.prepare("run", sponsorTimesQuery, ["getUserInfo5", 1, 11, 2, "uuid000011", getHash("getuserinfo_user_05"), 9, 10, "sponsor", "skip", 0]); const insertWarningQuery = 'INSERT INTO warnings ("userID", "issueTime", "issuerUserID", "enabled", "reason") VALUES (?, ?, ?, ?, ?)'; diff --git a/test/cases/getUserInfoFree.ts b/test/cases/getUserInfoFree.ts new file mode 100644 index 00000000..fd32d6a3 --- /dev/null +++ b/test/cases/getUserInfoFree.ts @@ -0,0 +1,65 @@ +import { db } from "../../src/databases/databases"; +import { getHash } from "../../src/utils/getHash"; +import assert from "assert"; +import { client } from "../utils/httpClient"; + +describe("getUserInfo Free Chapters", () => { + const endpoint = "/api/userInfo"; + + const noQualifyUserID = "getUserInfo-Free-noQualify"; + const vipQualifyUserID = "getUserInfo-Free-VIP"; + const repQualifyUserID = "getUserInfo-Free-RepQualify"; + const oldQualifyUserID = "getUserInfo-Free-OldQualify"; + const postOldQualify = 1600000000000; + + before(async () => { + const sponsorTimesQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "reputation", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-0", getHash(repQualifyUserID), postOldQualify, 0, "sponsor", "skip", 20, 0]); + await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-1", getHash(oldQualifyUserID), 0, 0, "sponsor", "skip", 0, 0]); // submit at epoch + await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-2", getHash(noQualifyUserID), postOldQualify, 0, "sponsor", "skip", 0, 0]); + + await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(vipQualifyUserID)]); + }); + + const getUserInfo = (userID: string) => client.get(endpoint, { params: { userID, value: "freeChaptersAccess" } }); + + it("Should not get free access (noQuality)", (done) => { + getUserInfo(noQualifyUserID) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.freeChaptersAccess, false); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get free access (VIP)", (done) => { + getUserInfo(vipQualifyUserID) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.freeChaptersAccess, true); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get free access (rep)", (done) => { + getUserInfo(repQualifyUserID) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.freeChaptersAccess, true); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get free access (old)", (done) => { + getUserInfo(oldQualifyUserID) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.freeChaptersAccess, true); + done(); + }) + .catch(err => done(err)); + }); +}); From 9286f16e7b15d229acaf3efa4a5ea588706ec00a Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 27 Oct 2022 01:19:42 -0400 Subject: [PATCH 25/26] add ingores to tokenUtils --- src/utils/tokenUtils.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/utils/tokenUtils.ts b/src/utils/tokenUtils.ts index 7c5ad3c0..e936b455 100644 --- a/src/utils/tokenUtils.ts +++ b/src/utils/tokenUtils.ts @@ -58,12 +58,11 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis return licenseKey; } - } catch (e) { + break; + } catch (e) /* istanbul ignore next */ { Logger.error(`token creation: ${e}`); return null; } - - break; } case TokenType.local: { const licenseKey = generateToken(); @@ -74,7 +73,6 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis return licenseKey; } } - return null; } @@ -102,15 +100,12 @@ export async function refreshToken(type: TokenType, licenseKey: string, refreshT return true; } - } catch (e) { + } catch (e) /* istanbul ignore next */ { Logger.error(`token refresh: ${e}`); return false; } - - break; } } - return false; } @@ -136,9 +131,8 @@ export async function getPatreonIdentity(accessToken: string): Promise Date: Thu, 27 Oct 2022 01:47:12 -0400 Subject: [PATCH 26/26] test against new chapters access --- test/cases/getUserInfoFree.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/cases/getUserInfoFree.ts b/test/cases/getUserInfoFree.ts index fd32d6a3..847a59a0 100644 --- a/test/cases/getUserInfoFree.ts +++ b/test/cases/getUserInfoFree.ts @@ -6,25 +6,26 @@ import { client } from "../utils/httpClient"; describe("getUserInfo Free Chapters", () => { const endpoint = "/api/userInfo"; - const noQualifyUserID = "getUserInfo-Free-noQualify"; + const newQualifyUserID = "getUserInfo-Free-newQualify"; const vipQualifyUserID = "getUserInfo-Free-VIP"; const repQualifyUserID = "getUserInfo-Free-RepQualify"; const oldQualifyUserID = "getUserInfo-Free-OldQualify"; + const newNoQualityUserID = "getUserInfo-Free-newNoQualify"; const postOldQualify = 1600000000000; before(async () => { const sponsorTimesQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "reputation", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-0", getHash(repQualifyUserID), postOldQualify, 0, "sponsor", "skip", 20, 0]); await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-1", getHash(oldQualifyUserID), 0, 0, "sponsor", "skip", 0, 0]); // submit at epoch - await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-2", getHash(noQualifyUserID), postOldQualify, 0, "sponsor", "skip", 0, 0]); + await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-2", getHash(newQualifyUserID), postOldQualify, 0, "sponsor", "skip", 0, 0]); await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(vipQualifyUserID)]); }); const getUserInfo = (userID: string) => client.get(endpoint, { params: { userID, value: "freeChaptersAccess" } }); - it("Should not get free access (noQuality)", (done) => { - getUserInfo(noQualifyUserID) + it("Should not get free access under new rule (newNoQualify)", (done) => { + getUserInfo(newNoQualityUserID) .then(res => { assert.strictEqual(res.status, 200); assert.strictEqual(res.data.freeChaptersAccess, false); @@ -33,6 +34,16 @@ describe("getUserInfo Free Chapters", () => { .catch(err => done(err)); }); + it("Should get free access under new rule (newQualify)", (done) => { + getUserInfo(newQualifyUserID) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.freeChaptersAccess, true); + done(); + }) + .catch(err => done(err)); + }); + it("Should get free access (VIP)", (done) => { getUserInfo(vipQualifyUserID) .then(res => {