From 07c683e8f0a98032509112e12a5a960d3a1e7bc9 Mon Sep 17 00:00:00 2001 From: Ajay Date: Fri, 27 Jan 2023 22:36:29 -0500 Subject: [PATCH] Add vote/submission for titles and thumbnails --- databases/_private.db.sql | 12 +- databases/_sponsorTimes.db.sql | 4 +- src/app.ts | 6 + src/databases/Sqlite.ts | 15 ++ src/routes/getBranding.ts | 2 +- src/routes/postBranding.ts | 129 ++++++++++++ src/types/branding.model.ts | 27 ++- test/cases/postBranding.ts | 362 +++++++++++++++++++++++++++++++++ 8 files changed, 543 insertions(+), 14 deletions(-) create mode 100644 src/routes/postBranding.ts create mode 100644 test/cases/postBranding.ts diff --git a/databases/_private.db.sql b/databases/_private.db.sql index eb73bb2..50cf974 100644 --- a/databases/_private.db.sql +++ b/databases/_private.db.sql @@ -26,26 +26,18 @@ CREATE TABLE IF NOT EXISTS "config" ( "value" TEXT NOT NULL ); -CREATE TABLE IF NOT EXISTS "titles" ( - "UUID" TEXT NOT NULL PRIMARY KEY, - "hashedIP" TEXT NOT NULL -); - CREATE TABLE IF NOT EXISTS "titleVotes" ( "id" SERIAL PRIMARY KEY, + "videoID" TEXT NOT NULL, "UUID" TEXT NOT NULL, "userID" TEXT NOT NULL, "hashedIP" TEXT NOT NULL, "type" INTEGER NOT NULL ); -CREATE TABLE IF NOT EXISTS "thumbnails" ( - "UUID" TEXT NOT NULL PRIMARY KEY, - "hashedIP" TEXT NOT NULL -); - CREATE TABLE IF NOT EXISTS "thumbnailVotes" ( "id" SERIAL PRIMARY KEY, + "videoID" TEXT NOT NULL, "UUID" TEXT NOT NULL, "userID" TEXT NOT NULL, "hashedIP" TEXT NOT NULL, diff --git a/databases/_sponsorTimes.db.sql b/databases/_sponsorTimes.db.sql index a706283..8a5daf7 100644 --- a/databases/_sponsorTimes.db.sql +++ b/databases/_sponsorTimes.db.sql @@ -68,7 +68,7 @@ CREATE TABLE IF NOT EXISTS "thumbnails" ( CREATE TABLE IF NOT EXISTS "thumbnailTimestamps" ( "UUID" TEXT NOT NULL PRIMARY KEY, - "timestamp" INTEGER NOT NULL default 0 + "timestamp" INTEGER NOT NULL default 0, FOREIGN KEY("UUID") REFERENCES "thumbnails"("UUID") ); @@ -76,7 +76,7 @@ CREATE TABLE IF NOT EXISTS "thumbnailVotes" ( "UUID" TEXT NOT NULL PRIMARY KEY, "votes" INTEGER NOT NULL default 0, "locked" INTEGER NOT NULL default 0, - "shadowHidden" INTEGER NOT NULL default 0, + "shadowHidden" INTEGER NOT NULL default 0, FOREIGN KEY("UUID") REFERENCES "thumbnails"("UUID") ); diff --git a/src/app.ts b/src/app.ts index 61b2347..3827713 100644 --- a/src/app.ts +++ b/src/app.ts @@ -50,6 +50,8 @@ import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash"; import { addFeature } from "./routes/addFeature"; import { generateTokenRequest } from "./routes/generateToken"; import { verifyTokenRequest } from "./routes/verifyToken"; +import { getBranding, getBrandingByHashEndpoint } from "./routes/getBranding"; +import { postBranding } from "./routes/postBranding"; export function createServer(callback: () => void): Server { // Create a service (the app object is just a callback). @@ -206,6 +208,10 @@ function setupRoutes(router: Router) { router.get("/api/videoLabels", getVideoLabels); router.get("/api/videoLabels/:prefix", getVideoLabelsByHash); + router.get("/api/branding", getBranding); + router.get("/api/branding/:prefix", getBrandingByHashEndpoint); + router.post("/api/branding", postBranding); + /* istanbul ignore next */ if (config.postgres?.enabled) { router.get("/database", (req, res) => dumpDatabase(req, res, true)); diff --git a/src/databases/Sqlite.ts b/src/databases/Sqlite.ts index ac03753..2e61dfa 100644 --- a/src/databases/Sqlite.ts +++ b/src/databases/Sqlite.ts @@ -14,6 +14,21 @@ export class Sqlite implements IDatabase { // eslint-disable-next-line require-await async prepare(type: QueryType, query: string, params: any[] = []): Promise { + if (query.includes(";")) { + const promises = []; + let paramsCount = 0; + for (const q of query.split(";")) { + if (q.trim() !== "") { + const currentParamCount = q.match(/\?/g)?.length ?? 0; + promises.push(this.prepare(type, q, params.slice(paramsCount, paramsCount + currentParamCount))); + + paramsCount += currentParamCount; + } + } + + return (await Promise.all(promises)).flat(); + } + // Logger.debug(`prepare (sqlite): type: ${type}, query: ${query}, params: ${params}`); const preparedQuery = this.db.prepare(Sqlite.processQuery(query)); diff --git a/src/routes/getBranding.ts b/src/routes/getBranding.ts index dff3602..3d8e686 100644 --- a/src/routes/getBranding.ts +++ b/src/routes/getBranding.ts @@ -186,7 +186,7 @@ export async function getBranding(req: Request, res: Response) { return res.status(status).json(result); } -export async function getBrandingByHash(req: Request, res: Response) { +export async function getBrandingByHashEndpoint(req: Request, res: Response) { 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 diff --git a/src/routes/postBranding.ts b/src/routes/postBranding.ts new file mode 100644 index 0000000..ddcd6ed --- /dev/null +++ b/src/routes/postBranding.ts @@ -0,0 +1,129 @@ +import { Request, Response } from "express"; +import { config } from "../config"; +import { db, privateDB } from "../databases/databases"; + +import { BrandingSubmission, BrandingUUID, TimeThumbnailSubmission } from "../types/branding.model"; +import { HashedIP, IPAddress, VideoID } from "../types/segments.model"; +import { HashedUserID } from "../types/user.model"; +import { getHashCache } from "../utils/getHashCache"; +import { getIP } from "../utils/getIP"; +import { getService } from "../utils/getService"; +import { isUserVIP } from "../utils/isUserVIP"; +import { Logger } from "../utils/logger"; + +enum BrandingType { + Title, + Thumbnail +} + +interface ExistingVote { + UUID: BrandingUUID; + type: number; + id: number; +} + +export async function postBranding(req: Request, res: Response) { + const { videoID, userID, title, thumbnail } = req.body as BrandingSubmission; + const service = getService(req.body.service); + + if (!videoID || !userID || userID.length < 30 || !service + || ((!title || !title.title) + && (!thumbnail || thumbnail.original == null + || (!thumbnail.original && !(thumbnail as TimeThumbnailSubmission).timestamp)))) { + res.status(400).send("Bad Request"); + return; + } + + try { + const hashedUserID = await getHashCache(userID); + const isVip = await isUserVIP(hashedUserID); + const hashedVideoID = await getHashCache(videoID, 1); + const hashedIP = await getHashCache(getIP(req) + config.globalSalt as IPAddress); + + const now = Date.now(); + const voteType = 1; + + await Promise.all([(async () => { + if (title) { + const existingUUID = (await db.prepare("get", `SELECT "UUID" from "titles" where "videoID" = ? AND "title" = ?`, [videoID, title.title]))?.UUID; + const UUID = existingUUID || crypto.randomUUID(); + + const existingVote = await handleExistingVotes(BrandingType.Title, videoID, hashedUserID, UUID, hashedIP, voteType); + if (existingUUID) { + await updateVoteTotals(BrandingType.Title, existingVote, UUID, isVip); + } else { + await db.prepare("run", `INSERT INTO "titles" ("videoID", "title", "original", "userID", "service", "hashedVideoID", "timeSubmitted", "UUID") VALUES (?, ?, ?, ?, ?, ?, ?, ?); + INSERT INTO "titleVotes" ("UUID", "votes", "locked", "shadowHidden") VALUES (?, 0, ?, 0);`, + [videoID, title.title, title.original ? 1 : 0, hashedUserID, service, hashedVideoID, now, UUID, UUID, isVip ? 1 : 0]); + } + } + })(), (async () => { + if (thumbnail) { + const existingUUID = thumbnail.original + ? (await db.prepare("get", `SELECT "UUID" from "thumbnails" where "videoID" = ? AND "original" = 1`, [videoID]))?.UUID + : (await db.prepare("get", `SELECT "thumbnails"."UUID" from "thumbnailTimestamps" JOIN "thumbnails" ON "thumbnails"."UUID" = "thumbnailTimestamps"."UUID" + WHERE "thumbnailTimestamps"."timestamp" = ? AND "thumbnails"."videoID" = ?`, [(thumbnail as TimeThumbnailSubmission).timestamp, videoID]))?.UUID; + const UUID = existingUUID || crypto.randomUUID(); + + const existingVote = await handleExistingVotes(BrandingType.Thumbnail, videoID, hashedUserID, UUID, hashedIP, voteType); + if (existingUUID) { + await updateVoteTotals(BrandingType.Thumbnail, existingVote, UUID, isVip); + } else { + await db.prepare("run", `INSERT INTO "thumbnails" ("videoID", "original", "userID", "service", "hashedVideoID", "timeSubmitted", "UUID") VALUES (?, ?, ?, ?, ?, ?, ?); + INSERT INTO "thumbnailVotes" ("UUID", "votes", "locked", "shadowHidden") VALUES (?, 0, ?, 0); + ${thumbnail.original ? "" : `INSERT INTO "thumbnailTimestamps" ("UUID", "timestamp") VALUES (?, ?)`}`, + [videoID, thumbnail.original ? 1 : 0, hashedUserID, service, hashedVideoID, now, UUID, UUID, + isVip ? 1 : 0, thumbnail.original ? null : UUID, thumbnail.original ? null : (thumbnail as TimeThumbnailSubmission).timestamp]); + } + + } + })()]); + + res.status(200).send("OK"); + } catch (e) { + Logger.error(e as string); + res.status(500).send("Internal Server Error"); + } +} + +/** + * Finds an existing vote, if found, and it's for a different submission, it undoes it, and points to the new submission. + * If no existing vote, it adds one. + */ +async function handleExistingVotes(type: BrandingType, videoID: VideoID, + hashedUserID: HashedUserID, UUID: BrandingUUID, hashedIP: HashedIP, voteType: number): Promise { + const table = type === BrandingType.Title ? `"titleVotes"` : `"thumbnailVotes"`; + + const existingVote = await privateDB.prepare("get", `SELECT "id", "UUID", "type" from ${table} where "videoID" = ? AND "userID" = ?`, [videoID, hashedUserID]); + if (existingVote && existingVote.UUID !== UUID) { + if (existingVote.type === 1) { + await db.prepare("run", `UPDATE ${table} SET "votes" = "votes" - 1 WHERE "UUID" = ?`, [existingVote.UUID]); + } + + await privateDB.prepare("run", `UPDATE ${table} SET "type" = ?, "UUID" = ? WHERE "id" = ?`, [voteType, UUID, existingVote.id]); + } else if (!existingVote) { + await privateDB.prepare("run", `INSERT INTO ${table} ("videoID", "UUID", "userID", "hashedIP", "type") VALUES (?, ?, ?, ?, ?)`, + [videoID, UUID, hashedUserID, hashedIP, voteType]); + } + + return existingVote; +} + +/** + * Only called if an existing vote exists. + * Will update public vote totals and locked status. + */ +async function updateVoteTotals(type: BrandingType, existingVote: ExistingVote, UUID: BrandingUUID, isVip: boolean): Promise { + if (!existingVote) return; + + const table = type === BrandingType.Title ? `"titleVotes"` : `"thumbnailVotes"`; + + // Don't upvote if we vote on the same submission + if (!existingVote || existingVote.UUID !== UUID) { + await db.prepare("run", `UPDATE ${table} SET "votes" = "votes" + 1 WHERE "UUID" = ?`, [UUID]); + } + + if (isVip) { + await db.prepare("run", `UPDATE ${table} SET "locked" = 1 WHERE "UUID" = ?`, [UUID]); + } +} \ No newline at end of file diff --git a/src/types/branding.model.ts b/src/types/branding.model.ts index 228c93b..8662ad3 100644 --- a/src/types/branding.model.ts +++ b/src/types/branding.model.ts @@ -1,4 +1,5 @@ -import { VideoID, VideoIDHash } from "./segments.model"; +import { Service, VideoID, VideoIDHash } from "./segments.model"; +import { UserID } from "./user.model"; export type BrandingUUID = string & { readonly __brandingUUID: unique symbol }; @@ -53,4 +54,28 @@ export interface BrandingHashDBResult { export interface BrandingHashResult { branding: BrandingResult; +} + +export interface OriginalThumbnailSubmission { + original: true; +} + +export interface TimeThumbnailSubmission { + timestamp: number; + original: false; +} + +export type ThumbnailSubmission = OriginalThumbnailSubmission | TimeThumbnailSubmission; + +export interface TitleSubmission { + title: string; + original: boolean; +} + +export interface BrandingSubmission { + title: TitleSubmission; + thumbnail: ThumbnailSubmission; + videoID: VideoID; + userID: UserID; + service: Service; } \ No newline at end of file diff --git a/test/cases/postBranding.ts b/test/cases/postBranding.ts new file mode 100644 index 0000000..ccc15f1 --- /dev/null +++ b/test/cases/postBranding.ts @@ -0,0 +1,362 @@ +import { db } from "../../src/databases/databases"; +import { getHash } from "../../src/utils/getHash"; +import { client } from "../utils/httpClient"; +import assert from "assert"; +import { Service } from "../../src/types/segments.model"; + +describe("postBranding", () => { + + const vipUser = `VIPPostBrandingUser${".".repeat(16)}`; + const userID1 = `PostBrandingUser1${".".repeat(16)}`; + const userID2 = `PostBrandingUser2${".".repeat(16)}`; + const userID3 = `PostBrandingUser3${".".repeat(16)}`; + const userID4 = `PostBrandingUser4${".".repeat(16)}`; + const userID5 = `PostBrandingUser4${".".repeat(16)}`; + + const endpoint = "/api/branding"; + const postBranding = (data: Record) => client({ + method: "POST", + url: endpoint, + data + }); + + const queryTitleByVideo = (videoID: string) => db.prepare("get", `SELECT * FROM "titles" WHERE "videoID" = ? ORDER BY "timeSubmitted" DESC`, [videoID]); + const queryThumbnailByVideo = (videoID: string) => db.prepare("get", `SELECT * FROM "thumbnails" WHERE "videoID" = ? ORDER BY "timeSubmitted" DESC`, [videoID]); + const queryThumbnailTimestampsByUUID = (UUID: string) => db.prepare("get", `SELECT * FROM "thumbnailTimestamps" WHERE "UUID" = ?`, [UUID]); + const queryTitleVotesByUUID = (UUID: string) => db.prepare("get", `SELECT * FROM "titleVotes" WHERE "UUID" = ?`, [UUID]); + const queryThumbnailVotesByUUID = (UUID: string) => db.prepare("get", `SELECT * FROM "thumbnailVotes" WHERE "UUID" = ?`, [UUID]); + + before(() => { + const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)'; + db.prepare("run", insertVipUserQuery, [getHash(vipUser)]); + }); + + it("Submit only title", async () => { + const videoID = "postBrand1"; + const title = { + title: "Some title", + original: false + }; + + const res = await postBranding({ + title, + userID: userID1, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbTitle = await queryTitleByVideo(videoID); + const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID); + + assert.strictEqual(dbTitle.title, title.title); + assert.strictEqual(dbTitle.original, title.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Submit only original title", async () => { + const videoID = "postBrand2"; + const title = { + title: "Some title", + original: true + }; + + const res = await postBranding({ + title, + userID: userID2, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbTitle = await queryTitleByVideo(videoID); + const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID); + + assert.strictEqual(dbTitle.title, title.title); + assert.strictEqual(dbTitle.original, title.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Submit only original thumbnail", async () => { + const videoID = "postBrand3"; + const thumbnail = { + original: true + }; + + const res = await postBranding({ + thumbnail, + userID: userID3, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Submit only custom thumbnail", async () => { + const videoID = "postBrand4"; + const thumbnail = { + timestamp: 12.42, + original: false + }; + + const res = await postBranding({ + thumbnail, + userID: userID4, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbThumbnailTimestamps = await queryThumbnailTimestampsByUUID(dbThumbnail.UUID); + const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + + assert.strictEqual(dbThumbnailTimestamps.timestamp, thumbnail.timestamp); + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Submit title and thumbnail", async () => { + const videoID = "postBrand5"; + const title = { + title: "Some title", + original: false + }; + const thumbnail = { + timestamp: 12.42, + original: false + }; + + const res = await postBranding({ + title, + thumbnail, + userID: userID5, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbTitle = await queryTitleByVideo(videoID); + const dbTitleVotes = await queryTitleVotesByUUID(dbTitle.UUID); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbThumbnailTimestamps = await queryThumbnailTimestampsByUUID(dbThumbnail.UUID); + const dbThumbnailVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + + assert.strictEqual(dbTitle.title, title.title); + assert.strictEqual(dbTitle.original, title.original ? 1 : 0); + + assert.strictEqual(dbTitleVotes.votes, 0); + assert.strictEqual(dbTitleVotes.locked, 0); + assert.strictEqual(dbTitleVotes.shadowHidden, 0); + + assert.strictEqual(dbThumbnailTimestamps.timestamp, thumbnail.timestamp); + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbThumbnailVotes.votes, 0); + assert.strictEqual(dbThumbnailVotes.locked, 0); + assert.strictEqual(dbThumbnailVotes.shadowHidden, 0); + }); + + it("Submit title and thumbnail as VIP", async () => { + const videoID = "postBrand6"; + const title = { + title: "Some title", + original: false + }; + const thumbnail = { + timestamp: 12.42, + original: false + }; + + const res = await postBranding({ + title, + thumbnail, + userID: vipUser, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbTitle = await queryTitleByVideo(videoID); + const dbTitleVotes = await queryTitleVotesByUUID(dbTitle.UUID); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbThumbnailTimestamps = await queryThumbnailTimestampsByUUID(dbThumbnail.UUID); + const dbThumbnailVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + + assert.strictEqual(dbTitle.title, title.title); + assert.strictEqual(dbTitle.original, title.original ? 1 : 0); + + assert.strictEqual(dbTitleVotes.votes, 0); + assert.strictEqual(dbTitleVotes.locked, 1); + assert.strictEqual(dbTitleVotes.shadowHidden, 0); + + assert.strictEqual(dbThumbnailTimestamps.timestamp, thumbnail.timestamp); + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbThumbnailVotes.votes, 0); + assert.strictEqual(dbThumbnailVotes.locked, 1); + assert.strictEqual(dbThumbnailVotes.shadowHidden, 0); + }); + + it("Vote the same title again", async () => { + const videoID = "postBrand1"; + const title = { + title: "Some title", + original: false + }; + + const res = await postBranding({ + title, + userID: userID1, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbTitle = await queryTitleByVideo(videoID); + const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID); + + assert.strictEqual(dbTitle.title, title.title); + assert.strictEqual(dbTitle.original, title.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Vote for a different title", async () => { + const videoID = "postBrand1"; + const title = { + title: "Some other title", + original: false + }; + + const oldDbTitle = await queryTitleByVideo(videoID); + + const res = await postBranding({ + title, + userID: userID1, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbTitle = await queryTitleByVideo(videoID); + const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID); + const oldDBVotes = await queryTitleVotesByUUID(oldDbTitle.UUID); + + assert.strictEqual(dbTitle.title, title.title); + assert.strictEqual(dbTitle.original, title.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + + assert.strictEqual(oldDBVotes.votes, -1); + assert.strictEqual(oldDBVotes.locked, 0); + assert.strictEqual(oldDBVotes.shadowHidden, 0); + }); + + it("Vote for the same thumbnail again", async () => { + const videoID = "postBrand4"; + const thumbnail = { + timestamp: 12.42, + original: false + }; + + const res = await postBranding({ + thumbnail, + userID: userID4, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbThumbnailTimestamps = await queryThumbnailTimestampsByUUID(dbThumbnail.UUID); + const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + + assert.strictEqual(dbThumbnailTimestamps.timestamp, thumbnail.timestamp); + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Vote for the same thumbnail again original", async () => { + const videoID = "postBrand3"; + const thumbnail = { + original: true + }; + + const res = await postBranding({ + thumbnail, + userID: userID3, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + }); + + it("Vote for a different thumbnail", async () => { + const videoID = "postBrand4"; + const thumbnail = { + timestamp: 15.34, + original: false + }; + + const oldDbThumbnail = await queryThumbnailByVideo(videoID); + + const res = await postBranding({ + thumbnail, + userID: userID4, + service: Service.YouTube, + videoID + }); + + assert.strictEqual(res.status, 200); + const dbThumbnail = await queryThumbnailByVideo(videoID); + const dbThumbnailTimestamps = await queryThumbnailTimestampsByUUID(dbThumbnail.UUID); + const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID); + const oldDBVotes = await queryThumbnailVotesByUUID(oldDbThumbnail.UUID); + + assert.strictEqual(dbThumbnailTimestamps.timestamp, thumbnail.timestamp); + assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0); + + assert.strictEqual(dbVotes.votes, 0); + assert.strictEqual(dbVotes.locked, 0); + assert.strictEqual(dbVotes.shadowHidden, 0); + + assert.strictEqual(oldDBVotes.votes, -1); + assert.strictEqual(oldDBVotes.locked, 0); + assert.strictEqual(oldDBVotes.shadowHidden, 0); + }); +}); \ No newline at end of file