This commit is contained in:
Ajay 2022-10-29 21:04:01 -04:00
commit 68d9d3cbde
78 changed files with 2473 additions and 260 deletions

View file

@ -13,8 +13,8 @@ jobs:
steps:
# Initialization
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- name: Run Tests
timeout-minutes: 5

View file

@ -13,8 +13,8 @@ jobs:
steps:
# Initialization
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- name: Run Tests
timeout-minutes: 5

View file

@ -14,8 +14,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- name: Set config
run: |
@ -23,7 +23,7 @@ jobs:
- name: Run Server
timeout-minutes: 10
run: npm start
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: SponsorTimesDB.db
path: databases/sponsorTimes.db

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build the docker-compose stack
env:
PG_USER: ci_db_user
@ -20,10 +20,12 @@ jobs:
run: docker-compose -f docker/docker-compose-ci.yml up -d
- name: Check running containers
run: docker ps
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
- run: npm install
- name: Run Tests
env:
TEST_POSTGRES: true
timeout-minutes: 5
run: npm test
run: npx nyc --silent npm test
- name: Generate coverage report
run: npm run cover:report

3
.gitignore vendored
View file

@ -47,4 +47,5 @@ working
/dist/
# nyc coverage output
.nyc_output/
.nyc_output/
coverage/

View file

@ -1,5 +1,14 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"check-coverage": false,
"ski-full": true,
"reporter": ["text", "html"],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/routes/addUnlitedVideo.ts"
"src/routes/addUnlistedVideo.ts",
"src/cronjob/downvoteSegmentArchiveJob.ts",
"src/databases/*"
]
}

14
ci.json
View file

@ -4,11 +4,12 @@
"globalSalt": "testSalt",
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
"newLeafURLs": ["placeholder"],
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook",
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook",
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook",
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook",
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel",
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions",
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport",
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject",
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
"userCounterURL": "http://127.0.0.1:8081/UserCounter",
"behindProxy": true,
"postgres": {
"user": "ci_db_user",
@ -70,5 +71,10 @@
"statusCode": 200
}
},
"patreon": {
"clientId": "testClientID",
"clientSecret": "testClientSecret",
"redirectUri": "http://127.0.0.1/fake/callback"
},
"minReputationToSubmitFiller": -1
}

109
package-lock.json generated
View file

@ -23,6 +23,7 @@
"sync-mysql": "^3.0.1"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/better-sqlite3": "^7.5.0",
"@types/cron": "^2.0.0",
"@types/express": "^4.17.13",
@ -30,8 +31,10 @@
"@types/mocha": "^9.1.1",
"@types/node": "^18.0.3",
"@types/pg": "^8.6.5",
"@types/sinon": "^10.0.13",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"axios-mock-adapter": "^1.21.2",
"eslint": "^8.19.0",
"mocha": "^10.0.0",
"nodemon": "^2.0.19",
@ -630,6 +633,21 @@
"node": ">=8"
}
},
"node_modules/@istanbuljs/nyc-config-typescript": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz",
"integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==",
"dev": true,
"dependencies": {
"@istanbuljs/schema": "^0.1.2"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"nyc": ">=15"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@ -963,6 +981,21 @@
"@types/node": "*"
}
},
"node_modules/@types/sinon": {
"version": "10.0.13",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz",
"integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==",
"dev": true,
"dependencies": {
"@types/sinonjs__fake-timers": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
"integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
@ -1339,6 +1372,19 @@
"form-data": "^4.0.0"
}
},
"node_modules/axios-mock-adapter": {
"version": "1.21.2",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz",
"integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"is-buffer": "^2.0.5"
},
"peerDependencies": {
"axios": ">= 0.17.0"
}
},
"node_modules/babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
@ -3094,6 +3140,29 @@
"node": ">=8"
}
},
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"engines": {
"node": ">=4"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -6154,6 +6223,15 @@
}
}
},
"@istanbuljs/nyc-config-typescript": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz",
"integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==",
"dev": true,
"requires": {
"@istanbuljs/schema": "^0.1.2"
}
},
"@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@ -6453,6 +6531,21 @@
"@types/node": "*"
}
},
"@types/sinon": {
"version": "10.0.13",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz",
"integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==",
"dev": true,
"requires": {
"@types/sinonjs__fake-timers": "*"
}
},
"@types/sinonjs__fake-timers": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz",
"integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz",
@ -6698,6 +6791,16 @@
"form-data": "^4.0.0"
}
},
"axios-mock-adapter": {
"version": "1.21.2",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz",
"integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.3",
"is-buffer": "^2.0.5"
}
},
"babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
@ -7975,6 +8078,12 @@
"binary-extensions": "^2.0.0"
}
},
"is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"dev": true
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",

View file

@ -5,7 +5,8 @@
"main": "src/index.ts",
"scripts": {
"test": "npm run tsc && ts-node test/test.ts",
"test:coverage": "nyc npm run test",
"cover": "nyc npm test",
"cover:report": "nyc report",
"dev": "nodemon",
"dev:bash": "nodemon -x 'npm test ; npm start'",
"postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:alpine",
@ -32,6 +33,7 @@
"sync-mysql": "^3.0.1"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/better-sqlite3": "^7.5.0",
"@types/cron": "^2.0.0",
"@types/express": "^4.17.13",
@ -39,8 +41,10 @@
"@types/mocha": "^9.1.1",
"@types/node": "^18.0.3",
"@types/pg": "^8.6.5",
"@types/sinon": "^10.0.13",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"axios-mock-adapter": "^1.21.2",
"eslint": "^8.19.0",
"mocha": "^10.0.0",
"nodemon": "^2.0.19",

View file

@ -45,6 +45,8 @@ import { youtubeApiProxy } from "./routes/youtubeApiProxy";
import { getChapterNames } from "./routes/getChapterNames";
import { getTopCategoryUsers } from "./routes/getTopCategoryUsers";
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
import { endpoint as getVideoLabels } from "./routes/getVideoLabel";
import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash";
import { addFeature } from "./routes/addFeature";
import { generateTokenRequest } from "./routes/generateToken";
import { verifyTokenRequest } from "./routes/verifyToken";
@ -200,6 +202,11 @@ function setupRoutes(router: Router) {
router.get("/api/generateToken/:type", generateTokenRequest);
router.get("/api/verifyToken", verifyTokenRequest);
// labels
router.get("/api/videoLabels", getVideoLabels);
router.get("/api/videoLabels/:prefix", getVideoLabelsByHash);
/* istanbul ignore next */
if (config.postgres?.enabled) {
router.get("/database", (req, res) => dumpDatabase(req, res, true));
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
@ -211,4 +218,4 @@ function setupRoutes(router: Router) {
});
}
}
/* eslint-enable @typescript-eslint/no-misused-promises */
/* eslint-enable @typescript-eslint/no-misused-promises */

View file

@ -35,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
|| actionTypes && !Array.isArray(actionTypes)
|| actionTypes.length === 0
) {
return res.status(400).json({
@ -48,7 +49,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ
if (!userIsVIP) {
return res.status(403).json({
message: "Must be a VIP to mark videos.",
message: "Must be a VIP to lock videos.",
});
}

View file

@ -24,6 +24,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo
if (type === TokenType.patreon || (type === TokenType.local && adminUserIDHash === config.adminUserID)) {
const licenseKey = await createAndSaveToken(type, code);
/* istanbul ignore else */
if (licenseKey) {
return res.status(200).send(`
<h1>
@ -45,5 +46,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo
</h1>
`);
}
} else {
return res.sendStatus(403);
}
}

View file

@ -7,7 +7,11 @@ export async function getDaysSavedFormatted(req: Request, res: Response): Promis
if (row !== undefined) {
//send this result
return res.send({
daysSaved: row.daysSaved.toFixed(2),
daysSaved: row.daysSaved?.toFixed(2) ?? "0",
});
} else {
return res.send({
daysSaved: 0
});
}
}

View file

@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise<Respons
hashedUserID: hashedUserID,
vip: vipState,
});
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -33,7 +33,7 @@ export async function getLockCategories(req: Request, res: Response): Promise<Re
categories,
actionTypes
});
} catch (err) {
} catch (err) /* istanbul ignore next */{
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -44,14 +44,25 @@ const mergeLocks = (source: DBLock[], actionTypes: ActionType[]): LockResultByHa
export async function getLockCategoriesByHash(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
const actionTypes: ActionType[] = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
let actionTypes: ActionType[] = [];
try {
actionTypes = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
if (!Array.isArray(actionTypes)) {
//invalid request
return res.sendStatus(400);
}
} catch (err) {
//invalid request
return res.status(400).send("Invalid request: JSON parse error (actionTypes)");
}
if (!hashPrefixTester(req.params.prefix)) {
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
}
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
@ -62,7 +73,7 @@ export async function getLockCategoriesByHash(req: Request, res: Response): Prom
if (lockedRows.length === 0 || !lockedRows[0]) return res.sendStatus(404);
// merge all locks
return res.send(mergeLocks(lockedRows, actionTypes));
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -32,18 +32,24 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
return res.status(400).send("No videoID provided");
}
let categories: Category[] = [];
const actionTypes: ActionType[] = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
const possibleCategories = filterActionType(actionTypes);
if (!Array.isArray(actionTypes)) {
//invalid request
return res.status(400).send("actionTypes parameter does not match format requirements");
let actionTypes: ActionType[] = [];
try {
actionTypes = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
if (!Array.isArray(actionTypes)) {
//invalid request
return res.status(400).send("actionTypes parameter does not match format requirements");
}
} catch (error) {
return res.status(400).send("Bad parameter: actionTypes (invalid JSON)");
}
const possibleCategories = filterActionType(actionTypes);
try {
categories = req.query.categories
? JSON.parse(req.query.categories as string)
@ -64,11 +70,6 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
: categories.filter(x =>
possibleCategories.includes(x));
if (!videoID || !Array.isArray(actionTypes)) {
//invalid request
return res.sendStatus(400);
}
try {
// Get existing lock categories markers
const row = await db.prepare("all", 'SELECT "category", "reason", "actionType", "userID" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category, reason: string, actionType: ActionType, userID: string }[];
@ -115,7 +116,7 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
}
return res.send(results);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -27,7 +27,7 @@ export async function getSavedTimeForUser(req: Request, res: Response): Promise<
} else {
return res.sendStatus(404);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`getSavedTimeForUser ${err}`);
return res.sendStatus(500);
}

View file

@ -128,12 +128,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<searchSeg
const segments = await getSegmentsFromDBByVideoID(videoID, service);
if (segments === null || segments === undefined) {
res.sendStatus(500);
return false;
}
if (segments.length === 0) {
if (!segments?.length) {
res.sendStatus(404);
return false;
}
@ -155,6 +150,7 @@ function filterSegments(segments: DBSegment[], filters: Record<string, any>, pag
);
if (sortBy !== SortableFields.timeSubmitted) {
/* istanbul ignore next */
filteredSegments.sort((a,b) => {
const key = sortDir === "desc" ? 1 : -1;
if (a[sortBy] < b[sortBy]) {
@ -187,6 +183,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
return res.send(segmentResponse);
}
} catch (err) {
/* istanbul ignore next */
if (err instanceof SyntaxError) {
return res.status(400).send("Invalid array in parameters");
} else return res.sendStatus(500);

View file

@ -7,7 +7,7 @@ const isValidSegmentUUID = (str: string): boolean => /^([a-f0-9]{64}|[a-f0-9]{8}
async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise<DBSegment> {
try {
return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@ -62,7 +62,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
//send result
return res.send(DBSegments);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err instanceof SyntaxError) { // catch JSON.parse error
return res.status(400).send("UUIDs parameter does not match format requirements.");
} else return res.sendStatus(500);

View file

@ -107,7 +107,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
}
return processedSegments;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err) {
Logger.error(err as string);
return null;
@ -169,7 +169,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
}));
return segments;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return null;
}
@ -465,7 +465,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
//send result
return res.send(segments);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err instanceof SyntaxError) {
return res.status(400).send("Categories parameter does not match format requirements.");
} else return res.sendStatus(500);

View file

@ -67,8 +67,6 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
// Get all video id's that match hash prefix
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);
if (!segments) return res.status(404).json([]);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
hash: data.hash,

View file

@ -18,7 +18,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
processTime = Date.now() - dbStartTime;
return e.value;
})
.catch(e => {
.catch(e => /* istanbul ignore next */ {
Logger.error(`status: SQL query timed out: ${e}`);
return -1;
});
@ -28,7 +28,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
.then(e => {
redisProcessTime = Date.now() - redisStartTime;
return e;
}).catch(e => {
}).catch(e => /* istanbul ignore next */ {
Logger.error(`status: redis increment timed out ${e}`);
return [-1];
});
@ -36,7 +36,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
const statusValues: Record<string, any> = {
uptime: process.uptime(),
commit: (global as any).HEADCOMMIT || "unknown",
commit: (global as any)?.HEADCOMMIT ?? "unknown",
db: Number(dbVersion),
startTime,
processTime,
@ -48,7 +48,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
activeRedisRequests: getRedisActiveRequests(),
};
return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -75,11 +75,6 @@ export async function getTopUsers(req: Request, res: Response): Promise<Response
const sortType = parseInt(req.query.sortType as string);
const categoryStatsEnabled = req.query.categoryStats;
if (sortType == undefined) {
//invalid request
return res.sendStatus(400);
}
//setup which sort type to use
let sortBy = "";
if (sortType == 0) {

View file

@ -12,7 +12,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
try {
return db.prepare("all", `SELECT "userName", "userID" FROM "userNames" WHERE "userName"
LIKE ? ESCAPE '\\' LIMIT 10`, [userName]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@ -20,7 +20,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
function getExactUserID(userName: string): Promise<{userName: string, userID: UserID }[]> {
try {
return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]);
} catch (err) {
} catch (err) /* istanbul ignore next */{
return null;
}
}
@ -42,6 +42,7 @@ export async function getUserID(req: Request, res: Response): Promise<Response>
: await getFuzzyUserID(userName);
if (results === undefined || results === null) {
/* istanbul ignore next */
return res.sendStatus(500);
} else if (results.length === 0) {
return res.sendStatus(404);

View file

@ -28,7 +28,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
segmentCount: 0,
};
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@ -37,7 +37,7 @@ async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise<number> {
try {
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
return row?.ignoredSegmentCount ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@ -46,7 +46,7 @@ async function dbGetUsername(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
return row?.userName ?? userID;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@ -55,7 +55,7 @@ async function dbGetViewsForUser(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID], { useReplica: true });
return row?.viewCount ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@ -64,7 +64,7 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
return row?.ignoredViewCount ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@ -73,7 +73,7 @@ async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
try {
const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID], { useReplica: true });
return row?.total ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
return 0;
}
@ -83,7 +83,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUI
try {
const row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID], { useReplica: true });
return row?.UUID ?? null;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@ -92,7 +92,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<st
try {
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID], { useReplica: true });
return row?.reason ?? "";
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
return "";
}
@ -102,7 +102,7 @@ async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
try {
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true });
return row?.userCount > 0 ?? false;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@ -194,7 +194,7 @@ async function getUserInfo(req: Request, res: Response): Promise<Response> {
export async function endpoint(req: Request, res: Response): Promise<Response> {
try {
return await getUserInfo(req, res);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err instanceof SyntaxError) { // catch JSON.parse error
return res.status(400).send("Invalid values JSON");
} else return res.sendStatus(500);

View file

@ -75,7 +75,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea
};
}
return result;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return null;
}
@ -85,7 +85,7 @@ async function dbGetUsername(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
return row?.userName ?? userID;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}

View file

@ -27,7 +27,7 @@ export async function getUsername(req: Request, res: Response): Promise<Response
userName: userID,
});
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

167
src/routes/getVideoLabel.ts Normal file
View file

@ -0,0 +1,167 @@
import { Request, Response } from "express";
import { db } from "../databases/databases";
import { videoLabelsHashKey, videoLabelsKey } from "../utils/redisKeys";
import { SBRecord } from "../types/lib.model";
import { DBSegment, Segment, Service, VideoData, VideoID, VideoIDHash } from "../types/segments.model";
import { Logger } from "../utils/logger";
import { QueryCacher } from "../utils/queryCacher";
import { getService } from "../utils/getService";
function transformDBSegments(segments: DBSegment[]): Segment[] {
return segments.map((chosenSegment) => ({
category: chosenSegment.category,
actionType: chosenSegment.actionType,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
locked: chosenSegment.locked,
votes: chosenSegment.votes,
videoDuration: chosenSegment.videoDuration,
userID: chosenSegment.userID,
description: chosenSegment.description
}));
}
async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise<Segment[]> {
try {
const segments: DBSegment[] = await getSegmentsFromDBByVideoID(videoID, service);
return chooseSegment(segments);
} catch (err) {
if (err) {
Logger.error(err as string);
return null;
}
}
}
async function getLabelsByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<SBRecord<VideoID, VideoData>> {
const segments: SBRecord<VideoID, VideoData> = {};
try {
type SegmentWithHashPerVideoID = SBRecord<VideoID, { hash: VideoIDHash, segments: DBSegment[] }>;
const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service))
.reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
acc[segment.videoID] = acc[segment.videoID] || {
hash: segment.hashedVideoID,
segments: []
};
acc[segment.videoID].segments ??= [];
acc[segment.videoID].segments.push(segment);
return acc;
}, {});
for (const [videoID, videoData] of Object.entries(segmentPerVideoID)) {
const data: VideoData = {
hash: videoData.hash,
segments: chooseSegment(videoData.segments),
};
if (data.segments.length > 0) {
segments[videoID] = data;
}
}
return segments;
} catch (err) {
Logger.error(err as string);
return null;
}
}
async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<DBSegment[]> {
const fetchFromDB = () => db
.prepare(
"all",
`SELECT "startTime", "endTime", "videoID", "votes", "locked", "UUID", "userID", "category", "actionType", "hashedVideoID", "description" FROM "sponsorTimes"
WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`,
[`${hashedVideoIDPrefix}%`, service]
) as Promise<DBSegment[]>;
if (hashedVideoIDPrefix.length === 4) {
return await QueryCacher.get(fetchFromDB, videoLabelsHashKey(hashedVideoIDPrefix, service));
}
return await fetchFromDB();
}
async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
const fetchFromDB = () => db
.prepare(
"all",
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "description" FROM "sponsorTimes"
WHERE "videoID" = ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`,
[videoID, service]
) as Promise<DBSegment[]>;
return await QueryCacher.get(fetchFromDB, videoLabelsKey(videoID, service));
}
function chooseSegment<T extends DBSegment>(choices: T[]): Segment[] {
// filter out -2 segments
choices = choices.filter((segment) => segment.votes > -2);
const results = [];
// trivial decisions
if (choices.length === 0) {
return [];
} else if (choices.length === 1) {
return transformDBSegments(choices);
}
// if locked, only choose from locked
const locked = choices.filter((segment) => segment.locked);
if (locked.length > 0) {
choices = locked;
}
//no need to filter, just one label
if (choices.length === 1) {
return transformDBSegments(choices);
}
// sponsor > exclusive > selfpromo
const findCategory = (category: string) => choices.find((segment) => segment.category === category);
const categoryResult = findCategory("sponsor") ?? findCategory("exclusive_access") ?? findCategory("selfpromo");
if (categoryResult) results.push(categoryResult);
return transformDBSegments(results);
}
async function handleGetLabel(req: Request, res: Response): Promise<Segment[] | false> {
const videoID = req.query.videoID as VideoID;
if (!videoID) {
res.status(400).send("videoID not specified");
return false;
}
const service = getService(req.query.service, req.body.service);
const segments = await getLabelsByVideoID(videoID, service);
if (!segments || segments.length === 0) {
res.sendStatus(404);
return false;
}
return segments;
}
async function endpoint(req: Request, res: Response): Promise<Response> {
try {
const segments = await handleGetLabel(req, res);
// If false, res.send has already been called
if (segments) {
//send result
return res.send(segments);
}
} catch (err) {
if (err instanceof SyntaxError) {
return res.status(400).send("Categories parameter does not match format requirements.");
} else return res.sendStatus(500);
}
}
export {
getLabelsByVideoID,
getLabelsByHash,
endpoint
};

View file

@ -0,0 +1,27 @@
import { hashPrefixTester } from "../utils/hashPrefixTester";
import { getLabelsByHash } from "./getVideoLabel";
import { Request, Response } from "express";
import { VideoIDHash, Service } from "../types/segments.model";
import { getService } from "../utils/getService";
export async function getVideoLabelsByHash(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
if (!req.params.prefix || !hashPrefixTester(req.params.prefix)) {
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
}
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
const service: Service = getService(req.query.service, req.body.service);
// Get all video id's that match hash prefix
const segments = await getLabelsByHash(hashPrefix, service);
if (!segments) return res.status(404).json([]);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
hash: data.hash,
segments: data.segments,
}));
return res.status(output.length === 0 ? 404 : 200).json(output);
}

View file

@ -25,7 +25,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise<Resp
} else {
return res.sendStatus(404);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -47,7 +47,7 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
return res.status(200).json({
message: `Cache cleared on video ${videoID}`
});
} catch(err) {
} catch(err) /* istanbul ignore next */ {
return res.sendStatus(500);
}
}

View file

@ -37,7 +37,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
if (!userIsVIP) {
res.status(403).json({
message: "Must be a VIP to mark videos.",
message: "Must be a VIP to lock videos.",
});
return;
}
@ -66,7 +66,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
for (const lock of locksToApply) {
try {
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "actionType", "category", "hashedVideoID", "reason", "service") VALUES(?, ?, ?, ?, ?, ?, ?)`, [videoID, userID, lock.actionType, lock.category, hashedVideoID, reason, service]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
Logger.error(err as string);
res.status(500).json({
@ -82,7 +82,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
await db.prepare("run",
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "actionType" = ? AND "category" = ? AND "service" = ?',
[reason, userID, videoID, lock.actionType, lock.category, service]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
Logger.error(err as string);
res.status(500).json({

View file

@ -37,7 +37,7 @@ export async function postPurgeAllSegments(req: Request, res: Response): Promise
service
});
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -91,7 +91,7 @@ export async function postSegmentShift(req: Request, res: Response): Promise<Res
break;
}
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -56,7 +56,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
return res.sendStatus(200);
}
}
catch (error) {
catch (error) /* istanbul ignore next */ {
Logger.error(error as string);
return res.sendStatus(500);
}
@ -83,7 +83,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
await logUserNameChange(userID, userName, oldUserName, adminUserIDInput !== undefined);
return res.sendStatus(200);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View file

@ -4,7 +4,6 @@ import { config } from "../config";
import { privateDB } from "../databases/databases";
import { Logger } from "../utils/logger";
import { getPatreonIdentity, PatronStatus, refreshToken, TokenType } from "../utils/tokenUtils";
import FormData from "form-data";
interface VerifyTokenRequest extends Request {
query: {
@ -12,14 +11,16 @@ interface VerifyTokenRequest extends Request {
}
}
export const validatelicenseKeyRegex = (token: string) =>
new RegExp(/[A-Za-z0-9]{40}|[A-Za-z0-9-]{35}/).test(token);
export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise<Response> {
const { query: { licenseKey } } = req;
if (!licenseKey) {
return res.status(400).send("Invalid request");
}
const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/);
if (!licenseRegex.test(licenseKey)) {
} else if (!validatelicenseKeyRegex(licenseKey)) {
// fast check for invalid licence key
return res.status(200).send({
allowed: false
});
@ -34,6 +35,7 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error);
}
/* istanbul ignore else */
if (identity) {
const membership = identity.included?.[0]?.attributes;
const allowed = !!membership && ((membership.patron_status === PatronStatus.active && membership.currently_entitled_amount_cents > 0)
@ -65,20 +67,13 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
async function checkAllGumroadProducts(licenseKey: string): Promise<boolean> {
for (const link of config.gumroad.productPermalinks) {
try {
const formData = new FormData();
formData.append("product_permalink", link);
formData.append("license_key", licenseKey);
const result = await axios.request({
url: "https://api.gumroad.com/v2/licenses/verify",
data: formData,
method: "POST",
headers: formData.getHeaders()
const result = await axios.post("https://api.gumroad.com/v2/licenses/verify", {
params: { product_permalink: link, license_key: licenseKey }
});
const allowed = result.status === 200 && result.data?.success;
if (allowed) return allowed;
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`Gumroad fetch for ${link} failed: ${e}`);
}
}

View file

@ -5,7 +5,7 @@ import { HashedUserID, UserID } from "./user.model";
export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type VideoID = string & { __videoIDBrand: unknown };
export type VideoDuration = number & { __videoDurationBrand: unknown };
export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "filler" | "poi_highlight" | "chapter") & { __categoryBrand: unknown };
export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight" | "chapter" | "filler" | "exclusive_access") & { __categoryBrand: unknown };
export type VideoIDHash = VideoID & HashedValue;
export type IPAddress = string & { __ipAddressBrand: unknown };
export type HashedIP = IPAddress & HashedValue;

View file

@ -3,6 +3,9 @@ import { Request } from "express";
import { IPAddress } from "../types/segments.model";
export function getIP(req: Request): IPAddress {
// if in testing mode, return immediately
if (config.mode === "test") return "127.0.0.1" as IPAddress;
if (config.behindProxy === true || config.behindProxy === "true") {
config.behindProxy = "X-Forwarded-For";
}
@ -15,6 +18,6 @@ export function getIP(req: Request): IPAddress {
case "X-Real-IP":
return req.headers["x-real-ip"] as IPAddress;
default:
return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress;
return req.socket?.remoteAddress as IPAddress;
}
}

View file

@ -18,6 +18,7 @@ async function getFromITube (videoID: string): Promise<innerTubeVideoDetails> {
const result = await axios.post(url, data, {
timeout: 3500
});
/* istanbul ignore else */
if (result.status === 200) {
return result.data.videoDetails;
} else {
@ -39,6 +40,7 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom
return data as innerTubeVideoDetails;
}
} catch (err) {
/* istanbul ignore next */
return Promise.reject(err);
}
}

View file

@ -1,6 +1,6 @@
import redis from "../utils/redis";
import { Logger } from "../utils/logger";
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey } from "./redisKeys";
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsKey, videoLabelsHashKey } from "./redisKeys";
import { Service, VideoID, VideoIDHash } from "../types/segments.model";
import { Feature, HashedUserID, UserID } from "../types/user.model";
import { config } from "../config";
@ -81,6 +81,8 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID
redis.del(skipSegmentsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(skipSegmentGroupsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(videoLabelsKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(videoLabelsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
if (videoInfo.userID) redis.del(reputationKey(videoInfo.userID)).catch((err) => Logger.error(err));
}
}

View file

@ -38,6 +38,16 @@ export function shaHashKey(singleIter: HashedValue): string {
export const tempVIPKey = (userID: HashedUserID): string =>
`vip.temp.${userID}`;
export const videoLabelsKey = (videoID: VideoID, service: Service): string =>
`labels.v1.${service}.videoID.${videoID}`;
export function videoLabelsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string {
hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash;
if (hashedVideoIDPrefix.length !== 4) Logger.warn(`Redis skip segment hash-prefix key is not length 4! ${hashedVideoIDPrefix}`);
return `labels.v1.${service}.${hashedVideoIDPrefix}`;
}
export function userFeatureKey (userID: HashedUserID, feature: Feature): string {
return `user.${userID}.feature.${feature}`;
}

View file

@ -58,12 +58,11 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis
return licenseKey;
}
} catch (e) {
break;
} catch (e) /* istanbul ignore next */ {
Logger.error(`token creation: ${e}`);
return null;
}
break;
}
case TokenType.local: {
const licenseKey = generateToken();
@ -74,7 +73,6 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis
return licenseKey;
}
}
return null;
}
@ -102,15 +100,12 @@ export async function refreshToken(type: TokenType, licenseKey: string, refreshT
return true;
}
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`token refresh: ${e}`);
return false;
}
break;
}
}
return false;
}
@ -136,9 +131,8 @@ export async function getPatreonIdentity(accessToken: string): Promise<PatreonId
if (identityRequest.status === 200) {
return identityRequest.data;
}
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`identity request: ${e}`);
}
return null;
}

View file

@ -4,11 +4,12 @@
"globalSalt": "testSalt",
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
"newLeafURLs": ["placeholder"],
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook",
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook",
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook",
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook",
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/webhook/ReportChannel",
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/webhook/FirstTimeSubmissions",
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/webhook/CompletelyIncorrectReport",
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/webhook/NeuralBlockReject",
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
"userCounterURL": "http://127.0.0.1:8081/UserCounter",
"behindProxy": true,
"db": ":memory:",
"privateDB": ":memory:",
@ -58,5 +59,10 @@
"statusCode": 200
}
},
"patreon": {
"clientId": "testClientID",
"clientSecret": "testClientSecret",
"redirectUri": "http://127.0.0.1/fake/callback"
},
"minReputationToSubmitFiller": -1
}

141
test/cases/addUserAsVIP.ts Normal file
View file

@ -0,0 +1,141 @@
import { getHash } from "../../src/utils/getHash";
import { HashedUserID } from "../../src/types/user.model";
import { client } from "../utils/httpClient";
import { db } from "../../src/databases/databases";
import assert from "assert";
// helpers
const checkUserVIP = (publicID: string) => db.prepare("get", `SELECT "userID" FROM "vipUsers" WHERE "userID" = ?`, [publicID]);
const adminPrivateUserID = "testUserId";
const permVIP1 = "addVIP_permaVIPOne";
const publicPermVIP1 = getHash(permVIP1) as HashedUserID;
const permVIP2 = "addVIP_permaVIPTwo";
const publicPermVIP2 = getHash(permVIP2) as HashedUserID;
const permVIP3 = "addVIP_permaVIPThree";
const publicPermVIP3 = getHash(permVIP3) as HashedUserID;
const endpoint = "/api/addUserAsVIP";
const addUserAsVIP = (userID: string, enabled: boolean, adminUserID = adminPrivateUserID) => client({
method: "POST",
url: endpoint,
params: {
userID,
adminUserID,
enabled: String(enabled)
}
});
describe("addVIP test", function() {
it("User should not already be VIP", (done) => {
checkUserVIP(publicPermVIP1)
.then(result => {
assert.ok(!result);
done();
})
.catch(err => done(err));
});
it("Should be able to add user as VIP", (done) => {
addUserAsVIP(publicPermVIP1, true)
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await checkUserVIP(publicPermVIP1);
assert.ok(row);
done();
})
.catch(err => done(err));
});
it("Should be able to add second user as VIP", (done) => {
addUserAsVIP(publicPermVIP2, true)
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await checkUserVIP(publicPermVIP2);
assert.ok(row);
done();
})
.catch(err => done(err));
});
it("Should return 403 with invalid adminID", (done) => {
addUserAsVIP(publicPermVIP1, true, "Invalid_Admin_User_ID")
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
it("Should return 400 with missing adminID", (done) => {
client({
method: "POST",
url: endpoint,
params: {
userID: publicPermVIP1,
enabled: String(true)
}
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 with missing userID", (done) => {
client({
method: "POST",
url: endpoint,
params: {
enabled: String(true),
adminUserID: adminPrivateUserID
}
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be able to remove VIP", (done) => {
addUserAsVIP(publicPermVIP1, false)
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await checkUserVIP(publicPermVIP1);
assert.ok(!row);
done();
})
.catch(err => done(err));
});
it("Should remove VIP if enabled is false", (done) => {
client({
method: "POST",
url: endpoint,
params: {
userID: publicPermVIP2,
adminUserID: adminPrivateUserID,
enabled: "invalid-text"
}
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await checkUserVIP(publicPermVIP2);
assert.ok(!row);
done();
})
.catch(err => done(err));
});
it("Should remove VIP if enabled is missing", (done) => {
client({
method: "POST",
url: endpoint,
params: {
userID: publicPermVIP3,
adminUserID: adminPrivateUserID
}
})
.then(async res => {
assert.strictEqual(res.status, 200);
const row = await checkUserVIP(publicPermVIP3);
assert.ok(!row);
done();
})
.catch(err => done(err));
});
});

View file

@ -0,0 +1,189 @@
import assert from "assert";
import { config } from "../../src/config";
import axios from "axios";
import { createAndSaveToken, TokenType } from "../../src/utils/tokenUtils";
import MockAdapter from "axios-mock-adapter";
let mock: MockAdapter;
import * as patreon from "../mocks/patreonMock";
import * as gumroad from "../mocks/gumroadMock";
import { client } from "../utils/httpClient";
import { validatelicenseKeyRegex } from "../../src/routes/verifyToken";
const generateEndpoint = "/api/generateToken";
const getGenerateToken = (type: string, code: string | null, adminUserID: string | null) => client({
url: `${generateEndpoint}/${type}`,
params: { code, adminUserID }
});
const verifyEndpoint = "/api/verifyToken";
const getVerifyToken = (licenseKey: string | null) => client({
url: verifyEndpoint,
params: { licenseKey }
});
let patreonLicense: string;
let localLicense: string;
const gumroadLicense = gumroad.generateLicense();
const extractLicenseKey = (data: string) => {
const regex = /([A-Za-z0-9]{40})/;
const match = data.match(regex);
if (!match) throw new Error("Failed to extract license key");
return match[1];
};
describe("generateToken test", function() {
before(function() {
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
});
after(function () {
mock.restore();
});
it("Should be able to create patreon token for active patron", function (done) {
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
if (!config?.patreon) this.skip();
getGenerateToken("patreon", "patreon_code", "").then(res => {
patreonLicense = extractLicenseKey(res.data);
assert.ok(validatelicenseKeyRegex(patreonLicense));
done();
}).catch(err => done(err));
});
it("Should create patreon token for invalid patron", function (done) {
mock.onGet(/identity/).reply(200, patreon.formerIdentityFail);
if (!config?.patreon) this.skip();
getGenerateToken("patreon", "patreon_code", "").then(res => {
patreonLicense = extractLicenseKey(res.data);
assert.ok(validatelicenseKeyRegex(patreonLicense));
done();
}).catch(err => done(err));
});
it("Should be able to create new local token", function (done) {
createAndSaveToken(TokenType.local).then((licenseKey) => {
assert.ok(validatelicenseKeyRegex(licenseKey));
localLicense = licenseKey;
done();
}).catch(err => done(err));
});
it("Should return 400 if missing code parameter", function (done) {
getGenerateToken("patreon", null, "").then(res => {
assert.strictEqual(res.status, 400);
done();
}).catch(err => done(err));
});
it("Should return 403 if missing adminuserID parameter", function (done) {
getGenerateToken("local", "fake-code", null).then(res => {
assert.strictEqual(res.status, 403);
done();
}).catch(err => done(err));
});
it("Should return 403 for invalid adminuserID parameter", function (done) {
getGenerateToken("local", "fake-code", "fakeAdminID").then(res => {
assert.strictEqual(res.status, 403);
done();
}).catch(err => done(err));
});
});
describe("verifyToken static tests", function() {
it("Should fast reject invalid token", function (done) {
getVerifyToken("00000").then(res => {
assert.strictEqual(res.status, 200);
assert.ok(!res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should return 400 if missing code token", function (done) {
getVerifyToken(null).then(res => {
assert.strictEqual(res.status, 400);
done();
}).catch(err => done(err));
});
});
describe("verifyToken mock tests", function() {
beforeEach(function() {
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
});
afterEach(function () {
mock.restore();
});
it("Should accept current patron", function (done) {
if (!config?.patreon) this.skip();
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
getVerifyToken(patreonLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should reject nonexistent patron", function (done) {
if (!config?.patreon) this.skip();
mock.onGet(/identity/).reply(200, patreon.invalidIdentity);
getVerifyToken(patreonLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(!res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should accept qualitying former patron", function (done) {
if (!config?.patreon) this.skip();
mock.onGet(/identity/).reply(200, patreon.formerIdentitySucceed);
getVerifyToken(patreonLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should reject unqualitifed former patron", function (done) {
if (!config?.patreon) this.skip();
mock.onGet(/identity/).reply(200, patreon.formerIdentityFail);
getVerifyToken(patreonLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(!res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should accept real gumroad key", function (done) {
mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseSuccess);
getVerifyToken(gumroadLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should reject fake gumroad key", function (done) {
mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseFail);
getVerifyToken(gumroadLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(!res.data.allowed);
done();
}).catch(err => done(err));
});
it("Should validate local license", function (done) {
getVerifyToken(localLicense).then(res => {
assert.strictEqual(res.status, 200);
assert.ok(res.data.allowed);
done();
}).catch(err => done(err));
});
});

View file

@ -0,0 +1,27 @@
import assert from "assert";
import { client } from "../utils/httpClient";
import sinon from "sinon";
import { db } from "../../src/databases/databases";
const endpoint = "/api/getDaysSavedFormatted";
describe("getDaysSavedFormatted", () => {
it("can get days saved", async () => {
const result = await client({ url: endpoint });
assert.ok(result.data.daysSaved >= 0);
});
it("returns 0 days saved if no segments", async () => {
const stub = sinon.stub(db, "prepare").resolves(undefined);
const result = await client({ url: endpoint });
assert.ok(result.data.daysSaved >= 0);
stub.restore();
});
it("returns days saved to 2 fixed points", async () => {
const stub = sinon.stub(db, "prepare").resolves({ daysSaved: 1.23456789 });
const result = await client({ url: endpoint });
assert.strictEqual(result.data.daysSaved, "1.23");
stub.restore();
});
});

109
test/cases/getIP.ts Normal file
View file

@ -0,0 +1,109 @@
import sinon from "sinon";
import { config } from "../../src/config";
import assert from "assert";
const mode = "production";
let stub: sinon.SinonStub;
let stub2: sinon.SinonStub;
import { createRequest } from "../mocks/mockExpressRequest";
import { getIP } from "../../src/utils/getIP";
const v4RequestOptions = {
headers: {
"x-forwarded-for": "127.0.1.1",
"cf-connecting-ip": "127.0.1.2",
"x-real-ip": "127.0.1.3",
},
ip: "127.0.1.5",
socket: {
remoteAddress: "127.0.1.4"
}
};
const v6RequestOptions = {
headers: {
"x-forwarded-for": "[100::1]",
"cf-connecting-ip": "[100::2]",
"x-real-ip": "[100::3]",
},
ip: "[100::5]",
socket: {
remoteAddress: "[100::4]"
}
};
const v4MockRequest = createRequest(v4RequestOptions);
const v6MockRequest = createRequest(v6RequestOptions);
const expectedIP4 = {
"X-Forwarded-For": "127.0.1.1",
"Cloudflare": "127.0.1.2",
"X-Real-IP": "127.0.1.3",
"default": "127.0.1.4",
};
const expectedIP6 = {
"X-Forwarded-For": "[100::1]",
"Cloudflare": "[100::2]",
"X-Real-IP": "[100::3]",
"default": "[100::4]",
};
describe("getIP stubs", () => {
before(() => stub = sinon.stub(config, "mode").value(mode));
after(() => stub.restore());
it("Should return production mode if stub worked", (done) => {
assert.strictEqual(config.mode, mode);
done();
});
});
describe("getIP array tests", () => {
beforeEach(() => stub = sinon.stub(config, "mode").value(mode));
afterEach(() => {
stub.restore();
stub2.restore();
});
for (const [key, value] of Object.entries(expectedIP4)) {
it(`Should return correct IPv4 from ${key}`, (done) => {
stub2 = sinon.stub(config, "behindProxy").value(key);
const ip = getIP(v4MockRequest);
assert.strictEqual(config.behindProxy, key);
assert.strictEqual(ip, value);
done();
});
}
for (const [key, value] of Object.entries(expectedIP6)) {
it(`Should return correct IPv6 from ${key}`, (done) => {
stub2 = sinon.stub(config, "behindProxy").value(key);
const ip = getIP(v6MockRequest);
assert.strictEqual(config.behindProxy, key);
assert.strictEqual(ip, value);
done();
});
}
});
describe("getIP true tests", () => {
before(() => stub = sinon.stub(config, "mode").value(mode));
after(() => {
stub.restore();
stub2.restore();
});
it(`Should return correct IPv4 from with bool true`, (done) => {
stub2 = sinon.stub(config, "behindProxy").value(true);
const ip = getIP(v4MockRequest);
assert.strictEqual(config.behindProxy, "X-Forwarded-For");
assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]);
done();
});
it(`Should return correct IPv4 from with string true`, (done) => {
stub2 = sinon.stub(config, "behindProxy").value("true");
const ip = getIP(v4MockRequest);
assert.strictEqual(config.behindProxy, "X-Forwarded-For");
assert.strictEqual(ip, expectedIP4["X-Forwarded-For"]);
done();
});
});

View file

@ -166,17 +166,77 @@ describe("getLockCategoriesByHash", () => {
.catch(err => done(err));
});
it("Should be able to get by actionType", (done) => {
getLockCategories(fakeHash.substring(0,5), [ActionType.Full])
it("should return 400 if invalid actionTypes", (done) => {
client.get(`${endpoint}/aaaa`, { params: { actionTypes: 3 } })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("should return 400 if invalid actionTypes JSON", (done) => {
client.get(`${endpoint}/aaaa`, { params: { actionTypes: "{3}" } })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be able to get single lock", (done) => {
const videoID = "getLockHash2";
const hash = getHash(videoID, 1);
getLockCategories(hash.substring(0,6))
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [{
videoID: "fakehash-2",
hash: fakeHash,
videoID,
hash,
categories: [
"sponsor"
"preview"
],
reason: "fake2-notshown"
reason: "2-reason"
}];
assert.deepStrictEqual(res.data, expected);
done();
})
.catch(err => done(err));
});
it("Should be able to get by actionType not in array", (done) => {
const videoID = "getLockHash2";
const hash = getHash(videoID, 1);
client.get(`${endpoint}/${hash.substring(0,6)}`, { params: { actionType: ActionType.Skip } })
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [{
videoID,
hash,
categories: [
"preview"
],
reason: "2-reason"
}];
assert.deepStrictEqual(res.data, expected);
done();
})
.catch(err => done(err));
});
it("Should be able to get by no actionType", (done) => {
const videoID = "getLockHash2";
const hash = getHash(videoID, 1);
client.get(`${endpoint}/${hash.substring(0,6)}`)
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [{
videoID,
hash,
categories: [
"preview"
],
reason: "2-reason"
}];
assert.deepStrictEqual(res.data, expected);
done();

View file

@ -55,6 +55,45 @@ describe("getLockReason", () => {
.catch(err => done(err));
});
it("Should be able to get with actionTypes array", (done) => {
client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionTypes: '["full"]' } })
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [
{ category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 }
];
assert.deepStrictEqual(res.data, expected);
done();
})
.catch(err => done(err));
});
it("Should be able to get with actionType", (done) => {
client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: "full" } })
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [
{ category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 }
];
assert.deepStrictEqual(res.data, expected);
done();
})
.catch(err => done(err));
});
it("Should be able to get with actionType array", (done) => {
client.get(endpoint, { params: { videoID: "getLockReason", category: "selfpromo", actionType: ["full"] } })
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [
{ category: "selfpromo", locked: 1, reason: "selfpromo-reason", userID: vipUserID2, userName: vipUserName2 }
];
assert.deepStrictEqual(res.data, expected);
done();
})
.catch(err => done(err));
});
it("Should be able to get empty locks", (done) => {
client.get(endpoint, { params: { videoID: "getLockReason", category: "intro" } })
.then(res => {
@ -118,8 +157,10 @@ describe("getLockReason", () => {
})
.catch(err => done(err));
});
});
it("should return 400 if no videoID specified", (done) => {
describe("getLockReason 400", () => {
it("Should return 400 with missing videoID", (done) => {
client.get(endpoint)
.then(res => {
assert.strictEqual(res.status, 400);
@ -128,15 +169,37 @@ describe("getLockReason", () => {
.catch(err => done(err));
});
it("should be able to get by actionType", (done) => {
client.get(endpoint, { params: { videoID: "getLockReason", actionType: "full" } })
it("Should return 400 with invalid actionTypes ", (done) => {
client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: 3 } })
.then(res => {
assert.strictEqual(res.status, 200);
const expected = [
{ category: "selfpromo", locked: 1, reason: "sponsor-reason", userID: vipUserID2, userName: vipUserName2 },
{ category: "sponsor", locked: 0, reason: "", userID: "", userName: "" }
];
partialDeepEquals(res.data, expected);
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 with invalid actionTypes JSON ", (done) => {
client.get(endpoint, { params: { videoID: "valid-videoid", actionTypes: "{3}" } })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 with invalid categories", (done) => {
client.get(endpoint, { params: { videoID: "valid-videoid", categories: 3 } })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 with invalid categories JSON", (done) => {
client.get(endpoint, { params: { videoID: "valid-videoid", categories: "{3}" } })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));

View file

@ -2,22 +2,31 @@ import { db } from "../../src/databases/databases";
import { getHash } from "../../src/utils/getHash";
import { deepStrictEqual } from "assert";
import { client } from "../utils/httpClient";
import assert from "assert";
// helpers
const endpoint = "/api/getSavedTimeForUser";
const getSavedTimeForUser = (userID: string) => client({
url: endpoint,
params: { userID }
});
describe("getSavedTimeForUser", () => {
const user1 = "getSavedTimeForUserUser";
const user1 = "getSavedTimeForUser1";
const user2 = "getSavedTimeforUser2";
const [ start, end, views ] = [1, 11, 50];
before(async () => {
const startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", "views", "shadowHidden") VALUES';
await db.prepare("run", `${startOfQuery}(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
["getSavedTimeForUser", 1, 11, 2, "gstfu0", getHash(user1), 0, 50, 0]);
["getSavedTimeForUser", start, end, 2, "getSavedTimeUUID0", getHash(user1), 0, views, 0]);
return;
});
it("Should be able to get a 200", (done) => {
client.get(endpoint, { params: { userID: user1 } })
it("Should be able to get a saved time", (done) => {
getSavedTimeForUser(user1)
.then(res => {
// (end-start)*minute * views
const savedMinutes = ((11-1)/60) * 50;
const savedMinutes = ((end-start)/60) * views;
const expected = {
timeSaved: savedMinutes
};
@ -26,4 +35,20 @@ describe("getSavedTimeForUser", () => {
})
.catch((err) => done(err));
});
it("Should return 404 if no submissions", (done) => {
getSavedTimeForUser(user2)
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch((err) => done(err));
});
it("Should return 400 if no userID", (done) => {
client({ url: endpoint })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch((err) => done(err));
});
});

View file

@ -80,6 +80,67 @@ describe("getSearchSegments", () => {
.catch(err => done(err));
});
it("Should be able to filter by category with categories string", (done) => {
client.get(endpoint, { params: { videoID: "searchTest0", categories: `["selfpromo"]` } })
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
const segments = data.segments;
assert.strictEqual(data.segmentCount, 1);
assert.strictEqual(data.page, 0);
assert.strictEqual(segments[0].UUID, "search-downvote");
done();
})
.catch(err => done(err));
});
it("Should be able to filter by category with categories array", (done) => {
client.get(endpoint, { params: { videoID: "searchTest0", category: ["selfpromo"] } })
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
const segments = data.segments;
assert.strictEqual(data.segmentCount, 1);
assert.strictEqual(data.page, 0);
assert.strictEqual(segments[0].UUID, "search-downvote");
done();
})
.catch(err => done(err));
});
it("Should be able to filter by category with actionTypes JSON", (done) => {
client.get(endpoint, { params: { videoID: "searchTest5", actionTypes: `["mute"]` } })
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
assert.strictEqual(data.segmentCount, 1);
done();
})
.catch(err => done(err));
});
it("Should be able to filter by category with actionType array", (done) => {
client.get(endpoint, { params: { videoID: "searchTest5", actionType: ["mute"] } })
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
assert.strictEqual(data.segmentCount, 1);
done();
})
.catch(err => done(err));
});
it("Should be able to filter by category with actionType string", (done) => {
client.get(endpoint, { params: { videoID: "searchTest5", actionType: "mute" } })
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
assert.strictEqual(data.segmentCount, 1);
done();
})
.catch(err => done(err));
});
it("Should be able to filter by lock status", (done) => {
client.get(endpoint, { params: { videoID: "searchTest0", locked: false } })
.then(res => {

View file

@ -0,0 +1,48 @@
import { client } from "../utils/httpClient";
import assert from "assert";
describe("getSearchSegments 4xx", () => {
const endpoint = "/api/searchSegments";
it("Should return 400 if no videoID", (done) => {
client.get(endpoint, { params: {} })
.then(res => {
assert.strictEqual(res.status, 400);
const data = res.data;
assert.strictEqual(data, "videoID not specified");
done();
})
.catch(err => done(err));
});
it("Should return 400 if invalid categories", (done) => {
client.get(endpoint, { params: { videoID: "nullVideo", categories: 3 } })
.then(res => {
assert.strictEqual(res.status, 400);
const data = res.data;
assert.strictEqual(data, "Categories parameter does not match format requirements.");
done();
})
.catch(err => done(err));
});
it("Should return 400 if invalid actionTypes", (done) => {
client.get(endpoint, { params: { videoID: "nullVideo", actionTypes: 3 } })
.then(res => {
assert.strictEqual(res.status, 400);
const data = res.data;
assert.strictEqual(data, "actionTypes parameter does not match format requirements.");
done();
})
.catch(err => done(err));
});
it("Should return 404 if no segments", (done) => {
client.get(endpoint, { params: { videoID: "nullVideo", actionType: "chapter" } })
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
});

View file

@ -3,7 +3,7 @@ import { partialDeepEquals, arrayPartialDeepEquals } from "../utils/partialDeepE
import { getHash } from "../../src/utils/getHash";
import { ImportMock, } from "ts-mock-imports";
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
import { YouTubeApiMock } from "../youtubeMock";
import { YouTubeApiMock } from "../mocks/youtubeMock";
import assert from "assert";
import { client } from "../utils/httpClient";
@ -581,4 +581,78 @@ describe("getSkipSegmentsByHash", () => {
})
.catch(err => done(err));
});
it("Should be able to get single segment with requiredSegments", (done) => {
const requiredSegment1 = "fbf0af454059733c8822f6a4ac8ec568e0787f8c0a5ee915dd5b05e0d7a9a388";
client.get(`${endpoint}/17bf?requiredSegment=${requiredSegment1}`)
.then(res => {
assert.strictEqual(res.status, 200);
const data = (res.data as Array<any>).sort((a, b) => a.videoID.localeCompare(b.videoID));
assert.strictEqual(data.length, 1);
const expected = [{
segments: [{
UUID: requiredSegment1
}]
}];
assert.ok(partialDeepEquals(data, expected));
assert.strictEqual(data[0].segments.length, 1);
done();
})
.catch(err => done(err));
});
it("Should return 400 if categories are is number", (done) => {
client.get(`${endpoint}/17bf?categories=3`)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 if actionTypes is number", (done) => {
client.get(`${endpoint}/17bf?actionTypes=3`)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 if actionTypes are invalid json", (done) => {
client.get(`${endpoint}/17bf?actionTypes={test}`)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 if requiredSegments is number", (done) => {
client.get(`${endpoint}/17bf?requiredSegments=3`)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 404 if requiredSegments is invalid json", (done) => {
client.get(`${endpoint}/17bf?requiredSegments={test}`)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 if requiredSegments is not present", (done) => {
client.get(`${endpoint}/17bf?requiredSegment=${fullCategoryVidHash}`)
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
});

View file

@ -2,6 +2,7 @@ import assert from "assert";
import { db } from "../../src/databases/databases";
import { client } from "../utils/httpClient";
import { config } from "../../src/config";
import sinon from "sinon";
let dbVersion: number;
describe("getStatus", () => {
@ -122,4 +123,16 @@ describe("getStatus", () => {
})
.catch(err => done(err));
});
it("Should return commit unkown if not present", (done) => {
sinon.stub((global as any), "HEADCOMMIT").value(undefined);
client.get(`${endpoint}/commit`)
.then(res => {
assert.strictEqual(res.status, 200);
assert.strictEqual(res.data, "test"); // commit should be test
done();
})
.catch(err => done(err));
sinon.restore();
});
});

View file

@ -38,6 +38,15 @@ describe("getTopUsers", () => {
.catch(err => done(err));
});
it("Should return 400 if undefined sortType provided", (done) => {
client.get(endpoint)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should be able to get by all sortTypes", (done) => {
client.get(endpoint, { params: { sortType: 0 } })// minutesSaved
.then(res => {

View file

@ -0,0 +1,17 @@
import assert from "assert";
import { client } from "../utils/httpClient";
const endpoint = "/api/getTotalStats";
describe("getTotalStats", () => {
it("Can get total stats", async () => {
const result = await client({ url: endpoint });
const data = result.data;
assert.ok(data?.userCount ?? true);
assert.ok(data.activeUsers >= 0);
assert.ok(data.apiUsers >= 0);
assert.ok(data.viewCount >= 0);
assert.ok(data.totalSubmissions >= 0);
assert.ok(data.minutesSaved >= 0);
});
});

View file

@ -21,6 +21,7 @@ describe("getUserInfo", () => {
await db.prepare("run", sponsorTimesQuery, ["getUserInfo0", 0, 36000, 2,"uuid000009", getHash("getuserinfo_user_03"), 8, 10, "sponsor", "skip", 0]);
await db.prepare("run", sponsorTimesQuery, ["getUserInfo3", 1, 11, 2, "uuid000006", getHash("getuserinfo_user_02"), 6, 10, "sponsor", "skip", 0]);
await db.prepare("run", sponsorTimesQuery, ["getUserInfo4", 1, 11, 2, "uuid000010", getHash("getuserinfo_user_04"), 9, 10, "chapter", "chapter", 0]);
await db.prepare("run", sponsorTimesQuery, ["getUserInfo5", 1, 11, 2, "uuid000011", getHash("getuserinfo_user_05"), 9, 10, "sponsor", "skip", 0]);
const insertWarningQuery = 'INSERT INTO warnings ("userID", "issueTime", "issuerUserID", "enabled", "reason") VALUES (?, ?, ?, ?, ?)';
@ -264,6 +265,15 @@ describe("getUserInfo", () => {
.catch(err => done(err));
});
it("Should throw 400 with invalid array", (done) => {
client.get(endpoint, { params: { userID: "x", values: 123 } })
.then(res => {
assert.strictEqual(res.status, 400);
done(); // pass
})
.catch(err => done(err));
});
it("Should return 200 on userID not found", (done) => {
client.get(endpoint, { params: { userID: "notused-userid" } })
.then(res => {
@ -309,6 +319,30 @@ describe("getUserInfo", () => {
.catch(err => done(err));
});
it("Should be able to get permissions", (done) => {
client.get(endpoint, { params: { userID: "getuserinfo_user_01", value: "permissions" } })
.then(res => {
assert.strictEqual(res.status, 200);
const expected = {
permissions: {
sponsor: true,
selfpromo: true,
exclusive_access: true,
interaction: true,
intro: true,
outro: true,
preview: true,
music_offtopic: true,
filler: true,
poi_highlight: true,
chapter: false,
},
};
assert.ok(partialDeepEquals(res.data, expected));
done(); // pass
});
});
it("Should ignore chapters for saved time calculations", (done) => {
client.get(endpoint, { params: { userID: "getuserinfo_user_04" } })
.then(res => {

View file

@ -0,0 +1,76 @@
import { db } from "../../src/databases/databases";
import { getHash } from "../../src/utils/getHash";
import assert from "assert";
import { client } from "../utils/httpClient";
describe("getUserInfo Free Chapters", () => {
const endpoint = "/api/userInfo";
const newQualifyUserID = "getUserInfo-Free-newQualify";
const vipQualifyUserID = "getUserInfo-Free-VIP";
const repQualifyUserID = "getUserInfo-Free-RepQualify";
const oldQualifyUserID = "getUserInfo-Free-OldQualify";
const newNoQualityUserID = "getUserInfo-Free-newNoQualify";
const postOldQualify = 1600000000000;
before(async () => {
const sponsorTimesQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "reputation", "shadowHidden") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-0", getHash(repQualifyUserID), postOldQualify, 0, "sponsor", "skip", 20, 0]);
await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-1", getHash(oldQualifyUserID), 0, 0, "sponsor", "skip", 0, 0]); // submit at epoch
await db.prepare("run", sponsorTimesQuery, ["getUserInfoFree", 1, 2, 0, "uuid-guif-2", getHash(newQualifyUserID), postOldQualify, 0, "sponsor", "skip", 0, 0]);
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES (?)`, [getHash(vipQualifyUserID)]);
});
const getUserInfo = (userID: string) => client.get(endpoint, { params: { userID, value: "freeChaptersAccess" } });
it("Should not get free access under new rule (newNoQualify)", (done) => {
getUserInfo(newNoQualityUserID)
.then(res => {
assert.strictEqual(res.status, 200);
assert.strictEqual(res.data.freeChaptersAccess, false);
done();
})
.catch(err => done(err));
});
it("Should get free access under new rule (newQualify)", (done) => {
getUserInfo(newQualifyUserID)
.then(res => {
assert.strictEqual(res.status, 200);
assert.strictEqual(res.data.freeChaptersAccess, true);
done();
})
.catch(err => done(err));
});
it("Should get free access (VIP)", (done) => {
getUserInfo(vipQualifyUserID)
.then(res => {
assert.strictEqual(res.status, 200);
assert.strictEqual(res.data.freeChaptersAccess, true);
done();
})
.catch(err => done(err));
});
it("Should get free access (rep)", (done) => {
getUserInfo(repQualifyUserID)
.then(res => {
assert.strictEqual(res.status, 200);
assert.strictEqual(res.data.freeChaptersAccess, true);
done();
})
.catch(err => done(err));
});
it("Should get free access (old)", (done) => {
getUserInfo(oldQualifyUserID)
.then(res => {
assert.strictEqual(res.status, 200);
assert.strictEqual(res.data.freeChaptersAccess, true);
done();
})
.catch(err => done(err));
});
});

53
test/cases/getUsername.ts Normal file
View file

@ -0,0 +1,53 @@
import { getHash } from "../../src/utils/getHash";
import { client } from "../utils/httpClient";
import assert from "assert";
// helpers
const getUsername = (userID: string) => client({
url: "/api/getUsername",
params: { userID }
});
const postSetUserName = (userID: string, username: string) => client({
method: "POST",
url: "/api/setUsername",
params: {
userID,
username,
}
});
const userOnePrivate = "getUsername_0";
const userOnePublic = getHash(userOnePrivate);
const userOneUsername = "getUsername_username";
describe("getUsername test", function() {
it("Should get back publicUserID if not set", (done) => {
getUsername(userOnePrivate)
.then(result => {
assert.strictEqual(result.data.userName, userOnePublic);
done();
})
.catch(err => done(err));
});
it("Should be able to get username after setting", (done) => {
postSetUserName(userOnePrivate, userOneUsername)
.then(async () => {
const result = await getUsername(userOnePrivate);
const actual = result.data.userName;
assert.strictEqual(actual, userOneUsername);
done();
})
.catch(err => done(err));
});
it("Should return 400 if no userID provided", (done) => {
client({
url: "/api/getUsername"
})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});

View file

@ -0,0 +1,184 @@
import { db } from "../../src/databases/databases";
import assert from "assert";
import { client } from "../utils/httpClient";
import { getHash } from "../../src/utils/getHash";
describe("getVideoLabelHash", () => {
const endpoint = "/api/videoLabels";
before(async () => {
const query = 'INSERT INTO "sponsorTimes" ("videoID", "hashedVideoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)';
await db.prepare("run", query, ["getLabelHashSponsor" , getHash("getLabelHashSponsor", 1) , 2, 0, "labelhash01", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashEA" , getHash("getLabelHashEA", 1) , 2, 0, "labelhash02", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashSelfpromo" , getHash("getLabelHashSelfpromo", 1) , 2, 0, "labelhash03", "labeluser", 0, "selfpromo", "full", 0, 0]);
// priority override
await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash04", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash05", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashPriority" , getHash("getLabelHashPriority", 1) , 2, 0, "labelhash06", "labeluser", 0, "selfpromo", "full", 0, 0]);
// locked only
await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 0, "labelhash07", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 0, "labelhash08", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashLocked" , getHash("getLabelHashLocked", 1) , 2, 1, "labelhash09", "labeluser", 0, "selfpromo", "full", 0, 0]);
// hidden segments
await db.prepare("run", query, ["getLabelHashDownvote" , getHash("getLabelHashDownvote", 1) , -2, 0, "labelhash10", "labeluser", 0, "selfpromo", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashHidden" , getHash("getLabelHashHidden", 1) , 2, 0, "labelhash11", "labeluser", 0, "selfpromo", "full", 1, 0]);
await db.prepare("run", query, ["getLabelHashShHidden" , getHash("getLabelHashShHidden", 1) , 2, 0, "labelhash12", "labeluser", 0, "selfpromo", "full", 0, 1]);
// priority override2
await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , -2, 0, "labelhash13", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , 2, 0, "labelhash14", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHashPriority2" , getHash("getLabelHashPriority2", 1) , 2, 0, "labelhash15", "labeluser", 0, "selfpromo", "full", 0, 0]);
return;
});
function validateLabel(data: any, videoID: string) {
assert.strictEqual(data[0].videoID, videoID);
assert.strictEqual(data[0].segments.length, 1);
assert.strictEqual(data[0].segments[0].segment[0], 0);
assert.strictEqual(data[0].segments[0].segment[1], 0);
assert.strictEqual(data[0].segments[0].actionType, "full");
assert.strictEqual(data[0].segments[0].userID, "labeluser");
}
const get = (videoID: string) => client.get(`${endpoint}/${getHash(videoID, 1).substring(0, 4)}`);
it("Should be able to get sponsor only label", (done) => {
const videoID = "getLabelHashSponsor";
get(videoID)
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data, videoID);
const result = data[0].segments[0];
assert.strictEqual(result.category, "sponsor");
assert.strictEqual(result.UUID, "labelhash01");
assert.strictEqual(result.locked, 0);
done();
})
.catch(err => done(err));
});
it("Should be able to get exclusive access only label", (done) => {
const videoID = "getLabelHashEA";
get(videoID)
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data, videoID);
const result = data[0].segments[0];
assert.strictEqual(result.category, "exclusive_access");
assert.strictEqual(result.UUID, "labelhash02");
assert.strictEqual(result.locked, 0);
done();
})
.catch(err => done(err));
});
it("Should be able to get selfpromo only label", (done) => {
const videoID = "getLabelHashSelfpromo";
get(videoID)
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data, videoID);
const result = data[0].segments[0];
assert.strictEqual(result.category, "selfpromo");
assert.strictEqual(result.UUID, "labelhash03");
assert.strictEqual(result.locked, 0);
done();
})
.catch(err => done(err));
});
it("Should get only sponsor if multiple segments exist", (done) => {
const videoID = "getLabelHashPriority";
get(videoID)
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data, videoID);
const result = data[0].segments[0];
assert.strictEqual(result.category, "sponsor");
assert.strictEqual(result.UUID, "labelhash04");
assert.strictEqual(result.locked, 0);
done();
})
.catch(err => done(err));
});
it("Should override priority if locked", (done) => {
const videoID = "getLabelHashLocked";
get(videoID)
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data, videoID);
const result = data[0].segments[0];
assert.strictEqual(result.category, "selfpromo");
assert.strictEqual(result.UUID, "labelhash09");
assert.strictEqual(result.locked, 1);
done();
})
.catch(err => done(err));
});
it("Should get highest priority category", (done) => {
const videoID = "getLabelHashPriority2";
get(videoID)
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data, videoID);
const result = data[0].segments[0];
assert.strictEqual(result.category, "exclusive_access");
assert.strictEqual(result.UUID, "labelhash14");
assert.strictEqual(result.locked, 0);
done();
})
.catch(err => done(err));
});
it("Should return 404 if all submissions are downvoted", (done) => {
get("getLabelHashDownvote")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 404 if all submissions are hidden", (done) => {
get("getLabelHashHidden")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 404 if all submissions are shadowhidden", (done) => {
get("getLabelHashShHidden")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 404 if no segment found", (done) => {
get("notarealvideo")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should get 400 if no videoID passed in", (done) => {
client.get(endpoint)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});

View file

@ -0,0 +1,170 @@
import { db } from "../../src/databases/databases";
import assert from "assert";
import { client } from "../utils/httpClient";
describe("getVideoLabels", () => {
const endpoint = "/api/videoLabels";
before(async () => {
const query = 'INSERT INTO "sponsorTimes" ("videoID", "votes", "locked", "UUID", "userID", "timeSubmitted", "category", "actionType", "hidden", "shadowHidden", "startTime", "endTime", "views") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)';
await db.prepare("run", query, ["getLabelSponsor" , 2, 0, "label01", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelEA" , 2, 0, "label02", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelSelfpromo" , 2, 0, "label03", "labeluser", 0, "selfpromo", "full", 0, 0]);
// priority override
await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label04", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label05", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelPriority" , 2, 0, "label06", "labeluser", 0, "selfpromo", "full", 0, 0]);
// locked only
await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label07", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelLocked" , 2, 0, "label08", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelLocked" , 2, 1, "label09", "labeluser", 0, "selfpromo", "full", 0, 0]);
// hidden segments
await db.prepare("run", query, ["getLabelDownvote" ,-2, 0, "label10", "labeluser", 0, "selfpromo", "full", 0, 0]);
await db.prepare("run", query, ["getLabelHidden" ,2, 0, "label11", "labeluser", 0, "selfpromo", "full", 1, 0]);
await db.prepare("run", query, ["getLabelShadowHidden",2, 0, "label12", "labeluser", 0, "selfpromo", "full", 0, 1]);
// priority override2
await db.prepare("run", query, ["getLabelPriority2" , -2, 0, "label13", "labeluser", 0, "sponsor", "full", 0, 0]);
await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label14", "labeluser", 0, "exclusive_access", "full", 0, 0]);
await db.prepare("run", query, ["getLabelPriority2" , 2, 0, "label15", "labeluser", 0, "selfpromo", "full", 0, 0]);
return;
});
function validateLabel(result: any) {
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].segment[0], 0);
assert.strictEqual(result[0].segment[1], 0);
assert.strictEqual(result[0].actionType, "full");
assert.strictEqual(result[0].userID, "labeluser");
}
const get = (videoID: string) => client.get(endpoint, { params: { videoID } });
it("Should be able to get sponsor only label", (done) => {
get("getLabelSponsor")
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data);
assert.strictEqual(data[0].category, "sponsor");
assert.strictEqual(data[0].UUID, "label01");
assert.strictEqual(data[0].locked, 0);
done();
})
.catch(err => done(err));
});
it("Should be able to get exclusive access only label", (done) => {
get("getLabelEA")
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data);
assert.strictEqual(data[0].category, "exclusive_access");
assert.strictEqual(data[0].UUID, "label02");
assert.strictEqual(data[0].locked, 0);
done();
})
.catch(err => done(err));
});
it("Should be able to get selfpromo only label", (done) => {
get("getLabelSelfpromo")
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data);
assert.strictEqual(data[0].category, "selfpromo");
assert.strictEqual(data[0].UUID, "label03");
assert.strictEqual(data[0].locked, 0);
done();
})
.catch(err => done(err));
});
it("Should get only sponsor if multiple segments exist", (done) => {
get("getLabelPriority")
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data);
assert.strictEqual(data[0].category, "sponsor");
assert.strictEqual(data[0].UUID, "label04");
assert.strictEqual(data[0].locked, 0);
done();
})
.catch(err => done(err));
});
it("Should override priority if locked", (done) => {
get("getLabelLocked")
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data);
assert.strictEqual(data[0].category, "selfpromo");
assert.strictEqual(data[0].UUID, "label09");
assert.strictEqual(data[0].locked, 1);
done();
})
.catch(err => done(err));
});
it("Should get highest priority category", (done) => {
get("getLabelPriority2")
.then(res => {
assert.strictEqual(res.status, 200);
const data = res.data;
validateLabel(data);
assert.strictEqual(data[0].category, "exclusive_access");
assert.strictEqual(data[0].UUID, "label14");
assert.strictEqual(data[0].locked, 0);
done();
})
.catch(err => done(err));
});
it("Should return 404 if all submissions are downvoted", (done) => {
get("getLabelDownvote")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 404 if all submissions are hidden", (done) => {
get("getLabelHidden")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 404 if all submissions are shadowhidden", (done) => {
get("getLabelShadowHidden")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 404 if no segment found", (done) => {
get("notarealvideo")
.then(res => {
assert.strictEqual(res.status, 404);
done();
})
.catch(err => done(err));
});
it("Should get 400 if no videoID passed in", (done) => {
client.get(endpoint)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});

View file

@ -0,0 +1,62 @@
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases";
import { client } from "../utils/httpClient";
import assert from "assert";
// helpers
const endpoint = "/api/getViewsForUser";
const getViewsForUser = (userID: string) => client({
url: endpoint,
params: { userID }
});
const getViewUserOne = "getViewUser1";
const userOneViewsFirst = 30;
const userOneViewsSecond = 0;
const getViewUserTwo = "getViewUser2";
const userTwoViews = 0;
const getViewUserThree = "getViewUser3";
describe("getViewsForUser", function() {
before(() => {
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo0", getHash(getViewUserOne), 0, userOneViewsFirst, "sponsor", "skip", 0, 0, "getViewUserVideo"]);
db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo1", getHash(getViewUserOne), 0, userOneViewsSecond, "sponsor", "skip", 0, 0, "getViewUserVideo"]);
db.prepare("run", insertSponsorTimeQuery, ["getViewUserVideo", 0, 1, 0, "getViewUserVideo2", getHash(getViewUserTwo), 0, userTwoViews, "sponsor", "skip", 0, 0, "getViewUserVideo"]);
});
it("Should get back views for user one", (done) => {
getViewsForUser(getViewUserOne)
.then(result => {
assert.strictEqual(result.data.viewCount, userOneViewsFirst + userOneViewsSecond);
done();
})
.catch(err => done(err));
});
it("Should get back views for user two", (done) => {
getViewsForUser(getViewUserTwo)
.then(result => {
assert.strictEqual(result.data.viewCount, userTwoViews);
done();
})
.catch(err => done(err));
});
it("Should get 404 if no submissions", (done) => {
getViewsForUser(getViewUserThree)
.then(result => {
assert.strictEqual(result.status, 404);
done();
})
.catch(err => done(err));
});
it("Should return 400 if no userID provided", (done) => {
client({ url: endpoint })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
});

View file

@ -0,0 +1,252 @@
import assert from "assert";
import { client } from "../utils/httpClient";
import { getHash } from "../../src/utils/getHash";
import { db } from "../../src/databases/databases";
import { UserID } from "../../src/types/user.model";
import { Category, VideoID } from "../../src/types/segments.model";
interface LockCategory {
category: Category,
reason: string,
videoID: VideoID,
userID: UserID
}
const lockVIPUser = "lockCategoriesHttpVIPUser";
const lockVIPUserHash = getHash(lockVIPUser);
const endpoint = "/api/lockCategories";
const checkLockCategories = (videoID: string): Promise<LockCategory[]> => db.prepare("all", 'SELECT * FROM "lockCategories" WHERE "videoID" = ?', [videoID]);
const goodResponse = (): any => ({
videoID: "test-videoid",
userID: "not-vip-test-userid",
categories: ["sponsor"],
actionTypes: ["skip"]
});
describe("POST lockCategories HTTP submitting", () => {
before(async () => {
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
await db.prepare("run", insertVipUserQuery, [lockVIPUserHash]);
});
it("Should update the database version when starting the application", async () => {
const version = (await db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"])).value;
assert.ok(version > 1);
});
it("should be able to add poi type category by type skip", (done) => {
const videoID = "add-record-poi";
client.post(endpoint, {
videoID,
userID: lockVIPUser,
categories: ["poi_highlight"],
actionTypes: ["skip"]
})
.then(res => {
assert.strictEqual(res.status, 200);
checkLockCategories(videoID)
.then(result => {
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0], "poi_highlight");
});
done();
})
.catch(err => done(err));
});
it("Should not add lock of invalid type", (done) => {
const videoID = "add_invalid_type";
client.post(endpoint, {
videoID,
userID: lockVIPUser,
categories: ["future_unused_invalid_type"],
actionTypes: ["skip"]
})
.then(res => {
assert.strictEqual(res.status, 200);
checkLockCategories(videoID)
.then(result => {
assert.strictEqual(result.length, 0);
});
done();
})
.catch(err => done(err));
});
});
describe("DELETE lockCategories 403/400 tests", () => {
it(" Should return 400 for no data", (done) => {
client.delete(endpoint, {})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for no categories", (done) => {
const json: any = {
videoID: "test",
userID: "test",
categories: [],
};
client.delete(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for no userID", (done) => {
const json: any = {
videoID: "test",
userID: null,
categories: ["sponsor"],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for no videoID", (done) => {
const json: any = {
videoID: null,
userID: "test",
categories: ["sponsor"],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for invalid category array", (done) => {
const json = {
videoID: "test",
userID: "test",
categories: {},
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for bad format categories", (done) => {
const json = {
videoID: "test",
userID: "test",
categories: "sponsor",
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 403 if user is not VIP", (done) => {
const json = {
videoID: "test",
userID: "test",
categories: [
"sponsor",
],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
});
describe("manual DELETE/POST lockCategories 400 tests", () => {
it("DELETE Should return 400 for no data", (done) => {
client.delete(endpoint, { data: {} })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("POST Should return 400 for no data", (done) => {
client.post(endpoint, {})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("DELETE Should return 400 for bad format categories", (done) => {
const data = goodResponse();
data.categories = "sponsor";
client.delete(endpoint, { data })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("POST Should return 400 for bad format categories", (done) => {
const data = goodResponse();
data.categories = "sponsor";
client.post(endpoint, data)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("DELETE Should return 403 if user is not VIP", (done) => {
const data = goodResponse();
client.delete(endpoint, { data })
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
it("POST Should return 403 if user is not VIP", (done) => {
const data = goodResponse();
client.post(endpoint, data)
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
});
describe("array of DELETE/POST lockCategories 400 tests", () => {
for (const key of [ "videoID", "userID", "categories" ]) {
for (const method of ["DELETE", "POST"]) {
it(`${method} - Should return 400 for invalid ${key}`, (done) => {
const data = goodResponse();
data[key] = null;
client(endpoint, { data, method })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
}
}
});

View file

@ -266,106 +266,6 @@ describe("lockCategoriesRecords", () => {
.catch(err => done(err));
});
it("Should return 400 for missing params", (done) => {
client.post(endpoint, {})
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for no categories", (done) => {
const json: any = {
videoID: "test",
userID: "test",
categories: [],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for no userID", (done) => {
const json: any = {
videoID: "test",
userID: null,
categories: ["sponsor"],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 for no videoID", (done) => {
const json: any = {
videoID: null,
userID: "test",
categories: ["sponsor"],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 object categories", (done) => {
const json = {
videoID: "test",
userID: "test",
categories: {},
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 bad format categories", (done) => {
const json = {
videoID: "test",
userID: "test",
categories: "sponsor",
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 403 if user is not VIP", (done) => {
const json = {
videoID: "test",
userID: "test",
categories: [
"sponsor",
],
};
client.post(endpoint, json)
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
it("Should be able to delete a lockCategories record", (done) => {
const videoID = "delete-record";
const json = {

View file

@ -4,7 +4,7 @@ import { partialDeepEquals, arrayDeepEquals } from "../utils/partialDeepEquals";
import { db } from "../../src/databases/databases";
import { ImportMock } from "ts-mock-imports";
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
import { YouTubeApiMock } from "../youtubeMock";
import { YouTubeApiMock } from "../mocks/youtubeMock";
import assert from "assert";
import { client } from "../utils/httpClient";
import { Feature } from "../../src/types/user.model";

View file

@ -187,10 +187,34 @@ describe("shadowBanUser", () => {
})
.then(async res => {
assert.strictEqual(res.status, 200);
const videoRow = await getShadowBanSegmentCategory(userID, 1);
const videoRow = await getShadowBanSegmentCategory(userID, 0);
const shadowRow = await getShadowBan(userID);
assert.ok(shadowRow); // ban still exists
assert.strictEqual(videoRow.length, 1); // videos should be hidden
assert.strictEqual(videoRow.length, 0); // videos should be hidden
done();
})
.catch(err => done(err));
});
it("Should be able to un-shadowban user to restore old submissions", (done) => {
const userID = "shadowBanned4";
client({
method: "POST",
url: endpoint,
params: {
userID,
adminUserID: VIPuserID,
enabled: false,
categories: `["sponsor"]`,
unHideOldSubmissions: true
}
})
.then(async res => {
assert.strictEqual(res.status, 200);
const videoRow = await getShadowBanSegmentCategory(userID, 0);
const shadowRow = await getShadowBan(userID);
assert.ok(!shadowRow); // ban still exists
assert.strictEqual(videoRow.length, 1); // videos should be visible
assert.strictEqual(videoRow[0].category, "sponsor");
done();
})

View file

@ -0,0 +1,48 @@
import { db } from "../../src/databases/databases";
import { getHash } from "../../src/utils/getHash";
import assert from "assert";
import { client } from "../utils/httpClient";
const endpoint = "/api/shadowBanUser";
const postShadowBan = (params: Record<string, string>) => client({
method: "POST",
url: endpoint,
params
});
describe("shadowBanUser 4xx", () => {
const VIPuserID = "shadow-ban-4xx-vip";
before(async () => {
await db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES(?)`, [getHash(VIPuserID)]);
});
it("Should return 400 if no adminUserID", (done) => {
const userID = "shadowBanned";
postShadowBan({ userID })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 400 if no userID", (done) => {
postShadowBan({ adminUserID: VIPuserID })
.then(res => {
assert.strictEqual(res.status, 400);
done();
})
.catch(err => done(err));
});
it("Should return 403 if not authorized", (done) => {
postShadowBan({ adminUserID: "notVIPUserID", userID: "shadowBanned" })
.then(res => {
assert.strictEqual(res.status, 403);
done();
})
.catch(err => done(err));
});
});

View file

@ -1,5 +1,5 @@
import assert from "assert";
import { partialDeepEquals } from "../utils/partialDeepEquals";
import { partialDeepEquals, mixedDeepEquals } from "../utils/partialDeepEquals";
describe("Test utils ", () => {
it("objectContain", () => {
@ -135,4 +135,45 @@ describe("Test utils ", () => {
}
), "Did not match partial child array");
});
it("mixedDeepEquals exists", () => {
assert(!mixedDeepEquals({
name: "lorem",
values: [{
name: "ipsum",
}],
child: {
name: "dolor",
},
ignore: true
}, {
name: "lorem",
values: [{
name: "ipsum",
}],
child: {
name: "dolor",
},
ignore: false
}));
});
it("mixedDeepEquals noProperty", () => {
assert(!mixedDeepEquals({
name: "lorem",
values: [{
name: "ipsum",
}],
child: {
name: "dolor",
}
}, {
name: "lorem",
values: [{
name: "ipsum",
}],
child: {
name: "dolor",
},
ignore: false
}));
});
});

50
test/cases/tokenUtils.ts Normal file
View file

@ -0,0 +1,50 @@
import assert from "assert";
import { config } from "../../src/config";
import axios from "axios";
import * as tokenUtils from "../../src/utils/tokenUtils";
import MockAdapter from "axios-mock-adapter";
import { validatelicenseKeyRegex } from "../../src/routes/verifyToken";
let mock: MockAdapter;
import * as patreon from "../mocks/patreonMock";
const validateToken = validatelicenseKeyRegex;
describe("tokenUtils test", function() {
before(function() {
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
});
it("Should be able to create patreon token", function (done) {
if (!config?.patreon) this.skip();
tokenUtils.createAndSaveToken(tokenUtils.TokenType.patreon, "test_code").then((licenseKey) => {
assert.ok(validateToken(licenseKey));
done();
});
});
it("Should be able to create local token", (done) => {
tokenUtils.createAndSaveToken(tokenUtils.TokenType.local).then((licenseKey) => {
assert.ok(validateToken(licenseKey));
done();
});
});
it("Should be able to get patreon identity", function (done) {
if (!config?.patreon) this.skip();
tokenUtils.getPatreonIdentity("fake_access_token").then((result) => {
assert.deepEqual(result, patreon.activeIdentity);
done();
});
});
it("Should be able to refresh token", function (done) {
if (!config?.patreon) this.skip();
tokenUtils.refreshToken(tokenUtils.TokenType.patreon, "fake-licence-Key", "fake_refresh_token").then((result) => {
assert.strictEqual(result, true);
done();
});
});
after(function () {
mock.restore();
});
});

View file

@ -3,10 +3,9 @@ import assert from "assert";
import { config } from "../../src/config";
import { getHash } from "../../src/utils/getHash";
describe("userCounter", () => {
it("Should return 200", (done) => {
if (!config.userCounterURL) return done(); // skip if no userCounterURL is set
it("Should return 200", function (done) {
if (!config.userCounterURL) this.skip(); // skip if no userCounterURL is set
axios.request({
method: "POST",
baseURL: config.userCounterURL,

View file

@ -3,7 +3,7 @@ import { db, privateDB } from "../../src/databases/databases";
import { getHash } from "../../src/utils/getHash";
import { ImportMock } from "ts-mock-imports";
import * as YouTubeAPIModule from "../../src/utils/youtubeApi";
import { YouTubeApiMock } from "../youtubeMock";
import { YouTubeApiMock } from "../mocks/youtubeMock";
import assert from "assert";
import { client } from "../utils/httpClient";
import { arrayDeepEquals } from "../utils/partialDeepEquals";

View file

@ -1,23 +1,24 @@
import express from "express";
import { config } from "../src/config";
import { Server } from "http";
import { UserCounter } from "./mocks/UserCounter";
const app = express();
app.post("/ReportChannelWebhook", (req, res) => {
app.post("/webhook/ReportChannel", (req, res) => {
res.sendStatus(200);
});
app.post("/FirstTimeSubmissionsWebhook", (req, res) => {
app.post("/webhook/FirstTimeSubmissions", (req, res) => {
res.sendStatus(200);
});
app.post("/CompletelyIncorrectReportWebhook", (req, res) => {
app.post("/webhook/CompletelyIncorrectReport", (req, res) => {
res.sendStatus(200);
});
// Testing NeuralBlock
app.post("/NeuralBlockRejectWebhook", (req, res) => {
app.post("/webhook/NeuralBlockReject", (req, res) => {
res.sendStatus(200);
});
@ -47,6 +48,9 @@ app.post("/CustomWebhook", (req, res) => {
res.sendStatus(200);
});
// mocks
app.use("/UserCounter", UserCounter);
export function createMockServer(callback: () => void): Server {
return app.listen(config.mockPort, callback);
}

11
test/mocks/UserCounter.ts Normal file
View file

@ -0,0 +1,11 @@
import { Router } from "express";
export const UserCounter = Router();
UserCounter.post("/api/v1/addIP", (req, res) => {
res.sendStatus(200);
});
UserCounter.get("/api/v1/userCount", (req, res) => {
res.send({
userCount: 100
});
});

22
test/mocks/gumroadMock.ts Normal file
View file

@ -0,0 +1,22 @@
export const licenseSuccess = {
success: true,
uses: 4,
purchase: {}
};
export const licenseFail = {
success: false,
message: "That license does not exist for the provided product."
};
const subCode = (length = 8) => {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += characters[(Math.floor(Math.random() * characters.length))];
}
return result;
};
export const generateLicense = (): string => `${subCode()}-${subCode()}-${subCode()}-${subCode()}`;

View file

@ -0,0 +1,33 @@
const nullStub = (): any => null;
export const createRequest = (options: any) => ({
app: {},
baseUrl: "",
body: {},
cookies: {},
fresh: true,
headers: {},
hostname: "example.com",
ip: "",
ips: [],
method: "GET",
originalUrl: "/",
params: {},
path: "/",
protocol: "https",
query: {},
route: {},
secure: true,
signedCookies: {},
stale: false,
subdomains: [],
xhr: true,
accepts: nullStub(),
acceptsCharsets: nullStub(),
acceptsEncodings: nullStub(),
acceptsLanguages: nullStub(),
get: nullStub(),
is: nullStub(),
range: nullStub(),
...options
});

59
test/mocks/patreonMock.ts Normal file
View file

@ -0,0 +1,59 @@
export const activeIdentity = {
data: {},
links: {},
included: [
{
attributes: {
is_monthly: true,
currently_entitled_amount_cents: 100,
patron_status: "active_patron",
},
id: "id",
type: "campaign"
}
],
};
export const invalidIdentity = {
data: {},
links: {},
included: [{}],
};
export const formerIdentitySucceed = {
data: {},
links: {},
included: [
{
attributes: {
is_monthly: true,
campaign_lifetime_support_cents: 500,
patron_status: "former_patron",
},
id: "id",
type: "campaign"
}
],
};
export const formerIdentityFail = {
data: {},
links: {},
included: [
{
attributes: {
is_monthly: true,
campaign_lifetime_support_cents: 1,
patron_status: "former_patron",
},
id: "id",
type: "campaign"
}
],
};
export const fakeOauth = {
access_token: "test_access_token",
refresh_token: "test_refresh_token",
expires_in: 3600,
};

View file

@ -1,4 +1,4 @@
import { APIVideoData, APIVideoInfo } from "../src/types/youtubeApi.model";
import { APIVideoData, APIVideoInfo } from "../../src/types/youtubeApi.model";
export class YouTubeApiMock {
// eslint-disable-next-line require-await