Add casual mode endpoint

This commit is contained in:
Ajay 2025-02-05 03:38:55 -05:00
parent ab9cab8ff5
commit 07435b9af1
13 changed files with 409 additions and 16 deletions

View file

@ -44,4 +44,15 @@ CREATE TABLE IF NOT EXISTS "thumbnailVotes" (
"type" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "casualVotes" (
"UUID" SERIAL PRIMARY KEY,
"videoID" TEXT NOT NULL,
"service" TEXT NOT NULL,
"userID" TEXT NOT NULL,
"hashedIP" TEXT NOT NULL,
"category" TEXT NOT NULL,
"type" INTEGER NOT NULL,
"timeSubmitted" INTEGER NOT NULL
);
COMMIT;

View file

@ -23,4 +23,11 @@ CREATE INDEX IF NOT EXISTS "categoryVotes_UUID"
CREATE INDEX IF NOT EXISTS "ratings_videoID"
ON public."ratings" USING btree
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" ASC NULLS LAST, "timeSubmitted" ASC NULLS LAST)
TABLESPACE pg_default;
-- casualVotes
CREATE INDEX IF NOT EXISTS "casualVotes_videoID"
ON public."casualVotes" USING btree
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, "service" COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;

View file

@ -84,6 +84,17 @@ CREATE TABLE IF NOT EXISTS "thumbnailVotes" (
FOREIGN KEY("UUID") REFERENCES "thumbnails"("UUID")
);
CREATE TABLE IF NOT EXISTS "casualVotes" (
"UUID" TEXT PRIMARY KEY,
"videoID" TEXT NOT NULL,
"service" TEXT NOT NULL,
"hashedVideoID" TEXT NOT NULL,
"category" TEXT NOT NULL,
"upvotes" INTEGER NOT NULL default 0,
"downvotes" INTEGER NOT NULL default 0,
"timeSubmitted" INTEGER NOT NULL
);
CREATE EXTENSION IF NOT EXISTS pgcrypto; --!sqlite-ignore
CREATE EXTENSION IF NOT EXISTS pg_trgm; --!sqlite-ignore

View file

@ -173,4 +173,26 @@ CREATE INDEX IF NOT EXISTS "thumbnails_hashedVideoID_2"
CREATE INDEX IF NOT EXISTS "thumbnailVotes_votes"
ON public."thumbnailVotes" USING btree
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST, "votes" DESC NULLS LAST)
TABLESPACE pg_default;
-- casualVotes
CREATE INDEX IF NOT EXISTS "casualVotes_timeSubmitted"
ON public."casualVotes" USING btree
("timeSubmitted" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "casualVotes_userID_timeSubmitted"
ON public."casualVotes" USING btree
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, "service" COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" DESC NULLS LAST, "timeSubmitted" DESC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "casualVotes_videoID"
ON public."casualVotes" USING btree
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, "service" COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
CREATE INDEX IF NOT EXISTS "casualVotes_hashedVideoID_2"
ON public."casualVotes" USING btree
(service COLLATE pg_catalog."default" ASC NULLS LAST, "hashedVideoID" text_pattern_ops ASC NULLS LAST, "timeSubmitted" ASC NULLS LAST)
TABLESPACE pg_default;

View file

@ -59,6 +59,7 @@ import { getFeatureFlag } from "./routes/getFeatureFlag";
import { getReady } from "./routes/getReady";
import { getMetrics } from "./routes/getMetrics";
import { getSegmentID } from "./routes/getSegmentID";
import { postCasual } from "./routes/postCasual";
export function createServer(callback: () => void): Server {
// Create a service (the app object is just a callback).
@ -234,6 +235,8 @@ function setupRoutes(router: Router, server: Server) {
router.get("/api/branding/:prefix", getBrandingByHashEndpoint);
router.post("/api/branding", postBranding);
router.post("/api/casual", postCasual);
/* istanbul ignore next */
if (config.postgres?.enabled) {
router.get("/database", (req, res) => dumpDatabase(req, res, true));

View file

@ -20,6 +20,7 @@ addDefaults(config, {
readOnly: false,
webhooks: [],
categoryList: ["sponsor", "selfpromo", "exclusive_access", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"],
casualCategoryList: ["funny", "creative", "clever", "descriptive", "other"],
categorySupport: {
sponsor: ["skip", "mute", "full"],
selfpromo: ["skip", "mute", "full"],

View file

@ -3,7 +3,7 @@ import { isEmpty } from "lodash";
import { config } from "../config";
import { db, privateDB } from "../databases/databases";
import { Postgres } from "../databases/Postgres";
import { BrandingDBSubmission, BrandingDBSubmissionData, BrandingHashDBResult, BrandingResult, BrandingSegmentDBResult, BrandingSegmentHashDBResult, ThumbnailDBResult, ThumbnailResult, TitleDBResult, TitleResult } from "../types/branding.model";
import { BrandingDBSubmission, BrandingDBSubmissionData, BrandingHashDBResult, BrandingResult, BrandingSegmentDBResult, BrandingSegmentHashDBResult, CasualVoteDBResult, CasualVoteHashDBResult, ThumbnailDBResult, ThumbnailResult, TitleDBResult, TitleResult } from "../types/branding.model";
import { HashedIP, IPAddress, Service, VideoID, VideoIDHash, Visibility } from "../types/segments.model";
import { shuffleArray } from "../utils/array";
import { getHashCache } from "../utils/getHashCache";
@ -51,10 +51,20 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
{ useReplica: true }
) as Promise<BrandingSegmentDBResult[]>;
const getCasualVotes = () => db.prepare(
"all",
`SELECT "category", "upvotes", "downvotes" FROM "casualVotes"
WHERE "videoID" = ? AND "service" = ?
ORDER BY "timeSubmitted" ASC`,
[videoID, service],
{ useReplica: true }
) as Promise<CasualVoteDBResult[]>;
const getBranding = async () => {
const titles = getTitles();
const thumbnails = getThumbnails();
const segments = getSegments();
const casualVotes = getCasualVotes();
for (const title of await titles) {
title.title = title.title.replace("<", "");
@ -63,7 +73,8 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
return {
titles: await titles,
thumbnails: await thumbnails,
segments: await segments
segments: await segments,
casualVotes: await casualVotes
};
};
@ -85,7 +96,8 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
currentIP: null as Promise<HashedIP> | null
};
return filterAndSortBranding(videoID, returnUserID, fetchAll, branding.titles, branding.thumbnails, branding.segments, ip, cache);
return filterAndSortBranding(videoID, returnUserID, fetchAll, branding.titles,
branding.thumbnails, branding.segments, branding.casualVotes, ip, cache);
}
export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, service: Service, ip: IPAddress, returnUserID: boolean, fetchAll: boolean): Promise<Record<VideoID, BrandingResult>> {
@ -117,12 +129,22 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
{ useReplica: true }
) as Promise<BrandingSegmentHashDBResult[]>;
const getCasualVotes = () => db.prepare(
"all",
`SELECT "videoID", "category", "upvotes", "downvotes" FROM "casualVotes"
WHERE "hashedVideoID" LIKE ? AND "service" = ?
ORDER BY "timeSubmitted" ASC`,
[`${videoHashPrefix}%`, service],
{ useReplica: true }
) as Promise<CasualVoteHashDBResult[]>;
const branding = await QueryCacher.get(async () => {
// Make sure they are both called in parallel
const branding = {
titles: getTitles(),
thumbnails: getThumbnails(),
segments: getSegments()
segments: getSegments(),
casualVotes: getCasualVotes()
};
const dbResult: Record<VideoID, BrandingHashDBResult> = {};
@ -130,7 +152,8 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
dbResult[submission.videoID] = dbResult[submission.videoID] || {
titles: [],
thumbnails: [],
segments: []
segments: [],
casualVotes: []
};
};
@ -150,6 +173,11 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
dbResult[segment.videoID].segments.push(segment);
});
(await branding.casualVotes).forEach((casualVote) => {
initResult(casualVote);
dbResult[casualVote.videoID].casualVotes.push(casualVote);
});
return dbResult;
}, brandingHashKey(videoHashPrefix, service));
@ -162,14 +190,14 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
await Promise.all(Object.keys(branding).map(async (key) => {
const castedKey = key as VideoID;
processedResult[castedKey] = await filterAndSortBranding(castedKey, returnUserID, fetchAll, branding[castedKey].titles,
branding[castedKey].thumbnails, branding[castedKey].segments, ip, cache);
branding[castedKey].thumbnails, branding[castedKey].segments, branding[castedKey].casualVotes, ip, cache);
}));
return processedResult;
}
async function filterAndSortBranding(videoID: VideoID, returnUserID: boolean, fetchAll: boolean, dbTitles: TitleDBResult[],
dbThumbnails: ThumbnailDBResult[], dbSegments: BrandingSegmentDBResult[],
dbThumbnails: ThumbnailDBResult[], dbSegments: BrandingSegmentDBResult[], dbCasualVotes: CasualVoteDBResult[],
ip: IPAddress, cache: { currentIP: Promise<HashedIP> | null }): Promise<BrandingResult> {
const shouldKeepTitles = shouldKeepSubmission(dbTitles, BrandingSubmissionType.Title, ip, cache);
@ -202,11 +230,17 @@ async function filterAndSortBranding(videoID: VideoID, returnUserID: boolean, fe
}))
.filter((a) => (fetchAll && !a.original) || a.votes >= 1 || (a.votes >= 0 && !a.original) || a.locked) as ThumbnailResult[];
const casualVotes = dbCasualVotes.map((r) => ({
id: r.category,
count: r.upvotes - r.downvotes
})).filter((a) => a.count > 0);
const videoDuration = dbSegments.filter(s => s.videoDuration !== 0)[0]?.videoDuration ?? null;
return {
titles,
thumbnails,
casualVotes,
randomTime: findRandomTime(videoID, dbSegments, videoDuration),
videoDuration: videoDuration,
};
@ -303,7 +337,7 @@ export async function getBranding(req: Request, res: Response) {
.then(etag => res.set("ETag", etag))
.catch(() => null);
const status = result.titles.length > 0 || result.thumbnails.length > 0 ? 200 : 404;
const status = result.titles.length > 0 || result.thumbnails.length > 0 || result.casualVotes.length > 0 ? 200 : 404;
return res.status(status).json(result);
} catch (e) {
Logger.error(e as string);

111
src/routes/postCasual.ts Normal file
View file

@ -0,0 +1,111 @@
import { Request, Response } from "express";
import { config } from "../config";
import { db, privateDB } from "../databases/databases";
import { BrandingUUID, CasualCategory, CasualVoteSubmission } from "../types/branding.model";
import { HashedIP, IPAddress, Service, 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 { Logger } from "../utils/logger";
import crypto from "crypto";
import { QueryCacher } from "../utils/queryCacher";
import { acquireLock } from "../utils/redisLock";
import { checkBanStatus } from "../utils/checkBan";
enum CasualVoteType {
Upvote = 1,
Downvote = 2
}
interface ExistingVote {
UUID: BrandingUUID;
type: number;
}
export async function postCasual(req: Request, res: Response) {
const { videoID, userID, downvote, category } = req.body as CasualVoteSubmission;
const service = getService(req.body.service);
if (!videoID || !userID || userID.length < 30 || !service || !category) {
return res.status(400).send("Bad Request");
}
if (!config.casualCategoryList.includes(category)) {
return res.status(400).send("Invalid category");
}
try {
const hashedUserID = await getHashCache(userID);
const hashedVideoID = await getHashCache(videoID, 1);
const hashedIP = await getHashCache(getIP(req) + config.globalSalt as IPAddress);
const isBanned = await checkBanStatus(hashedUserID, hashedIP);
const lock = await acquireLock(`postCasual:${videoID}.${hashedUserID}`);
if (!lock.status) {
res.status(429).send("Vote already in progress");
return;
}
if (isBanned) {
return res.status(200).send("OK");
}
const now = Date.now();
const voteType: CasualVoteType = downvote ? CasualVoteType.Downvote : CasualVoteType.Upvote;
const existingUUID = (await db.prepare("get", `SELECT "UUID" from "casualVotes" where "videoID" = ? AND "category" = ?`, [videoID, category]))?.UUID;
const UUID = existingUUID || crypto.randomUUID();
const alreadyVotedTheSame = await handleExistingVotes(videoID, service, UUID, hashedUserID, hashedIP, category, voteType, now);
if (existingUUID) {
if (!alreadyVotedTheSame) {
if (downvote) {
await db.prepare("run", `UPDATE "casualVotes" SET "downvotes" = "downvotes" + 1 WHERE "UUID" = ?`, [UUID]);
} else {
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" + 1 WHERE "UUID" = ?`, [UUID]);
}
}
} else {
if (downvote) {
throw new Error("Title submission doesn't exist");
}
await db.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "hashedVideoID", "timeSubmitted", "UUID", "category", "upvotes", "downvotes") VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[videoID, service, hashedVideoID, now, UUID, category, downvote ? 0 : 1, downvote ? 1 : 0]);
}
//todo: cache clearing
QueryCacher.clearBrandingCache({ videoID, hashedVideoID, service });
res.status(200).send("OK");
lock.unlock();
} catch (e) {
Logger.error(e as string);
res.status(500).send("Internal Server Error");
}
}
async function handleExistingVotes(videoID: VideoID, service: Service, UUID: string,
hashedUserID: HashedUserID, hashedIP: HashedIP, category: CasualCategory, voteType: CasualVoteType, now: number): Promise<boolean> {
const existingVote = await privateDB.prepare("get", `SELECT "UUID", "type" from "casualVotes" WHERE "videoID" = ? AND "service" = ? AND "userID" = ? AND category = ?`, [videoID, service, hashedUserID, category]) as ExistingVote;
if (existingVote) {
if (existingVote.type === voteType) {
return true;
}
if (existingVote.type === CasualVoteType.Upvote) {
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" - 1 WHERE "UUID" = ?`, [UUID]);
} else {
await db.prepare("run", `UPDATE "casualVotes" SET "downvotes" = "downvotes" - 1 WHERE "UUID" = ?`, [UUID]);
}
await privateDB.prepare("run", `DELETE FROM "casualVotes" WHERE "UUID" = ?`, [existingVote.UUID]);
}
await privateDB.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "userID", "hashedIP", "category", "type", "timeSubmitted") VALUES (?, ?, ?, ?, ?, ?, ?)`,
[videoID, service, hashedUserID, hashedIP, category, voteType, now]);
return false;
}

View file

@ -3,6 +3,8 @@ import { UserID } from "./user.model";
export type BrandingUUID = string & { readonly __brandingUUID: unique symbol };
export type CasualCategory = ("funny" | "creative" | "clever" | "descriptive" | "other") & { __casualCategoryBrand: unknown };
export interface BrandingDBSubmissionData {
videoID: VideoID,
}
@ -50,17 +52,24 @@ export interface ThumbnailResult {
userID?: UserID
}
export interface CasualVote {
id: string,
count: number
}
export interface BrandingResult {
titles: TitleResult[],
thumbnails: ThumbnailResult[],
casualVotes: CasualVote[],
randomTime: number,
videoDuration: number | null
}
export interface BrandingHashDBResult {
titles: TitleDBResult[],
thumbnails: ThumbnailDBResult[],
segments: BrandingSegmentDBResult[]
titles: TitleDBResult[];
thumbnails: ThumbnailDBResult[];
segments: BrandingSegmentDBResult[];
casualVotes: CasualVoteDBResult[];
}
export interface OriginalThumbnailSubmission {
@ -89,6 +98,15 @@ export interface BrandingSubmission {
downvote: boolean | undefined;
videoDuration: number | undefined;
wasWarned: boolean | undefined;
casualMode: boolean | undefined;
}
export interface CasualVoteSubmission {
videoID: VideoID;
userID: UserID;
service: Service;
downvote: boolean | undefined;
category: CasualCategory;
}
export interface BrandingSegmentDBResult {
@ -98,9 +116,21 @@ export interface BrandingSegmentDBResult {
videoDuration: number;
}
export interface CasualVoteDBResult {
category: CasualCategory;
upvotes: number;
downvotes: number;
}
export interface BrandingSegmentHashDBResult extends BrandingDBSubmissionData {
startTime: number;
endTime: number;
category: Category;
videoDuration: number;
}
export interface CasualVoteHashDBResult extends BrandingDBSubmissionData {
category: CasualCategory;
upvotes: number;
downvotes: number;
}

View file

@ -73,6 +73,7 @@ export interface SBSConfig {
readOnly: boolean;
webhooks: WebhookConfig[];
categoryList: string[];
casualCategoryList: string[];
deArrowTypes: DeArrowType[];
categorySupport: Record<string, string[]>;
maxTitleLength: number;

View file

@ -19,13 +19,13 @@ export function skipSegmentsHashKey(hashedVideoIDPrefix: VideoIDHash, service: S
}
export const brandingKey = (videoID: VideoID, service: Service): string =>
`branding.v2.${service}.videoID.${videoID}`;
`branding.v3.${service}.videoID.${videoID}`;
export function brandingHashKey(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 `branding.v2.${service}.${hashedVideoIDPrefix}`;
return `branding.v3.${service}.${hashedVideoIDPrefix}`;
}
export const brandingIPKey = (uuid: BrandingUUID): string =>

View file

@ -3,7 +3,7 @@ import assert from "assert";
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases";
import { Service } from "../../src/types/segments.model";
import { BrandingUUID, ThumbnailResult, TitleResult } from "../../src/types/branding.model";
import { BrandingUUID, CasualVote, ThumbnailResult, TitleResult } from "../../src/types/branding.model";
import { partialDeepEquals } from "../utils/partialDeepEquals";
describe("getBranding", () => {
@ -14,6 +14,8 @@ describe("getBranding", () => {
const videoIDRandomTime = "videoID5";
const videoIDUnverified = "videoID6";
const videoIDvidDuration = "videoID7";
const videoIDCasual = "videoIDCasual";
const videoIDCasualDownvoted = "videoIDCasualDownvoted";
const videoID1Hash = getHash(videoID1, 1).slice(0, 4);
const videoID2LockedHash = getHash(videoID2Locked, 1).slice(0, 4);
@ -22,6 +24,8 @@ describe("getBranding", () => {
const videoIDRandomTimeHash = getHash(videoIDRandomTime, 1).slice(0, 4);
const videoIDUnverifiedHash = getHash(videoIDUnverified, 1).slice(0, 4);
const videoIDvidDurationHash = getHash(videoIDUnverified, 1).slice(0, 4);
const videoIDCasualHash = getHash(videoIDCasual, 1).slice(0, 4);
const videoIDCasualDownvotedHash = getHash(videoIDCasualDownvoted, 1).slice(0, 4);
const endpoint = "/api/branding";
const getBranding = (params: Record<string, any>) => client({
@ -43,6 +47,7 @@ describe("getBranding", () => {
const thumbnailTimestampsQuery = `INSERT INTO "thumbnailTimestamps" ("UUID", "timestamp") VALUES (?, ?)`;
const thumbnailVotesQuery = `INSERT INTO "thumbnailVotes" ("UUID", "votes", "locked", "shadowHidden", "downvotes", "removed") VALUES (?, ?, ?, ?, ?, ?)`;
const segmentQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "hidden", "shadowHidden", "description", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const insertCasualVotesQuery = `INSERT INTO "casualVotes" ("UUID", "videoID", "service", "hashedVideoID", "category", "upvotes", "downvotes", "timeSubmitted") VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
await Promise.all([
db.prepare("run", titleQuery, [videoID1, "title1", 0, "userID1", Service.YouTube, videoID1Hash, 1, "UUID1"]),
@ -143,6 +148,12 @@ describe("getBranding", () => {
db.prepare("run", segmentQuery, [videoIDvidDuration, 0, 6, 0, 0, "uuidvd6", "testman", 15, 0, "sponsor", "skip", "YouTube", 21.37, 0, 0, "", videoIDvidDurationHash]), // not the oldest visible
db.prepare("run", segmentQuery, [videoIDvidDuration, 0, 7, -2, 0, "uuidvd7", "testman", 16, 0, "sponsor", "skip", "YouTube", 21.38, 0, 0, "", videoIDvidDurationHash]), // downvoted, not the oldest
]);
await Promise.all([
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual1", videoIDCasual, Service.YouTube, videoIDCasualHash, "clever", 1, 0, Date.now()]),
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual2", videoIDCasualDownvoted, Service.YouTube, videoIDCasualDownvotedHash, "clever", 1, 1, Date.now()]),
db.prepare("run", insertCasualVotesQuery, ["postBrandCasual3", videoIDCasualDownvoted, Service.YouTube, videoIDCasualDownvotedHash, "other", 4, 1, Date.now()])
]);
});
it("should get top titles and thumbnails", async () => {
@ -335,9 +346,28 @@ describe("getBranding", () => {
assert.strictEqual(result2.data[videoIDvidDuration].videoDuration, correctDuration);
});
it("should get casual votes", async () => {
await checkVideo(videoIDCasual, videoIDCasualHash, true, {
casualVotes: [{
id: "clever",
count: 1
}]
});
});
it("should not get casual votes with downvotes", async () => {
await checkVideo(videoIDCasualDownvoted, videoIDCasualDownvotedHash, true, {
casualVotes: [{
id: "other",
count: 3
}]
});
});
async function checkVideo(videoID: string, videoIDHash: string, fetchAll: boolean, expected: {
titles: TitleResult[],
thumbnails: ThumbnailResult[]
titles?: TitleResult[],
thumbnails?: ThumbnailResult[],
casualVotes?: CasualVote[]
}) {
const result1 = await getBranding({ videoID, fetchAll });
const result2 = await getBrandingByHash(videoIDHash, { fetchAll });

132
test/cases/postCasual.ts Normal file
View file

@ -0,0 +1,132 @@
import { db } from "../../src/databases/databases";
import { client } from "../utils/httpClient";
import assert from "assert";
import { Service } from "../../src/types/segments.model";
describe("postCasual", () => {
const userID1 = `PostCasualUser1${".".repeat(16)}`;
const userID2 = `PostCasualUser2${".".repeat(16)}`;
const userID3 = `PostCasualUser3${".".repeat(16)}`;
const endpoint = "/api/casual";
const postCasual = (data: Record<string, any>) => client({
method: "POST",
url: endpoint,
data
});
const queryCasualVotesByVideo = (videoID: string, all = false) => db.prepare(all ? "all" : "get", `SELECT * FROM "casualVotes" WHERE "videoID" = ? ORDER BY "timeSubmitted" DESC`, [videoID]);
it("submit casual vote", async () => {
const videoID = "postCasual1";
const res = await postCasual({
category: "clever",
userID: userID1,
service: Service.YouTube,
videoID
});
assert.strictEqual(res.status, 200);
const dbVotes = await queryCasualVotesByVideo(videoID);
assert.strictEqual(dbVotes.category, "clever");
assert.strictEqual(dbVotes.upvotes, 1);
assert.strictEqual(dbVotes.downvotes, 0);
});
it("submit same casual vote again", async () => {
const videoID = "postCasual1";
const res = await postCasual({
category: "clever",
userID: userID1,
service: Service.YouTube,
videoID
});
assert.strictEqual(res.status, 200);
const dbVotes = await queryCasualVotesByVideo(videoID);
assert.strictEqual(dbVotes.category, "clever");
assert.strictEqual(dbVotes.upvotes, 1);
assert.strictEqual(dbVotes.downvotes, 0);
});
it("submit casual upvote", async () => {
const videoID = "postCasual1";
const res = await postCasual({
category: "clever",
userID: userID2,
service: Service.YouTube,
videoID
});
assert.strictEqual(res.status, 200);
const dbVotes = await queryCasualVotesByVideo(videoID);
assert.strictEqual(dbVotes.category, "clever");
assert.strictEqual(dbVotes.upvotes, 2);
assert.strictEqual(dbVotes.downvotes, 0);
});
it("submit casual downvote from same user", async () => {
const videoID = "postCasual1";
const res = await postCasual({
category: "clever",
downvote: true,
userID: userID1,
service: Service.YouTube,
videoID
});
assert.strictEqual(res.status, 200);
const dbVotes = await queryCasualVotesByVideo(videoID);
assert.strictEqual(dbVotes.category, "clever");
assert.strictEqual(dbVotes.upvotes, 1);
assert.strictEqual(dbVotes.downvotes, 1);
});
it("submit casual downvote from different user", async () => {
const videoID = "postCasual1";
const res = await postCasual({
category: "clever",
downvote: true,
userID: userID3,
service: Service.YouTube,
videoID
});
assert.strictEqual(res.status, 200);
const dbVotes = await queryCasualVotesByVideo(videoID);
assert.strictEqual(dbVotes.category, "clever");
assert.strictEqual(dbVotes.upvotes, 1);
assert.strictEqual(dbVotes.downvotes, 2);
});
it("submit casual upvote from same user", async () => {
const videoID = "postCasual1";
const res = await postCasual({
category: "clever",
downvote: false,
userID: userID3,
service: Service.YouTube,
videoID
});
assert.strictEqual(res.status, 200);
const dbVotes = await queryCasualVotesByVideo(videoID);
assert.strictEqual(dbVotes.category, "clever");
assert.strictEqual(dbVotes.upvotes, 2);
assert.strictEqual(dbVotes.downvotes, 1);
});
});