From 310b5219b38427f0c2c7ba57225afdd8f3064380 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 8 Jul 2020 15:51:46 +0200 Subject: [PATCH] Add new abuses tests --- .../abuse-list/abuse-list.component.html | 4 +- .../users/user-notification.model.ts | 2 +- .../users/user-notifications.component.html | 2 +- .../video-report.component.ts | 1 - server/controllers/api/abuse.ts | 4 +- server/controllers/api/users/my-history.ts | 2 - server/helpers/custom-validators/abuses.ts | 6 +- .../custom-validators/video-comments.ts | 4 +- server/helpers/middlewares/abuses.ts | 2 +- .../migrations/0520-abuses-split.ts | 2 - server/lib/emailer.ts | 1 + .../emails/video-comment-abuse-new/html.pug | 3 +- server/lib/notifier.ts | 2 +- server/middlewares/validators/abuse.ts | 2 +- server/models/abuse/abuse.ts | 10 +- server/models/abuse/video-comment-abuse.ts | 8 +- server/models/account/user-notification.ts | 4 +- server/models/video/video-comment.ts | 42 +- server/tests/api/check-params/abuses.ts | 4 +- server/tests/api/moderation/abuses.ts | 931 +++++++++++++----- .../notifications/moderation-notifications.ts | 73 +- server/tests/api/server/email.ts | 2 +- server/types/models/user/user-notification.ts | 2 +- server/typings/express/index.d.ts | 1 - shared/extra-utils/moderation/abuses.ts | 14 +- shared/extra-utils/server/servers.ts | 4 +- .../extra-utils/users/user-notifications.ts | 75 +- .../models/users/user-notification.model.ts | 2 +- support/doc/api/openapi.yaml | 24 +- 29 files changed, 858 insertions(+), 375 deletions(-) diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html index 167f32fe6..333438269 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html @@ -138,8 +138,8 @@
- No video abuses found matching current filters. - No video abuses found. + No abuses found matching current filters. + No abuses found.
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index 389a242fd..a137f8c62 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts @@ -118,7 +118,7 @@ export class UserNotification implements UserNotificationServer { this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] break - case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: + case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: this.abuseUrl = '/admin/moderation/abuses/list' if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index 8d31eab0d..2b341af2c 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html @@ -42,7 +42,7 @@ - +
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts index b8d9f8d27..7977e4cca 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/video-report.component.ts @@ -140,7 +140,6 @@ export class VideoReportComponent extends FormReactive implements OnInit { const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value this.abuseService.reportVideo({ - accountId: this.video.account.id, reason, predefinedReasons, video: { diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts index 38808021d..04a0c06e3 100644 --- a/server/controllers/api/abuse.ts +++ b/server/controllers/api/abuse.ts @@ -100,7 +100,7 @@ async function updateAbuse (req: express.Request, res: express.Response) { return abuse.save({ transaction: t }) }) - // Do not send the delete to other instances, we updated OUR copy of this video abuse + // Do not send the delete to other instances, we updated OUR copy of this abuse return res.type('json').status(204).end() } @@ -112,7 +112,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) { return abuse.destroy({ transaction: t }) }) - // Do not send the delete to other instances, we delete OUR copy of this video abuse + // Do not send the delete to other instances, we delete OUR copy of this abuse return res.type('json').status(204).end() } diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts index 77a15e5fc..dc915977f 100644 --- a/server/controllers/api/users/my-history.ts +++ b/server/controllers/api/users/my-history.ts @@ -50,7 +50,5 @@ async function removeUserHistory (req: express.Request, res: express.Response) { return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t) }) - // Do not send the delete to other instances, we delete OUR copy of this video abuse - return res.type('json').status(204).end() } diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts index c21468caa..0ca06a252 100644 --- a/server/helpers/custom-validators/abuses.ts +++ b/server/helpers/custom-validators/abuses.ts @@ -3,10 +3,10 @@ import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, A import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' import { exists, isArray } from './misc' -const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES +const ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES function isAbuseReasonValid (value: string) { - return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) + return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.REASON) } function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) { @@ -32,7 +32,7 @@ function isAbuseTimestampCoherent (endAt: number, { req }) { } function isAbuseModerationCommentValid (value: string) { - return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) + return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) } function isAbuseStateValid (value: string) { diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts index a01680cbe..455ff4241 100644 --- a/server/helpers/custom-validators/video-comments.ts +++ b/server/helpers/custom-validators/video-comments.ts @@ -68,7 +68,7 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r async function doesCommentIdExist (idArg: number | string, res: express.Response) { const id = parseInt(idArg + '', 10) - const videoComment = await VideoCommentModel.loadById(id) + const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) if (!videoComment) { res.status(404) @@ -77,7 +77,7 @@ async function doesCommentIdExist (idArg: number | string, res: express.Response return false } - res.locals.videoComment = videoComment + res.locals.videoCommentFull = videoComment return true } diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts index b102273a2..be8c8b449 100644 --- a/server/helpers/middlewares/abuses.ts +++ b/server/helpers/middlewares/abuses.ts @@ -30,7 +30,7 @@ async function doesAbuseExist (abuseId: number | string, res: Response) { if (!abuse) { res.status(404) - .json({ error: 'Video abuse not found' }) + .json({ error: 'Abuse not found' }) return false } diff --git a/server/initializers/migrations/0520-abuses-split.ts b/server/initializers/migrations/0520-abuses-split.ts index 5898d501f..b02a21989 100644 --- a/server/initializers/migrations/0520-abuses-split.ts +++ b/server/initializers/migrations/0520-abuses-split.ts @@ -43,12 +43,10 @@ async function up (utils: { await utils.sequelize.query(` CREATE TABLE IF NOT EXISTS "commentAbuse" ( "id" serial, - "deletedComment" jsonb DEFAULT NULL, "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE, "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE, "createdAt" timestamp WITH time zone NOT NULL, "updatedAt" timestamp WITH time zone NOT NULL, - "commentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE, PRIMARY KEY ("id") ); `) diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index a5664408d..5a6f37bb9 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -325,6 +325,7 @@ class Emailer { subject: `New comment abuse report from ${reporter}`, locals: { commentUrl, + videoName: comment.Video.name, isLocal: comment.isOwned(), commentCreatedAt: new Date(comment.createdAt).toLocaleString(), reason: abuse.reason, diff --git a/server/lib/emails/video-comment-abuse-new/html.pug b/server/lib/emails/video-comment-abuse-new/html.pug index 170b79576..fc1c3e4e7 100644 --- a/server/lib/emails/video-comment-abuse-new/html.pug +++ b/server/lib/emails/video-comment-abuse-new/html.pug @@ -7,7 +7,8 @@ block title block content p | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment " - a(href=commentUrl) of #{flaggedAccount} + a(href=commentUrl) on video #{videoName} + | of #{flaggedAccount} | created on #{commentCreatedAt} p The reporter, #{reporter}, cited the following reason(s): diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 969e393fa..c567e1c20 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -371,7 +371,7 @@ class Notifier { async function notificationCreator (user: MUserWithNotificationSetting) { const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, + type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS, userId: user.id, abuseId: abuse.id }) diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index 048dbead0..966d1f7fb 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts @@ -128,7 +128,7 @@ const abuseListValidator = [ .custom(exists).withMessage('Should have a valid search'), query('state') .optional() - .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), + .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'), query('videoIs') .optional() .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 28ecf8253..dffd503b3 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts @@ -362,8 +362,8 @@ export class AbuseModel extends Model { const countReportsForReporter = this.get('countReportsForReporter') as number const countReportsForReportee = this.get('countReportsForReportee') as number - let video: VideoAbuse - let comment: VideoCommentAbuse + let video: VideoAbuse = null + let comment: VideoCommentAbuse = null if (this.VideoAbuse) { const abuseModel = this.VideoAbuse @@ -391,13 +391,13 @@ export class AbuseModel extends Model { if (this.VideoCommentAbuse) { const abuseModel = this.VideoCommentAbuse - const entity = abuseModel.VideoComment || abuseModel.deletedComment + const entity = abuseModel.VideoComment comment = { id: entity.id, - text: entity.text, + text: entity.text ?? '', - deleted: !abuseModel.VideoComment, + deleted: entity.isDeleted(), video: { id: entity.Video.id, diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts index de9f4d5fd..8b34009b4 100644 --- a/server/models/abuse/video-comment-abuse.ts +++ b/server/models/abuse/video-comment-abuse.ts @@ -1,5 +1,4 @@ -import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' -import { VideoComment } from '@shared/models' +import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' import { VideoCommentModel } from '../video/video-comment' import { AbuseModel } from './abuse' @@ -22,11 +21,6 @@ export class VideoCommentAbuseModel extends Model { @UpdatedAt updatedAt: Date - @AllowNull(true) - @Default(null) - @Column(DataType.JSONB) - deletedComment: VideoComment & { Video: { name: string, id: number, uuid: string }} - @ForeignKey(() => AbuseModel) @Column abuseId: number diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 07db5a2db..2945bf709 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -109,7 +109,7 @@ function buildAccountInclude (required: boolean, withActor = false) { required: true, include: [ { - attributes: [ 'uuid' ], + attributes: [ 'id', 'name', 'uuid' ], model: VideoModel.unscoped(), required: true } @@ -492,6 +492,8 @@ export class UserNotificationModel extends Model { threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), video: { + id: abuse.VideoCommentAbuse.VideoComment.Video.id, + name: abuse.VideoCommentAbuse.VideoComment.Video.name, uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid } } : undefined diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index fb6078ed8..fa4d13c3b 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -3,7 +3,6 @@ import { uniq } from 'lodash' import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' import { AllowNull, - BeforeDestroy, BelongsTo, Column, CreatedAt, @@ -16,7 +15,6 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { logger } from '@server/helpers/logger' import { getServerActor } from '@server/models/application/application' import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' import { VideoPrivacy } from '@shared/models' @@ -242,51 +240,13 @@ export class VideoCommentModel extends Model { @HasMany(() => VideoCommentAbuseModel, { foreignKey: { - name: 'commentId', + name: 'videoCommentId', allowNull: true }, onDelete: 'set null' }) CommentAbuses: VideoCommentAbuseModel[] - @BeforeDestroy - static async saveEssentialDataToAbuses (instance: VideoCommentModel, options) { - const tasks: Promise[] = [] - - if (!Array.isArray(instance.CommentAbuses)) { - instance.CommentAbuses = await instance.$get('CommentAbuses') - - if (instance.CommentAbuses.length === 0) return undefined - } - - if (!instance.Video) { - instance.Video = await instance.$get('Video') - } - - logger.info('Saving video comment %s for abuse.', instance.url) - - const details = Object.assign(instance.toFormattedJSON(), { - Video: { - id: instance.Video.id, - name: instance.Video.name, - uuid: instance.Video.uuid - } - }) - - for (const abuse of instance.CommentAbuses) { - abuse.deletedComment = details - - tasks.push(abuse.save({ transaction: options.transaction })) - } - - Promise.all(tasks) - .catch(err => { - logger.error('Some errors when saving details of comment %s in its abuses before destroy hook.', instance.url, { err }) - }) - - return undefined - } - static loadById (id: number, t?: Transaction): Bluebird { const query: FindOptions = { where: { diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts index ba7c0833f..8964c0ab2 100644 --- a/server/tests/api/check-params/abuses.ts +++ b/server/tests/api/check-params/abuses.ts @@ -21,9 +21,7 @@ import { checkBadStartPagination } from '../../../../shared/extra-utils/requests/check-api-params' -// FIXME: deprecated in 2.3. Remove this controller - -describe('Test video abuses API validators', function () { +describe('Test abuses API validators', function () { const basePath = '/api/v1/abuses/' let server: ServerInfo diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts index 28c5a5531..f186f7ea0 100644 --- a/server/tests/api/moderation/abuses.ts +++ b/server/tests/api/moderation/abuses.ts @@ -2,21 +2,30 @@ import 'mocha' import * as chai from 'chai' -import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models' +import { Abuse, AbuseFilter, AbusePredefinedReasonsString, AbuseState, VideoComment, Account } from '@shared/models' import { + addVideoCommentThread, cleanupTests, createUser, - deleteVideoAbuse, + deleteAbuse, + deleteVideoComment, flushAndRunMultipleServers, - getVideoAbusesList, + getAbusesList, + getVideoCommentThreads, + getVideoIdFromUUID, getVideosList, + immutableAssign, removeVideo, - reportVideoAbuse, + reportAbuse, ServerInfo, setAccessTokensToServers, - updateVideoAbuse, + updateAbuse, uploadVideo, - userLogin + uploadVideoAndGetId, + userLogin, + getAccount, + removeUser, + generateUserAccessToken } from '../../../../shared/extra-utils/index' import { doubleFollow } from '../../../../shared/extra-utils/server/follows' import { waitJobs } from '../../../../shared/extra-utils/server/jobs' @@ -31,6 +40,7 @@ const expect = chai.expect describe('Test abuses', function () { let servers: ServerInfo[] = [] + let abuseServer1: Abuse let abuseServer2: Abuse before(async function () { @@ -44,338 +54,721 @@ describe('Test abuses', function () { // Server 1 and server 2 follow each other await doubleFollow(servers[0], servers[1]) + }) - // Upload some videos on each servers - const video1Attributes = { - name: 'my super name for server 1', - description: 'my super description for server 1' - } - await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes) + describe('Video abuses', function () { - const video2Attributes = { - name: 'my super name for server 2', - description: 'my super description for server 2' - } - await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes) + before(async function () { + this.timeout(50000) - // Wait videos propagation, server 2 has transcoding enabled - await waitJobs(servers) + // Upload some videos on each servers + const video1Attributes = { + name: 'my super name for server 1', + description: 'my super description for server 1' + } + await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes) - const res = await getVideosList(servers[0].url) - const videos = res.body.data + const video2Attributes = { + name: 'my super name for server 2', + description: 'my super description for server 2' + } + await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes) - expect(videos.length).to.equal(2) + // Wait videos propagation, server 2 has transcoding enabled + await waitJobs(servers) - servers[0].video = videos.find(video => video.name === 'my super name for server 1') - servers[1].video = videos.find(video => video.name === 'my super name for server 2') - }) + const res = await getVideosList(servers[0].url) + const videos = res.body.data - it('Should not have video abuses', async function () { - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(videos.length).to.equal(2) - expect(res.body.total).to.equal(0) - expect(res.body.data).to.be.an('array') - expect(res.body.data.length).to.equal(0) - }) + servers[0].video = videos.find(video => video.name === 'my super name for server 1') + servers[1].video = videos.find(video => video.name === 'my super name for server 2') + }) - it('Should report abuse on a local video', async function () { - this.timeout(15000) + it('Should not have abuses', async function () { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - const reason = 'my super bad reason' - await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[0].video.id, reason) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.be.an('array') + expect(res.body.data.length).to.equal(0) + }) - // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2 - await waitJobs(servers) - }) + it('Should report abuse on a local video', async function () { + this.timeout(15000) - it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { - const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - - expect(res1.body.total).to.equal(1) - expect(res1.body.data).to.be.an('array') - expect(res1.body.data.length).to.equal(1) - - const abuse: Abuse = res1.body.data[0] - expect(abuse.reason).to.equal('my super bad reason') - expect(abuse.reporterAccount.name).to.equal('root') - expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) - expect(abuse.video.id).to.equal(servers[0].video.id) - expect(abuse.video.channel).to.exist - expect(abuse.count).to.equal(1) - expect(abuse.nth).to.equal(1) - expect(abuse.countReportsForReporter).to.equal(1) - expect(abuse.countReportsForReportee).to.equal(1) - - const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res2.body.total).to.equal(0) - expect(res2.body.data).to.be.an('array') - expect(res2.body.data.length).to.equal(0) - }) + const reason = 'my super bad reason' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[0].video.id, reason }) - it('Should report abuse on a remote video', async function () { - this.timeout(10000) + // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2 + await waitJobs(servers) + }) - const reason = 'my super bad reason 2' - await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[1].video.id, reason) + it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { + const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - // We wait requests propagation - await waitJobs(servers) - }) + expect(res1.body.total).to.equal(1) + expect(res1.body.data).to.be.an('array') + expect(res1.body.data.length).to.equal(1) - it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { - const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res1.body.total).to.equal(2) - expect(res1.body.data).to.be.an('array') - expect(res1.body.data.length).to.equal(2) - - const abuse1: Abuse = res1.body.data[0] - expect(abuse1.reason).to.equal('my super bad reason') - expect(abuse1.reporterAccount.name).to.equal('root') - expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port) - expect(abuse1.video.id).to.equal(servers[0].video.id) - expect(abuse1.state.id).to.equal(AbuseState.PENDING) - expect(abuse1.state.label).to.equal('Pending') - expect(abuse1.moderationComment).to.be.null - expect(abuse1.count).to.equal(1) - expect(abuse1.nth).to.equal(1) - - const abuse2: Abuse = res1.body.data[1] - expect(abuse2.reason).to.equal('my super bad reason 2') - expect(abuse2.reporterAccount.name).to.equal('root') - expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port) - expect(abuse2.video.id).to.equal(servers[1].video.id) - expect(abuse2.state.id).to.equal(AbuseState.PENDING) - expect(abuse2.state.label).to.equal('Pending') - expect(abuse2.moderationComment).to.be.null - - const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res2.body.total).to.equal(1) - expect(res2.body.data).to.be.an('array') - expect(res2.body.data.length).to.equal(1) - - abuseServer2 = res2.body.data[0] - expect(abuseServer2.reason).to.equal('my super bad reason 2') - expect(abuseServer2.reporterAccount.name).to.equal('root') - expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port) - expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) - expect(abuseServer2.state.label).to.equal('Pending') - expect(abuseServer2.moderationComment).to.be.null - }) + const abuse: Abuse = res1.body.data[0] + expect(abuse.reason).to.equal('my super bad reason') - it('Should update the state of a video abuse', async function () { - const body = { state: AbuseState.REJECTED } - await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) + expect(abuse.reporterAccount.name).to.equal('root') + expect(abuse.reporterAccount.host).to.equal(servers[0].host) - const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED) - }) + expect(abuse.video.id).to.equal(servers[0].video.id) + expect(abuse.video.channel).to.exist - it('Should add a moderation comment', async function () { - const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' } - await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) + expect(abuse.comment).to.be.null - const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED) - expect(res.body.data[0].moderationComment).to.equal('It is valid') - }) + expect(abuse.flaggedAccount.name).to.equal('root') + expect(abuse.flaggedAccount.host).to.equal(servers[0].host) + + expect(abuse.video.countReports).to.equal(1) + expect(abuse.video.nthReport).to.equal(1) + + expect(abuse.countReportsForReporter).to.equal(1) + expect(abuse.countReportsForReportee).to.equal(1) - it('Should hide video abuses from blocked accounts', async function () { - this.timeout(10000) + const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) + expect(res2.body.total).to.equal(0) + expect(res2.body.data).to.be.an('array') + expect(res2.body.data.length).to.equal(0) + }) - { - await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this') + it('Should report abuse on a remote video', async function () { + this.timeout(10000) + + const reason = 'my super bad reason 2' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[1].video.id, reason }) + + // We wait requests propagation await waitJobs(servers) + }) - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res.body.total).to.equal(3) - } + it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { + const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - const accountToBlock = 'root@localhost:' + servers[1].port + expect(res1.body.total).to.equal(2) + expect(res1.body.data.length).to.equal(2) - { - await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) + const abuse1: Abuse = res1.body.data[0] + expect(abuse1.reason).to.equal('my super bad reason') + expect(abuse1.reporterAccount.name).to.equal('root') + expect(abuse1.reporterAccount.host).to.equal(servers[0].host) - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res.body.total).to.equal(2) + expect(abuse1.video.id).to.equal(servers[0].video.id) + expect(abuse1.video.countReports).to.equal(1) + expect(abuse1.video.nthReport).to.equal(1) - const abuse = res.body.data.find(a => a.reason === 'will mute this') - expect(abuse).to.be.undefined - } + expect(abuse1.comment).to.be.null - { - await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) + expect(abuse1.flaggedAccount.name).to.equal('root') + expect(abuse1.flaggedAccount.host).to.equal(servers[0].host) - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res.body.total).to.equal(3) - } - }) + expect(abuse1.state.id).to.equal(AbuseState.PENDING) + expect(abuse1.state.label).to.equal('Pending') + expect(abuse1.moderationComment).to.be.null - it('Should hide video abuses from blocked servers', async function () { - const serverToBlock = servers[1].host + const abuse2: Abuse = res1.body.data[1] + expect(abuse2.reason).to.equal('my super bad reason 2') - { - await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host) + expect(abuse2.reporterAccount.name).to.equal('root') + expect(abuse2.reporterAccount.host).to.equal(servers[0].host) - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res.body.total).to.equal(2) + expect(abuse2.video.id).to.equal(servers[1].video.id) - const abuse = res.body.data.find(a => a.reason === 'will mute this') - expect(abuse).to.be.undefined - } + expect(abuse2.comment).to.be.null - { - await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock) + expect(abuse2.flaggedAccount.name).to.equal('root') + expect(abuse2.flaggedAccount.host).to.equal(servers[1].host) - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res.body.total).to.equal(3) - } - }) + expect(abuse2.state.id).to.equal(AbuseState.PENDING) + expect(abuse2.state.label).to.equal('Pending') + expect(abuse2.moderationComment).to.be.null - it('Should keep the video abuse when deleting the video', async function () { - this.timeout(10000) + const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) + expect(res2.body.total).to.equal(1) + expect(res2.body.data.length).to.equal(1) - await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid) + abuseServer2 = res2.body.data[0] + expect(abuseServer2.reason).to.equal('my super bad reason 2') + expect(abuseServer2.reporterAccount.name).to.equal('root') + expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) - await waitJobs(servers) + expect(abuse2.flaggedAccount.name).to.equal('root') + expect(abuse2.flaggedAccount.host).to.equal(servers[1].host) - const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res.body.total).to.equal(2, "wrong number of videos returned") - expect(res.body.data.length).to.equal(2, "wrong number of videos returned") - expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") + expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) + expect(abuseServer2.state.label).to.equal('Pending') + expect(abuseServer2.moderationComment).to.be.null + }) - const abuse: Abuse = res.body.data[0] - expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") - expect(abuse.video.channel).to.exist - expect(abuse.video.deleted).to.be.true - }) + it('Should hide video abuses from blocked accounts', async function () { + this.timeout(10000) - it('Should include counts of reports from reporter and reportee', async function () { - this.timeout(10000) + { + const videoId = await getVideoIdFromUUID(servers[1].url, servers[0].video.uuid) + await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'will mute this' }) + await waitJobs(servers) - // register a second user to have two reporters/reportees - const user = { username: 'user2', password: 'password' } - await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user }) - const userAccessToken = await userLogin(servers[0], user) + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(res.body.total).to.equal(3) + } - // upload a third video via this user - const video3Attributes = { - name: 'my second super name for server 1', - description: 'my second super description for server 1' - } - await uploadVideo(servers[0].url, userAccessToken, video3Attributes) - - const res1 = await getVideosList(servers[0].url) - const videos = res1.body.data - const video3 = videos.find(video => video.name === 'my second super name for server 1') - - // resume with the test - const reason3 = 'my super bad reason 3' - await reportVideoAbuse(servers[0].url, servers[0].accessToken, video3.id, reason3) - const reason4 = 'my super bad reason 4' - await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4) - - const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - - { - for (const abuse of res2.body.data as Abuse[]) { - if (abuse.video.id === video3.id) { - expect(abuse.count).to.equal(1, "wrong reports count for video 3") - expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3") - expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") - expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") - } - if (abuse.video.id === servers[0].video.id) { - expect(abuse.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse") + const accountToBlock = 'root@' + servers[1].host + + { + await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(res.body.total).to.equal(2) + + const abuse = res.body.data.find(a => a.reason === 'will mute this') + expect(abuse).to.be.undefined + } + + { + await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(res.body.total).to.equal(3) + } + }) + + it('Should hide video abuses from blocked servers', async function () { + const serverToBlock = servers[1].host + + { + await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host) + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(res.body.total).to.equal(2) + + const abuse = res.body.data.find(a => a.reason === 'will mute this') + expect(abuse).to.be.undefined + } + + { + await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock) + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(res.body.total).to.equal(3) + } + }) + + it('Should keep the video abuse when deleting the video', async function () { + this.timeout(10000) + + await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid) + + await waitJobs(servers) + + const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) + expect(res.body.total).to.equal(2, "wrong number of videos returned") + expect(res.body.data).to.have.lengthOf(2, "wrong number of videos returned") + + const abuse: Abuse = res.body.data[0] + expect(abuse.id).to.equal(abuseServer2.id, "wrong origin server id for first video") + expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") + expect(abuse.video.channel).to.exist + expect(abuse.video.deleted).to.be.true + }) + + it('Should include counts of reports from reporter and reportee', async function () { + this.timeout(10000) + + // register a second user to have two reporters/reportees + const user = { username: 'user2', password: 'password' } + await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user }) + const userAccessToken = await userLogin(servers[0], user) + + // upload a third video via this user + const video3Attributes = { + name: 'my second super name for server 1', + description: 'my second super description for server 1' + } + await uploadVideo(servers[0].url, userAccessToken, video3Attributes) + + const res1 = await getVideosList(servers[0].url) + const videos = res1.body.data + const video3 = videos.find(video => video.name === 'my second super name for server 1') + + // resume with the test + const reason3 = 'my super bad reason 3' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video3.id, reason: reason3 }) + + const reason4 = 'my super bad reason 4' + await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: reason4 }) + + { + const res2 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + const abuses = res2.body.data as Abuse[] + + const abuseVideo3 = res2.body.data.find(a => a.video.id === video3.id) + expect(abuseVideo3).to.not.be.undefined + expect(abuseVideo3.video.countReports).to.equal(1, "wrong reports count for video 3") + expect(abuseVideo3.video.nthReport).to.equal(1, "wrong report position in report list for video 3") + expect(abuseVideo3.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") + expect(abuseVideo3.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") + + const abuseServer1 = abuses.find(a => a.video.id === servers[0].video.id) + expect(abuseServer1.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse") + } + }) + + it('Should list predefined reasons as well as timestamps for the reported video', async function () { + this.timeout(10000) + + const reason5 = 'my super bad reason 5' + const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] + const createdAbuse = (await reportAbuse({ + url: servers[0].url, + token: servers[0].accessToken, + videoId: servers[0].video.id, + reason: reason5, + predefinedReasons: predefinedReasons5, + startAt: 1, + endAt: 5 + })).body.abuse + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + + { + const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id) + expect(abuse.reason).to.equals(reason5) + expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") + expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") + expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") + } + }) + + it('Should delete the video abuse', async function () { + this.timeout(10000) + + await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id) + + await waitJobs(servers) + + { + const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) + expect(res.body.total).to.equal(1) + expect(res.body.data.length).to.equal(1) + expect(res.body.data[0].id).to.not.equal(abuseServer2.id) + } + + { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) + expect(res.body.total).to.equal(6) + } + }) + + it('Should list and filter video abuses', async function () { + this.timeout(10000) + + async function list (query: Omit[0], 'url' | 'token'>) { + const options = { + url: servers[0].url, + token: servers[0].accessToken } + + Object.assign(options, query) + + const res = await getAbusesList(options) + + return res.body.data as Abuse[] } - } - }) - it('Should list predefined reasons as well as timestamps for the reported video', async function () { - this.timeout(10000) - - const reason5 = 'my super bad reason 5' - const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] - const createdAbuse = (await reportVideoAbuse( - servers[0].url, - servers[0].accessToken, - servers[0].video.id, - reason5, - predefinedReasons5, - 1, - 5 - )).body.abuse - - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - - { - const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id) - expect(abuse.reason).to.equals(reason5) - expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") - expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") - expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") - } + expect(await list({ id: 56 })).to.have.lengthOf(0) + expect(await list({ id: 1 })).to.have.lengthOf(1) + + expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4) + expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0) + + expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1) + + expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4) + expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0) + + expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) + expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) + + expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) + expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) + + expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) + expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) + + expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) + expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) + + expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) + expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) + }) }) - it('Should delete the video abuse', async function () { - this.timeout(10000) + describe('Comment abuses', function () { - await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id) + async function getComment (url: string, videoIdArg: number | string) { + const videoId = typeof videoIdArg === 'string' + ? await getVideoIdFromUUID(url, videoIdArg) + : videoIdArg - await waitJobs(servers) + const res = await getVideoCommentThreads(url, videoId, 0, 5) - { - const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res.body.total).to.equal(1) - expect(res.body.data.length).to.equal(1) - expect(res.body.data[0].id).to.not.equal(abuseServer2.id) + return res.body.data[0] as VideoComment } - { - const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) - expect(res.body.total).to.equal(6) - } - }) + before(async function () { + this.timeout(50000) - it('Should list and filter video abuses', async function () { - async function list (query: Omit[0], 'url' | 'token'>) { - const options = { - url: servers[0].url, - token: servers[0].accessToken + servers[0].video = await uploadVideoAndGetId({ server: servers[0], videoName: 'server 1' }) + servers[1].video = await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' }) + + await addVideoCommentThread(servers[0].url, servers[0].accessToken, servers[0].video.id, 'comment server 1') + await addVideoCommentThread(servers[1].url, servers[1].accessToken, servers[1].video.id, 'comment server 2') + + await waitJobs(servers) + }) + + it('Should report abuse on a comment', async function () { + this.timeout(15000) + + const comment = await getComment(servers[0].url, servers[0].video.id) + + const reason = 'it is a bad comment' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason }) + + await waitJobs(servers) + }) + + it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () { + { + const comment = await getComment(servers[0].url, servers[0].video.id) + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + + const abuse: Abuse = res.body.data[0] + expect(abuse.reason).to.equal('it is a bad comment') + + expect(abuse.reporterAccount.name).to.equal('root') + expect(abuse.reporterAccount.host).to.equal(servers[0].host) + + expect(abuse.video).to.be.null + + expect(abuse.comment.deleted).to.be.false + expect(abuse.comment.id).to.equal(comment.id) + expect(abuse.comment.text).to.equal(comment.text) + expect(abuse.comment.video.name).to.equal('server 1') + expect(abuse.comment.video.id).to.equal(servers[0].video.id) + expect(abuse.comment.video.uuid).to.equal(servers[0].video.uuid) + + expect(abuse.countReportsForReporter).to.equal(5) + expect(abuse.countReportsForReportee).to.equal(5) + } + + { + const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) + expect(res.body.total).to.equal(0) + expect(res.body.data.length).to.equal(0) + } + }) + + it('Should report abuse on a remote comment', async function () { + this.timeout(10000) + + const comment = await getComment(servers[0].url, servers[1].video.uuid) + + const reason = 'it is a really bad comment' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason }) + + await waitJobs(servers) + }) + + it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () { + const commentServer2 = await getComment(servers[0].url, servers[1].video.id) + + const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) + expect(res1.body.total).to.equal(2) + expect(res1.body.data.length).to.equal(2) + + const abuse: Abuse = res1.body.data[0] + expect(abuse.reason).to.equal('it is a bad comment') + expect(abuse.countReportsForReporter).to.equal(6) + expect(abuse.countReportsForReportee).to.equal(5) + + const abuse2: Abuse = res1.body.data[1] + + expect(abuse2.reason).to.equal('it is a really bad comment') + + expect(abuse2.reporterAccount.name).to.equal('root') + expect(abuse2.reporterAccount.host).to.equal(servers[0].host) + + expect(abuse2.video).to.be.null + + expect(abuse2.comment.deleted).to.be.false + expect(abuse2.comment.id).to.equal(commentServer2.id) + expect(abuse2.comment.text).to.equal(commentServer2.text) + expect(abuse2.comment.video.name).to.equal('server 2') + expect(abuse2.comment.video.uuid).to.equal(servers[1].video.uuid) + + expect(abuse2.state.id).to.equal(AbuseState.PENDING) + expect(abuse2.state.label).to.equal('Pending') + + expect(abuse2.moderationComment).to.be.null + + expect(abuse2.countReportsForReporter).to.equal(6) + expect(abuse2.countReportsForReportee).to.equal(2) + + const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) + expect(res2.body.total).to.equal(1) + expect(res2.body.data.length).to.equal(1) + + abuseServer2 = res2.body.data[0] + expect(abuseServer2.reason).to.equal('it is a really bad comment') + expect(abuseServer2.reporterAccount.name).to.equal('root') + expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) + + expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) + expect(abuseServer2.state.label).to.equal('Pending') + + expect(abuseServer2.moderationComment).to.be.null + + expect(abuseServer2.countReportsForReporter).to.equal(1) + expect(abuseServer2.countReportsForReportee).to.equal(1) + }) + + it('Should keep the comment abuse when deleting the comment', async function () { + this.timeout(10000) + + const commentServer2 = await getComment(servers[0].url, servers[1].video.id) + + await deleteVideoComment(servers[0].url, servers[0].accessToken, servers[1].video.uuid, commentServer2.id) + + await waitJobs(servers) + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) + expect(res.body.total).to.equal(2) + expect(res.body.data).to.have.lengthOf(2) + + const abuse = (res.body.data as Abuse[]).find(a => a.comment?.id === commentServer2.id) + expect(abuse).to.not.be.undefined + + expect(abuse.comment.text).to.be.empty + expect(abuse.comment.video.name).to.equal('server 2') + expect(abuse.comment.deleted).to.be.true + }) + + it('Should delete the comment abuse', async function () { + this.timeout(10000) + + await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id) + + await waitJobs(servers) + + { + const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) + expect(res.body.total).to.equal(0) + expect(res.body.data.length).to.equal(0) + } + + { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) + expect(res.body.total).to.equal(2) + } + }) + + it('Should list and filter video abuses', async function () { + { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'foo' }) + expect(res.body.total).to.equal(0) + } + + { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'ot' }) + expect(res.body.total).to.equal(2) + } + + { + const baseParams = { url: servers[0].url, token: servers[0].accessToken, filter: 'comment' as AbuseFilter, start: 1, count: 1 } + + const res1 = await getAbusesList(immutableAssign(baseParams, { sort: 'createdAt' })) + expect(res1.body.data).to.have.lengthOf(1) + expect(res1.body.data[0].comment.text).to.be.empty + + const res2 = await getAbusesList(immutableAssign(baseParams, { sort: '-createdAt' })) + expect(res2.body.data).to.have.lengthOf(1) + expect(res2.body.data[0].comment.text).to.equal('comment server 1') } + }) + }) - Object.assign(options, query) + describe('Account abuses', function () { - const res = await getVideoAbusesList(options) + async function getAccountFromServer (url: string, name: string, server: ServerInfo) { + const res = await getAccount(url, name + '@' + server.host) - return res.body.data as Abuse[] + return res.body as Account } - expect(await list({ id: 56 })).to.have.lengthOf(0) - expect(await list({ id: 1 })).to.have.lengthOf(1) + before(async function () { + this.timeout(50000) + + await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'user_1', password: 'donald' }) + + const token = await generateUserAccessToken(servers[1], 'user_2') + await uploadVideo(servers[1].url, token, { name: 'super video' }) + + await waitJobs(servers) + }) + + it('Should report abuse on an account', async function () { + this.timeout(15000) + + const account = await getAccountFromServer(servers[0].url, 'user_1', servers[0]) + + const reason = 'it is a bad account' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason }) + + await waitJobs(servers) + }) + + it('Should have 1 account abuse on server 1 and 0 on server 2', async function () { + { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + + const abuse: Abuse = res.body.data[0] + expect(abuse.reason).to.equal('it is a bad account') + + expect(abuse.reporterAccount.name).to.equal('root') + expect(abuse.reporterAccount.host).to.equal(servers[0].host) + + expect(abuse.video).to.be.null + expect(abuse.comment).to.be.null + + expect(abuse.flaggedAccount.name).to.equal('user_1') + expect(abuse.flaggedAccount.host).to.equal(servers[0].host) + } + + { + const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) + expect(res.body.total).to.equal(0) + expect(res.body.data.length).to.equal(0) + } + }) + + it('Should report abuse on a remote account', async function () { + this.timeout(10000) + + const account = await getAccountFromServer(servers[0].url, 'user_2', servers[1]) + + const reason = 'it is a really bad account' + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason }) + + await waitJobs(servers) + }) + + it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () { + const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) + expect(res1.body.total).to.equal(2) + expect(res1.body.data.length).to.equal(2) + + const abuse: Abuse = res1.body.data[0] + expect(abuse.reason).to.equal('it is a bad account') - expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4) - expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0) + const abuse2: Abuse = res1.body.data[1] + expect(abuse2.reason).to.equal('it is a really bad account') - expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1) + expect(abuse2.reporterAccount.name).to.equal('root') + expect(abuse2.reporterAccount.host).to.equal(servers[0].host) - expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4) - expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0) + expect(abuse2.video).to.be.null + expect(abuse2.comment).to.be.null + + expect(abuse2.state.id).to.equal(AbuseState.PENDING) + expect(abuse2.state.label).to.equal('Pending') + + expect(abuse2.moderationComment).to.be.null + + const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' }) + expect(res2.body.total).to.equal(1) + expect(res2.body.data.length).to.equal(1) + + abuseServer2 = res2.body.data[0] + + expect(abuseServer2.reason).to.equal('it is a really bad account') + + expect(abuseServer2.reporterAccount.name).to.equal('root') + expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) + + expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) + expect(abuseServer2.state.label).to.equal('Pending') + + expect(abuseServer2.moderationComment).to.be.null + }) + + it('Should keep the account abuse when deleting the account', async function () { + this.timeout(10000) + + const account = await getAccountFromServer(servers[1].url, 'user_2', servers[1]) + await removeUser(servers[1].url, account.userId, servers[1].accessToken) + + await waitJobs(servers) + + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) + expect(res.body.total).to.equal(2) + expect(res.body.data).to.have.lengthOf(2) + + const abuse = (res.body.data as Abuse[]).find(a => a.reason === 'it is a really bad account') + expect(abuse).to.not.be.undefined + }) + + it('Should delete the account abuse', async function () { + this.timeout(10000) + + await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id) + + await waitJobs(servers) + + { + const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' }) + expect(res.body.total).to.equal(0) + expect(res.body.data.length).to.equal(0) + } + + { + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) + expect(res.body.total).to.equal(2) + + abuseServer1 = res.body.data[0] + } + }) + }) - expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) - expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) + describe('Common actions on abuses', function () { - expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) - expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) + it('Should update the state of an abuse', async function () { + const body = { state: AbuseState.REJECTED } + await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body) - expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) - expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id }) + expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED) + }) - expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) - expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) + it('Should add a moderation comment', async function () { + const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' } + await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body) - expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) - expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) + const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id }) + expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED) + expect(res.body.data[0].moderationComment).to.equal('It is valid') + }) }) after(async function () { diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index a27681603..a8517600a 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts @@ -3,10 +3,16 @@ import 'mocha' import { v4 as uuidv4 } from 'uuid' import { + addVideoCommentThread, addVideoToBlacklist, cleanupTests, + createUser, follow, + generateUserAccessToken, + getAccount, getCustomConfig, + getVideoCommentThreads, + getVideoIdFromUUID, immutableAssign, MockInstancesIndex, registerUser, @@ -23,7 +29,9 @@ import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { checkAutoInstanceFollowing, CheckerBaseParams, + checkNewAccountAbuseForModerators, checkNewBlacklistOnMyVideo, + checkNewCommentAbuseForModerators, checkNewInstanceFollower, checkNewVideoAbuseForModerators, checkNewVideoFromSubscription, @@ -91,11 +99,74 @@ describe('Test moderation notifications', function () { await waitJobs(servers) - await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId: video.id, reason: 'super reason' }) + const videoId = await getVideoIdFromUUID(servers[1].url, video.uuid) + await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'super reason' }) await waitJobs(servers) await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence') }) + + it('Should send a notification to moderators on local comment abuse', async function () { + this.timeout(10000) + + const name = 'video for abuse ' + uuidv4() + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) + const video = resVideo.body.video + const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4()) + const comment = resComment.body.comment + + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason: 'super reason' }) + + await waitJobs(servers) + await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence') + }) + + it('Should send a notification to moderators on remote comment abuse', async function () { + this.timeout(10000) + + const name = 'video for abuse ' + uuidv4() + const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) + const video = resVideo.body.video + await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4()) + + await waitJobs(servers) + + const resComments = await getVideoCommentThreads(servers[1].url, video.uuid, 0, 5) + const commentId = resComments.body.data[0].id + await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, commentId, reason: 'super reason' }) + + await waitJobs(servers) + await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence') + }) + + it('Should send a notification to moderators on local account abuse', async function () { + this.timeout(10000) + + const username = 'user' + new Date().getTime() + const resUser = await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username, password: 'donald' }) + const accountId = resUser.body.user.account.id + + await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId, reason: 'super reason' }) + + await waitJobs(servers) + await checkNewAccountAbuseForModerators(baseParams, username, 'presence') + }) + + it('Should send a notification to moderators on remote account abuse', async function () { + this.timeout(10000) + + const username = 'user' + new Date().getTime() + const tmpToken = await generateUserAccessToken(servers[0], username) + await uploadVideo(servers[0].url, tmpToken, { name: 'super video' }) + + await waitJobs(servers) + + const resAccount = await getAccount(servers[1].url, username + '@' + servers[0].host) + await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, accountId: resAccount.body.id, reason: 'super reason' }) + + await waitJobs(servers) + await checkNewAccountAbuseForModerators(baseParams, username, 'presence') + }) }) describe('Video blacklist on my video', function () { diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index 9c3299618..b01a91d48 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts @@ -180,7 +180,7 @@ describe('Test emails', function () { }) }) - describe('When creating a video abuse', function () { + describe('When creating an abuse', function () { it('Should send the notification email', async function () { this.timeout(10000) diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index 92ea16768..f59eb7260 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts @@ -53,7 +53,7 @@ export module UserNotificationIncludes { Pick & PickWith & - PickWith>> + PickWith>> export type AbuseInclude = Pick & diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index b1afffcd4..7595e6d86 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts @@ -91,7 +91,6 @@ declare module 'express' { accountVideoRate?: MAccountVideoRateAccountVideo - videoComment?: MComment videoCommentFull?: MCommentOwnerVideoReply videoCommentThread?: MComment diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts index 1af703f92..62af9556e 100644 --- a/shared/extra-utils/moderation/abuses.ts +++ b/shared/extra-utils/moderation/abuses.ts @@ -57,10 +57,15 @@ function reportAbuse (options: { function getAbusesList (options: { url: string token: string + + start?: number + count?: number + sort?: string + id?: number predefinedReason?: AbusePredefinedReasonsString search?: string - filter?: AbuseFilter, + filter?: AbuseFilter state?: AbuseState videoIs?: AbuseVideoIs searchReporter?: string @@ -71,6 +76,9 @@ function getAbusesList (options: { const { url, token, + start, + count, + sort, id, predefinedReason, search, @@ -85,13 +93,15 @@ function getAbusesList (options: { const path = '/api/v1/abuses' const query = { - sort: 'createdAt', id, predefinedReason, search, state, filter, videoIs, + start, + count, + sort: sort || 'createdAt', searchReporter, searchReportee, searchVideo, diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 0f883d839..994aac628 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts @@ -37,8 +37,8 @@ interface ServerInfo { video?: { id: number uuid: string - name: string - account: { + name?: string + account?: { name: string } } diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts index 4a5bc30fe..2061e3353 100644 --- a/shared/extra-utils/users/user-notifications.ts +++ b/shared/extra-utils/users/user-notifications.ts @@ -139,13 +139,17 @@ async function checkNotification ( } function checkVideo (video: any, videoName?: string, videoUUID?: string) { - expect(video.name).to.be.a('string') - expect(video.name).to.not.be.empty - if (videoName) expect(video.name).to.equal(videoName) + if (videoName) { + expect(video.name).to.be.a('string') + expect(video.name).to.not.be.empty + expect(video.name).to.equal(videoName) + } - expect(video.uuid).to.be.a('string') - expect(video.uuid).to.not.be.empty - if (videoUUID) expect(video.uuid).to.equal(videoUUID) + if (videoUUID) { + expect(video.uuid).to.be.a('string') + expect(video.uuid).to.not.be.empty + expect(video.uuid).to.equal(videoUUID) + } expect(video.id).to.be.a('number') } @@ -436,7 +440,7 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, } async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { - const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS + const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS function notificationChecker (notification: UserNotification, type: CheckerType) { if (type === 'presence') { @@ -460,6 +464,56 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU await checkNotification(base, notificationChecker, emailNotificationFinder, type) } +async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { + const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS + + function notificationChecker (notification: UserNotification, type: CheckerType) { + if (type === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.be.a('number') + checkVideo(notification.abuse.comment.video, videoName, videoUUID) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.abuse === undefined || n.abuse.comment.video.uuid !== videoUUID + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1 + } + + await checkNotification(base, notificationChecker, emailNotificationFinder, type) +} + +async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displayName: string, type: CheckerType) { + const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS + + function notificationChecker (notification: UserNotification, type: CheckerType) { + if (type === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.be.a('number') + expect(notification.abuse.account.displayName).to.equal(displayName) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1 + } + + await checkNotification(base, notificationChecker, emailNotificationFinder, type) +} + async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS @@ -541,6 +595,9 @@ async function prepareNotificationsTest (serversCount = 3) { smtp: { hostname: 'localhost', port + }, + signup: { + limit: 20 } } const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) @@ -623,5 +680,7 @@ export { markAsReadNotifications, getLastNotification, checkNewInstanceFollower, - prepareNotificationsTest + prepareNotificationsTest, + checkNewCommentAbuseForModerators, + checkNewAccountAbuseForModerators } diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index 39090f5a1..11d96fd50 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts @@ -3,7 +3,7 @@ import { FollowState } from '../actors' export enum UserNotificationType { NEW_VIDEO_FROM_SUBSCRIPTION = 1, NEW_COMMENT_ON_MY_VIDEO = 2, - NEW_VIDEO_ABUSE_FOR_MODERATORS = 3, + NEW_ABUSE_FOR_MODERATORS = 3, BLACKLIST_ON_MY_VIDEO = 4, UNBLACKLIST_ON_MY_VIDEO = 5, diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 03e60925b..3b381bbb5 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -106,9 +106,9 @@ tags: Managing plugins installed from a local path or from NPM, or search for new ones. externalDocs: url: https://docs.joinpeertube.org/#/api-plugins - - name: Video Abuses + - name: Abuses description: | - Video abuses deal with reports of local or remote videos alike. + Abuses deal with reports of local or remote videos/comments/accounts alike. - name: Video description: | Operations dealing with listing, uploading, fetching or modifying videos. @@ -166,7 +166,7 @@ x-tagGroups: - Search - name: Moderation tags: - - Video Abuses + - Abuses - Video Blocks - Account Blocks - Server Blocks @@ -1474,13 +1474,13 @@ paths: /videos/abuse: get: deprecated: true - summary: List video abuses + summary: List abuses security: - OAuth2: - admin - moderator tags: - - Video Abuses + - Abuses parameters: - name: id in: query @@ -1508,7 +1508,7 @@ paths: type: string - name: state in: query - description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' + description: 'The abuse state (Pending = `1`, Rejected = `2`, Accepted = `3`)' schema: type: integer enum: @@ -1554,7 +1554,7 @@ paths: security: - OAuth2: [] tags: - - Video Abuses + - Abuses - Videos parameters: - $ref: '#/components/parameters/idOrUUID' @@ -1607,7 +1607,7 @@ paths: - admin - moderator tags: - - Video Abuses + - Abuses parameters: - $ref: '#/components/parameters/idOrUUID' - $ref: '#/components/parameters/abuseId' @@ -1626,11 +1626,11 @@ paths: '204': description: successful operation '404': - description: video abuse not found + description: abuse not found delete: deprecated: true tags: - - Video Abuses + - Abuses summary: Delete an abuse security: - OAuth2: @@ -3320,7 +3320,7 @@ components: name: abuseId in: path required: true - description: Video abuse id + description: Abuse id schema: type: integer captionLanguage: @@ -5098,7 +5098,7 @@ components: - `2` NEW_COMMENT_ON_MY_VIDEO - - `3` NEW_VIDEO_ABUSE_FOR_MODERATORS + - `3` NEW_ABUSE_FOR_MODERATORS - `4` BLACKLIST_ON_MY_VIDEO -- 2.41.0