Merge pull request #610 from mini-bomba/warning-history

Keep a history of warnings in the public database
This commit is contained in:
Ajay Ramachandran
2025-09-19 02:15:59 -04:00
committed by GitHub
39 changed files with 1001 additions and 1238 deletions

View File

@@ -31,15 +31,15 @@ module.exports = {
}, },
overrides: [ overrides: [
{ {
files: ["src/**/*.ts"], files: ["**/*.ts"],
parserOptions: { parserOptions: {
project: ["./tsconfig.json"], project: ["./tsconfig.eslint.json"],
}, },
rules: { rules: {
"@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-floating-promises" : "warn" "@typescript-eslint/no-floating-promises" : "error"
} }
}, },
], ],

View File

@@ -56,7 +56,6 @@
] ]
} }
], ],
"hoursAfterWarningExpires": 24,
"rateLimit": { "rateLimit": {
"vote": { "vote": {
"windowMs": 900000, "windowMs": 900000,

View File

@@ -25,8 +25,6 @@
"webhooks": [], "webhooks": [],
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "poi_highlight"], // List of supported categories any other category will be rejected "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "poi_highlight"], // List of supported categories any other category will be rejected
"getTopUsersCacheTimeMinutes": 5, // cacheTime for getTopUsers result in minutes "getTopUsersCacheTimeMinutes": 5, // cacheTime for getTopUsers result in minutes
"maxNumberOfActiveWarnings": 3, // Users with this number of warnings will be blocked until warnings expire
"hoursAfterWarningExpire": 24,
"rateLimit": { "rateLimit": {
"vote": { "vote": {
"windowMs": 900000, // 15 minutes "windowMs": 900000, // 15 minutes

View File

@@ -0,0 +1,7 @@
BEGIN TRANSACTION;
ALTER TABLE "warnings" ADD "disableTime" INTEGER NULL;
UPDATE "config" SET value = 45 WHERE key = 'version';
COMMIT;

View File

@@ -37,8 +37,6 @@ addDefaults(config, {
}, },
deArrowTypes: ["title", "thumbnail"], deArrowTypes: ["title", "thumbnail"],
maxTitleLength: 110, maxTitleLength: 110,
maxNumberOfActiveWarnings: 1,
hoursAfterWarningExpires: 16300000,
adminUserID: "", adminUserID: "",
discordCompletelyIncorrectReportWebhookURL: null, discordCompletelyIncorrectReportWebhookURL: null,
discordFirstTimeSubmissionsWebhookURL: null, discordFirstTimeSubmissionsWebhookURL: null,

View File

@@ -3,7 +3,6 @@ import { CronJob } from "cron";
import { config as serverConfig } from "../config"; import { config as serverConfig } from "../config";
import { Logger } from "../utils/logger"; import { Logger } from "../utils/logger";
import { db } from "../databases/databases"; import { db } from "../databases/databases";
import { DBSegment } from "../types/segments.model";
const jobConfig = serverConfig?.crons?.downvoteSegmentArchive; const jobConfig = serverConfig?.crons?.downvoteSegmentArchive;
@@ -14,18 +13,18 @@ export const archiveDownvoteSegment = async (dayLimit: number, voteLimit: number
Logger.info(`DownvoteSegmentArchiveJob starts at ${timeNow}`); Logger.info(`DownvoteSegmentArchiveJob starts at ${timeNow}`);
try { try {
// insert into archive sponsorTime // insert into archive sponsorTime
await db.prepare( await db.prepare(
"run", "run",
`INSERT INTO "archivedSponsorTimes" `INSERT INTO "archivedSponsorTimes"
SELECT * SELECT *
FROM "sponsorTimes" FROM "sponsorTimes"
WHERE "votes" < ? AND (? - "timeSubmitted") > ?`, WHERE "votes" < ? AND (? - "timeSubmitted") > ?`,
[ [
voteLimit, voteLimit,
timeNow, timeNow,
threshold threshold
] ]
) as DBSegment[]; );
} catch (err) { } catch (err) {
Logger.error("Execption when insert segment in archivedSponsorTimes"); Logger.error("Execption when insert segment in archivedSponsorTimes");
@@ -35,15 +34,15 @@ export const archiveDownvoteSegment = async (dayLimit: number, voteLimit: number
// remove from sponsorTime // remove from sponsorTime
try { try {
await db.prepare( await db.prepare(
"run", "run",
'DELETE FROM "sponsorTimes" WHERE "votes" < ? AND (? - "timeSubmitted") > ?', 'DELETE FROM "sponsorTimes" WHERE "votes" < ? AND (? - "timeSubmitted") > ?',
[ [
voteLimit, voteLimit,
timeNow, timeNow,
threshold threshold
] ]
) as DBSegment[]; );
} catch (err) { } catch (err) {
Logger.error("Execption when deleting segment in sponsorTimes"); Logger.error("Execption when deleting segment in sponsorTimes");

View File

@@ -6,7 +6,10 @@ export interface QueryOption {
export interface IDatabase { export interface IDatabase {
init(): Promise<void>; init(): Promise<void>;
prepare(type: QueryType, query: string, params?: any[], options?: QueryOption): Promise<any | any[] | void>; prepare(type: "run", query: string, params?: any[], options?: QueryOption): Promise<void>;
prepare(type: "get", query: string, params?: any[], options?: QueryOption): Promise<any>;
prepare(type: "all", query: string, params?: any[], options?: QueryOption): Promise<any[]>;
prepare(type: QueryType, query: string, params?: any[], options?: QueryOption): Promise<any>;
highLoad(): boolean; highLoad(): boolean;

View File

@@ -109,7 +109,7 @@ export class Postgres implements IDatabase {
} }
} }
async prepare(type: QueryType, query: string, params?: any[], options: QueryOption = {}): Promise<any[]> { async prepare(type: QueryType, query: string, params?: any[], options: QueryOption = {}): Promise<any> {
// Convert query to use numbered parameters // Convert query to use numbered parameters
let count = 1; let count = 1;
for (let char = 0; char < query.length; char++) { for (let char = 0; char < query.length; char++) {

View File

@@ -13,7 +13,7 @@ export class Sqlite implements IDatabase {
} }
// eslint-disable-next-line require-await // eslint-disable-next-line require-await
async prepare(type: QueryType, query: string, params: any[] = []): Promise<any[]> { async prepare(type: QueryType, query: string, params: any[] = []): Promise<any> {
// Logger.debug(`prepare (sqlite): type: ${type}, query: ${query}, params: ${params}`); // Logger.debug(`prepare (sqlite): type: ${type}, query: ${query}, params: ${params}`);
const preparedQuery = this.db.prepare(Sqlite.processQuery(query)); const preparedQuery = this.db.prepare(Sqlite.processQuery(query));

View File

@@ -122,7 +122,7 @@ function chooseSegment<T extends DBSegment>(choices: T[]): FullVideoSegmentVideo
return { return {
segments: transformDBSegments(choices), segments: transformDBSegments(choices),
hasStartSegment hasStartSegment
} };
} }
// if locked, only choose from locked // if locked, only choose from locked
const locked = choices.filter((segment) => segment.locked); const locked = choices.filter((segment) => segment.locked);

View File

@@ -164,20 +164,15 @@ async function autoModerateSubmission(apiVideoDetails: videoDetails,
} }
async function checkUserActiveWarning(userID: HashedUserID): Promise<CheckResult> { async function checkUserActiveWarning(userID: HashedUserID): Promise<CheckResult> {
const MILLISECONDS_IN_HOUR = 3600000; const warning = await db.prepare("get",
const now = Date.now();
const warnings = (await db.prepare("all",
`SELECT "reason" `SELECT "reason"
FROM warnings FROM warnings
WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1 AND type = 0 WHERE "userID" = ? AND enabled = 1 AND type = 0
ORDER BY "issueTime" DESC`, ORDER BY "issueTime" DESC`,
[ [userID],
userID, ) as {reason: string};
Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))
],
) as {reason: string}[]).sort((a, b) => (b?.reason?.length ?? 0) - (a?.reason?.length ?? 0));
if (warnings?.length >= config.maxNumberOfActiveWarnings) { if (warning != null) {
const defaultMessage = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes" const defaultMessage = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes"
+ " that are not malicious, and we just want to clarify the rules. " + " that are not malicious, and we just want to clarify the rules. "
+ "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? " + "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
@@ -185,7 +180,7 @@ async function checkUserActiveWarning(userID: HashedUserID): Promise<CheckResult
return { return {
pass: false, pass: false,
errorMessage: defaultMessage + (warnings[0]?.reason?.length > 0 ? `\n\nTip message: '${warnings[0].reason}'` : ""), errorMessage: defaultMessage + (warning.reason?.length > 0 ? `\n\nTip message: '${warning.reason}'` : ""),
errorCode: 403 errorCode: 403
}; };
} }

View File

@@ -4,7 +4,6 @@ import { db } from "../databases/databases";
import { isUserVIP } from "../utils/isUserVIP"; import { isUserVIP } from "../utils/isUserVIP";
import { getHashCache } from "../utils/getHashCache"; import { getHashCache } from "../utils/getHashCache";
import { HashedUserID, UserID } from "../types/user.model"; import { HashedUserID, UserID } from "../types/user.model";
import { config } from "../config";
import { generateWarningDiscord, warningData, dispatchEvent } from "../utils/webhookUtils"; import { generateWarningDiscord, warningData, dispatchEvent } from "../utils/webhookUtils";
import { WarningType } from "../types/warning.model"; import { WarningType } from "../types/warning.model";
@@ -16,12 +15,7 @@ type warningEntry = {
reason: string reason: string
} }
function checkExpiredWarning(warning: warningEntry): boolean { const MAX_EDIT_DELAY = 900000; // 15 mins
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const expiry = Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR));
return warning.issueTime > expiry && !warning.enabled;
}
const getUsername = (userID: HashedUserID) => db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID], { useReplica: true }); const getUsername = (userID: HashedUserID) => db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID], { useReplica: true });
@@ -44,30 +38,30 @@ export async function postWarning(req: Request, res: Response): Promise<Response
try { try {
if (enabled) { if (enabled) {
const previousWarning = await db.prepare("get", 'SELECT * FROM "warnings" WHERE "userID" = ? AND "issuerUserID" = ? AND "type" = ?', [userID, issuerUserID, type]) as warningEntry; if (!reason) {
return res.status(400).json({ "message": "Missing warning reason" });
}
const previousWarning = await db.prepare("get", 'SELECT * FROM "warnings" WHERE "userID" = ? AND "type" = ? AND "enabled" = 1', [userID, type]) as warningEntry;
if (!previousWarning) { if (!previousWarning) {
if (!reason) {
return res.status(400).json({ "message": "Missing warning reason" });
}
await db.prepare( await db.prepare(
"run", "run",
'INSERT INTO "warnings" ("userID", "issueTime", "issuerUserID", "enabled", "reason", "type") VALUES (?, ?, ?, 1, ?, ?)', 'INSERT INTO "warnings" ("userID", "issueTime", "issuerUserID", "enabled", "reason", "type") VALUES (?, ?, ?, 1, ?, ?)',
[userID, issueTime, issuerUserID, reason, type] [userID, issueTime, issuerUserID, reason, type]
); );
resultStatus = "issued to"; resultStatus = "issued to";
// check if warning is still within issue time and warning is not enabled // allow a warning to be edited by the same vip within 15 mins of issuing
} else if (checkExpiredWarning(previousWarning) ) { } else if (issuerUserID === previousWarning.issuerUserID && (Date.now() - MAX_EDIT_DELAY) < previousWarning.issueTime) {
await db.prepare( await db.prepare(
"run", 'UPDATE "warnings" SET "enabled" = 1, "reason" = ? WHERE "userID" = ? AND "issueTime" = ?', "run", 'UPDATE "warnings" SET "reason" = ? WHERE "userID" = ? AND "issueTime" = ?',
[reason, userID, previousWarning.issueTime] [reason, userID, previousWarning.issueTime]
); );
resultStatus = "re-enabled for"; resultStatus = "edited for";
} else { } else {
return res.sendStatus(409); return res.sendStatus(409);
} }
} else { } else {
await db.prepare("run", 'UPDATE "warnings" SET "enabled" = 0 WHERE "userID" = ? AND "type" = ?', [userID, type]); await db.prepare("run", 'UPDATE "warnings" SET "enabled" = 0, "disableTime" = ? WHERE "userID" = ? AND "type" = ? AND "enabled" = 1', [issueTime, userID, type]);
resultStatus = "removed from"; resultStatus = "removed from";
} }

View File

@@ -7,7 +7,7 @@ import { isUserBanned } from "../utils/checkBan";
import { HashedUserID } from "../types/user.model"; import { HashedUserID } from "../types/user.model";
import { isRequestInvalid } from "../utils/requestValidator"; import { isRequestInvalid } from "../utils/requestValidator";
function logUserNameChange(userID: string, newUserName: string, oldUserName: string, updatedByAdmin: boolean): Promise<Response> { function logUserNameChange(userID: string, newUserName: string, oldUserName: string, updatedByAdmin: boolean): Promise<void> {
return privateDB.prepare("run", return privateDB.prepare("run",
`INSERT INTO "userNameLogs"("userID", "newUserName", "oldUserName", "updatedByAdmin", "updatedAt") VALUES(?, ?, ?, ?, ?)`, `INSERT INTO "userNameLogs"("userID", "newUserName", "oldUserName", "updatedByAdmin", "updatedAt") VALUES(?, ?, ?, ?, ?)`,
[userID, newUserName, oldUserName, + updatedByAdmin, new Date().getTime()] [userID, newUserName, oldUserName, + updatedByAdmin, new Date().getTime()]

View File

@@ -379,14 +379,12 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
return { status: 400 }; return { status: 400 };
} }
const MILLISECONDS_IN_HOUR = 3600000; const warning = (await db.prepare("get", `SELECT "reason" FROM warnings WHERE "userID" = ? AND enabled = 1 AND type = 0`,
const now = Date.now(); [nonAnonUserID],
const warnings = (await db.prepare("all", `SELECT "reason" FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1 AND type = 0`,
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
)); ));
if (warnings.length >= config.maxNumberOfActiveWarnings) { if (warning != null) {
const warningReason = warnings[0]?.reason; const warningReason = warning.reason;
lock.unlock(); lock.unlock();
return { status: 403, message: "Vote rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. " + return { status: 403, message: "Vote rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. " +
"Could you please send a message in Discord or Matrix so we can further help you?" + "Could you please send a message in Discord or Matrix so we can further help you?" +

View File

@@ -109,8 +109,6 @@ export interface SBSConfig {
categorySupport: Record<string, string[]>; categorySupport: Record<string, string[]>;
maxTitleLength: number; maxTitleLength: number;
getTopUsersCacheTimeMinutes: number; getTopUsersCacheTimeMinutes: number;
maxNumberOfActiveWarnings: number;
hoursAfterWarningExpires: number;
rateLimit: { rateLimit: {
vote: RateLimitConfig; vote: RateLimitConfig;
view: RateLimitConfig; view: RateLimitConfig;

View File

@@ -50,7 +50,6 @@
] ]
} }
], ],
"hoursAfterWarningExpires": 24,
"rateLimit": { "rateLimit": {
"vote": { "vote": {
"windowMs": 900000, "windowMs": 900000,

View File

@@ -73,11 +73,11 @@ describe("getUserID", () => {
) )
); );
it("Should be able to get multiple fuzzy user info from middle", () => { it("Should be able to get multiple fuzzy user info from middle", () =>
validateSearch("user", validateSearch("user",
[users["fuzzy_1"], users["fuzzy_2"], users["specific_1"]] [users["fuzzy_1"], users["fuzzy_2"], users["specific_1"]]
); )
}); );
it("Should be able to get with fuzzy public ID", () => { it("Should be able to get with fuzzy public ID", () => {
const userID = users["public_1"].pubID.substring(0,60); const userID = users["public_1"].pubID.substring(0,60);

View File

@@ -81,19 +81,19 @@ describe("getUserInfo", () => {
// warnings & bans // warnings & bans
// warn-0 // warn-0
insertWarning(db, users["warn-0"].pubID, { reason: "warning0-0", issueTime: 10 }); await insertWarning(db, users["warn-0"].pubID, { reason: "warning0-0", issueTime: 10 });
// warn-1 // warn-1
insertWarning(db, users["warn-1"].pubID, { reason: "warning1-0", issueTime: 20 }); await insertWarning(db, users["warn-1"].pubID, { reason: "warning1-0", issueTime: 20 });
insertWarning(db, users["warn-1"].pubID, { reason: "warning1-1", issueTime: 30 }); await insertWarning(db, users["warn-1"].pubID, { reason: "warning1-1", issueTime: 30 });
// warn -2 // warn -2
insertWarning(db, users["warn-2"].pubID, { reason: "warning2-0", issueTime: 40, enabled: false }); await insertWarning(db, users["warn-2"].pubID, { reason: "warning2-0", issueTime: 40, enabled: false });
// warn-3 // warn-3
insertWarning(db, users["warn-3"].pubID, { reason: "warning3-0", issueTime: 50 }); await insertWarning(db, users["warn-3"].pubID, { reason: "warning3-0", issueTime: 50 });
insertWarning(db, users["warn-3"].pubID, { reason: "warning3-1", issueTime: 60, enabled: false }); await insertWarning(db, users["warn-3"].pubID, { reason: "warning3-1", issueTime: 60, enabled: false });
// ban- // ban-
insertBan(db, users["ban-1"].pubID); await insertBan(db, users["ban-1"].pubID);
insertBan(db, users["ban-2"].pubID); await insertBan(db, users["ban-2"].pubID);
}); });
it("Should be able to get a 200", () => statusTest(200, { userID: users["n-1"].privID })); it("Should be able to get a 200", () => statusTest(200, { userID: users["n-1"].privID }));

View File

@@ -33,11 +33,11 @@ const checkUserViews = (user: User) =>
}); });
describe("getViewsForUser", function() { describe("getViewsForUser", function() {
before(() => { before(async () => {
// add views for users // add views for users
insertSegment(db, { userID: users["u-1"].pubID, views: users["u-1"].info.views1 }); await insertSegment(db, { userID: users["u-1"].pubID, views: users["u-1"].info.views1 });
insertSegment(db, { userID: users["u-1"].pubID, views: users["u-1"].info.views2 }); await insertSegment(db, { userID: users["u-1"].pubID, views: users["u-1"].info.views2 });
insertSegment(db, { userID: users["u-2"].pubID, views: users["u-2"].info.views }); await insertSegment(db, { userID: users["u-2"].pubID, views: users["u-2"].info.views });
}); });
it("Should get back views for user one", () => it("Should get back views for user one", () =>
checkUserViews(users["u-1"]) checkUserViews(users["u-1"])

View File

@@ -15,7 +15,6 @@ describe("postBranding", () => {
const userID5 = `PostBrandingUser5${".".repeat(16)}`; const userID5 = `PostBrandingUser5${".".repeat(16)}`;
const userID6 = `PostBrandingUser6${".".repeat(16)}`; const userID6 = `PostBrandingUser6${".".repeat(16)}`;
const userID7 = `PostBrandingUser7${".".repeat(16)}`; const userID7 = `PostBrandingUser7${".".repeat(16)}`;
const userID8 = `PostBrandingUser8${".".repeat(16)}`;
const bannedUser = `BannedPostBrandingUser${".".repeat(16)}`; const bannedUser = `BannedPostBrandingUser${".".repeat(16)}`;

View File

@@ -50,12 +50,12 @@ describe("postSkipSegments", () => {
const queryDatabaseActionType = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseActionType = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]); const queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]);
before(() => { before(async () => {
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
db.prepare("run", insertSponsorTimeQuery, ["full_video_segment", 0, 0, 0, "full-video-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 0, 0, "full_video_segment"]); await db.prepare("run", insertSponsorTimeQuery, ["full_video_segment", 0, 0, 0, "full-video-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 0, 0, "full_video_segment"]);
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)'; const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
db.prepare("run", insertVipUserQuery, [getHash(submitVIPuser)]); await db.prepare("run", insertVipUserQuery, [getHash(submitVIPuser)]);
}); });
it("Should be able to submit a single time (Params method)", (done) => { it("Should be able to submit a single time (Params method)", (done) => {

View File

@@ -19,11 +19,11 @@ describe("postSkipSegments - Automod 80%", () => {
const queryDatabaseCategory = (videoID: string) => db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]); const queryDatabaseCategory = (videoID: string) => db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]);
before(() => { before(async () => {
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 0, 1000, 0, "80percent-uuid-0", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]); await db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 0, 1000, 0, "80percent-uuid-0", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]);
db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 1001, 1005, 0, "80percent-uuid-1", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]); await db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 1001, 1005, 0, "80percent-uuid-1", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]);
db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 0, 5000, -2, "80percent-uuid-2", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]); await db.prepare("run", insertSponsorTimeQuery, [over80VideoID, 0, 5000, -2, "80percent-uuid-2", userIDHash, 0, 0, "interaction", "skip", 0, 0, over80VideoID]);
}); });
it("Should allow multiple times if total is under 80% of video (JSON method)", (done) => { it("Should allow multiple times if total is under 80% of video (JSON method)", (done) => {

View File

@@ -21,10 +21,10 @@ describe("postSkipSegments - duration", () => {
const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
before(() => { before(async () => {
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 0, 0, 0, "full-video-duration-uuid-0", userIDTwo, 0, 0, "sponsor", "full", 123, 0, "full_video_duration_segment"]); await db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 0, 0, 0, "full-video-duration-uuid-0", userIDTwo, 0, 0, "sponsor", "full", 123, 0, "full_video_duration_segment"]);
db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 25, 30, 0, "full-video-duration-uuid-1", userIDTwo, 0, 0, "sponsor", "skip", 123, 0, "full_video_duration_segment"]); await db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 25, 30, 0, "full-video-duration-uuid-1", userIDTwo, 0, 0, "sponsor", "skip", 123, 0, "full_video_duration_segment"]);
}); });
it("Should be able to submit a single time with a precise duration close to the one from the YouTube API (JSON method)", (done) => { it("Should be able to submit a single time with a precise duration close to the one from the YouTube API (JSON method)", (done) => {

View File

@@ -21,17 +21,17 @@ describe("postSkipSegments Features - Chapters", () => {
}; };
} }
before(() => { before(async () => {
const submitNumberOfTimes = 10; const submitNumberOfTimes = 10;
const submitUser_reputationHash = getHash(submitUser_reputation); const submitUser_reputationHash = getHash(submitUser_reputation);
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "actionType", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "actionType", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
for (let i = 0; i < submitNumberOfTimes; i++) { for (let i = 0; i < submitNumberOfTimes; i++) {
const uuid = `post_reputation_uuid-${i}`; const uuid = `post_reputation_uuid-${i}`;
const videoID = `post_reputation_video-${i}`; const videoID = `post_reputation_video-${i}`;
db.prepare("run", insertSponsorTimeQuery, [videoID, 1, 11, 5, 1, uuid, submitUser_reputationHash, 1597240000000, 50, "sponsor", "skip", 0]); await db.prepare("run", insertSponsorTimeQuery, [videoID, 1, 11, 5, 1, uuid, submitUser_reputationHash, 1597240000000, 50, "sponsor", "skip", 0]);
} }
// user feature // user feature
db.prepare("run", `INSERT INTO "userFeatures" ("userID", "feature", "issuerUserID", "timeSubmitted") VALUES(?, ?, ?, ?)`, [getHash(submitUser_feature), Feature.ChapterSubmitter, "generic-VIP", 0]); await db.prepare("run", `INSERT INTO "userFeatures" ("userID", "feature", "issuerUserID", "timeSubmitted") VALUES(?, ?, ?, ?)`, [getHash(submitUser_feature), Feature.ChapterSubmitter, "generic-VIP", 0]);
}); });
it("Should be able to submit a single chapter due to reputation (JSON method)", (done) => { it("Should be able to submit a single chapter due to reputation (JSON method)", (done) => {

View File

@@ -9,10 +9,10 @@ describe("postSkipSegments - LockedVideos", () => {
const videoID = "lockedVideo"; const videoID = "lockedVideo";
const userID = userIDOne; const userID = userIDOne;
before(() => { before(async () => {
const insertLockCategoriesQuery = `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") VALUES(?, ?, ?, ?)`; const insertLockCategoriesQuery = `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") VALUES(?, ?, ?, ?)`;
db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "sponsor", "Custom Reason"]); await db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "sponsor", "Custom Reason"]);
db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "intro", ""]); await db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "intro", ""]);
}); });
it("Should return 403 and custom reason for submiting in lockedCategory", (done) => { it("Should return 403 and custom reason for submiting in lockedCategory", (done) => {

View File

@@ -19,8 +19,8 @@ describe("postSkipSegments - shadowban", () => {
const queryDatabaseShadowhidden = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]); const queryDatabaseShadowhidden = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
before(() => { before(async () => {
db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, [banUser01Hash]); await db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, [banUser01Hash]);
}); });
it("Should automatically shadowban segments if user is banned", (done) => { it("Should automatically shadowban segments if user is banned", (done) => {

View File

@@ -21,10 +21,10 @@ describe("postSkipSegments - userAgent", () => {
}; };
const dbFormatSegment = convertSingleToDBFormat(segment); const dbFormatSegment = convertSingleToDBFormat(segment);
before(() => { before(async () => {
const insertLockCategoriesQuery = `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") VALUES(?, ?, ?, ?)`; const insertLockCategoriesQuery = `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason") VALUES(?, ?, ?, ?)`;
db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "sponsor", "Custom Reason"]); await db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "sponsor", "Custom Reason"]);
db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "intro", ""]); await db.prepare("run", insertLockCategoriesQuery, [getHash(VIPLockUser), videoID, "intro", ""]);
}); });
it("Should be able to submit with empty user-agent", (done) => { it("Should be able to submit with empty user-agent", (done) => {

View File

@@ -1,20 +1,11 @@
import { config } from "../../src/config";
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases"; import { db } from "../../src/databases/databases";
import assert from "assert"; import assert from "assert";
import { client } from "../utils/httpClient"; import { client } from "../utils/httpClient";
import { usersForSuite } from "../utils/randomUsers";
describe("postSkipSegments Warnings", () => { describe("postSkipSegments Warnings", () => {
// Constant and helpers // Constant and helpers
const warnUser01 = "warn-user01"; const users = usersForSuite("postSkipSegmentsWarnings");
const warnUser01Hash = getHash(warnUser01);
const warnUser02 = "warn-user02";
const warnUser02Hash = getHash(warnUser02);
const warnUser03 = "warn-user03";
const warnUser03Hash = getHash(warnUser03);
const warnUser04 = "warn-user04";
const warnUser04Hash = getHash(warnUser04);
const warnVideoID = "postSkipSegments-warn-video"; const warnVideoID = "postSkipSegments-warn-video";
const endpoint = "/api/skipSegments"; const endpoint = "/api/skipSegments";
@@ -24,104 +15,92 @@ describe("postSkipSegments Warnings", () => {
data data
}); });
before(() => { before(async () => {
const now = Date.now(); const now = Date.now();
const warnVip01Hash = getHash("postSkipSegmentsWarnVIP");
const reason01 = "Reason01"; const reason01 = "Reason01";
const reason02 = ""; const reason02 = "";
const reason03 = "Reason03"; const reason03 = "Reason03";
const MILLISECONDS_IN_HOUR = 3600000;
const WARNING_EXPIRATION_TIME = config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR;
const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)'; const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)';
// User 1 | 1 active | custom reason // User 1 | 1 active | custom reason
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, now]); await db.prepare("run", insertWarningQuery, [users.u01.public, users.vip01.public, 1, reason01, now]);
// User 2 | 1 inactive | default reason // User 2 | 1 inactive | default reason
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 0, reason02, now]); await db.prepare("run", insertWarningQuery, [users.u02.public, users.vip01.public, 0, reason02, now]);
// User 3 | 1 expired, active | custom reason // User 3 | 1 inactive, 1 active | different reasons
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - WARNING_EXPIRATION_TIME - 1000)]); await db.prepare("run", insertWarningQuery, [users.u03.public, users.vip01.public, 0, reason01, now]);
await db.prepare("run", insertWarningQuery, [users.u03.public, users.vip01.public, 1, reason03, now+1]);
// User 4 | 1 active | default reason // User 4 | 1 active | default reason
db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 1, reason02, now]); await db.prepare("run", insertWarningQuery, [users.u04.public, users.vip01.public, 1, reason02, now]);
}); });
it("Should be rejected with custom message if user has active warnings", (done) => { it("Should be rejected with custom message if user has active warnings", async () => {
postSkipSegmentJSON({ const res = await postSkipSegmentJSON({
userID: warnUser01, userID: users.u01.private,
videoID: warnVideoID, videoID: warnVideoID,
segments: [{ segments: [{
segment: [0, 10], segment: [0, 10],
category: "sponsor", category: "sponsor",
}], }],
}) });
.then(res => { assert.strictEqual(res.status, 403);
assert.strictEqual(res.status, 403); const errorMessage = res.data;
const errorMessage = res.data; const reason = "Reason01";
const reason = "Reason01"; const expected = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes"
const expected = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes" + " that are not malicious, and we just want to clarify the rules. "
+ " that are not malicious, and we just want to clarify the rules. " + "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
+ "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? " + `Your userID is ${users.u01.public}.\n\nTip message: '${reason}'`;
+ `Your userID is ${warnUser01Hash}.\n\nTip message: '${reason}'`;
assert.strictEqual(errorMessage, expected); assert.strictEqual(errorMessage, expected);
done();
})
.catch(err => done(err));
}); });
it("Should be accepted if user has inactive warning", (done) => { it("Should be accepted if user has inactive warning", async () => {
postSkipSegmentJSON({ const res = await postSkipSegmentJSON({
userID: warnUser02, userID: users.u02.private,
videoID: warnVideoID, videoID: warnVideoID,
segments: [{ segments: [{
segment: [50, 60], segment: [50, 60],
category: "sponsor", category: "sponsor",
}], }],
}) });
.then(res => { assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`);
assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`);
done();
})
.catch(err => done(err));
}); });
it("Should be accepted if user has expired warning", (done) => { it("Should be rejected with custom message if user has active warnings, even if has one inactive warning, should use current message", async () => {
postSkipSegmentJSON({ const res = await postSkipSegmentJSON({
userID: warnUser03, userID: users.u03.private,
videoID: warnVideoID, videoID: warnVideoID,
segments: [{ segments: [{
segment: [53, 60], segment: [10, 20],
category: "sponsor", category: "sponsor",
}], }],
}) });
.then(res => { assert.strictEqual(res.status, 403);
assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`); const errorMessage = res.data;
done(); const reason = "Reason03";
}) const expected = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes"
.catch(err => done(err)); + " that are not malicious, and we just want to clarify the rules. "
+ "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
+ `Your userID is ${users.u03.public}.\n\nTip message: '${reason}'`;
assert.strictEqual(errorMessage, expected);
}); });
it("Should be rejected with default message if user has active warning", (done) => { it("Should be rejected with default message if user has active warning", async () => {
postSkipSegmentJSON({ const res = await postSkipSegmentJSON({
userID: warnUser04, userID: users.u04.private,
videoID: warnVideoID, videoID: warnVideoID,
segments: [{ segments: [{
segment: [0, 10], segment: [0, 10],
category: "sponsor", category: "sponsor",
}], }],
}) });
.then(res => { assert.strictEqual(res.status, 403);
assert.strictEqual(res.status, 403); const errorMessage = res.data;
const errorMessage = res.data; const expected = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes"
const expected = "Submission rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes" + " that are not malicious, and we just want to clarify the rules. "
+ " that are not malicious, and we just want to clarify the rules. " + "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
+ "Could you please send a message in discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? " + `Your userID is ${users.u04.public}.`;
+ `Your userID is ${warnUser04Hash}.`; assert.strictEqual(errorMessage, expected);
assert.strictEqual(errorMessage, expected);
done();
})
.catch(err => done(err));
}); });
}); });

View File

@@ -3,199 +3,329 @@ import { db } from "../../src/databases/databases";
import { getHash } from "../../src/utils/getHash"; import { getHash } from "../../src/utils/getHash";
import assert from "assert"; import assert from "assert";
import { client } from "../utils/httpClient"; import { client } from "../utils/httpClient";
import { usersForSuite } from "../utils/randomUsers";
describe("postWarning", () => { describe("postWarning", () => {
// constants // constants
const endpoint = "/api/warnUser"; const endpoint = "/api/warnUser";
const getWarning = (userID: string, type = 0) => db.prepare("get", `SELECT "userID", "issueTime", "issuerUserID", enabled, "reason" FROM warnings WHERE "userID" = ? AND "type" = ?`, [userID, type]); const getWarning = (userID: string, type = 0) => db.prepare("all", `SELECT * FROM warnings WHERE "userID" = ? AND "type" = ? ORDER BY "issueTime" ASC`, [userID, type]);
const warneduserOneID = "warning-0"; const users = usersForSuite("postWarning");
const warnedUserTwoID = "warning-1";
const warnedUserOnePublicID = getHash(warneduserOneID);
const warnedUserTwoPublicID = getHash(warnedUserTwoID);
const warningVipOne = "warning-vip-1";
const warningVipTwo = "warning-vip-2";
const nonVipUser = "warning-non-vip";
before(async () => { before(async () => {
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(warningVipOne)]); const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)';
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(warningVipTwo)]); const insertWarningQueryWithDisableTime = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime", "disableTime") VALUES(?, ?, ?, ?, ?, ?)';
const HOUR = 60 * 60 * 1000;
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [users.vip1.public]);
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [users.vip2.public]);
await db.prepare("run", insertWarningQuery, [users.u1.public, users.vip1.public, 1, "warn reason 1", (Date.now() - 24 * HOUR)]); // 24 hours is much past the edit deadline
await db.prepare("run", insertWarningQuery, [users.u2.public, users.vip1.public, 1, "warn reason 2", (Date.now() - 24 * HOUR)]);
await db.prepare("run", insertWarningQuery, [users.u3.public, users.vip1.public, 1, "warn reason 3", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u4.public, users.vip1.public, 1, "warn reason 4", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u6.public, users.vip1.public, 1, "warn reason 6", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u9.public, users.vip1.public, 0, "warn reason 9", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u10.public, users.vip1.public, 1, "warn reason 10", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u11.public, users.vip1.public, 1, "warn reason 11", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u12.public, users.vip1.public, 0, "warn reason 12", Date.now()]);
await db.prepare("run", insertWarningQuery, [users.u13.public, users.vip1.public, 0, "warn reason 13", Date.now()]);
await db.prepare("run", insertWarningQueryWithDisableTime, [users.u14.public, users.vip1.public, 0, "warn reason 14", 123, 12345]);
await db.prepare("run", insertWarningQuery, [users.u14.public, users.vip1.public, 1, "another reason 14", Date.now()]);
}); });
it("Should be able to create warning if vip (exp 200)", (done) => { it("Should be able to create warning if vip (exp 200)", async () => {
const json = { const json = {
issuerUserID: warningVipOne, issuerUserID: users.vip1.private,
userID: warnedUserOnePublicID, userID: users.u0.public,
reason: "warning-reason-0" reason: "warning-reason-0"
}; };
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getWarning(json.userID);
const row = await getWarning(json.userID); const expected = {
const expected = { enabled: 1,
enabled: 1, issuerUserID: getHash(json.issuerUserID),
issuerUserID: getHash(json.issuerUserID), reason: json.reason,
reason: json.reason, };
}; assert.equal(row.length, 1);
assert.ok(partialDeepEquals(row, expected)); assert.ok(partialDeepEquals(row[0], expected));
done();
})
.catch(err => done(err));
}); });
it("Should be not be able to create a duplicate warning if vip", (done) => { it("Should be not be able to edit a warning if past deadline and same vip", async () => {
const json = { const json = {
issuerUserID: warningVipOne, issuerUserID: users.vip1.private,
userID: warnedUserOnePublicID, userID: users.u1.public,
reason: "edited reason 1",
}; };
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(async res => { assert.strictEqual(res.status, 409);
assert.strictEqual(res.status, 409); const row = await getWarning(json.userID);
const row = await getWarning(json.userID); const expected = {
const expected = { enabled: 1,
enabled: 1, issuerUserID: users.vip1.public,
issuerUserID: getHash(json.issuerUserID), };
}; assert.equal(row.length, 1);
assert.ok(partialDeepEquals(row, expected)); assert.ok(partialDeepEquals(row[0], expected));
done();
})
.catch(err => done(err));
}); });
it("Should be able to remove warning if vip", (done) => { it("Should be not be able to edit a warning if past deadline and different vip", async () => {
const json = { const json = {
issuerUserID: warningVipOne, issuerUserID: users.vip2.private,
userID: warnedUserOnePublicID, userID: users.u2.public,
reason: "edited reason 2",
};
const res = await client.post(endpoint, json);
assert.strictEqual(res.status, 409);
const row = await getWarning(json.userID);
const expected = {
enabled: 1,
issuerUserID: users.vip1.public,
};
assert.equal(row.length, 1);
assert.ok(partialDeepEquals(row[0], expected));
});
it("Should be able to remove warning if same vip as issuer", async () => {
const json = {
issuerUserID: users.vip1.private,
userID: users.u3.public,
enabled: false enabled: false
}; };
const beforeTime = Date.now();
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(async res => { const afterTime = Date.now();
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const row = await getWarning(json.userID); const row = await getWarning(json.userID);
const expected = { const expected = {
enabled: 0 enabled: 0
};
assert.ok(partialDeepEquals(row, expected));
done();
})
.catch(err => done(err));
});
it("Should not be able to create warning if not vip (exp 403)", (done) => {
const json = {
issuerUserID: nonVipUser,
userID: warnedUserOnePublicID,
}; };
assert.equal(row.length, 1);
client.post(endpoint, json) assert.ok(partialDeepEquals(row[0], expected));
.then(res => { // check disableTime
assert.strictEqual(res.status, 403); assert.ok(row[0].disableTime >= beforeTime && row[0].disableTime <= afterTime, "expected disableTime to be somewhere between execution start and end");
done();
})
.catch(err => done(err));
}); });
it("Should return 400 if missing body", (done) => { it("Should be able to remove warning if not the same vip as issuer", async () => {
client.post(endpoint, {})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should re-enable disabled warning", (done) => {
const json = { const json = {
issuerUserID: warningVipOne, issuerUserID: users.vip2.private,
userID: warnedUserOnePublicID, userID: users.u4.public,
enabled: true
};
client.post(endpoint, json)
.then(async res => {
assert.strictEqual(res.status, 200);
const data = await getWarning(json.userID);
const expected = {
enabled: 1
};
assert.ok(partialDeepEquals(data, expected));
done();
})
.catch(err => done(err));
});
it("Should be able to remove your own warning", (done) => {
const json = {
userID: warneduserOneID,
enabled: false enabled: false
}; };
const beforeTime = Date.now();
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(async res => { const afterTime = Date.now();
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const data = await getWarning(warnedUserOnePublicID); const row = await getWarning(json.userID);
const expected = { const expected = {
enabled: 0 enabled: 0
}; };
assert.ok(partialDeepEquals(data, expected)); assert.equal(row.length, 1);
done(); assert.ok(partialDeepEquals(row[0], expected));
}) // check disableTime
.catch(err => done(err)); assert.ok(row[0].disableTime >= beforeTime && row[0].disableTime <= afterTime, "expected disableTime to be somewhere between execution start and end");
}); });
it("Should not be able to add your own warning", (done) => { it("Should not be able to create warning if not vip (exp 403)", async () => {
const json = { const json = {
userID: warneduserOneID, issuerUserID: users.nonvip.private,
userID: users.u5.public,
reason: "warn reason 5",
};
const res = await client.post(endpoint, json);
assert.strictEqual(res.status, 403);
});
it("Should return 400 if missing body", async () => {
const res = await client.post(endpoint, {});
assert.strictEqual(res.status, 400);
});
it("Should be able to remove your own warning", async () => {
const json = {
userID: users.u6.private,
enabled: false
};
const beforeTime = Date.now();
const res = await client.post(endpoint, json);
const afterTime = Date.now();
assert.strictEqual(res.status, 200);
const row = await getWarning(users.u6.public);
const expected = {
enabled: 0
};
assert.equal(row.length, 1);
assert.ok(partialDeepEquals(row[0], expected));
// check disableTime
assert.ok(row[0].disableTime >= beforeTime && row[0].disableTime <= afterTime, "expected disableTime to be somewhere between execution start and end");
});
it("Should not be able to add your own warning", async () => {
const json = {
userID: users.u7.private,
enabled: true,
reason: "warn reason 7",
};
const res = await client.post(endpoint, json);
assert.strictEqual(res.status, 403);
const data = await getWarning(users.u7.public);
assert.equal(data.length, 0);
});
it("Should not be able to warn a user without reason", async () => {
const json = {
issuerUserID: users.vip1.private,
userID: users.u8.public,
enabled: true enabled: true
}; };
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(async res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 403);
const data = await getWarning(warnedUserOnePublicID);
const expected = {
enabled: 0
};
assert.ok(partialDeepEquals(data, expected));
done();
})
.catch(err => done(err));
}); });
it("Should not be able to warn a user without reason", (done) => { it("Should not be able to re-warn a user without reason", async () => {
const json = { const json = {
issuerUserID: warningVipOne, issuerUserID: users.vip1.private,
userID: warnedUserTwoPublicID, userID: users.u9.public,
enabled: true enabled: true
}; };
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
}); });
it("Should be able to re-warn a user without reason", (done) => { it("Should be able to edit a warning if within deadline and same vip", async () => {
const json = { const json = {
issuerUserID: warningVipOne, issuerUserID: users.vip1.private,
userID: warnedUserOnePublicID, userID: users.u10.public,
enabled: true enabled: true,
reason: "edited reason 10",
}; };
client.post(endpoint, json) const res = await client.post(endpoint, json);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getWarning(json.userID);
const data = await getWarning(warnedUserOnePublicID); const expected = {
const expected = { enabled: 1,
enabled: 1 issuerUserID: users.vip1.public,
}; reason: json.reason,
assert.ok(partialDeepEquals(data, expected)); };
done(); assert.equal(row.length, 1);
}) assert.ok(partialDeepEquals(row[0], expected));
.catch(err => done(err)); });
it("Should not be able to edit a warning if within deadline and different vip", async () => {
const json = {
issuerUserID: users.vip2.private,
userID: users.u11.public,
enabled: true,
reason: "edited reason 11",
};
const res = await client.post(endpoint, json);
assert.strictEqual(res.status, 409);
const row = await getWarning(json.userID);
const expected = {
enabled: 1,
issuerUserID: users.vip1.public,
reason: "warn reason 11",
};
assert.equal(row.length, 1);
assert.ok(partialDeepEquals(row[0], expected));
});
it("Should be able to warn a previously warned user again (same vip)", async () => {
const json = {
issuerUserID: users.vip1.private,
userID: users.u12.public,
enabled: true,
reason: "new reason 12",
};
const res = await client.post(endpoint, json);
assert.strictEqual(res.status, 200);
const row = await getWarning(json.userID);
const expected = [
{
enabled: 0,
issuerUserID: users.vip1.public,
reason: "warn reason 12",
},
{
enabled: 1,
issuerUserID: users.vip1.public,
reason: "new reason 12",
}
];
assert.equal(row.length, 2);
assert.ok(partialDeepEquals(row[0], expected[0]), "warning 1");
assert.ok(partialDeepEquals(row[1], expected[1]), "warning 2");
});
it("Should be able to warn a previously warned user again (different vip)", async () => {
const json = {
issuerUserID: users.vip2.private,
userID: users.u13.public,
enabled: true,
reason: "new reason 13",
};
const res = await client.post(endpoint, json);
assert.strictEqual(res.status, 200);
const row = await getWarning(json.userID);
const expected = [
{
enabled: 0,
issuerUserID: users.vip1.public,
reason: "warn reason 13",
},
{
enabled: 1,
issuerUserID: users.vip2.public,
reason: "new reason 13",
}
];
assert.equal(row.length, 2);
assert.ok(partialDeepEquals(row[0], expected[0]), "warning 1");
assert.ok(partialDeepEquals(row[1], expected[1]), "warning 2");
});
it("Disabling a warning should only set disableTime for the active warning", async () => {
const json = {
issuerUserID: users.vip2.private,
userID: users.u14.public,
enabled: false,
};
const beforeTime = Date.now();
const res = await client.post(endpoint, json);
const afterTime = Date.now();
assert.strictEqual(res.status, 200);
const row = await getWarning(json.userID);
const expected = [
{
enabled: 0,
issuerUserID: users.vip1.public,
reason: "warn reason 14",
disableTime: 12345,
},
{
enabled: 0,
issuerUserID: users.vip1.public,
reason: "another reason 14",
}
];
assert.equal(row.length, 2);
assert.ok(partialDeepEquals(row[0], expected[0]), "warning 1");
assert.ok(partialDeepEquals(row[1], expected[1]), "warning 2");
// check disableTime
assert.ok(row[1].disableTime >= beforeTime && row[1].disableTime <= afterTime, "expected disableTime to be somewhere between execution start and end");
}); });
}); });

View File

@@ -9,44 +9,32 @@ const randKey2 = genRandom(16);
const randKey3 = genRandom(); const randKey3 = genRandom();
const randValue3 = genRandom(); const randValue3 = genRandom();
const redisGetCheck = (key: string, expected: string | null, done: Mocha.Done): Promise<void> =>
redis.get(key)
.then(res => {
assert.strictEqual(res, expected);
done();
}).catch(err => done(err));
describe("redis test", function() { describe("redis test", function() {
before(async function() { before(async function() {
if (!config.redis?.enabled) this.skip(); if (!config.redis?.enabled) this.skip();
await redis.set(randKey1, randValue1); await redis.set(randKey1, randValue1);
}); });
it("Should get stored value", (done) => { it("Should get stored value", async () => {
redisGetCheck(randKey1, randValue1, done); const res = await redis.get(randKey1);
assert.strictEqual(res, randValue1);
}); });
it("Should not be able to get not stored value", (done) => { it("Should not be able to get not stored value", async () => {
redis.get(randKey2) const res = await redis.get(randKey2);
.then(res => { assert.strictEqual(res, null, "Value should not be found");
if (res) done("Value should not be found");
done();
}).catch(err => done(err));
}); });
it("Should be able to delete stored value", (done) => { it("Should be able to delete stored value", async () => {
redis.del(randKey1) await redis.del(randKey1);
.catch(err => done(err)) const res = await redis.get(randKey1);
.then(() => redisGetCheck(randKey1, null, done)); assert.strictEqual(res, null, "Deleted key should not be found");
}); });
it("Should be able to set expiring value", (done) => { it("Should be able to set expiring value", async () => {
redis.setEx(randKey3, 8400, randValue3) await redis.setEx(randKey3, 8400, randValue3);
.catch(err => done(err)) const res = await redis.get(randKey3);
.then(() => redisGetCheck(randKey3, randValue3, done)); assert.strictEqual(res, randValue3);
}); });
it("Should continue when undefined value is fetched", (done) => { it("Should continue when undefined value is fetched", async () => {
const undefkey = `undefined.${genRandom()}`; const undefkey = `undefined.${genRandom()}`;
redis.get(undefkey) const res = await redis.get(undefkey);
.then(result => { assert.strictEqual(res, null);
assert.ok(!result); // result should be falsy
done();
});
}); });
}); });

View File

@@ -56,12 +56,11 @@ function wellFormatUserName(userName: string) {
return userName.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""); return userName.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
} }
async function testUserNameChangelog(userID: string, newUserName: string, oldUserName: string, byAdmin: boolean, done: Mocha.Done) { async function testUserNameChangelog(userID: string, newUserName: string, oldUserName: string, byAdmin: boolean) {
const log = await getLastLogUserNameChange(userID); const log = await getLastLogUserNameChange(userID);
assert.strictEqual(newUserName, log.newUserName); assert.strictEqual(newUserName, log.newUserName);
assert.strictEqual(oldUserName, log.oldUserName); assert.strictEqual(oldUserName, log.oldUserName);
assert.strictEqual(byAdmin, Boolean(log.updatedByAdmin)); assert.strictEqual(byAdmin, Boolean(log.updatedByAdmin));
return done();
} }
const endpoint = "/api/setUsername"; const endpoint = "/api/setUsername";
@@ -111,147 +110,104 @@ describe("setUsername", () => {
} }
}); });
it("Should be able to set username that has never been set", (done) => { it("Should be able to set username that has never been set", async () => {
postSetUserName(user00PrivateUserID, username00) const res = await postSetUserName(user00PrivateUserID, username00);
.then(async res => { const usernameInfo = await getUsernameInfo(getHash(user00PrivateUserID));
const usernameInfo = await getUsernameInfo(getHash(user00PrivateUserID)); assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); assert.strictEqual(usernameInfo.userName, username00);
assert.strictEqual(usernameInfo.userName, username00); assert.notStrictEqual(usernameInfo.locked, 1, "username should not be locked");
assert.notStrictEqual(usernameInfo.locked, 1, "username should not be locked");
done();
})
.catch((err) => done(err));
}); });
it("Should return 200", (done) => { it("Should return 200", async () => {
const username = "Changed%20Username"; const username = "Changed%20Username";
postSetUserName(user01PrivateUserID, username) const res = await postSetUserName(user01PrivateUserID, username);
.then(res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); await testUserNameChangelog(user01PrivateUserID, username, username01, false);
testUserNameChangelog(user01PrivateUserID, username, username01, false, done);
})
.catch((err) => done(err));
}); });
it('Should return 400 for missing param "userID"', (done) => { it('Should return 400 for missing param "userID"', async () => {
client({ const res = await client({
method: "POST", method: "POST",
url: endpoint, url: endpoint,
data: { data: {
userName: "MyUsername" userName: "MyUsername"
} }
}) });
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch((err) => done(err));
}); });
it('Should return 400 for missing param "username"', (done) => { it('Should return 400 for missing param "username"', async () => {
client({ const res = await client({
method: "POST", method: "POST",
url: endpoint, url: endpoint,
data: { data: {
userID: "test" userID: "test"
} }
}) });
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch((err) => done(err));
}); });
it('Should return 400 for "username" longer then 64 characters', (done) => { it('Should return 400 for "username" longer then 64 characters', async () => {
const username65 = "0000000000000000000000000000000000000000000000000000000000000000X"; const username65 = "0000000000000000000000000000000000000000000000000000000000000000X";
postSetUserName("test", username65) const res = await postSetUserName("test", username65);
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch((err) => done(err));
}); });
it('Should not change username if it contains "discord"', (done) => { it('Should not change username if it contains "discord"', async () => {
const newUsername = "discord.me"; const newUsername = "discord.me";
postSetUserName(user02PrivateUserID, newUsername) const res = await postSetUserName(user02PrivateUserID, newUsername);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const userNameInfo = await getUsernameInfo(getHash(user02PrivateUserID));
const userNameInfo = await getUsernameInfo(getHash(user02PrivateUserID)); assert.notStrictEqual(userNameInfo.userName, newUsername);
assert.notStrictEqual(userNameInfo.userName, newUsername);
done();
})
.catch((err) => done(err));
}); });
it("Should be able to change username", (done) => { it("Should be able to change username", async () => {
const newUsername = "newUsername"; const newUsername = "newUsername";
postSetUserName(user03PrivateUserID, newUsername) await postSetUserName(user03PrivateUserID, newUsername);
.then(async () => { const usernameInfo = await getUsernameInfo(getHash(user03PrivateUserID));
const usernameInfo = await getUsernameInfo(getHash(user03PrivateUserID)); assert.strictEqual(usernameInfo.userName, newUsername, "Username should change");
assert.strictEqual(usernameInfo.userName, newUsername, "Username should change"); assert.notStrictEqual(usernameInfo.locked, 1, "Username should not be locked");
assert.notStrictEqual(usernameInfo.locked, 1, "Username should not be locked"); await testUserNameChangelog(user03PrivateUserID, newUsername, username03, false);
testUserNameChangelog(user03PrivateUserID, newUsername, username03, false, done);
})
.catch((err) => done(err));
}); });
it("Should not be able to change locked username", (done) => { it("Should not be able to change locked username", async () => {
const newUsername = "newUsername"; const newUsername = "newUsername";
postSetUserName(user04PrivateUserID, newUsername) await postSetUserName(user04PrivateUserID, newUsername);
.then(async () => { const usernameInfo = await getUsernameInfo(getHash(user04PrivateUserID));
const usernameInfo = await getUsernameInfo(getHash(user04PrivateUserID)); assert.notStrictEqual(usernameInfo.userName, newUsername, "Username should not be changed");
assert.notStrictEqual(usernameInfo.userName, newUsername, "Username should not be changed"); assert.strictEqual(usernameInfo.locked, 1, "username should be locked");
assert.strictEqual(usernameInfo.locked, 1, "username should be locked");
done();
})
.catch((err) => done(err));
}); });
it("Should filter out unicode control characters", (done) => { it("Should filter out unicode control characters", async () => {
const newUsername = "This\nUsername+has\tInvalid+Characters"; const newUsername = "This\nUsername+has\tInvalid+Characters";
postSetUserName(user05PrivateUserID, newUsername) await postSetUserName(user05PrivateUserID, newUsername);
.then(async () => { const usernameInfo = await getUsernameInfo(getHash(user05PrivateUserID));
const usernameInfo = await getUsernameInfo(getHash(user05PrivateUserID)); assert.notStrictEqual(usernameInfo.userName, newUsername, "Username should not contain control characters");
assert.notStrictEqual(usernameInfo.userName, newUsername, "Username should not contain control characters"); await testUserNameChangelog(user05PrivateUserID, wellFormatUserName(newUsername), username05, false);
testUserNameChangelog(user05PrivateUserID, wellFormatUserName(newUsername), username05, false, done);
})
.catch((err) => done(err));
}); });
it("Incorrect adminUserID should return 403", (done) => { it("Incorrect adminUserID should return 403", async () => {
const newUsername = "New Username"; const newUsername = "New Username";
postSetUserNameAdmin(getHash(user06PrivateUserID), newUsername,"invalidAdminID") const res = await postSetUserNameAdmin(getHash(user06PrivateUserID), newUsername,"invalidAdminID");
.then(res => { assert.strictEqual(res.status, 403);
assert.strictEqual(res.status, 403);
done();
})
.catch((err) => done(err));
}); });
it("Admin should be able to change username", (done) => { it("Admin should be able to change username", async () => {
const newUsername = "New Username"; const newUsername = "New Username";
postSetUserNameAdmin(getHash(user06PrivateUserID), newUsername, adminPrivateUserID) await postSetUserNameAdmin(getHash(user06PrivateUserID), newUsername, adminPrivateUserID);
.then(async () => { const usernameInfo = await getUsernameInfo(getHash(user06PrivateUserID));
const usernameInfo = await getUsernameInfo(getHash(user06PrivateUserID)); assert.strictEqual(usernameInfo.userName, newUsername, "username should be changed");
assert.strictEqual(usernameInfo.userName, newUsername, "username should be changed"); assert.strictEqual(usernameInfo.locked, 1, "Username should be locked");
assert.strictEqual(usernameInfo.locked, 1, "Username should be locked"); await testUserNameChangelog(user06PrivateUserID, newUsername, username06, true);
testUserNameChangelog(user06PrivateUserID, newUsername, username06, true, done);
})
.catch((err) => done(err));
}); });
it("Admin should be able to change locked username", (done) => { it("Admin should be able to change locked username", async () => {
const newUsername = "New Username"; const newUsername = "New Username";
postSetUserNameAdmin(getHash(user07PrivateUserID), newUsername, adminPrivateUserID) await postSetUserNameAdmin(getHash(user07PrivateUserID), newUsername, adminPrivateUserID);
.then(async () => { const usernameInfo = await getUsernameInfo(getHash(user06PrivateUserID));
const usernameInfo = await getUsernameInfo(getHash(user06PrivateUserID)); assert.strictEqual(usernameInfo.userName, newUsername, "Username should be changed");
assert.strictEqual(usernameInfo.userName, newUsername, "Username should be changed"); assert.strictEqual(usernameInfo.locked, 1, "Username should be locked");
assert.strictEqual(usernameInfo.locked, 1, "Username should be locked"); await testUserNameChangelog(user07PrivateUserID, newUsername, username07, true);
testUserNameChangelog(user07PrivateUserID, newUsername, username07, true, done);
})
.catch((err) => done(err));
}); });
it("Should delete existing username if new username is same as publicID", async () => { it("Should delete existing username if new username is same as publicID", async () => {

View File

@@ -59,71 +59,47 @@ describe("setUsernamePrivate tests", () => {
before(() => sinon.stub(config, "minUserIDLength").value(USERID_LIMIT)); before(() => sinon.stub(config, "minUserIDLength").value(USERID_LIMIT));
after(() => sinon.restore()); after(() => sinon.restore());
it("Existing privateID = username under Limit should retreive successfully", (done) => { it("Existing privateID = username under Limit should retreive successfully", async () => {
const privateID = preExisting_underLimit; const privateID = preExisting_underLimit;
hasSetUsername(getHash(privateID)) assert.ok(await hasSetUsername(getHash(privateID)));
.then((usernameInfo) => {
assert.ok(usernameInfo);
done();
});
}); });
it("Existing privateID = username over Limit should retreive successfully", (done) => { it("Existing privateID = username over Limit should retreive successfully", async () => {
const privateID = preExisting_overLimit; const privateID = preExisting_overLimit;
hasSetUsername(getHash(privateID)) assert.ok(await hasSetUsername(getHash(privateID)));
.then((usernameInfo) => {
assert.ok(usernameInfo);
done();
});
}); });
it("Should return error if trying to set userID = username under limit", (done) => { it("Should return error if trying to set userID = username under limit", async () => {
const privateID = newUser_underLimit; const privateID = newUser_underLimit;
postSetUserName(privateID, privateID) const res = await postSetUserName(privateID, privateID);
.then(async (res) => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400); const usernameInfo = await hasSetUsername(getHash(privateID));
const usernameInfo = await hasSetUsername(getHash(privateID)); assert.ok(!usernameInfo);
assert.ok(!usernameInfo);
done();
})
.catch((err) => done(err));
}); });
it("Should return error if trying to set username = other privateID over limit", (done) => { it("Should return error if trying to set username = other privateID over limit", async () => {
const privateID = newUser_overLimit; const privateID = newUser_overLimit;
postSetUserName(privateID, privateID) const res = await postSetUserName(privateID, privateID);
.then(async (res) => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400); const usernameInfo = await hasSetUsername(getHash(privateID));
const usernameInfo = await hasSetUsername(getHash(privateID)); assert.ok(!usernameInfo);
assert.ok(!usernameInfo);
done();
})
.catch((err) => done(err));
}); });
it("Should return error if trying to set username = other privateID over limit", (done) => { it("Should return error if trying to set username = other privateID over limit", async () => {
const privateID = otherUser; const privateID = otherUser;
const otherUserPrivate = preExisting_overLimit; const otherUserPrivate = preExisting_overLimit;
postSetUserName(privateID, otherUserPrivate) const res = await postSetUserName(privateID, otherUserPrivate);
.then(async (res) => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400); const usernameInfo = await hasSetUsername(getHash(privateID));
const usernameInfo = await hasSetUsername(getHash(privateID)); assert.ok(!usernameInfo);
assert.ok(!usernameInfo);
done();
})
.catch((err) => done(err));
}); });
it("Should not return error if trying to set username = other privateID under limit", (done) => { it("Should not return error if trying to set username = other privateID under limit", async () => {
const privateID = otherUser; const privateID = otherUser;
const otherUserPrivate = preExisting_underLimit; const otherUserPrivate = preExisting_underLimit;
postSetUserName(privateID, otherUserPrivate) const res = await postSetUserName(privateID, otherUserPrivate);
.then(async (res) => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const usernameInfo = await hasSetUsername(getHash(privateID));
const usernameInfo = await hasSetUsername(getHash(privateID)); assert.ok(usernameInfo);
assert.ok(usernameInfo);
done();
})
.catch((err) => done(err));
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { db, privateDB } from "../../src/databases/databases"; import { db } from "../../src/databases/databases";
import { getHash } from "../../src/utils/getHash"; import { getHash } from "../../src/utils/getHash";
import assert from "assert"; import assert from "assert";
import { Category, Service } from "../../src/types/segments.model"; import { Category, Service } from "../../src/types/segments.model";

View File

@@ -68,191 +68,118 @@ describe("tempVIP test", function() {
await redis.del(tempVIPKey(publicTempVIPOne)); await redis.del(tempVIPKey(publicTempVIPOne));
}); });
it("Should update db version when starting the application", () => { it("Should update db version when starting the application", async () => {
privateDB.prepare("get", "SELECT key, value FROM config where key = ?", ["version"]) const row = await privateDB.prepare("get", "SELECT key, value FROM config where key = ?", ["version"]);
.then(row => { assert.ok(row.value >= 5, `Versions are not at least 5. private is ${row.value}`);
assert.ok(row.value >= 5, `Versions are not at least 5. private is ${row.value}`);
});
}); });
it("User should not already be temp VIP", (done) => { it("User should not already be temp VIP", async () => {
checkUserVIP(publicTempVIPOne) assert.ok(!await checkUserVIP(publicTempVIPOne));
.then(result => { const row = await privateDB.prepare("get", `SELECT * FROM "tempVipLog" WHERE "targetUserID" = ?`, [publicTempVIPOne]);
assert.ok(!result); assert.ok(!row?.enabled);
})
.then(async () => {
const row = await privateDB.prepare("get", `SELECT * FROM "tempVipLog" WHERE "targetUserID" = ?`, [publicTempVIPOne]);
assert.ok(!row?.enabled);
done();
})
.catch(err => done(err));
}); });
it("Should be able to normal upvote as a user", (done) => { it("Should be able to normal upvote as a user", async () => {
postVote(tempVIPOne, UUID0, 1) const res = await postVote(tempVIPOne, UUID0, 1);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getSegment(UUID0);
const row = await getSegment(UUID0); assert.strictEqual(row.votes, 1);
assert.strictEqual(row.votes, 1);
done();
})
.catch(err => done(err));
}); });
it("Should be able to add tempVIP", (done) => { it("Should be able to add tempVIP", async () => {
addTempVIP("true", permVIP1, publicTempVIPOne) const res = await addTempVIP("true", permVIP1, publicTempVIPOne);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); // check redis
// check redis const vip = await checkUserVIP(publicTempVIPOne);
const vip = await checkUserVIP(publicTempVIPOne); assert.strictEqual(vip, "ChannelID");
assert.strictEqual(vip, "ChannelID"); assert.strictEqual(res.data, "Temp VIP added on channel ChannelAuthor");
assert.strictEqual(res.data, "Temp VIP added on channel ChannelAuthor"); // check privateDB
// check privateDB const row = await privateDB.prepare("get", `SELECT * FROM "tempVipLog" WHERE "targetUserID" = ?`, [publicTempVIPOne]);
const row = await privateDB.prepare("get", `SELECT * FROM "tempVipLog" WHERE "targetUserID" = ?`, [publicTempVIPOne]); assert.ok(row.enabled);
assert.ok(row.enabled);
done();
})
.catch(err => done(err));
}); });
it("Should be able to VIP downvote", (done) => { it("Should be able to VIP downvote", async () => {
postVote(tempVIPOne, UUID0, 0) const res = await postVote(tempVIPOne, UUID0, 0);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getSegment(UUID0);
const row = await getSegment(UUID0); assert.strictEqual(row.votes, -2);
assert.strictEqual(row.votes, -2);
done();
})
.catch(err => done(err));
}); });
it("Should not be able to lock segment", (done) => { it("Should not be able to lock segment", async () => {
postVote(tempVIPOne, UUID0, 1) const res = await postVote(tempVIPOne, UUID0, 1);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getSegment(UUID0);
const row = await getSegment(UUID0); assert.strictEqual(row.votes, 1);
assert.strictEqual(row.votes, 1); assert.strictEqual(row.locked, 0);
assert.strictEqual(row.locked, 0);
done();
})
.catch(err => done(err));
}); });
it("Should be able to change category but not lock", (done) => { it("Should be able to change category but not lock", async () => {
postVoteCategory(tempVIPOne, UUID0, "filler") const res = await postVoteCategory(tempVIPOne, UUID0, "filler");
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getSegment(UUID0);
const row = await getSegment(UUID0); assert.strictEqual(row.category, "filler");
assert.strictEqual(row.category, "filler"); assert.strictEqual(row.locked, 0);
assert.strictEqual(row.locked, 0);
done();
})
.catch(err => done(err));
}); });
it("Should be able to remove tempVIP prematurely", (done) => { it("Should be able to remove tempVIP prematurely", async () => {
addTempVIP("false", permVIP1, publicTempVIPOne) const res = await addTempVIP("false", permVIP1, publicTempVIPOne);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const vip = await checkUserVIP(publicTempVIPOne);
const vip = await checkUserVIP(publicTempVIPOne); assert.strictEqual(res.data, "Temp VIP removed");
assert.strictEqual(res.data, "Temp VIP removed"); assert.ok(!vip, "Should be no listed channelID");
assert.ok(!vip, "Should be no listed channelID");
done();
})
.catch(err => done(err));
}); });
it("Should not be able to VIP downvote", (done) => { it("Should not be able to VIP downvote", async () => {
postVote(tempVIPOne, UUID1, 0) const res = await postVote(tempVIPOne, UUID1, 0);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getSegment(UUID1);
const row = await getSegment(UUID1); assert.strictEqual(row.votes, 0);
assert.strictEqual(row.votes, 0);
done();
})
.catch(err => done(err));
}); });
it("Should not be able to VIP change category", (done) => { it("Should not be able to VIP change category", async () => {
postVoteCategory(tempVIPOne, UUID1, "filler") const res = await postVoteCategory(tempVIPOne, UUID1, "filler");
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const row = await getSegment(UUID1);
const row = await getSegment(UUID1); assert.strictEqual(row.category, "sponsor");
assert.strictEqual(row.category, "sponsor");
done();
})
.catch(err => done(err));
}); });
// error code testing // error code testing
it("Should be able to add tempVIP after removal", (done) => { it("Should be able to add tempVIP after removal", async () => {
addTempVIP("true", permVIP1, publicTempVIPOne) const res = await addTempVIP("true", permVIP1, publicTempVIPOne);
.then(async res => { assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200); const vip = await checkUserVIP(publicTempVIPOne);
const vip = await checkUserVIP(publicTempVIPOne); assert.strictEqual(vip, "ChannelID");
assert.strictEqual(vip, "ChannelID");
done();
})
.catch(err => done(err));
}); });
it("Should not be able to add VIP without existing VIP (403)", (done) => { it("Should not be able to add VIP without existing VIP (403)", async () => {
const privateID = "non-vip-privateid"; const privateID = "non-vip-privateid";
addTempVIP("true", privateID, publicTempVIPOne) const res = await addTempVIP("true", privateID, publicTempVIPOne);
.then(async res => { assert.strictEqual(res.status, 403);
assert.strictEqual(res.status, 403); const vip = await checkUserVIP(getHash(privateID) as HashedUserID);
const vip = await checkUserVIP(getHash(privateID) as HashedUserID); assert.ok(!vip, "Should be no listed channelID");
assert.ok(!vip, "Should be no listed channelID");
done();
})
.catch(err => done(err));
}); });
it("Should not be able to add permanant VIP as temporary VIP (409)", (done) => { it("Should not be able to add permanant VIP as temporary VIP (409)", async () => {
addTempVIP("true", permVIP1, publicPermVIP2) const res = await addTempVIP("true", permVIP1, publicPermVIP2);
.then(async res => { assert.strictEqual(res.status, 409);
assert.strictEqual(res.status, 409); const vip = await checkUserVIP(publicPermVIP2);
const vip = await checkUserVIP(publicPermVIP2); assert.ok(!vip, "Should be no listed channelID");
assert.ok(!vip, "Should be no listed channelID");
done();
})
.catch(err => done(err));
}); });
it("Temp VIP should not be able to add another Temp VIP (403)", (done) => { it("Temp VIP should not be able to add another Temp VIP (403)", async () => {
const privateID = "non-vip-privateid"; const privateID = "non-vip-privateid";
const publicID = getHash(privateID) as HashedUserID; const publicID = getHash(privateID) as HashedUserID;
addTempVIP("true", tempVIPOne, publicID) const res = await addTempVIP("true", tempVIPOne, publicID);
.then(async res => { assert.strictEqual(res.status, 403);
assert.strictEqual(res.status, 403); const vip = await checkUserVIP(publicID);
const vip = await checkUserVIP(publicID); assert.ok(!vip, "Should be no listed channelID");
assert.ok(!vip, "Should be no listed channelID");
done();
})
.catch(err => done(err));
}); });
// error 40X testing // error 40X testing
it("Should return 404 with invalid videoID", (done) => { it("Should return 404 with invalid videoID", async () => {
const privateID = "non-vip-privateid"; const privateID = "non-vip-privateid";
const publicID = getHash(privateID) as HashedUserID; const publicID = getHash(privateID) as HashedUserID;
addTempVIP("true", permVIP1, publicID, "knownWrongID") const res = await addTempVIP("true", permVIP1, publicID, "knownWrongID");
.then(async res => { assert.strictEqual(res.status, 404);
assert.strictEqual(res.status, 404); const vip = await checkUserVIP(publicID);
const vip = await checkUserVIP(publicID); assert.ok(!vip, "Should be no listed channelID");
assert.ok(!vip, "Should be no listed channelID");
done();
})
.catch(err => done(err));
}); });
it("Should return 400 with invalid userID", (done) => { it("Should return 400 with invalid userID", async () => {
addTempVIP("true", permVIP1, "" as HashedUserID, "knownWrongID") const res = await addTempVIP("true", permVIP1, "" as HashedUserID, "knownWrongID");
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
}); });
it("Should return 400 with invalid adminUserID", (done) => { it("Should return 400 with invalid adminUserID", async () => {
addTempVIP("true", "", publicTempVIPOne) const res = await addTempVIP("true", "", publicTempVIPOne);
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
}); });
it("Should return 400 with invalid channelID", (done) => { it("Should return 400 with invalid channelID", async () => {
addTempVIP("true", permVIP1, publicTempVIPOne, "") const res = await addTempVIP("true", permVIP1, publicTempVIPOne, "");
.then(res => { assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
}); });
}); });

View File

@@ -16,32 +16,24 @@ describe("tokenUtils test", function() {
mock.onGet(/identity/).reply(200, patreon.activeIdentity); mock.onGet(/identity/).reply(200, patreon.activeIdentity);
}); });
it("Should be able to create patreon token", function (done) { it("Should be able to create patreon token", async function () {
if (!config?.patreon) this.skip(); if (!config?.patreon) return this.skip();
tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code").then((licenseKey) => { const licenseKey = await tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code");
assert.ok(validateToken(licenseKey[0])); assert.ok(validateToken(licenseKey[0]));
done();
});
}); });
it("Should be able to create local token", (done) => { it("Should be able to create local token", async () => {
tokenUtils.createAndSaveToken(tokenUtils.TokenType.local).then((licenseKey) => { const licenseKey = await tokenUtils.createAndSaveToken(tokenUtils.TokenType.local);
assert.ok(validateToken(licenseKey[0])); assert.ok(validateToken(licenseKey[0]));
done();
});
}); });
it("Should be able to get patreon identity", function (done) { it("Should be able to get patreon identity", async function () {
if (!config?.patreon) this.skip(); if (!config?.patreon) return this.skip();
tokenUtils.getPatreonIdentity("fake_access_token").then((result) => { const result = await tokenUtils.getPatreonIdentity("fake_access_token");
assert.deepEqual(result, patreon.activeIdentity); assert.deepEqual(result, patreon.activeIdentity);
done();
});
}); });
it("Should be able to refresh token", function (done) { it("Should be able to refresh token", async function () {
if (!config?.patreon) this.skip(); if (!config?.patreon) return this.skip();
tokenUtils.refreshToken(tokenUtils.TokenType.patreon, "fake-licence-Key", "fake_refresh_token").then((result) => { const result = await tokenUtils.refreshToken(tokenUtils.TokenType.patreon, "fake-licence-Key", "fake_refresh_token");
assert.strictEqual(result, true); assert.strictEqual(result, true);
done();
});
}); });
after(function () { after(function () {
@@ -56,17 +48,13 @@ describe("tokenUtils failing tests", function() {
mock.onGet(/identity/).reply(204, patreon.activeIdentity); mock.onGet(/identity/).reply(204, patreon.activeIdentity);
}); });
it("Should fail if patreon is not correctly stubbed", function (done) { it("Should fail if patreon is not correctly stubbed", async () => {
tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code").then((licenseKey) => { const licenseKey = await tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code");
assert.strictEqual(licenseKey, null); assert.strictEqual(licenseKey, null);
done();
});
}); });
it("Should fail if token type is invalid", (done) => { it("Should fail if token type is invalid", async () => {
tokenUtils.createAndSaveToken("invalidTokenType" as tokenUtils.TokenType).then((licenseKey) => { const licenseKey = await tokenUtils.createAndSaveToken("invalidTokenType" as tokenUtils.TokenType);
assert.strictEqual(licenseKey, null); assert.strictEqual(licenseKey, null);
done();
});
}); });
after(function () { after(function () {

File diff suppressed because it is too large Load Diff

View File

@@ -59,12 +59,13 @@ async function init() {
mocha.run((failures) => { mocha.run((failures) => {
mockServer.close(); mockServer.close();
server.close(); server.close();
redis.quit(); redis.quit().finally(() => {
process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures
process.exit(); process.exit();
});
}); });
}); });
}); });
} }
init(); void init();

51
test/utils/randomUsers.ts Normal file
View File

@@ -0,0 +1,51 @@
import { HashedUserID, UserID } from "../../src/types/user.model";
import { getHash } from "../../src/utils/getHash";
import { genRandom } from "./getRandom";
export interface TestUser {
private: UserID,
public: HashedUserID,
}
interface InternalTestUsers {
map: Map<any, TestUser>,
suiteName: string,
}
const userMapHandler = {
get(target: InternalTestUsers, property: string): TestUser {
const suiteName = Reflect.get(target, "suiteName");
const map = Reflect.get(target, "map");
let user = map.get(property);
if (user !== undefined) {
return user;
}
const priv = `${suiteName}-${property}-${genRandom}` as UserID;
user = {
private: priv,
public: getHash(priv),
};
map.set(property, user);
return user;
},
set: () => false, // nope
deleteProperty: () => false, // nope
has: () => true, // yep
defineProperty: () => false, // nope
preventExtensions: () => false, // nope
setPrototypeOf: () => false, // nope
};
/**
* Creates an object that generates test private/public userID pairs on demand
*
* @param suiteName the suite name, used as a prefix for the private userID
* @returns a proxy that generates & caches private/public userID pairs for each property access
*/
export function usersForSuite(suiteName: string): Record<any, TestUser> {
return new Proxy({
map: new Map<any, TestUser>(),
suiteName,
}, userMapHandler) as unknown as Record<any, TestUser>;
}

4
tsconfig.eslint.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["**/*"],
}