postSkipSegments improvements

- fix 80% check from same user
- split test cases into multiple files for easier viewing
This commit is contained in:
Michael C
2023-02-21 03:25:46 -05:00
parent 820a7eb02f
commit 6296761fe4
6 changed files with 696 additions and 610 deletions

View File

@@ -120,7 +120,7 @@ async function sendWebhooks(apiVideoDetails: videoDetails, userID: string, video
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(apiVideoDetails: videoDetails,
submission: { videoID: VideoID; userID: UserID; segments: IncomingSegment[], service: Service, videoDuration: number }) {
submission: { videoID: VideoID; userID: HashedUserID; segments: IncomingSegment[], service: Service, videoDuration: number }) {
// get duration from API
const apiDuration = apiVideoDetails.duration;
// if API fail or returns 0, get duration from client
@@ -156,7 +156,7 @@ async function autoModerateSubmission(apiVideoDetails: videoDetails,
return false;
}
async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
async function checkUserActiveWarning(userID: HashedUserID): Promise<CheckResult> {
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const warnings = (await db.prepare("all",
@@ -337,10 +337,10 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user
return CHECK_PASS;
}
async function checkByAutoModerator(videoID: any, userID: any, segments: Array<any>, service:string, apiVideoDetails: videoDetails, videoDuration: number): Promise<CheckResult> {
async function checkByAutoModerator(videoID: VideoID, userID: HashedUserID, segments: IncomingSegment[], service: Service, apiVideoDetails: videoDetails, videoDuration: number): Promise<CheckResult> {
// Auto moderator check
if (service == Service.YouTube) {
const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { userID, videoID, segments, service, videoDuration });
const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { videoID, userID, segments, service, videoDuration });
if (autoModerateResult) {
return {
pass: false,
@@ -492,7 +492,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
let { videoID, userID: paramUserID, service, videoDuration, videoDurationParam, segments, userAgent } = preprocessInput(req);
//hash the userID
const userID = await getHashCache(paramUserID || "");
if (!paramUserID) {
return res.status(400).send("No userID provided");
}
const userID: HashedUserID = await getHashCache(paramUserID);
const invalidCheckResult = await checkInvalidFields(videoID, paramUserID, userID, segments, videoDurationParam, userAgent, service);
if (!invalidCheckResult.pass) {

View File

@@ -1,4 +1,3 @@
import { config } from "../../src/config";
import { getHash } from "../../src/utils/getHash";
import { partialDeepEquals, arrayDeepEquals } from "../utils/partialDeepEquals";
import { db } from "../../src/databases/databases";
@@ -7,7 +6,33 @@ import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
import { YouTubeApiMock } from "../mocks/youtubeMock";
import assert from "assert";
import { client } from "../utils/httpClient";
import { Feature } from "../../src/types/user.model";
export type Segment = {
segment: number[];
category: string;
actionType?: string;
description?: string;
};
const endpoint = "/api/skipSegments";
export const postSkipSegmentJSON = (data: Record<string, any>) => client({
method: "POST",
url: endpoint,
data
});
export const postSkipSegmentParam = (params: Record<string, any>) => client({
method: "POST",
url: endpoint,
params
});
export const convertMultipleToDBFormat = (segments: Segment[]) =>
segments.map(segment => convertSingleToDBFormat(segment));
export const convertSingleToDBFormat = (segment: Segment) => ({
startTime: segment.segment[0],
endTime: segment.segment[1],
category: segment.category,
});
const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, "YouTubeAPI");
const sinonStub = mockManager.mock("listVideos");
@@ -16,108 +41,38 @@ sinonStub.callsFake(YouTubeApiMock.listVideos);
describe("postSkipSegments", () => {
// Constant and helpers
const submitUserOne = `PostSkipUser1${".".repeat(18)}`;
const submitUserOneHash = getHash(submitUserOne);
const submitUserTwo = `PostSkipUser2${".".repeat(18)}`;
const submitUserTwoHash = getHash(submitUserTwo);
const submitUserThree = `PostSkipUser3${".".repeat(18)}`;
const warnUser01 = "warn-user01-qwertyuiopasdfghjklzxcvbnm";
const warnUser01Hash = getHash(warnUser01);
const warnUser02 = "warn-user02-qwertyuiopasdfghjklzxcvbnm";
const warnUser02Hash = getHash(warnUser02);
const warnUser03 = "warn-user03-qwertyuiopasdfghjklzxcvbnm";
const warnUser03Hash = getHash(warnUser03);
const warnUser04 = "warn-user04-qwertyuiopasdfghjklzxcvbnm";
const warnUser04Hash = getHash(warnUser04);
const banUser01 = "ban-user01-loremipsumdolorsitametconsectetur";
const banUser01Hash = getHash(banUser01);
const submitVIPuser = `VIPPostSkipUser${".".repeat(16)}`;
const warnVideoID = "postSkip2";
const badInputVideoID = "dQw4w9WgXcQ";
const shadowBanVideoID = "postSkipBan";
const shadowBanVideoID2 = "postSkipBan2";
const queryDatabase = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "votes", "userID", "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 queryDatabaseChapter = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType", "description" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
const queryDatabaseVideoInfo = (videoID: string) => db.prepare("get", `SELECT * FROM "videoInfo" WHERE "videoID" = ?`, [videoID]);
const endpoint = "/api/skipSegments";
const postSkipSegmentJSON = (data: Record<string, any>) => client({
method: "POST",
url: endpoint,
data
});
const postSkipSegmentParam = (params: Record<string, any>) => client({
method: "POST",
url: endpoint,
params
});
before(() => {
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 1000, 0, "80percent-uuid-0", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]);
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 1001, 1005, 0, "80percent-uuid-1", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]);
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 5000, -2, "80percent-uuid-2", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]);
db.prepare("run", insertSponsorTimeQuery, ["full_video_segment", 0, 0, 0, "full-video-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 0, 0, "full_video_segment"]);
db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 0, 0, 0, "full-video-duration-uuid-0", submitUserTwoHash, 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", submitUserTwoHash, 0, 0, "sponsor", "skip", 123, 0, "full_video_duration_segment"]);
const reputationVideoID = "post_reputation_video";
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-0", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-1", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-2", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-3", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 2,"post_reputation-5-uuid-4", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 0,"post_reputation-5-uuid-6", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
db.prepare("run", insertSponsorTimeQuery, [reputationVideoID, 1, 11, 0,"post_reputation-5-uuid-7", submitUserOneHash, 1606240000000, 50, "sponsor", "skip", 0, 0, reputationVideoID]);
const now = Date.now();
const warnVip01Hash = getHash("warn-vip01-qwertyuiopasdfghjklzxcvbnm");
const reason01 = "Reason01";
const reason02 = "";
const reason03 = "Reason03";
const reason04 = "";
const MILLISECONDS_IN_HOUR = 3600000;
const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires;
const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)';
// User 1
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, now]);
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 1000)]);
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 2000)]);
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 3601000)]);
// User 2
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, now]);
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, (now - (warningExpireTime + 1000))]);
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, (now - (warningExpireTime + 2000))]);
// User 3
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 0, reason03, now]);
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 0, reason03, (now - 1000)]);
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - 2000)]);
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - 3601000)]);
// User 4
db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 0, reason04, now]);
db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 0, reason04, (now - 1000)]);
db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 1, reason04, (now - 2000)]);
db.prepare("run", insertWarningQuery, [warnUser04Hash, warnVip01Hash, 1, reason04, (now - 3601000)]);
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
db.prepare("run", insertVipUserQuery, [getHash(submitVIPuser)]);
// ban user
db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, [banUser01Hash]);
// user feature
db.prepare("run", `INSERT INTO "userFeatures" ("userID", "feature", "issuerUserID", "timeSubmitted") VALUES(?, ?, ?, ?)`, [submitUserTwoHash, Feature.ChapterSubmitter, "some-user", 0]);
});
it("Should be able to submit a single time (Params method)", (done) => {
const videoID = "postSkip1";
const videoID = "postSkipParamSingle";
postSkipSegmentParam({
videoID,
startTime: 2,
@@ -150,7 +105,7 @@ describe("postSkipSegments", () => {
});
it("Should be able to submit a single time (JSON method)", (done) => {
const videoID = "postSkip2";
const videoID = "postSkipJSONSingle";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
@@ -175,7 +130,7 @@ describe("postSkipSegments", () => {
});
it("Should be able to submit a single time with an action type (JSON method)", (done) => {
const videoID = "postSkip3";
const videoID = "postSkipJSONSingleActionType";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
@@ -200,141 +155,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
});
it("Should be able to submit a single chapter due to reputation (JSON method)", (done) => {
const videoID = "postSkipChapter1";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
}],
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await queryDatabaseChapter(videoID);
const expected = {
startTime: 0,
endTime: 10,
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
};
assert.ok(partialDeepEquals(row, expected));
done();
})
.catch(err => done(err));
});
it("Should be able to submit a single chapter due to user feature (JSON method)", (done) => {
const videoID = "postSkipChapter2";
postSkipSegmentJSON({
userID: submitUserTwo,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
}],
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await queryDatabaseChapter(videoID);
const expected = {
startTime: 0,
endTime: 10,
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
};
assert.ok(partialDeepEquals(row, expected));
done();
})
.catch(err => done(err));
});
it("Should not be able to submit an music_offtopic with mute action type (JSON method)", (done) => {
const videoID = "postSkip4";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "music_offtopic",
actionType: "mute"
}],
})
.then(async res => {
assert.strictEqual(res.status, 400);
const row = await queryDatabaseActionType(videoID);
assert.strictEqual(row, undefined);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a chapter without permission (JSON method)", (done) => {
const videoID = "postSkipChapter3";
postSkipSegmentJSON({
userID: submitUserThree,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "chapter",
description: "This is a chapter"
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a chapter with skip action type (JSON method)", (done) => {
const videoID = "postSkipChapter4";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "skip"
}],
})
.then(async res => {
assert.strictEqual(res.status, 400);
const row = await queryDatabaseActionType(videoID);
assert.strictEqual(row, undefined);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a sponsor with a description (JSON method)", (done) => {
const videoID = "postSkipChapter5";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 10],
category: "sponsor",
description: "This is a sponsor"
}],
})
.then(async res => {
assert.strictEqual(res.status, 400);
const row = await queryDatabaseActionType(videoID);
assert.strictEqual(row, undefined);
done();
})
.catch(err => done(err));
});
it("Should be able to submit a single time with a duration from the YouTube API (JSON method)", (done) => {
const videoID = "postSkip5";
postSkipSegmentJSON({
@@ -556,7 +376,7 @@ describe("postSkipSegments", () => {
});
it("Should be able to submit multiple times (JSON method)", (done) => {
const videoID = "postSkip11";
const videoID = "postSkipJSONMultiple";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
@@ -586,117 +406,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
}).timeout(5000);
it("Should allow multiple times if total is under 80% of video(JSON method)", (done) => {
const videoID = "postSkip9";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [3, 3000],
category: "sponsor",
}, {
segment: [3002, 3050],
category: "intro",
}, {
segment: [45, 100],
category: "interaction",
}, {
segment: [99, 170],
category: "sponsor",
}],
})
.then(async res => {
assert.strictEqual(res.status, 200);
const rows = await db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]);
const expected = [{
startTime: 3,
endTime: 3000,
category: "sponsor"
}, {
startTime: 3002,
endTime: 3050,
category: "intro"
}, {
startTime: 45,
endTime: 100,
category: "interaction"
}, {
startTime: 99,
endTime: 170,
category: "sponsor"
}];
assert.ok(arrayDeepEquals(rows, expected));
done();
})
.catch(err => done(err));
}).timeout(5000);
it("Should reject multiple times if total is over 80% of video (JSON method)", (done) => {
const videoID = "n9rIGdXnSJc";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [0, 2000],
category: "interaction",
}, {
segment: [3000, 4000],
category: "sponsor",
}, {
segment: [1500, 2750],
category: "sponsor",
}, {
segment: [4050, 4750],
category: "intro",
}],
})
.then(async res => {
assert.strictEqual(res.status, 403);
const rows = await db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]);
assert.deepStrictEqual(rows, []);
done();
})
.catch(err => done(err));
}).timeout(5000);
it("Should reject multiple times if total is over 80% of video including previosuly submitted times(JSON method)", (done) => {
const videoID = "80percent_video";
postSkipSegmentJSON({
userID: submitUserOne,
videoID,
segments: [{
segment: [2000, 4000],
category: "sponsor",
}, {
segment: [1500, 2750],
category: "sponsor",
}, {
segment: [4050, 4750],
category: "sponsor",
}],
})
.then(async res => {
assert.strictEqual(res.status, 403);
const expected = [{
category: "interaction",
startTime: 0,
endTime: 1000
}, {
category: "interaction",
startTime: 1001,
endTime: 1005
}, {
category: "interaction",
startTime: 0,
endTime: 5000
}];
const rows = await db.prepare("all", `SELECT "category", "startTime", "endTime" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
assert.ok(arrayDeepEquals(rows, expected));
done();
})
.catch(err => done(err));
}).timeout(5000);
it("Should be accepted if a non-sponsor is less than 1 second", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
@@ -713,22 +422,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
});
it("Should be rejected if segment starts and ends at the same time", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
videoID,
startTime: 90,
endTime: 90,
userID: submitUserTwo,
category: "intro"
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be accepted if highlight segment starts and ends at the same time", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
@@ -745,223 +438,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
});
it("Should be rejected if highlight segment doesn't start and end at the same time", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
videoID,
startTime: 30,
endTime: 30.5,
userID: submitUserTwo,
category: "poi_highlight"
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be rejected if a sponsor is less than 1 second", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
videoID,
startTime: 30,
endTime: 30.5,
userID: submitUserTwo
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be rejected if over 80% of the video", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
videoID,
startTime: 30,
endTime: 1000000,
userID: submitUserTwo,
category: "sponsor"
})
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
it("Should be rejected with custom message if user has to many active warnings", (done) => {
postSkipSegmentJSON({
userID: warnUser01,
videoID: warnVideoID,
segments: [{
segment: [0, 10],
category: "sponsor",
}],
})
.then(res => {
assert.strictEqual(res.status, 403);
const errorMessage = res.data;
const reason = "Reason01";
const expected = "Submission rejected due to a warning 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.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
+ `Your userID is ${warnUser01Hash}.\n\nWarning reason: '${reason}'`;
assert.strictEqual(errorMessage, expected);
done();
})
.catch(err => done(err));
});
it("Should be accepted if user has some active warnings", (done) => {
postSkipSegmentJSON({
userID: warnUser02,
videoID: warnVideoID,
segments: [{
segment: [50, 60],
category: "sponsor",
}],
})
.then(res => {
if (res.status === 200) {
done(); // success
} else {
done(`Status code was ${res.status} ${res.data}`);
}
})
.catch(err => done(err));
});
it("Should be accepted if user has some warnings removed", (done) => {
postSkipSegmentJSON({
userID: warnUser03,
videoID: warnVideoID,
segments: [{
segment: [53, 60],
category: "sponsor",
}],
})
.then(res => {
if (res.status === 200) {
done(); // success
} else {
done(`Status code was ${res.status} ${res.data}`);
}
})
.catch(err => done(err));
});
it("Should return 400 for missing params (Params method)", (done) => {
postSkipSegmentParam({
startTime: 9,
endTime: 10,
userID: submitUserOne
})
.then(res => {
if (res.status === 400) done();
else done(true);
})
.catch(err => done(err));
});
it("Should be rejected with default message if user has to many active warnings", (done) => {
postSkipSegmentJSON({
userID: warnUser01,
videoID: warnVideoID,
segments: [{
segment: [0, 10],
category: "sponsor",
}],
})
.then(res => {
assert.strictEqual(res.status, 403);
const errorMessage = res.data;
assert.notStrictEqual(errorMessage, "");
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 1", (done) => {
postSkipSegmentJSON({
userID: submitUserOne,
segments: [{
segment: [9, 10],
category: "sponsor",
}, {
segment: [31, 60],
category: "intro",
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 2", (done) => {
postSkipSegmentJSON({
userID: submitUserOne,
videoID: badInputVideoID,
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 3", (done) => {
postSkipSegmentJSON({
userID: submitUserOne,
videoID: badInputVideoID,
segments: [{
segment: [0],
category: "sponsor",
}, {
segment: [31, 60],
category: "intro",
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 4", (done) => {
postSkipSegmentJSON({
userID: submitUserOne,
videoID: badInputVideoID,
segments: [{
segment: [9, 10],
}, {
segment: [31, 60],
category: "intro",
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 5", (done) => {
postSkipSegmentJSON({
userID: submitUserOne,
videoID: badInputVideoID,
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 403 and custom reason for submiting in lockedCategory", (done) => {
const videoID = "lockedVideo";
db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason")
@@ -1130,22 +606,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
});
it("Should be rejected if a POI is at less than 1 second", (done) => {
const videoID = "qqwerty";
postSkipSegmentParam({
videoID,
startTime: 0.5,
endTime: 0.5,
category: "poi_highlight",
userID: submitUserTwo
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should allow submitting full video sponsor", (done) => {
const videoID = "qqwerth";
postSkipSegmentParam({
@@ -1200,23 +660,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
});
it("Should not allow submitting full video sponsor not at zero seconds", (done) => {
const videoID = "qqwerth";
postSkipSegmentParam({
videoID,
startTime: 0,
endTime: 1,
category: "sponsor",
actionType: "full",
userID: submitUserTwo
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit with colons in timestamps", (done) => {
const videoID = "colon-1";
postSkipSegmentJSON({
@@ -1277,22 +720,6 @@ describe("postSkipSegments", () => {
.catch(err => done(err));
});
it("Should return 400 if videoID is empty", (done) => {
const videoID = null as unknown as string;
postSkipSegmentParam({
videoID,
startTime: 1,
endTime: 5,
category: "sponsor",
userID: submitUserTwo
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should successfully submit if video is private", (done) => {
const videoID = "private-video";
postSkipSegmentParam({
@@ -1308,4 +735,28 @@ describe("postSkipSegments", () => {
})
.catch(err => done(err));
});
it("Should throw 409 on duplicate submission", (done) => {
const videoID = "private-video";
postSkipSegmentParam({
videoID,
startTime: 5.555,
endTime: 8.888,
category: "sponsor",
userID: submitUserTwo
})
.then(res => assert.strictEqual(res.status, 200) )
.then(() => postSkipSegmentParam({
videoID,
startTime: 5.555,
endTime: 8.888,
category: "sponsor",
userID: submitUserTwo
}))
.then(res => {
assert.strictEqual(res.status, 409);
done();
})
.catch(err => done(err));
});
});

View File

@@ -0,0 +1,300 @@
import assert from "assert";
import { postSkipSegmentJSON, postSkipSegmentParam } from "./postSkipSegments";
const videoID = "postSkipSegments-404-video";
const userID = "postSkipSegments-404-user";
describe("postSkipSegments 400 - missing params", () => {
it("Should return 400 for missing params (JSON method) 1", (done) => {
postSkipSegmentJSON({
userID,
segments: [{
segment: [9, 10],
category: "sponsor",
}, {
segment: [31, 60],
category: "intro",
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 2", (done) => {
postSkipSegmentJSON({
userID,
videoID,
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 3", (done) => {
postSkipSegmentJSON({
userID,
videoID,
segments: [{
segment: [0],
category: "sponsor",
}, {
segment: [31, 60],
category: "intro",
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 4", (done) => {
postSkipSegmentJSON({
userID,
videoID,
segments: [{
segment: [9, 10],
}, {
segment: [31, 60],
category: "intro",
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing params (JSON method) 5", (done) => {
postSkipSegmentJSON({
userID,
videoID,
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for missing multiple params (Params method)", (done) => {
postSkipSegmentParam({
startTime: 9,
endTime: 10,
userID
})
.then(res => {
if (res.status === 400) done();
else done(true);
})
.catch(err => done(err));
});
it("Should return 400 if videoID is empty", (done) => {
const videoID = null as unknown as string;
postSkipSegmentParam({
videoID,
startTime: 1,
endTime: 5,
category: "sponsor",
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});
describe("postSkipSegments 400 - Chapters", () => {
const actionType = "chapter";
const category = actionType;
it("Should not be able to submit a chapter name that is too long", (done) => {
postSkipSegmentParam({
videoID,
startTime: 1,
endTime: 5,
category,
actionType,
description: "a".repeat(256),
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});
describe("postSkipSegments 400 - POI", () => {
const category = "poi_highlight";
it("Should be rejected if a POI is at less than 1 second", (done) => {
postSkipSegmentParam({
videoID,
startTime: 0.5,
endTime: 0.5,
category,
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be rejected if highlight segment doesn't start and end at the same time", (done) => {
postSkipSegmentParam({
videoID,
startTime: 30,
endTime: 30.5,
category,
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});
describe("postSkipSegments 400 - Automod", () => {
it("Should be rejected if over 80% of the video", (done) => {
postSkipSegmentParam({
videoID,
startTime: 30,
endTime: 1000000,
userID,
category: "sponsor"
})
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
it("Should be rejected if a sponsor is less than 1 second", (done) => {
postSkipSegmentParam({
videoID,
category: "sponsor",
startTime: 30,
endTime: 30.5,
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be rejected if non-POI segment starts and ends at the same time", (done) => {
postSkipSegmentParam({
videoID,
startTime: 90,
endTime: 90,
userID,
category: "intro"
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should not allow submitting full video not at zero seconds", (done) => {
postSkipSegmentParam({
videoID,
startTime: 0,
endTime: 1,
category: "sponsor",
actionType: "full",
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit an music_offtopic with mute action type (JSON method)", (done) => {
postSkipSegmentJSON({
userID,
videoID,
segments: [{
segment: [0, 10],
category: "music_offtopic",
actionType: "mute"
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});
describe("postSkipSegments 400 - Mismatched Types", () => {
it("Should not be able to submit with a category that does not exist", (done) => {
postSkipSegmentParam({
videoID,
startTime: 1,
endTime: 5,
category: "this-category-will-never-exist",
userID
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a chapter with skip action type (JSON method)", (done) => {
postSkipSegmentJSON({
userID,
videoID,
segments: [{
segment: [0, 10],
category: "chapter",
actionType: "skip"
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a sponsor with a description (JSON method)", (done) => {
const videoID = "postSkipChapter5";
postSkipSegmentJSON({
userID,
videoID,
segments: [{
segment: [0, 10],
category: "sponsor",
description: "This is a sponsor"
}],
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});

View File

@@ -0,0 +1,121 @@
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases";
import assert from "assert";
import { arrayDeepEquals } from "../utils/partialDeepEquals";
import { postSkipSegmentJSON, convertMultipleToDBFormat } from "./postSkipSegments";
import { YouTubeApiMock } from "../mocks/youtubeMock";
import { ImportMock } from "ts-mock-imports";
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
const mockManager = ImportMock.mockStaticClass(YouTubeAPIModule, "YouTubeAPI");
const sinonStub = mockManager.mock("listVideos");
sinonStub.callsFake(YouTubeApiMock.listVideos);
describe("postSkipSegments - Automod 80%", () => {
const userID = "postSkipSegments-automodSubmit";
const userIDHash = getHash(userID);
const over80VideoID = "80percent_video";
const queryDatabaseCategory = (videoID: string) => db.prepare("all", `SELECT "startTime", "endTime", "category" FROM "sponsorTimes" WHERE "videoID" = ? and "votes" > -1`, [videoID]);
before(() => {
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]);
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]);
});
it("Should allow multiple times if total is under 80% of video (JSON method)", (done) => {
const videoID = "postSkipSegments_80percent_video_blank1";
const segments = [{
segment: [3, 3000],
category: "sponsor",
}, {
segment: [3002, 3050],
category: "intro",
}, {
segment: [45, 100],
category: "interaction",
}, {
segment: [99, 170],
category: "sponsor",
}];
postSkipSegmentJSON({
userID,
videoID,
segments
})
.then(async res => {
assert.strictEqual(res.status, 200);
const rows = await queryDatabaseCategory(videoID);
const expected = convertMultipleToDBFormat(segments);
assert.ok(arrayDeepEquals(rows, expected));
done();
})
.catch(err => done(err));
}).timeout(5000);
it("Should reject multiple times if total is over 80% of video (JSON method)", (done) => {
const videoID = "postSkipSegments_80percent_video_blank2";
const segments = [{
segment: [0, 2000],
category: "interaction",
}, {
segment: [3000, 4000],
category: "sponsor",
}, {
segment: [1500, 2750],
category: "sponsor",
}, {
segment: [4050, 4750],
category: "intro",
}];
postSkipSegmentJSON({
userID,
videoID,
segments
})
.then(async res => {
assert.strictEqual(res.status, 403);
const rows = await queryDatabaseCategory(videoID);
assert.deepStrictEqual(rows, []);
done();
})
.catch(err => done(err));
}).timeout(5000);
it("Should reject multiple times if total is over 80% of video including previosuly submitted times (JSON method)", (done) => {
const segments = [{
segment: [2000, 4000], // adds 2000
category: "sponsor",
}, {
segment: [1500, 2750], // adds 500
category: "sponsor",
}, {
segment: [4050, 4570], // adds 520
category: "sponsor",
}];
const expected = [{
startTime: 0,
endTime: 1000,
category: "interaction"
}, {
startTime: 1001,
endTime: 1005,
category: "interaction"
}];
postSkipSegmentJSON({
userID,
videoID: over80VideoID,
segments: segments
})
.then(async res => {
assert.strictEqual(res.status, 403);
const rows = await queryDatabaseCategory(over80VideoID);
assert.ok(arrayDeepEquals(rows, expected, true));
done();
})
.catch(err => done(err));
}).timeout(5000);
});

View File

@@ -0,0 +1,84 @@
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases";
import assert from "assert";
import { partialDeepEquals } from "../utils/partialDeepEquals";
import { genRandom } from "../utils/getRandom";
import { Feature } from "../../src/types/user.model";
import { Segment, postSkipSegmentJSON, convertSingleToDBFormat } from "./postSkipSegments";
describe("postSkipSegments Features - Chapters", () => {
const submitUser_noPermissions = "postSkipSegments-chapters-noperm";
const submitUser_reputation = "postSkipSegments-chapters-reputation";
const submitUser_feature = "postSkipSegments-chapters-feature";
const queryDatabaseChapter = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "category", "actionType", "description" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
function createSegment(): Segment {
return {
segment: [0, 10],
category: "chapter",
actionType: "chapter",
description: genRandom()
};
}
before(() => {
const submitNumberOfTimes = 10;
const submitUser_reputationHash = getHash(submitUser_reputation);
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++) {
const uuid = `post_reputation_uuid-${i}`;
const videoID = `post_reputation_video-${i}`;
db.prepare("run", insertSponsorTimeQuery, [videoID, 1, 11, 5, 1, uuid, submitUser_reputationHash, 1597240000000, 50, "sponsor", "skip", 0]);
}
// user feature
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) => {
const segment = createSegment();
const videoID = "postSkipSegments-chapter-reputation";
postSkipSegmentJSON({
userID: submitUser_reputation,
videoID,
segments: [segment]
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await queryDatabaseChapter(videoID);
assert.ok(partialDeepEquals(row, convertSingleToDBFormat(segment)));
done();
})
.catch(err => done(err));
});
it("Should be able to submit a single chapter due to user feature (JSON method)", (done) => {
const segment = createSegment();
const videoID = "postSkipSegments-chapter-feature";
postSkipSegmentJSON({
userID: submitUser_feature,
videoID,
segments: [segment]
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await queryDatabaseChapter(videoID);
assert.ok(partialDeepEquals(row, convertSingleToDBFormat(segment)));
done();
})
.catch(err => done(err));
});
it("Should not be able to submit a chapter without permission (JSON method)", (done) => {
const videoID = "postSkipSegments-chapter-submit";
postSkipSegmentJSON({
userID: submitUser_noPermissions,
videoID,
segments: [createSegment()]
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});

View File

@@ -0,0 +1,127 @@
import { config } from "../../src/config";
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases";
import assert from "assert";
import { client } from "../utils/httpClient";
describe("postSkipSegments Warnings", () => {
// Constant and helpers
const warnUser01 = "warn-user01-qwertyuiopasdfghjklzxcvbnm";
const warnUser01Hash = getHash(warnUser01);
const warnUser02 = "warn-user02-qwertyuiopasdfghjklzxcvbnm";
const warnUser02Hash = getHash(warnUser02);
const warnUser03 = "warn-user03-qwertyuiopasdfghjklzxcvbnm";
const warnUser03Hash = getHash(warnUser03);
const warnVideoID = "postSkipSegments-warn-video";
const endpoint = "/api/skipSegments";
const postSkipSegmentJSON = (data: Record<string, any>) => client({
method: "POST",
url: endpoint,
data
});
before(() => {
const now = Date.now();
const warnVip01Hash = getHash("postSkipSegmentsWarnVIP");
const reason01 = "Reason01";
const reason02 = "";
const reason03 = "Reason03";
const MILLISECONDS_IN_HOUR = 3600000;
const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires;
const insertWarningQuery = 'INSERT INTO warnings ("userID", "issuerUserID", "enabled", "reason", "issueTime") VALUES(?, ?, ?, ?, ?)';
// User 1 - 4 active warnings
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, now]);
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 1000)]);
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 2000)]);
db.prepare("run", insertWarningQuery, [warnUser01Hash, warnVip01Hash, 1, reason01, (now - 3601000)]);
// User 2 - 3 active warnings
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, now]);
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, (now - (warningExpireTime + 1000))]);
db.prepare("run", insertWarningQuery, [warnUser02Hash, warnVip01Hash, 1, reason02, (now - (warningExpireTime + 2000))]);
// User 3 - 2 active warnings, 2 expired warnings
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 0, reason03, now]);
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 0, reason03, (now - 1000)]);
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - 2000)]);
db.prepare("run", insertWarningQuery, [warnUser03Hash, warnVip01Hash, 1, reason03, (now - 3601000)]);
});
it("Should be rejected with custom message if user has too many active warnings", (done) => {
postSkipSegmentJSON({
userID: warnUser01,
videoID: warnVideoID,
segments: [{
segment: [0, 10],
category: "sponsor",
}],
})
.then(res => {
assert.strictEqual(res.status, 403);
const errorMessage = res.data;
const reason = "Reason01";
const expected = "Submission rejected due to a warning 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.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
+ `Your userID is ${warnUser01Hash}.\n\nWarning reason: '${reason}'`;
assert.strictEqual(errorMessage, expected);
done();
})
.catch(err => done(err));
});
it("Should be accepted if user has some active warnings", (done) => {
postSkipSegmentJSON({
userID: warnUser02,
videoID: warnVideoID,
segments: [{
segment: [50, 60],
category: "sponsor",
}],
})
.then(res => {
assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`);
done();
})
.catch(err => done(err));
});
it("Should be accepted if user has some warnings removed", (done) => {
postSkipSegmentJSON({
userID: warnUser03,
videoID: warnVideoID,
segments: [{
segment: [53, 60],
category: "sponsor",
}],
})
.then(res => {
assert.ok(res.status === 200, `Status code was ${res.status} ${res.data}`);
done();
})
.catch(err => done(err));
});
it("Should be rejected with default message if user has to many active warnings", (done) => {
postSkipSegmentJSON({
userID: warnUser01,
videoID: warnVideoID,
segments: [{
segment: [0, 10],
category: "sponsor",
}],
})
.then(res => {
assert.strictEqual(res.status, 403);
const errorMessage = res.data;
assert.notStrictEqual(errorMessage, "");
done();
})
.catch(err => done(err));
});
});