From e6bf20937d82b9d16daee888c34114be81cd3c8f Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 31 Mar 2022 03:47:06 -0400 Subject: [PATCH 01/35] endpoint + tests for getVideoLabels --- src/app.ts | 6 + src/routes/getVideoLabel.ts | 176 ++++++++++++++++++++++++++++++ src/routes/getVideoLabelByHash.ts | 27 +++++ src/utils/redisKeys.ts | 12 +- test/cases/getVideoLabels.ts | 170 +++++++++++++++++++++++++++++ 5 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 src/routes/getVideoLabel.ts create mode 100644 src/routes/getVideoLabelByHash.ts create mode 100644 test/cases/getVideoLabels.ts diff --git a/src/app.ts b/src/app.ts index 267e44d..c922cb4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -48,6 +48,8 @@ import { getRating } from "./routes/ratings/getRating"; import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache"; import { getTopCategoryUsers } from "./routes/getTopCategoryUsers"; import { addUserAsTempVIP } from "./routes/addUserAsTempVIP"; +import { endpoint as getVideoLabels } from "./routes/getVideoLabel"; +import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash"; export function createServer(callback: () => void): Server { // Create a service (the app object is just a callback). @@ -202,6 +204,10 @@ function setupRoutes(router: Router) { router.post("/api/ratings/rate", postRateEndpoints); router.post("/api/ratings/clearCache", ratingPostClearCache); + // labels + router.get("/api/videoLabels", getVideoLabels); + router.get("/api/videoLabels/:prefix", getVideoLabelsByHash); + if (config.postgres) { router.get("/database", (req, res) => dumpDatabase(req, res, true)); router.get("/database.json", (req, res) => dumpDatabase(req, res, false)); diff --git a/src/routes/getVideoLabel.ts b/src/routes/getVideoLabel.ts new file mode 100644 index 0000000..fc98c5b --- /dev/null +++ b/src/routes/getVideoLabel.ts @@ -0,0 +1,176 @@ +import { Request, Response } from "express"; +import { db } from "../databases/databases"; +import { videoLabelsHashKey, videoLabelsKey } from "../utils/redisKeys"; +import { SBRecord } from "../types/lib.model"; +import { DBSegment, Segment, Service, VideoData, VideoID, VideoIDHash } from "../types/segments.model"; +import { Logger } from "../utils/logger"; +import { QueryCacher } from "../utils/queryCacher"; +import { getService } from "../utils/getService"; + +function transformDBSegments(segments: DBSegment[]): Segment[] { + return segments.map((chosenSegment) => ({ + category: chosenSegment.category, + actionType: chosenSegment.actionType, + segment: [chosenSegment.startTime, chosenSegment.endTime], + UUID: chosenSegment.UUID, + locked: chosenSegment.locked, + votes: chosenSegment.votes, + videoDuration: chosenSegment.videoDuration, + userID: chosenSegment.userID, + description: chosenSegment.description + })); +} + +async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise { + try { + const segments: DBSegment[] = await getSegmentsFromDBByVideoID(videoID, service); + console.log("dbSegments", segments); + return chooseSegment(segments); + } catch (err) { + if (err) { + Logger.error(err as string); + return null; + } + } +} + +async function getLabelsbyHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise> { + const segments: SBRecord = {}; + + try { + type SegmentWithHashPerVideoID = SBRecord; + + const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service)) + .reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => { + acc[segment.videoID] = acc[segment.videoID] || { + hash: segment.hashedVideoID, + segments: [] + }; + + acc[segment.videoID].segments ??= []; + acc[segment.videoID].segments.push(segment); + + return acc; + }, {}); + + for (const [videoID, videoData] of Object.entries(segmentPerVideoID)) { + const data: VideoData = { + hash: videoData.hash, + segments: chooseSegment(videoData.segments), + }; + + if (data.segments.length > 0) { + segments[videoID] = data; + } + } + + return segments; + } catch (err) { + Logger.error(err as string); + return null; + } +} + +async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise { + const fetchFromDB = () => db + .prepare( + "all", + `SELECT "startTime", "endTime", "videoID", "votes", "locked", "UUID", "userID", "category", "actionType", "hashedVideoID", "description" FROM "sponsorTimes" + WHERE "hashedVideoID" LIKE ? AND "service" = ? AND actionType = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`, + [`${hashedVideoIDPrefix}%`, service] + ) as Promise; + + if (hashedVideoIDPrefix.length === 4) { + return await QueryCacher.get(fetchFromDB, videoLabelsHashKey(hashedVideoIDPrefix, service)); + } + + return await fetchFromDB(); +} + +async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise { + const fetchFromDB = () => db + .prepare( + "all", + `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "description" FROM "sponsorTimes" + WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 AND "shadowHidden" = 0`, + [videoID, service] + ) as Promise; + + return await QueryCacher.get(fetchFromDB, videoLabelsKey(videoID, service)); +} + +function chooseSegment(choices: T[]): Segment[] { + // filter out -2 segments + choices = choices.filter((segment) => segment.votes > -2); + const results = []; + // trivial decisions + if (choices.length === 0) { + return []; + } else if (choices.length === 1) { + return transformDBSegments(choices); + } + // if locked, only choose from locked + const locked = choices.filter((segment) => segment.locked); + if (locked.length > 0) { + choices = locked; + } + //no need to filter, just one label + if (choices.length === 1) { + return transformDBSegments(choices); + } + // sponsor > exclusive > selfpromo + const sponsorResult = choices.find((segment) => segment.category === "sponsor"); + const eaResult = choices.find((segment) => segment.category === "exclusive_access"); + const selfpromoResult = choices.find((segment) => segment.category === "selfpromo"); + if (sponsorResult) { + results.push(sponsorResult); + } else if (eaResult) { + results.push(eaResult); + } else if (selfpromoResult) { + results.push(selfpromoResult); + } + console.log("chosenresults", results); + return transformDBSegments(results); +} + +async function handleGetLabel(req: Request, res: Response): Promise { + const videoID = req.query.videoID as VideoID; + if (!videoID) { + res.status(400).send("videoID not specified"); + return false; + } + + const service = getService(req.query.service, req.body.service); + const segments = await getLabelsByVideoID(videoID, service); + + console.log("segments", segments); + + if (!segments || segments.length === 0) { + res.sendStatus(404); + return false; + } + + return segments; +} + +async function endpoint(req: Request, res: Response): Promise { + try { + const segments = await handleGetLabel(req, res); + + // If false, res.send has already been called + if (segments) { + //send result + return res.send(segments); + } + } catch (err) { + if (err instanceof SyntaxError) { + return res.status(400).send("Categories parameter does not match format requirements."); + } else return res.sendStatus(500); + } +} + +export { + getLabelsByVideoID, + getLabelsbyHash, + endpoint +}; diff --git a/src/routes/getVideoLabelByHash.ts b/src/routes/getVideoLabelByHash.ts new file mode 100644 index 0000000..7ffc0e3 --- /dev/null +++ b/src/routes/getVideoLabelByHash.ts @@ -0,0 +1,27 @@ +import { hashPrefixTester } from "../utils/hashPrefixTester"; +import { getLabelsbyHash } from "./getVideoLabel"; +import { Request, Response } from "express"; +import { VideoIDHash, Service } from "../types/segments.model"; +import { getService } from "../utils/getService"; + +export async function getVideoLabelsByHash(req: Request, res: Response): Promise { + let hashPrefix = req.params.prefix as VideoIDHash; + if (!req.params.prefix || !hashPrefixTester(req.params.prefix)) { + return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix + } + hashPrefix = hashPrefix.toLowerCase() as VideoIDHash; + + const service: Service = getService(req.query.service, req.body.service); + + // Get all video id's that match hash prefix + const segments = await getLabelsbyHash(hashPrefix, service); + + if (!segments) return res.status(404).json([]); + + const output = Object.entries(segments).map(([videoID, data]) => ({ + videoID, + hash: data.hash, + segments: data.segments, + })); + return res.status(output.length === 0 ? 404 : 200).json(output); +} diff --git a/src/utils/redisKeys.ts b/src/utils/redisKeys.ts index 2da45a8..90f7343 100644 --- a/src/utils/redisKeys.ts +++ b/src/utils/redisKeys.ts @@ -36,4 +36,14 @@ export function shaHashKey(singleIter: HashedValue): string { } export const tempVIPKey = (userID: HashedUserID): string => - `vip.temp.${userID}`; \ No newline at end of file + `vip.temp.${userID}`; + +export const videoLabelsKey = (videoID: VideoID, service: Service): string => + `labels.v1.${service}.videoID.${videoID}`; + +export function videoLabelsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string { + hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash; + if (hashedVideoIDPrefix.length !== 4) Logger.warn(`Redis skip segment hash-prefix key is not length 4! ${hashedVideoIDPrefix}`); + + return `labels.v1.${service}.${hashedVideoIDPrefix}`; +} \ No newline at end of file diff --git a/test/cases/getVideoLabels.ts b/test/cases/getVideoLabels.ts new file mode 100644 index 0000000..7202d2c --- /dev/null +++ b/test/cases/getVideoLabels.ts @@ -0,0 +1,170 @@ +import { db } from "../../src/databases/databases"; +import assert from "assert"; +import { client } from "../utils/httpClient"; + +describe("getVideoLabels", () => { + const endpoint = "/api/videoLabels"; + before(async () => { + const query = 'INSERT INTO "sponsorTimes" ("videoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)'; + await db.prepare("run", query, ["getLabelSponsor" , 2, 0, "label01", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelEA" , 2, 0, "label02", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelSelfpromo" , 2, 0, "label03", "labeluser", 0, "selfpromo", "full", 0, 0]); + // priority override + await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label04", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label05", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label06", "labeluser", 0, "selfpromo", "full", 0, 0]); + // locked only + await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label07", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label08", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelLocked" , 2, 1, "label09", "labeluser", 0, "selfpromo", "full", 0, 0]); + // hidden segments + await db.prepare("run", query, ["getLabelDownvote" ,-2, 0, "label10", "labeluser", 0, "selfpromo", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHidden" ,2, 0, "label11", "labeluser", 0, "selfpromo", "full", 1, 0]); + await db.prepare("run", query, ["getLabelShadowHidden",2, 0, "label12", "labeluser", 0, "selfpromo", "full", 0, 1]); + // priority override2 + await db.prepare("run", query, ["getLabelPriority2" , -2, 0, "label13", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label14", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label15", "labeluser", 0, "selfpromo", "full", 0, 0]); + + return; + }); + + function validateLabel(result: any) { + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].segment[0], 0); + assert.strictEqual(result[0].segment[1], 0); + assert.strictEqual(result[0].actionType, "full"); + assert.strictEqual(result[0].userID, "labeluser"); + } + + const get = (videoID: string) => client.get(endpoint, { params: { videoID } }); + + it("Should be able to get sponsor only label", (done) => { + get("getLabelSponsor") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "sponsor"); + assert.strictEqual(data[0].UUID, "label01"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get exclusive access only label", (done) => { + get("getLabelEA") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "exclusive_access"); + assert.strictEqual(data[0].UUID, "label02"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get selfpromo only label", (done) => { + get("getLabelSelfpromo") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "selfpromo"); + assert.strictEqual(data[0].UUID, "label03"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get only sponsor if multiple segments exist", (done) => { + get("getLabelPriority") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "sponsor"); + assert.strictEqual(data[0].UUID, "label04"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should override priority if locked", (done) => { + get("getLabelLocked") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "selfpromo"); + assert.strictEqual(data[0].UUID, "label09"); + assert.strictEqual(data[0].locked, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get highest priority category", (done) => { + get("getLabelPriority2") + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data); + assert.strictEqual(data[0].category, "exclusive_access"); + assert.strictEqual(data[0].UUID, "label14"); + assert.strictEqual(data[0].locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are downvoted", (done) => { + get("getLabelDownvote") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are hidden", (done) => { + get("getLabelHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are shadowhidden", (done) => { + get("getLabelShadowHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if no segment found", (done) => { + get("notarealvideo") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get 400 if no videoID passed in", (done) => { + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); From 267320bc563c04395c7e0c57a89db901f5ea8bcc Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 31 Mar 2022 03:47:25 -0400 Subject: [PATCH 02/35] add filler and exclusive_access to category enum --- src/types/segments.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/segments.model.ts b/src/types/segments.model.ts index 5edb3e0..c4aa23e 100644 --- a/src/types/segments.model.ts +++ b/src/types/segments.model.ts @@ -5,7 +5,7 @@ import { HashedUserID, UserID } from "./user.model"; export type SegmentUUID = string & { __segmentUUIDBrand: unknown }; export type VideoID = string & { __videoIDBrand: unknown }; export type VideoDuration = number & { __videoDurationBrand: unknown }; -export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight" | "chapter") & { __categoryBrand: unknown }; +export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight" | "chapter" | "filler" | "exclusive_access") & { __categoryBrand: unknown }; export type VideoIDHash = VideoID & HashedValue; export type IPAddress = string & { __ipAddressBrand: unknown }; export type HashedIP = IPAddress & HashedValue; From ed560425a08ab80350e3096ce776409cb94965bf Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 31 Mar 2022 14:05:07 -0400 Subject: [PATCH 03/35] removed logging, fixed labelByHash test cases --- src/routes/getVideoLabel.ts | 8 +- test/cases/getVideoLabelByHash.ts | 184 ++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 test/cases/getVideoLabelByHash.ts diff --git a/src/routes/getVideoLabel.ts b/src/routes/getVideoLabel.ts index fc98c5b..48a2504 100644 --- a/src/routes/getVideoLabel.ts +++ b/src/routes/getVideoLabel.ts @@ -24,7 +24,6 @@ function transformDBSegments(segments: DBSegment[]): Segment[] { async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise { try { const segments: DBSegment[] = await getSegmentsFromDBByVideoID(videoID, service); - console.log("dbSegments", segments); return chooseSegment(segments); } catch (err) { if (err) { @@ -76,7 +75,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service .prepare( "all", `SELECT "startTime", "endTime", "videoID", "votes", "locked", "UUID", "userID", "category", "actionType", "hashedVideoID", "description" FROM "sponsorTimes" - WHERE "hashedVideoID" LIKE ? AND "service" = ? AND actionType = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`, + WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`, [`${hashedVideoIDPrefix}%`, service] ) as Promise; @@ -92,7 +91,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P .prepare( "all", `SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "description" FROM "sponsorTimes" - WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 AND "shadowHidden" = 0`, + WHERE "videoID" = ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`, [videoID, service] ) as Promise; @@ -129,7 +128,6 @@ function chooseSegment(choices: T[]): Segment[] { } else if (selfpromoResult) { results.push(selfpromoResult); } - console.log("chosenresults", results); return transformDBSegments(results); } @@ -143,8 +141,6 @@ async function handleGetLabel(req: Request, res: Response): Promise { + const endpoint = "/api/videoLabels"; + before(async () => { + const query = 'INSERT INTO "sponsorTimes" ("videoID", "hashedVideoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)'; + await db.prepare("run", query, ["getLabelHashSponsor" , getHash("getLabelHashSponsor", 1) , 2, 0, "labelhash01", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashEA" , getHash("getLabelHashEA", 1) , 2, 0, "labelhash02", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashSelfpromo" , getHash("getLabelHashSelfpromo", 1) , 2, 0, "labelhash03", "labeluser", 0, "selfpromo", "full", 0, 0]); + // priority override + await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash04", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash05", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash06", "labeluser", 0, "selfpromo", "full", 0, 0]); + // locked only + await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 0, "labelhash07", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 0, "labelhash08", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 1, "labelhash09", "labeluser", 0, "selfpromo", "full", 0, 0]); + // hidden segments + await db.prepare("run", query, ["getLabelHashDownvote" , getHash("getLabelHashDownvote", 1) , -2, 0, "labelhash10", "labeluser", 0, "selfpromo", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashHidden" , getHash("getLabelHashHidden", 1) , 2, 0, "labelhash11", "labeluser", 0, "selfpromo", "full", 1, 0]); + await db.prepare("run", query, ["getLabelHashShHidden" , getHash("getLabelHashShHidden", 1) , 2, 0, "labelhash12", "labeluser", 0, "selfpromo", "full", 0, 1]); + // priority override2 + await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , -2, 0, "labelhash13", "labeluser", 0, "sponsor", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , 2, 0, "labelhash14", "labeluser", 0, "exclusive_access", "full", 0, 0]); + await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , 2, 0, "labelhash15", "labeluser", 0, "selfpromo", "full", 0, 0]); + + return; + }); + + function validateLabel(data: any, videoID: string) { + assert.strictEqual(data[0].videoID, videoID); + assert.strictEqual(data[0].segments.length, 1); + assert.strictEqual(data[0].segments[0].segment[0], 0); + assert.strictEqual(data[0].segments[0].segment[1], 0); + assert.strictEqual(data[0].segments[0].actionType, "full"); + assert.strictEqual(data[0].segments[0].userID, "labeluser"); + } + + const get = (videoID: string) => client.get(`${endpoint}/${getHash(videoID, 1).substring(0, 4)}`); + + it("Should be able to get sponsor only label", (done) => { + const videoID = "getLabelHashSponsor"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "sponsor"); + assert.strictEqual(result.UUID, "labelhash01"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get exclusive access only label", (done) => { + const videoID = "getLabelHashEA"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "exclusive_access"); + assert.strictEqual(result.UUID, "labelhash02"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get selfpromo only label", (done) => { + const videoID = "getLabelHashSelfpromo"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "selfpromo"); + assert.strictEqual(result.UUID, "labelhash03"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get only sponsor if multiple segments exist", (done) => { + const videoID = "getLabelHashPriority"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "sponsor"); + assert.strictEqual(result.UUID, "labelhash04"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should override priority if locked", (done) => { + const videoID = "getLabelHashLocked"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "selfpromo"); + assert.strictEqual(result.UUID, "labelhash09"); + assert.strictEqual(result.locked, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get highest priority category", (done) => { + const videoID = "getLabelHashPriority2"; + get(videoID) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + validateLabel(data, videoID); + const result = data[0].segments[0]; + assert.strictEqual(result.category, "exclusive_access"); + assert.strictEqual(result.UUID, "labelhash14"); + assert.strictEqual(result.locked, 0); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are downvoted", (done) => { + get("getLabelHashDownvote") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are hidden", (done) => { + get("getLabelHashHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if all submissions are shadowhidden", (done) => { + get("getLabelHashShHidden") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if no segment found", (done) => { + get("notarealvideo") + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); + + it("Should get 400 if no videoID passed in", (done) => { + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); From 2fb7f4faa66d2ec24ac79a56b06add3bfbb92bef Mon Sep 17 00:00:00 2001 From: "Michael M. Chang" Date: Thu, 31 Mar 2022 14:05:52 -0400 Subject: [PATCH 04/35] consistent function naming Co-authored-by: Ajay Ramachandran --- src/routes/getVideoLabel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/getVideoLabel.ts b/src/routes/getVideoLabel.ts index 48a2504..1ddc98b 100644 --- a/src/routes/getVideoLabel.ts +++ b/src/routes/getVideoLabel.ts @@ -33,7 +33,7 @@ async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise> { +async function getLabelsByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise> { const segments: SBRecord = {}; try { From 65a661ca4d6b659c7c6b9dea8e7db0797670bee4 Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 31 Mar 2022 14:19:06 -0400 Subject: [PATCH 05/35] fix naming regression, simplify findCategory --- src/routes/getVideoLabel.ts | 17 ++++++----------- src/routes/getVideoLabelByHash.ts | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/routes/getVideoLabel.ts b/src/routes/getVideoLabel.ts index 1ddc98b..02ba7c5 100644 --- a/src/routes/getVideoLabel.ts +++ b/src/routes/getVideoLabel.ts @@ -118,16 +118,11 @@ function chooseSegment(choices: T[]): Segment[] { return transformDBSegments(choices); } // sponsor > exclusive > selfpromo - const sponsorResult = choices.find((segment) => segment.category === "sponsor"); - const eaResult = choices.find((segment) => segment.category === "exclusive_access"); - const selfpromoResult = choices.find((segment) => segment.category === "selfpromo"); - if (sponsorResult) { - results.push(sponsorResult); - } else if (eaResult) { - results.push(eaResult); - } else if (selfpromoResult) { - results.push(selfpromoResult); - } + const findCategory = (category: string) => choices.find((segment) => segment.category === category); + + const categoryResult = findCategory("sponsor") ?? findCategory("exclusive_access") ?? findCategory("selfpromo"); + if (categoryResult) results.push(categoryResult); + return transformDBSegments(results); } @@ -167,6 +162,6 @@ async function endpoint(req: Request, res: Response): Promise { export { getLabelsByVideoID, - getLabelsbyHash, + getLabelsByHash, endpoint }; diff --git a/src/routes/getVideoLabelByHash.ts b/src/routes/getVideoLabelByHash.ts index 7ffc0e3..e3a3615 100644 --- a/src/routes/getVideoLabelByHash.ts +++ b/src/routes/getVideoLabelByHash.ts @@ -1,5 +1,5 @@ import { hashPrefixTester } from "../utils/hashPrefixTester"; -import { getLabelsbyHash } from "./getVideoLabel"; +import { getLabelsByHash } from "./getVideoLabel"; import { Request, Response } from "express"; import { VideoIDHash, Service } from "../types/segments.model"; import { getService } from "../utils/getService"; @@ -14,7 +14,7 @@ export async function getVideoLabelsByHash(req: Request, res: Response): Promise const service: Service = getService(req.query.service, req.body.service); // Get all video id's that match hash prefix - const segments = await getLabelsbyHash(hashPrefix, service); + const segments = await getLabelsByHash(hashPrefix, service); if (!segments) return res.status(404).json([]); From b652102c42370402c4a30413f26c80cd0c3fa416 Mon Sep 17 00:00:00 2001 From: Michael C Date: Tue, 20 Sep 2022 23:18:35 -0400 Subject: [PATCH 06/35] 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 098f9c0..0923fdb 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 1369282..c0bb36c 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 11126b3..755735f 100644 --- a/test/cases/voteOnSponsorTime.ts +++ b/test/cases/voteOnSponsorTime.ts @@ -3,7 +3,7 @@ import { db, privateDB } from "../../src/databases/databases"; import { getHash } from "../../src/utils/getHash"; import { ImportMock } from "ts-mock-imports"; import * as YouTubeAPIModule from "../../src/utils/youtubeApi"; -import { YouTubeApiMock } from "../youtubeMock"; +import { YouTubeApiMock } from "../mocks/youtubeMock"; import assert from "assert"; import { client } from "../utils/httpClient"; import { arrayDeepEquals } from "../utils/partialDeepEquals"; diff --git a/test/youtubeMock.ts b/test/mocks/youtubeMock.ts similarity index 97% rename from test/youtubeMock.ts rename to test/mocks/youtubeMock.ts index f0b89d1..0ed18b7 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 07/35] add nyc output to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 25260b5..79d8a36 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ working /dist/ # nyc coverage output -.nyc_output/ \ No newline at end of file +.nyc_output/ +coverage/ \ No newline at end of file From 3f470a72f51572c5ee6491e3d1eb489879287063 Mon Sep 17 00:00:00 2001 From: Michael C Date: Tue, 20 Sep 2022 23:22:21 -0400 Subject: [PATCH 08/35] 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 61046e3..bd906a6 100644 --- a/src/routes/getDaysSavedFormatted.ts +++ b/src/routes/getDaysSavedFormatted.ts @@ -7,7 +7,11 @@ export async function getDaysSavedFormatted(req: Request, res: Response): Promis if (row !== undefined) { //send this result return res.send({ - daysSaved: row.daysSaved.toFixed(2), + daysSaved: row.daysSaved?.toFixed(2) ?? "0", + }); + } else { + return res.send({ + daysSaved: 0 }); } } diff --git a/test/cases/addUserAsVIP.ts b/test/cases/addUserAsVIP.ts new file mode 100644 index 0000000..49aba60 --- /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 0000000..8414b73 --- /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 7ed809f..b7e538a 100644 --- a/test/cases/getSavedTimeForUser.ts +++ b/test/cases/getSavedTimeForUser.ts @@ -2,22 +2,31 @@ import { db } from "../../src/databases/databases"; import { getHash } from "../../src/utils/getHash"; import { deepStrictEqual } from "assert"; import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers const endpoint = "/api/getSavedTimeForUser"; +const getSavedTimeForUser = (userID: string) => client({ + url: endpoint, + params: { userID } +}); describe("getSavedTimeForUser", () => { - const user1 = "getSavedTimeForUserUser"; + const user1 = "getSavedTimeForUser1"; + const user2 = "getSavedTimeforUser2"; + const [ start, end, views ] = [1, 11, 50]; + before(async () => { const startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", "views", "shadowHidden") VALUES'; await db.prepare("run", `${startOfQuery}(?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ["getSavedTimeForUser", 1, 11, 2, "gstfu0", getHash(user1), 0, 50, 0]); + ["getSavedTimeForUser", start, end, 2, "getSavedTimeUUID0", getHash(user1), 0, views, 0]); return; }); - - it("Should be able to get a 200", (done) => { - client.get(endpoint, { params: { userID: user1 } }) + it("Should be able to get a saved time", (done) => { + getSavedTimeForUser(user1) .then(res => { // (end-start)*minute * views - const savedMinutes = ((11-1)/60) * 50; + const savedMinutes = ((end-start)/60) * views; const expected = { timeSaved: savedMinutes }; @@ -26,4 +35,20 @@ describe("getSavedTimeForUser", () => { }) .catch((err) => done(err)); }); + it("Should return 404 if no submissions", (done) => { + getSavedTimeForUser(user2) + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch((err) => done(err)); + }); + it("Should return 400 if no userID", (done) => { + client({ url: endpoint }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch((err) => done(err)); + }); }); diff --git a/test/cases/getTotalStats.ts b/test/cases/getTotalStats.ts new file mode 100644 index 0000000..04eab62 --- /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 0000000..5394434 --- /dev/null +++ b/test/cases/getUsername.ts @@ -0,0 +1,53 @@ +import { getHash } from "../../src/utils/getHash"; +import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers +const getUsername = (userID: string) => client({ + url: "/api/getUsername", + params: { userID } +}); + +const postSetUserName = (userID: string, username: string) => client({ + method: "POST", + url: "/api/setUsername", + params: { + userID, + username, + } +}); + +const userOnePrivate = "getUsername_0"; +const userOnePublic = getHash(userOnePrivate); +const userOneUsername = "getUsername_username"; + +describe("getUsername test", function() { + it("Should get back publicUserID if not set", (done) => { + getUsername(userOnePrivate) + .then(result => { + assert.strictEqual(result.data.userName, userOnePublic); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to get username after setting", (done) => { + postSetUserName(userOnePrivate, userOneUsername) + .then(async () => { + const result = await getUsername(userOnePrivate); + const actual = result.data.userName; + assert.strictEqual(actual, userOneUsername); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 if no userID provided", (done) => { + client({ + url: "/api/getUsername" + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/cases/getViewsForUser.ts b/test/cases/getViewsForUser.ts new file mode 100644 index 0000000..9471e18 --- /dev/null +++ b/test/cases/getViewsForUser.ts @@ -0,0 +1,62 @@ +import { getHash } from "../../src/utils/getHash"; +import { db } from "../../src/databases/databases"; +import { client } from "../utils/httpClient"; +import assert from "assert"; + +// helpers +const endpoint = "/api/getViewsForUser"; +const getViewsForUser = (userID: string) => client({ + url: endpoint, + params: { userID } +}); + +const getViewUserOne = "getViewUser1"; +const userOneViewsFirst = 30; +const userOneViewsSecond = 0; + +const getViewUserTwo = "getViewUser2"; +const userTwoViews = 0; + +const getViewUserThree = "getViewUser3"; + + +describe("getViewsForUser", function() { + before(() => { + const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo0", getHash(getViewUserOne), 0, userOneViewsFirst, "sponsor", "skip", 0, 0, "getViewUserVideo"]); + db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo1", getHash(getViewUserOne), 0, userOneViewsSecond, "sponsor", "skip", 0, 0, "getViewUserVideo"]); + db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo2", getHash(getViewUserTwo), 0, userTwoViews, "sponsor", "skip", 0, 0, "getViewUserVideo"]); + }); + it("Should get back views for user one", (done) => { + getViewsForUser(getViewUserOne) + .then(result => { + assert.strictEqual(result.data.viewCount, userOneViewsFirst + userOneViewsSecond); + done(); + }) + .catch(err => done(err)); + }); + it("Should get back views for user two", (done) => { + getViewsForUser(getViewUserTwo) + .then(result => { + assert.strictEqual(result.data.viewCount, userTwoViews); + done(); + }) + .catch(err => done(err)); + }); + it("Should get 404 if no submissions", (done) => { + getViewsForUser(getViewUserThree) + .then(result => { + assert.strictEqual(result.status, 404); + done(); + }) + .catch(err => done(err)); + }); + it("Should return 400 if no userID provided", (done) => { + client({ url: endpoint }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file From f683ed4f2941cf8119131299052a719e0bfdbe1f Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 01:14:22 -0400 Subject: [PATCH 09/35] 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 30e2f41..4c9267b 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 0d2dd90..85a7192 100644 --- a/src/utils/getIP.ts +++ b/src/utils/getIP.ts @@ -3,6 +3,9 @@ import { Request } from "express"; import { IPAddress } from "../types/segments.model"; export function getIP(req: Request): IPAddress { + // if in testing mode, return immediately + if (config.mode === "test") return "127.0.0.1" as IPAddress; + if (config.behindProxy === true || config.behindProxy === "true") { config.behindProxy = "X-Forwarded-For"; } @@ -15,6 +18,6 @@ export function getIP(req: Request): IPAddress { case "X-Real-IP": return req.headers["x-real-ip"] as IPAddress; default: - return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress; + return req.socket?.remoteAddress as IPAddress; } } \ No newline at end of file diff --git a/test.json b/test.json index 593977b..5692e76 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 26a1adb..120e0e8 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 a5c578e..d6673bf 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -1,23 +1,24 @@ import express from "express"; import { config } from "../src/config"; import { Server } from "http"; +import { UserCounter } from "./mocks/UserCounter"; const app = express(); -app.post("/ReportChannelWebhook", (req, res) => { +app.post("/webhook/ReportChannel", (req, res) => { res.sendStatus(200); }); -app.post("/FirstTimeSubmissionsWebhook", (req, res) => { +app.post("/webhook/FirstTimeSubmissions", (req, res) => { res.sendStatus(200); }); -app.post("/CompletelyIncorrectReportWebhook", (req, res) => { +app.post("/webhook/CompletelyIncorrectReport", (req, res) => { res.sendStatus(200); }); // Testing NeuralBlock -app.post("/NeuralBlockRejectWebhook", (req, res) => { +app.post("/webhook/NeuralBlockReject", (req, res) => { res.sendStatus(200); }); @@ -47,6 +48,9 @@ app.post("/CustomWebhook", (req, res) => { res.sendStatus(200); }); +// mocks +app.use("/UserCounter", UserCounter); + export function createMockServer(callback: () => void): Server { return app.listen(config.mockPort, callback); } diff --git a/test/mocks/UserCounter.ts b/test/mocks/UserCounter.ts new file mode 100644 index 0000000..d4ba32a --- /dev/null +++ b/test/mocks/UserCounter.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +export const UserCounter = Router(); + +UserCounter.post("/api/v1/addIP", (req, res) => { + res.sendStatus(200); +}); +UserCounter.get("/api/v1/userCount", (req, res) => { + res.send({ + userCount: 100 + }); +}); \ No newline at end of file From dd7656d14341b6cfebd6aedf85f95d8386b66059 Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 02:27:48 -0400 Subject: [PATCH 10/35] 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 4f8d3ae..acc6ed1 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 7d6bd4a..49e06af 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 11/35] 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 4c9267b..5a3ee2f 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 00e507e..1656aa3 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 511aa19..0f505b0 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 59910ef..2b18b5a 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 5692e76..16ace5c 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 0000000..bd0b90a --- /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 12/35] 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 10a3ec9..3340f10 100644 --- a/src/routes/deleteLockCategories.ts +++ b/src/routes/deleteLockCategories.ts @@ -35,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ || !categories || !Array.isArray(categories) || categories.length === 0 + || actionTypes && !Array.isArray(actionTypes) || actionTypes.length === 0 ) { return res.status(400).json({ @@ -48,7 +49,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ if (!userIsVIP) { return res.status(403).json({ - message: "Must be a VIP to mark videos.", + message: "Must be a VIP to lock videos.", }); } diff --git a/src/routes/postLockCategories.ts b/src/routes/postLockCategories.ts index 13f7f04..88e9cf0 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 49e06af..ebfc421 100644 --- a/test/cases/lockCategoriesRecords.ts +++ b/test/cases/lockCategoriesRecords.ts @@ -266,106 +266,6 @@ describe("lockCategoriesRecords", () => { .catch(err => done(err)); }); - it("Should return 400 for missing params", (done) => { - client.post(endpoint, {}) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for no categories", (done) => { - const json: any = { - videoID: "test", - userID: "test", - categories: [], - }; - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for no userID", (done) => { - const json: any = { - videoID: "test", - userID: null, - categories: ["sponsor"], - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 for no videoID", (done) => { - const json: any = { - videoID: null, - userID: "test", - categories: ["sponsor"], - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 object categories", (done) => { - const json = { - videoID: "test", - userID: "test", - categories: {}, - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 400 bad format categories", (done) => { - const json = { - videoID: "test", - userID: "test", - categories: "sponsor", - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 400); - done(); - }) - .catch(err => done(err)); - }); - - it("Should return 403 if user is not VIP", (done) => { - const json = { - videoID: "test", - userID: "test", - categories: [ - "sponsor", - ], - }; - - client.post(endpoint, json) - .then(res => { - assert.strictEqual(res.status, 403); - done(); - }) - .catch(err => done(err)); - }); - it("Should be able to delete a lockCategories record", (done) => { const videoID = "delete-record"; const json = { @@ -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 13/35] 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 44500f8..6bf05b0 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 76027a5..af6eab0 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 1656aa3..a03b338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "sync-mysql": "^3.0.1" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/better-sqlite3": "^7.5.0", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", @@ -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 0f505b0..1c8077c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "npm run tsc && ts-node test/test.ts", - "test:coverage": "nyc npm run test", + "cover": "nyc npm test", + "cover:report": "nyc report", "dev": "nodemon", "dev:bash": "nodemon -x 'npm test ; npm start'", "postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:alpine", @@ -32,6 +33,7 @@ "sync-mysql": "^3.0.1" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/better-sqlite3": "^7.5.0", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", From a00048aaac8d11b3afac368ede45b59f47067aaa Mon Sep 17 00:00:00 2001 From: Michael C Date: Wed, 21 Sep 2022 19:39:03 -0400 Subject: [PATCH 14/35] 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 af6eab0..ae148eb 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 f68507e..e7ba2c1 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 49aba60..ebbcd6f 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 0000000..f761e01 --- /dev/null +++ b/test/cases/getIP.ts @@ -0,0 +1,109 @@ +import sinon from "sinon"; +import { config } from "../../src/config"; +import assert from "assert"; +const mode = "production"; +let stub: sinon.SinonStub; +let stub2: sinon.SinonStub; +import { createRequest } from "../mocks/mockExpressRequest"; +import { getIP } from "../../src/utils/getIP"; + +const v4RequestOptions = { + headers: { + "x-forwarded-for": "127.0.1.1", + "cf-connecting-ip": "127.0.1.2", + "x-real-ip": "127.0.1.3", + }, + ip: "127.0.1.5", + socket: { + remoteAddress: "127.0.1.4" + } +}; +const v6RequestOptions = { + headers: { + "x-forwarded-for": "[100::1]", + "cf-connecting-ip": "[100::2]", + "x-real-ip": "[100::3]", + }, + ip: "[100::5]", + socket: { + remoteAddress: "[100::4]" + } +}; +const v4MockRequest = createRequest(v4RequestOptions); +const v6MockRequest = createRequest(v6RequestOptions); + +const expectedIP4 = { + "X-Forwarded-For": "127.0.1.1", + "Cloudflare": "127.0.1.2", + "X-Real-IP": "127.0.1.3", + "default": "127.0.1.4", +}; + +const expectedIP6 = { + "X-Forwarded-For": "[100::1]", + "Cloudflare": "[100::2]", + "X-Real-IP": "[100::3]", + "default": "[100::4]", +}; + +describe("getIP stubs", () => { + before(() => stub = sinon.stub(config, "mode").value(mode)); + after(() => stub.restore()); + + it("Should return production mode if stub worked", (done) => { + assert.strictEqual(config.mode, mode); + done(); + }); +}); + +describe("getIP array tests", () => { + beforeEach(() => stub = sinon.stub(config, "mode").value(mode)); + afterEach(() => { + stub.restore(); + stub2.restore(); + }); + + for (const [key, value] of Object.entries(expectedIP4)) { + it(`Should return correct IPv4 from ${key}`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value(key); + const ip = getIP(v4MockRequest); + assert.strictEqual(config.behindProxy, key); + assert.strictEqual(ip, value); + done(); + }); + } + + for (const [key, value] of Object.entries(expectedIP6)) { + it(`Should return correct IPv6 from ${key}`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value(key); + const ip = getIP(v6MockRequest); + assert.strictEqual(config.behindProxy, key); + assert.strictEqual(ip, value); + done(); + }); + } +}); + +describe("getIP true tests", () => { + before(() => stub = sinon.stub(config, "mode").value(mode)); + after(() => { + stub.restore(); + stub2.restore(); + }); + + it(`Should return correct IPv4 from with bool true`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value(true); + const ip = getIP(v4MockRequest); + assert.strictEqual(config.behindProxy, "X-Forwarded-For"); + assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]); + done(); + }); + + it(`Should return correct IPv4 from with string true`, (done) => { + stub2 = sinon.stub(config, "behindProxy").value("true"); + const ip = getIP(v4MockRequest); + assert.strictEqual(config.behindProxy, "X-Forwarded-For"); + assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]); + done(); + }); +}); \ No newline at end of file diff --git a/test/cases/testUtils.ts b/test/cases/testUtils.ts index 78818c8..29bfe60 100644 --- a/test/cases/testUtils.ts +++ b/test/cases/testUtils.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import { partialDeepEquals } from "../utils/partialDeepEquals"; +import { partialDeepEquals, mixedDeepEquals } from "../utils/partialDeepEquals"; describe("Test utils ", () => { it("objectContain", () => { @@ -135,4 +135,45 @@ describe("Test utils ", () => { } ), "Did not match partial child array"); }); + it("mixedDeepEquals exists", () => { + assert(!mixedDeepEquals({ + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + }, + ignore: true + }, { + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + }, + ignore: false + })); + }); + it("mixedDeepEquals noProperty", () => { + assert(!mixedDeepEquals({ + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + } + }, { + name: "lorem", + values: [{ + name: "ipsum", + }], + child: { + name: "dolor", + }, + ignore: false + })); + }); }); \ No newline at end of file diff --git a/test/cases/userCounter.ts b/test/cases/userCounter.ts index 120e0e8..8215ce1 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 0000000..7ad4ae0 --- /dev/null +++ b/test/mocks/mockExpressRequest.ts @@ -0,0 +1,33 @@ +const nullStub = (): any => null; + +export const createRequest = (options: any) => ({ + app: {}, + baseUrl: "", + body: {}, + cookies: {}, + fresh: true, + headers: {}, + hostname: "example.com", + ip: "", + ips: [], + method: "GET", + originalUrl: "/", + params: {}, + path: "/", + protocol: "https", + query: {}, + route: {}, + secure: true, + signedCookies: {}, + stale: false, + subdomains: [], + xhr: true, + accepts: nullStub(), + acceptsCharsets: nullStub(), + acceptsEncodings: nullStub(), + acceptsLanguages: nullStub(), + get: nullStub(), + is: nullStub(), + range: nullStub(), + ...options +}); From 7457b51aa49dc0826c2d63665d86bf49452bf81b Mon Sep 17 00:00:00 2001 From: Michael C Date: Sat, 24 Sep 2022 21:43:52 -0400 Subject: [PATCH 15/35] 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 663fd6d..454f232 100644 --- a/src/routes/getSkipSegmentsByHash.ts +++ b/src/routes/getSkipSegmentsByHash.ts @@ -67,8 +67,6 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis // Get all video id's that match hash prefix const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service); - if (!segments) return res.status(404).json([]); - const output = Object.entries(segments).map(([videoID, data]) => ({ videoID, hash: data.hash, diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index 0923fdb..3129f91 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 16/35] 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 bd0b90a..be360c5 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 0000000..3b5c9ae --- /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 17/35] 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 04eab62..f95c292 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 18/35] 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 4c0771e..8b06612 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 2b18b5a..dfbb016 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 0000000..fc11cc5 --- /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 be360c5..4b3890e 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 0000000..349e5c4 --- /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 3b5c9ae..aafe26e 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 19/35] 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 09f4347..c8d048d 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 20/35] 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 5b00aac..6a9ba24 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 349e5c4..00ae831 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 21/35] 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 e7ba2c1..caee4fa 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 c8d048d..a9a3f3e 100644 --- a/src/routes/getIsUserVIP.ts +++ b/src/routes/getIsUserVIP.ts @@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise /^([a-f0-9]{64}|[a-f0-9]{8} async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise { try { return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]); - } catch (err) { + } catch (err) /* istanbul ignore next */ { return null; } } @@ -62,7 +62,7 @@ async function endpoint(req: Request, res: Response): Promise { //send result return res.send(DBSegments); } - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { // catch JSON.parse error return res.status(400).send("UUIDs parameter does not match format requirements."); } else return res.sendStatus(500); diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 7da485e..6c93822 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -107,7 +107,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: } return processedSegments; - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err) { Logger.error(err as string); return null; @@ -169,7 +169,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, })); return segments; - } catch (err) { + } catch (err) /* istanbul ignore next */ { Logger.error(err as string); return null; } @@ -465,7 +465,7 @@ async function endpoint(req: Request, res: Response): Promise { //send result return res.send(segments); } - } catch (err) { + } catch (err) /* istanbul ignore next */ { if (err instanceof SyntaxError) { return res.status(400).send("Categories parameter does not match format requirements."); } else return res.sendStatus(500); diff --git a/src/routes/getStatus.ts b/src/routes/getStatus.ts index 7f764f5..1c56cee 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 c93fa62..5acf659 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 c0b1955..6492fdd 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 63185f4..ea239d1 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 28098fe..b3a2a7b 100644 --- a/src/routes/getUsername.ts +++ b/src/routes/getUsername.ts @@ -27,7 +27,7 @@ export async function getUsername(req: Request, res: Response): Promise Date: Sun, 25 Sep 2022 03:30:33 -0400 Subject: [PATCH 22/35] 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 227f68a..1aee441 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 ef4e5a3..59ab528 100644 --- a/src/routes/getLockReason.ts +++ b/src/routes/getLockReason.ts @@ -32,18 +32,24 @@ export async function getLockReason(req: Request, res: Response): Promise possibleCategories.includes(x)); - if (!videoID || !Array.isArray(actionTypes)) { - //invalid request - return res.sendStatus(400); - } - try { // Get existing lock categories markers const row = await db.prepare("all", 'SELECT "category", "reason", "actionType", "userID" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category, reason: string, actionType: ActionType, userID: string }[]; @@ -115,7 +116,7 @@ export async function getLockReason(req: Request, res: Response): Promise Date: Sun, 25 Sep 2022 03:31:25 -0400 Subject: [PATCH 23/35] 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 fc11cc5..b1f5630 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 8414b73..2b2ae23 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 9695731..f6b2757 100644 --- a/test/cases/getLockCategoriesByHash.ts +++ b/test/cases/getLockCategoriesByHash.ts @@ -166,17 +166,77 @@ describe("getLockCategoriesByHash", () => { .catch(err => done(err)); }); - it("Should be able to get by actionType", (done) => { - getLockCategories(fakeHash.substring(0,5), [ActionType.Full]) + it("should return 400 if invalid actionTypes", (done) => { + client.get(`${endpoint}/aaaa`, { params: { actionTypes: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("should return 400 if invalid actionTypes JSON", (done) => { + client.get(`${endpoint}/aaaa`, { params: { actionTypes: "{3}" } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get single lock", (done) => { + const videoID = "getLockHash2"; + const hash = getHash(videoID, 1); + getLockCategories(hash.substring(0,6)) .then(res => { assert.strictEqual(res.status, 200); const expected = [{ - videoID: "fakehash-2", - hash: fakeHash, + videoID, + hash, categories: [ - "sponsor" + "preview" ], - reason: "fake2-notshown" + reason: "2-reason" + }]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get by actionType not in array", (done) => { + const videoID = "getLockHash2"; + const hash = getHash(videoID, 1); + client.get(`${endpoint}/${hash.substring(0,6)}`, { params: { actionType: ActionType.Skip } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [{ + videoID, + hash, + categories: [ + "preview" + ], + reason: "2-reason" + }]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get by no actionType", (done) => { + const videoID = "getLockHash2"; + const hash = getHash(videoID, 1); + client.get(`${endpoint}/${hash.substring(0,6)}`) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [{ + videoID, + hash, + categories: [ + "preview" + ], + reason: "2-reason" }]; assert.deepStrictEqual(res.data, expected); done(); diff --git a/test/cases/getLockReason.ts b/test/cases/getLockReason.ts index 9bbb35b..1b3c36b 100644 --- a/test/cases/getLockReason.ts +++ b/test/cases/getLockReason.ts @@ -55,6 +55,45 @@ describe("getLockReason", () => { .catch(err => done(err)); }); + it("Should be able to get with actionTypes array", (done) => { + client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionTypes: '["full"]' } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [ + { category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 } + ]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get with actionType", (done) => { + client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: "full" } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [ + { category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 } + ]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get with actionType array", (done) => { + client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: ["full"] } }) + .then(res => { + assert.strictEqual(res.status, 200); + const expected = [ + { category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 } + ]; + assert.deepStrictEqual(res.data, expected); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to get empty locks", (done) => { client.get(endpoint, { params: { videoID: "getLockReason", category: "intro" } }) .then(res => { @@ -118,8 +157,10 @@ describe("getLockReason", () => { }) .catch(err => done(err)); }); +}); - it("should return 400 if no videoID specified", (done) => { +describe("getLockReason 400", () => { + it("Should return 400 with missing videoID", (done) => { client.get(endpoint) .then(res => { assert.strictEqual(res.status, 400); @@ -128,15 +169,37 @@ describe("getLockReason", () => { .catch(err => done(err)); }); - it("should be able to get by actionType", (done) => { - client.get(endpoint, { params: { videoID: "getLockReason", actionType: "full" } }) + it("Should return 400 with invalid actionTypes ", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: 3 } }) .then(res => { - assert.strictEqual(res.status, 200); - const expected = [ - { category: "selfpromo", locked: 1, reason: "sponsor-reason", userID: vipUserID2, userName: vipUserName2 }, - { category: "sponsor", locked: 0, reason: "", userID: "", userName: "" } - ]; - partialDeepEquals(res.data, expected); + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 with invalid actionTypes JSON ", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: "{3}" } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 with invalid categories", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", categories: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 with invalid categories JSON", (done) => { + client.get(endpoint, { params: { videoID: "valid-videoid", categories: "{3}" } }) + .then(res => { + assert.strictEqual(res.status, 400); done(); }) .catch(err => done(err)); diff --git a/test/cases/getTopUsers.ts b/test/cases/getTopUsers.ts index 8808910..07ef80c 100644 --- a/test/cases/getTopUsers.ts +++ b/test/cases/getTopUsers.ts @@ -38,6 +38,15 @@ describe("getTopUsers", () => { .catch(err => done(err)); }); + it("Should return 400 if undefined sortType provided", (done) => { + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to get by all sortTypes", (done) => { client.get(endpoint, { params: { sortType: 0 } })// minutesSaved .then(res => { From 9ef0eafac1e4c798c875e0c1e7c73ceafc813dae Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:56:59 -0400 Subject: [PATCH 24/35] 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 8b06612..cc88992 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 c330e92..475cda9 100644 --- a/src/utils/innerTubeAPI.ts +++ b/src/utils/innerTubeAPI.ts @@ -18,6 +18,7 @@ async function getFromITube (videoID: string): Promise { const result = await axios.post(url, data, { timeout: 3500 }); + /* istanbul ignore else */ if (result.status === 200) { return result.data.videoDetails; } else { @@ -39,6 +40,7 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom return data as innerTubeVideoDetails; } } catch (err) { + /* istanbul ignore next */ return Promise.reject(err); } } From 0b9e7029c5b5f8c8ec50cab0f0b4be413107af22 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:57:33 -0400 Subject: [PATCH 25/35] 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 d8fc860..9dec9df 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 d453cc4..c7413ff 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 26/35] 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 b1f5630..ef02a5f 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 7f4ffaf..f471404 100644 --- a/test/cases/getStatus.ts +++ b/test/cases/getStatus.ts @@ -2,6 +2,7 @@ import assert from "assert"; import { db } from "../../src/databases/databases"; import { client } from "../utils/httpClient"; import { config } from "../../src/config"; +import sinon from "sinon"; let dbVersion: number; describe("getStatus", () => { @@ -122,4 +123,16 @@ describe("getStatus", () => { }) .catch(err => done(err)); }); + + it("Should return commit unkown if not present", (done) => { + sinon.stub((global as any), "HEADCOMMIT").value(undefined); + client.get(`${endpoint}/commit`) + .then(res => { + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data, "test"); // commit should be test + done(); + }) + .catch(err => done(err)); + sinon.restore(); + }); }); From d163b1d43627bc634052ecb10e217e19665426f4 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:20 -0400 Subject: [PATCH 27/35] 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 834382c..a0b321c 100644 --- a/test/cases/shadowBanUser.ts +++ b/test/cases/shadowBanUser.ts @@ -187,10 +187,34 @@ describe("shadowBanUser", () => { }) .then(async res => { assert.strictEqual(res.status, 200); - const videoRow = await getShadowBanSegmentCategory(userID, 1); + const videoRow = await getShadowBanSegmentCategory(userID, 0); const shadowRow = await getShadowBan(userID); assert.ok(shadowRow); // ban still exists - assert.strictEqual(videoRow.length, 1); // videos should be hidden + assert.strictEqual(videoRow.length, 0); // videos should be hidden + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to un-shadowban user to restore old submissions", (done) => { + const userID = "shadowBanned4"; + client({ + method: "POST", + url: endpoint, + params: { + userID, + adminUserID: VIPuserID, + enabled: false, + categories: `["sponsor"]`, + unHideOldSubmissions: true + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const videoRow = await getShadowBanSegmentCategory(userID, 0); + const shadowRow = await getShadowBan(userID); + assert.ok(!shadowRow); // ban still exists + assert.strictEqual(videoRow.length, 1); // videos should be visible assert.strictEqual(videoRow[0].category, "sponsor"); done(); }) diff --git a/test/cases/shadowBanUser4xx.ts b/test/cases/shadowBanUser4xx.ts new file mode 100644 index 0000000..c9cdda9 --- /dev/null +++ b/test/cases/shadowBanUser4xx.ts @@ -0,0 +1,48 @@ +import { db } from "../../src/databases/databases"; +import { getHash } from "../../src/utils/getHash"; +import assert from "assert"; +import { client } from "../utils/httpClient"; + +const endpoint = "/api/shadowBanUser"; + +const postShadowBan = (params: Record) => client({ + method: "POST", + url: endpoint, + params +}); + +describe("shadowBanUser 4xx", () => { + const VIPuserID = "shadow-ban-4xx-vip"; + + before(async () => { + await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES(?)`, [getHash(VIPuserID)]); + }); + + it("Should return 400 if no adminUserID", (done) => { + const userID = "shadowBanned"; + postShadowBan({ userID }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if no userID", (done) => { + postShadowBan({ adminUserID: VIPuserID }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 403 if not authorized", (done) => { + postShadowBan({ adminUserID: "notVIPUserID", userID: "shadowBanned" }) + .then(res => { + assert.strictEqual(res.status, 403); + done(); + }) + .catch(err => done(err)); + }); +}); From 67eb165b53d793f3ffd5a824dbb5ff42ae88e5e7 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:37 -0400 Subject: [PATCH 28/35] 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 0084204..c18bef2 100644 --- a/test/cases/getSearchSegments.ts +++ b/test/cases/getSearchSegments.ts @@ -80,6 +80,67 @@ describe("getSearchSegments", () => { .catch(err => done(err)); }); + it("Should be able to filter by category with categories string", (done) => { + client.get(endpoint, { params: { videoID: "searchTest0", categories: `["selfpromo"]` } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + const segments = data.segments; + assert.strictEqual(data.segmentCount, 1); + assert.strictEqual(data.page, 0); + assert.strictEqual(segments[0].UUID, "search-downvote"); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with categories array", (done) => { + client.get(endpoint, { params: { videoID: "searchTest0", category: ["selfpromo"] } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + const segments = data.segments; + assert.strictEqual(data.segmentCount, 1); + assert.strictEqual(data.page, 0); + assert.strictEqual(segments[0].UUID, "search-downvote"); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with actionTypes JSON", (done) => { + client.get(endpoint, { params: { videoID: "searchTest5", actionTypes: `["mute"]` } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.segmentCount, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with actionType array", (done) => { + client.get(endpoint, { params: { videoID: "searchTest5", actionType: ["mute"] } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.segmentCount, 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to filter by category with actionType string", (done) => { + client.get(endpoint, { params: { videoID: "searchTest5", actionType: "mute" } }) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.strictEqual(data.segmentCount, 1); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to filter by lock status", (done) => { client.get(endpoint, { params: { videoID: "searchTest0", locked: false } }) .then(res => { diff --git a/test/cases/getSearchSegments4xx.ts b/test/cases/getSearchSegments4xx.ts new file mode 100644 index 0000000..708a0b0 --- /dev/null +++ b/test/cases/getSearchSegments4xx.ts @@ -0,0 +1,48 @@ +import { client } from "../utils/httpClient"; +import assert from "assert"; + +describe("getSearchSegments 4xx", () => { + const endpoint = "/api/searchSegments"; + + it("Should return 400 if no videoID", (done) => { + client.get(endpoint, { params: {} }) + .then(res => { + assert.strictEqual(res.status, 400); + const data = res.data; + assert.strictEqual(data, "videoID not specified"); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if invalid categories", (done) => { + client.get(endpoint, { params: { videoID: "nullVideo", categories: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + const data = res.data; + assert.strictEqual(data, "Categories parameter does not match format requirements."); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 400 if invalid actionTypes", (done) => { + client.get(endpoint, { params: { videoID: "nullVideo", actionTypes: 3 } }) + .then(res => { + assert.strictEqual(res.status, 400); + const data = res.data; + assert.strictEqual(data, "actionTypes parameter does not match format requirements."); + done(); + }) + .catch(err => done(err)); + }); + + it("Should return 404 if no segments", (done) => { + client.get(endpoint, { params: { videoID: "nullVideo", actionType: "chapter" } }) + .then(res => { + assert.strictEqual(res.status, 404); + done(); + }) + .catch(err => done(err)); + }); +}); From 95dd36a782ee57492e4adb14332dfeb2b80b6c61 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 30 Sep 2022 22:58:49 -0400 Subject: [PATCH 29/35] 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 632774a..9bcf043 100644 --- a/test/cases/getUserInfo.ts +++ b/test/cases/getUserInfo.ts @@ -21,6 +21,7 @@ describe("getUserInfo", () => { await db.prepare("run", sponsorTimesQuery, ["getUserInfo0", 0, 36000, 2,"uuid000009", getHash("getuserinfo_user_03"), 8, 10, "sponsor", "skip", 0]); await db.prepare("run", sponsorTimesQuery, ["getUserInfo3", 1, 11, 2, "uuid000006", getHash("getuserinfo_user_02"), 6, 10, "sponsor", "skip", 0]); await db.prepare("run", sponsorTimesQuery, ["getUserInfo4", 1, 11, 2, "uuid000010", getHash("getuserinfo_user_04"), 9, 10, "chapter", "chapter", 0]); + await db.prepare("run", sponsorTimesQuery, ["getUserInfo5", 1, 11, 2, "uuid000011", getHash("getuserinfo_user_05"), 9, 10, "sponsor", "skip", 0]); const insertWarningQuery = 'INSERT INTO warnings ("userID", "issueTime", "issuerUserID", "enabled", "reason") VALUES (?, ?, ?, ?, ?)'; diff --git a/test/cases/getUserInfoFree.ts b/test/cases/getUserInfoFree.ts new file mode 100644 index 0000000..fd32d6a --- /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 30/35] 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 7c5ad3c..e936b45 100644 --- a/src/utils/tokenUtils.ts +++ b/src/utils/tokenUtils.ts @@ -58,12 +58,11 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis return licenseKey; } - } catch (e) { + break; + } catch (e) /* istanbul ignore next */ { Logger.error(`token creation: ${e}`); return null; } - - break; } case TokenType.local: { const licenseKey = generateToken(); @@ -74,7 +73,6 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis return licenseKey; } } - return null; } @@ -102,15 +100,12 @@ export async function refreshToken(type: TokenType, licenseKey: string, refreshT return true; } - } catch (e) { + } catch (e) /* istanbul ignore next */ { Logger.error(`token refresh: ${e}`); return false; } - - break; } } - return false; } @@ -136,9 +131,8 @@ export async function getPatreonIdentity(accessToken: string): Promise Date: Thu, 27 Oct 2022 01:47:12 -0400 Subject: [PATCH 31/35] 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 fd32d6a..847a59a 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 => { From 7f92ac961d60dcc713550962c32b6b0d62acc38d Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 28 Oct 2022 16:42:56 -0400 Subject: [PATCH 32/35] clear label cache --- src/utils/queryCacher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/queryCacher.ts b/src/utils/queryCacher.ts index cb1faeb..b1f3192 100644 --- a/src/utils/queryCacher.ts +++ b/src/utils/queryCacher.ts @@ -1,6 +1,6 @@ import redis from "../utils/redis"; import { Logger } from "../utils/logger"; -import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey } from "./redisKeys"; +import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsKey } from "./redisKeys"; import { Service, VideoID, VideoIDHash } from "../types/segments.model"; import { Feature, HashedUserID, UserID } from "../types/user.model"; import { config } from "../config"; @@ -81,6 +81,7 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID redis.del(skipSegmentsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentGroupsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); + redis.del(videoLabelsKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); if (videoInfo.userID) redis.del(reputationKey(videoInfo.userID)).catch((err) => Logger.error(err)); } } From cc6c5980a1453ccdd6dc34da906ef48b385c6d4b Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 28 Oct 2022 16:49:10 -0400 Subject: [PATCH 33/35] videoLabelsHashKey --- src/utils/queryCacher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/queryCacher.ts b/src/utils/queryCacher.ts index b1f3192..463b474 100644 --- a/src/utils/queryCacher.ts +++ b/src/utils/queryCacher.ts @@ -1,6 +1,6 @@ import redis from "../utils/redis"; import { Logger } from "../utils/logger"; -import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsKey } from "./redisKeys"; +import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsHashKey } from "./redisKeys"; import { Service, VideoID, VideoIDHash } from "../types/segments.model"; import { Feature, HashedUserID, UserID } from "../types/user.model"; import { config } from "../config"; @@ -81,7 +81,7 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID redis.del(skipSegmentsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentGroupsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); - redis.del(videoLabelsKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); + redis.del(videoLabelsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); if (videoInfo.userID) redis.del(reputationKey(videoInfo.userID)).catch((err) => Logger.error(err)); } } From db71af480d81529c2035f95f1d6c935bbae3e354 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 28 Oct 2022 16:53:04 -0400 Subject: [PATCH 34/35] clear full video labels --- src/utils/queryCacher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/queryCacher.ts b/src/utils/queryCacher.ts index 463b474..1b3f6a1 100644 --- a/src/utils/queryCacher.ts +++ b/src/utils/queryCacher.ts @@ -1,6 +1,6 @@ import redis from "../utils/redis"; import { Logger } from "../utils/logger"; -import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsHashKey } from "./redisKeys"; +import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsKey, videoLabelsHashKey } from "./redisKeys"; import { Service, VideoID, VideoIDHash } from "../types/segments.model"; import { Feature, HashedUserID, UserID } from "../types/user.model"; import { config } from "../config"; @@ -81,6 +81,7 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID redis.del(skipSegmentsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentGroupsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); + redis.del(videoLabelsKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); redis.del(videoLabelsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err)); if (videoInfo.userID) redis.del(reputationKey(videoInfo.userID)).catch((err) => Logger.error(err)); } From d83d620893c399e9764994507e4ffc3797219023 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 28 Oct 2022 17:00:01 -0400 Subject: [PATCH 35/35] update actions --- .github/workflows/ci.yml | 4 ++-- .github/workflows/eslint.yml | 4 ++-- .github/workflows/generate-sqlite-base.yml | 6 +++--- .github/workflows/postgres-redis-ci.yml | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 619bb9c..ced69af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: steps: # Initialization - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - run: npm install - name: Run Tests timeout-minutes: 5 diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index fab273b..a4b57b8 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -13,8 +13,8 @@ jobs: steps: # Initialization - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - run: npm install - name: Run Tests timeout-minutes: 5 diff --git a/.github/workflows/generate-sqlite-base.yml b/.github/workflows/generate-sqlite-base.yml index c320e58..2714dc7 100644 --- a/.github/workflows/generate-sqlite-base.yml +++ b/.github/workflows/generate-sqlite-base.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 - run: npm install - name: Set config run: | @@ -23,7 +23,7 @@ jobs: - name: Run Server timeout-minutes: 10 run: npm start - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: SponsorTimesDB.db path: databases/sponsorTimes.db diff --git a/.github/workflows/postgres-redis-ci.yml b/.github/workflows/postgres-redis-ci.yml index 6bf05b0..31ec50c 100644 --- a/.github/workflows/postgres-redis-ci.yml +++ b/.github/workflows/postgres-redis-ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build the docker-compose stack env: PG_USER: ci_db_user @@ -20,7 +20,7 @@ jobs: run: docker-compose -f docker/docker-compose-ci.yml up -d - name: Check running containers run: docker ps - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 - run: npm install - name: Run Tests env: