diff --git a/CHANGELOG.md b/CHANGELOG.md index c2134a60e..4f5b9b2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.19.0] - Not released +#### Changed +- The bans are fully symmetrical now. If A bans B, then: + - A doesn't see B's posts, comments content, likes and comment likes; + - B doesn't see A's posts, comments content, likes and comment likes. + + By default, comments are displayed as placeholders, but the viewer can turn + them off completely. Also, one can disable bans in some groups, see the + [2.7.0] release for the bans logic in this case. +- /!\ The ban symmetry is DELIBERATELY BROKEN for now. It works for likes and + comment likes, but the A's comments content is still visible for B. Such + comments have an additional `_hideType = 4` field in API responses ('4' is the + `Comment.HIDDEN_VIEWER_BANNED` constant). The client code can hide such + comments on its level. ## [2.18.5] - 2024-04-09 ### Fixed diff --git a/app/controllers/middlewares/comment-access-required.js b/app/controllers/middlewares/comment-access-required.js index 789a05c11..7222064d0 100644 --- a/app/controllers/middlewares/comment-access-required.js +++ b/app/controllers/middlewares/comment-access-required.js @@ -60,15 +60,21 @@ export function commentAccessRequired({ mustBeVisible }) { ctx.params.postId = comment.postId; await applyMiddleware(postAccessRequired(), ctx); - if (await dbAdapter.isCommentBannedForViewer(comment.id, viewer?.id)) { - if (mustBeVisible) { - throw new ForbiddenException('You have banned the author of this comment'); - } else { - comment.setHideType(Comment.HIDDEN_BANNED); - } + const banHideType = await dbAdapter.isCommentBannedForViewer(comment.id, viewer?.id); + + if (mustBeVisible && banHideType === Comment.HIDDEN_AUTHOR_BANNED) { + throw new ForbiddenException('You have banned the author of this comment'); + // } else if (mustBeVisible && banHideType === Comment.HIDDEN_VIEWER_BANNED) { + // throw new ForbiddenException('The author of this comment has banned you'); + } else if (banHideType) { + comment.setHideType(banHideType); } - if (comment.hideType !== Comment.VISIBLE && mustBeVisible) { + if ( + comment.hideType !== Comment.VISIBLE && + banHideType !== Comment.HIDDEN_VIEWER_BANNED && + mustBeVisible + ) { throw new ForbiddenException(`You don't have access to this comment`); } diff --git a/app/models.d.ts b/app/models.d.ts index 8492b0b14..cabb945c4 100644 --- a/app/models.d.ts +++ b/app/models.d.ts @@ -204,15 +204,19 @@ export class Attachment { export class Comment { static VISIBLE: 0; static DELETED: 1; - static HIDDEN_BANNED: 2; + // The author of the comment was banned by a viewer + static HIDDEN_AUTHOR_BANNED: 2; static HIDDEN_ARCHIVED: 3; + // A viewer was banned by the author of the comment + static HIDDEN_VIEWER_BANNED: 4; id: UUID; intId: number; body: string; userId: Nullable; - hideType: 0 | 1 | 2 | 3; + hideType: 0 | 1 | 2 | 3 | 4; postId: UUID; seqNumber: number; + static hiddenBody(hideType: number): string; constructor(params: { userId: UUID; body: string; postId: UUID }); create(): Promise; destroy(destroyedBy?: User): Promise; diff --git a/app/models/comment.js b/app/models/comment.js index e5d7ffe73..7499e26d6 100644 --- a/app/models/comment.js +++ b/app/models/comment.js @@ -20,8 +20,11 @@ export function addModel(dbAdapter) { class Comment { static VISIBLE = 0; static DELETED = 1; - static HIDDEN_BANNED = 2; + // The author of the comment was banned by a viewer + static HIDDEN_AUTHOR_BANNED = 2; static HIDDEN_ARCHIVED = 3; + // A viewer was banned by the author of the comment + static HIDDEN_VIEWER_BANNED = 4; id; intId; @@ -40,10 +43,12 @@ export function addModel(dbAdapter) { return 'Visible comment'; case this.DELETED: return 'Deleted comment'; - case this.HIDDEN_BANNED: - return 'Hidden comment'; + case this.HIDDEN_AUTHOR_BANNED: + return 'Comment from blocked user'; case this.HIDDEN_ARCHIVED: return 'Archived comment'; + case this.HIDDEN_VIEWER_BANNED: + return 'Comment from user who blocked you'; default: return 'Hidden comment'; } diff --git a/app/models/user-prefs.ts b/app/models/user-prefs.ts index b79f860ca..f92daffe3 100644 --- a/app/models/user-prefs.ts +++ b/app/models/user-prefs.ts @@ -24,7 +24,12 @@ const schema = { uniqueItems: true, items: { type: 'integer', - enum: [commentModel.DELETED, commentModel.HIDDEN_BANNED, commentModel.HIDDEN_ARCHIVED], + enum: [ + commentModel.DELETED, + commentModel.HIDDEN_AUTHOR_BANNED, + commentModel.HIDDEN_ARCHIVED, + commentModel.HIDDEN_VIEWER_BANNED, + ], }, }, sendNotificationsDigest: { diff --git a/app/pubsub-listener.js b/app/pubsub-listener.js index 70608dd44..3338a9220 100644 --- a/app/pubsub-listener.js +++ b/app/pubsub-listener.js @@ -335,15 +335,24 @@ export default class PubsubListener { } // See doc/visibility-rules.md for details - const bansMap = await dbAdapter.getUsersBansIdsMap(userIds); + const [bansMap, bannedByMap] = await Promise.all([ + dbAdapter.getUsersBansIdsMap(userIds), + dbAdapter.getUsersBanedByIdsMap(userIds), + ]); + /** @type {UUID[]} */ let usersDisabledBans = []; + /** @type {UUID[]} */ + let adminsDisabledBans = []; if (post) { const postGroups = await dbAdapter.getPostGroups(post.id); - usersDisabledBans = await dbAdapter - .getUsersWithDisabledBansInGroups(postGroups.map((g) => g.id)) - .then((us) => us.map((u) => u.user_id)); + + // Users/admins who have disabled bans in some post groups + const groupIds = postGroups.map((g) => g.id); + const disabledBans = await dbAdapter.getUsersWithDisabledBansInGroups(groupIds); + usersDisabledBans = disabledBans.map((u) => u.user_id); + adminsDisabledBans = disabledBans.filter((u) => u.is_admin).map((u) => u.user_id); } await Promise.all( @@ -355,32 +364,48 @@ export default class PubsubListener { // Bans if (post && userId) { - const banIds = (!usersDisabledBans.includes(userId) && bansMap.get(userId)) || []; + const bannedUserIds = (!usersDisabledBans.includes(userId) && bansMap.get(userId)) || []; + const bannedByUserIds = + (!adminsDisabledBans.includes(userId) && bannedByMap.get(userId)) || []; + + const isBanned = (id) => bannedUserIds.includes(id) || bannedByUserIds.includes(id); if ( - (type === eventNames.COMMENT_UPDATED && banIds.includes(data.comments.createdBy)) || - (type === eventNames.LIKE_ADDED && banIds.includes(data.users.id)) || + (type === eventNames.COMMENT_UPDATED && isBanned(data.comments.createdBy)) || + (type === eventNames.LIKE_ADDED && isBanned(data.users.id)) || ((type === eventNames.COMMENT_LIKE_ADDED || type === eventNames.COMMENT_LIKE_REMOVED) && - (banIds.includes(data.comments.createdBy) || banIds.includes(data.comments.userId))) + (isBanned(data.comments.createdBy) || isBanned(data.comments.userId))) ) { return; } // A very special case: comment author is banned, but the viewer chooses // to see such comments as placeholders. - if (type === eventNames.COMMENT_CREATED && banIds.includes(data.comments.createdBy)) { - const user = await dbAdapter.getUserById(userId); + if (type === eventNames.COMMENT_CREATED) { + let hideType = null; - if (user.getHiddenCommentTypes().includes(Comment.HIDDEN_BANNED)) { - return; + if (bannedUserIds.includes(data.comments.createdBy)) { + hideType = Comment.HIDDEN_AUTHOR_BANNED; + // } else if (bannedByUserIds.includes(data.comments.createdBy)) { + // hideType = Comment.HIDDEN_VIEWER_BANNED; } - const { createdBy } = data.comments; - data.comments.hideType = Comment.HIDDEN_BANNED; - data.comments.body = Comment.hiddenBody(Comment.HIDDEN_BANNED); - data.comments.createdBy = null; - data.users = data.users.filter((u) => u.id !== createdBy); - data.admins = data.admins.filter((u) => u.id !== createdBy); + if (hideType !== null) { + const user = await dbAdapter.getUserById(userId); + + if (user.getHiddenCommentTypes().includes(hideType)) { + return; + } + + const { createdBy } = data.comments; + data.comments.hideType = hideType; + data.comments.body = Comment.hiddenBody(hideType); + data.comments.createdBy = null; + data.users = data.users.filter((u) => u.id !== createdBy); + data.admins = data.admins.filter((u) => u.id !== createdBy); + } else if (bannedByUserIds.includes(data.comments.createdBy)) { + data.comments._hideType = Comment.HIDDEN_VIEWER_BANNED; + } } } diff --git a/app/serializers/v2/comment.js b/app/serializers/v2/comment.js index 3b8341a31..f2d513166 100644 --- a/app/serializers/v2/comment.js +++ b/app/serializers/v2/comment.js @@ -44,12 +44,15 @@ export async function serializeCommentsFull(comments, viewerId) { createdBy: comment.userId, }; - if (bansMap[comment.id]) { + if ( + bansMap[comment.id] === Comment.HIDDEN_AUTHOR_BANNED // || + // bansMap[comment.id] === Comment.HIDDEN_VIEWER_BANNED + ) { ser.likes = 0; ser.hasOwnLike = false; - ser.hideType = Comment.HIDDEN_BANNED; - ser.body = Comment.hiddenBody(Comment.HIDDEN_BANNED); + ser.hideType = bansMap[comment.id]; + ser.body = Comment.hiddenBody(ser.hideType); ser.createdBy = null; } else { const commentLikesData = likesInfo.find((it) => it.uid === comment.id) ?? { @@ -59,6 +62,10 @@ export async function serializeCommentsFull(comments, viewerId) { ser.likes = parseInt(commentLikesData.c_likes); ser.hasOwnLike = commentLikesData.has_own_like; userIds.add(comment.userId); + + if (bansMap[comment.id]) { + ser._hideType = bansMap[comment.id]; + } } return ser; diff --git a/app/serializers/v2/post.js b/app/serializers/v2/post.js index 791a9b19d..0da98852b 100644 --- a/app/serializers/v2/post.js +++ b/app/serializers/v2/post.js @@ -14,6 +14,7 @@ export function serializeComment(comment) { 'createdAt', 'updatedAt', 'hideType', + '_hideType', 'likes', 'hasOwnLike', 'seqNumber', diff --git a/app/support/DbAdapter/bans.js b/app/support/DbAdapter/bans.js index 80530f9f5..06f8efce5 100644 --- a/app/support/DbAdapter/bans.js +++ b/app/support/DbAdapter/bans.js @@ -13,9 +13,9 @@ const bansTrait = (superClass) => } /** - * Returns Map. + * Returns Map * @param {string[]} userIds - * @return {Map.} + * @return {Promise>} */ async getUsersBansIdsMap(userIds) { const { rows } = await this.database.raw( @@ -29,6 +29,23 @@ const bansTrait = (superClass) => return new Map(rows.map((r) => [r.user_id, r.bans])); } + /** + * Returns Map + * @param {string[]} userIds + * @return {Promise>} + */ + async getUsersBanedByIdsMap(userIds) { + const { rows } = await this.database.raw( + ` + select banned_user_id, array_agg(user_id) as bans + from bans where banned_user_id = any(:userIds) + group by banned_user_id + `, + { userIds }, + ); + return new Map(rows.map((r) => [r.banned_user_id, r.bans])); + } + async getUserIdsWhoBannedUser(userId) { const res = await this.database('bans') .select('user_id') diff --git a/app/support/DbAdapter/comments.js b/app/support/DbAdapter/comments.js index 9448cdddd..f20e6c580 100644 --- a/app/support/DbAdapter/comments.js +++ b/app/support/DbAdapter/comments.js @@ -171,45 +171,6 @@ const commentsTrait = (superClass) => return parseInt(res[0].count); } - async getAllPostCommentsWithoutBannedUsers(postId, viewerUserId) { - let query = this.database('comments').orderBy('created_at', 'asc').where('post_id', postId); - - const [viewer, bannedUsersIds] = await Promise.all([ - viewerUserId ? this.getUserById(viewerUserId) : null, - viewerUserId ? this.getUserBansIds(viewerUserId) : [], - ]); - - if (viewerUserId) { - const hiddenCommentTypes = viewer.getHiddenCommentTypes(); - - if (hiddenCommentTypes.length > 0) { - if (hiddenCommentTypes.includes(Comment.HIDDEN_BANNED) && bannedUsersIds.length > 0) { - query = query.where('user_id', 'not in', bannedUsersIds); - } - - const ht = hiddenCommentTypes.filter( - (t) => t !== Comment.HIDDEN_BANNED && t !== Comment.VISIBLE, - ); - - if (ht.length > 0) { - query = query.where('hide_type', 'not in', ht); - } - } - } - - const responses = await query; - const comments = responses.map((comm) => { - if (bannedUsersIds.includes(comm.user_id)) { - comm.user_id = null; - comm.hide_type = Comment.HIDDEN_BANNED; - comm.body = Comment.hiddenBody(Comment.HIDDEN_BANNED); - } - - return comm; - }); - return comments.map(initCommentObject); - } - _deletePostComments(postId) { return this.database('comments').where({ post_id: postId }).delete(); } diff --git a/app/support/DbAdapter/index.d.ts b/app/support/DbAdapter/index.d.ts index 1cdae4408..825a9d84c 100644 --- a/app/support/DbAdapter/index.d.ts +++ b/app/support/DbAdapter/index.d.ts @@ -122,6 +122,8 @@ export class DbAdapter { getUsersByNormEmail(email: string): Promise; existsEmail(email: string): Promise; existsNormEmail(email: string): Promise; + getUsersBansIdsMap(ids: UUID[]): Promise>; + getUsersBanedByIdsMap(ids: UUID[]): Promise>; getUserIdsWhoBannedUser(id: UUID): Promise; getFeedOwnerById(id: UUID): Promise; getFeedOwnerByUsername(name: string): Promise; @@ -181,6 +183,9 @@ export class DbAdapter { // Bans getUserBansIds(id: UUID): Promise; getGroupsWithDisabledBans(userId: UUID, groupIds?: UUID[]): Promise; + getUsersWithDisabledBansInGroups( + groupIds: UUID[], + ): Promise<{ user_id: UUID; is_admin: boolean }[]>; disableBansInGroup(userId: UUID, groupId: UUID, doDisable: boolean): Promise; // Posts diff --git a/app/support/DbAdapter/timelines-posts.js b/app/support/DbAdapter/timelines-posts.js index 9b5cf0311..1801cdf59 100644 --- a/app/support/DbAdapter/timelines-posts.js +++ b/app/support/DbAdapter/timelines-posts.js @@ -343,6 +343,7 @@ const timelinesPostsTrait = (superClass) => ]); const notBannedSQLFabric = await this.notBannedActionsSQLFabric(viewerId); + const bannedSQLsFabric = await this.bannedActionsSQLsFabric(viewerId); const allLikesSQL = ` select @@ -377,23 +378,36 @@ const timelinesPostsTrait = (superClass) => const viewerIntId = viewerId ? await this._getUserIntIdByUUID(viewerId) : null; - const excludeBannedComments = params.hiddenCommentTypes.includes(Comment.HIDDEN_BANNED); + const excludeBannedByViewer = params.hiddenCommentTypes.includes( + Comment.HIDDEN_AUTHOR_BANNED, + ); + const excludeBannedByAuthor = params.hiddenCommentTypes.includes( + Comment.HIDDEN_VIEWER_BANNED, + ); + + const bannedOrVisibleTypes = [ + Comment.VISIBLE, + Comment.HIDDEN_AUTHOR_BANNED, + Comment.HIDDEN_VIEWER_BANNED, + ]; const otherExcludedTypes = params.hiddenCommentTypes.filter( - (t) => t !== Comment.HIDDEN_BANNED && t !== Comment.VISIBLE, + (t) => !bannedOrVisibleTypes.includes(t), ); - const bannedCommentsSQL = sqlNot(notBannedSQLFabric('c')); + const [bannedByViewerSQL, bannedByAuthorSQL] = bannedSQLsFabric('c'); const commentFilterSQL = andJoin([ sqlIn('c.post_id', uniqPostsIds), - excludeBannedComments ? sqlNot(bannedCommentsSQL) : 'true', + excludeBannedByViewer ? sqlNot(bannedByViewerSQL) : 'true', + excludeBannedByAuthor ? sqlNot(bannedByAuthorSQL) : 'true', sqlNotIn('c.hide_type', otherExcludedTypes), ]); const allCommentsSQL = pgFormat( `select ${commentFields.map((f) => `c.${f}`).join(', ')}, - ${bannedCommentsSQL} as hide_as_banned, + ${excludeBannedByViewer ? 'false' : bannedByViewerSQL} as banned_by_viewer, + ${excludeBannedByAuthor ? 'false' : bannedByAuthorSQL} as banned_by_author, rank() over (partition by c.post_id order by c.created_at, c.id), count(*) over (partition by c.post_id), (select coalesce(count(*), 0) from @@ -417,7 +431,9 @@ const timelinesPostsTrait = (superClass) => : ``; const commentsSQL = ` with comments as (${allCommentsSQL}) - select ${commentFields.join(', ')}, count, c_likes, has_own_like, hide_as_banned from comments + select ${commentFields.join(', ')}, count, c_likes, has_own_like, + banned_by_viewer, banned_by_author + from comments ${foldCommentsSql} order by created_at, id `; @@ -466,15 +482,26 @@ const timelinesPostsTrait = (superClass) => } for (const comm of commentsData) { - if (comm.hide_as_banned) { + if (comm.banned_by_viewer) { comm.user_id = null; - comm.hide_type = Comment.HIDDEN_BANNED; - comm.body = Comment.hiddenBody(Comment.HIDDEN_BANNED); + comm.hide_type = Comment.HIDDEN_AUTHOR_BANNED; + comm.body = Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED); comm.c_likes = '0'; comm.has_own_like = null; + // } else if (comm.banned_by_author) { + // comm.user_id = null; + // comm.hide_type = Comment.HIDDEN_VIEWER_BANNED; + // comm.body = Comment.hiddenBody(Comment.HIDDEN_VIEWER_BANNED); + // comm.c_likes = '0'; + // comm.has_own_like = null; } const comment = initCommentObject(comm); + + if (comm.banned_by_author) { + comment._hideType = Comment.HIDDEN_VIEWER_BANNED; + } + comment.likes = parseInt(comm.c_likes); comment.hasOwnLike = Boolean(comm.has_own_like); results[comm.post_id].comments.push(comment); diff --git a/app/support/DbAdapter/visibility.js b/app/support/DbAdapter/visibility.js index f625f0685..df2342442 100644 --- a/app/support/DbAdapter/visibility.js +++ b/app/support/DbAdapter/visibility.js @@ -1,8 +1,9 @@ import { intersection } from 'lodash'; import { List } from '../open-lists'; +import { Comment } from '../../models'; -import { andJoin, orJoin, sqlIntarrayIn, sqlNot, sqlNotIn } from './utils'; +import { andJoin, orJoin, sqlIn, sqlIntarrayIn, sqlNot, sqlNotIn } from './utils'; const visibilityTrait = (superClass) => class extends superClass { @@ -79,33 +80,87 @@ const visibilityTrait = (superClass) => * See doc/visibility-rules.md for the rules details. */ async notBannedActionsSQLFabric(viewerId = null) { + const fabric2 = await this.bannedActionsSQLsFabric(viewerId); + return (actionsTable, postsTable = 'p', useIntBanIds = false) => + sqlNot(orJoin(fabric2(actionsTable, postsTable, useIntBanIds))); + } + + /** + * This function creates a fabric that returns array of _two_ SQL + * sub-queries: + * 1. For actions that are invisible because the author of the action is + * banned by the viewer, and + * 2. For actions that are invisible because the viewer is banned by the + * author of the action. + * + * See doc/visibility-rules.md for the rules details. + */ + async bannedActionsSQLsFabric(viewerId = null) { if (!viewerId) { - return () => 'true'; + return () => ['false', 'false']; } - const [bannedByViewer, feedsWithDisabledBans] = await Promise.all([ + const [ + groupsWithDisabledBans, + managedGroups, + // Users banned by viewer + bannedByViewer, + // Users who banned viewer + viewerBannedBy, + ] = await Promise.all([ + this.getGroupsWithDisabledBans(viewerId), + this.getManagedGroupIds(viewerId), this.database.getAll( `select u.id, u.uid from bans b join users u on banned_user_id = u.uid where b.user_id = :viewerId`, { viewerId }, ), - this.database.getCol( - `select f.id from - feeds f join groups_without_bans g on f.user_id = g.group_id and f.name = 'Posts' - where g.user_id = :viewerId`, + this.database.getAll( + `select u.id, u.uid from + bans b join users u on user_id = u.uid + where b.banned_user_id = :viewerId`, { viewerId }, ), ]); - return (actionsTable, postsTable = 'p', useIntBanIds = false) => - orJoin([ - sqlNotIn( + const managedGroupsWithDisabledBans = intersection(managedGroups, groupsWithDisabledBans); + + const [feedsOfGroupsWithDisabledBans, feedsOfManagedGroupsWithDisabledBans] = + await Promise.all([ + this.getUsersNamedFeedsIntIds(groupsWithDisabledBans, ['Posts']), + this.getUsersNamedFeedsIntIds(managedGroupsWithDisabledBans, ['Posts']), + ]); + + return (actionsTable, postsTable = 'p', useIntBanIds = false) => [ + // 1. Actions that are invisible because the author of the action is banned by the viewer + andJoin([ + // The author of action is banned by the viewer + sqlIn( `${actionsTable}.user_id`, bannedByViewer.map((r) => r[useIntBanIds ? 'id' : 'uid']), ), - sqlIntarrayIn(`${postsTable}.destination_feed_ids`, feedsWithDisabledBans), - ]); + // And the post is not in some group with bans disabled + sqlNot( + sqlIntarrayIn(`${postsTable}.destination_feed_ids`, feedsOfGroupsWithDisabledBans), + ), + ]), + // 2. Actions that are invisible because the viewer is banned by the author of the action + andJoin([ + // The viewer is banned by the author of the action + sqlIn( + `${actionsTable}.user_id`, + viewerBannedBy.map((r) => r[useIntBanIds ? 'id' : 'uid']), + ), + // And the post is not in some group, managed bi viewer, with bans disabled + sqlNot( + sqlIntarrayIn( + `${postsTable}.destination_feed_ids`, + feedsOfManagedGroupsWithDisabledBans, + ), + ), + ]), + ]; } async isPostVisibleForViewer(postId, viewerId = null) { @@ -126,10 +181,14 @@ const visibilityTrait = (superClass) => } async areCommentsBannedForViewerAssoc(commentIds, viewerId = null) { - const notBannedSQLFabric = await this.notBannedActionsSQLFabric(viewerId); + const bannedSQLsFabric = await this.bannedActionsSQLsFabric(viewerId); + const [bannedByViewerSQL, bannedByAuthorSQL] = bannedSQLsFabric('c'); const rows = await this.database.getAll( - `select c.uid, ${sqlNot(notBannedSQLFabric('c'))} as banned from - comments c + `select + c.uid, + ${bannedByViewerSQL} as banned_by_viewer, + ${bannedByAuthorSQL} as banned_by_author + from comments c join posts p on p.uid = c.post_id where c.uid = any(:commentIds) `, @@ -138,7 +197,13 @@ const visibilityTrait = (superClass) => const result = {}; for (const row of rows) { - result[row.uid] = row.banned; + if (row.banned_by_viewer) { + result[row.uid] = Comment.HIDDEN_AUTHOR_BANNED; + } else if (row.banned_by_author) { + result[row.uid] = Comment.HIDDEN_VIEWER_BANNED; + } else { + result[row.uid] = false; + } } return result; @@ -229,19 +294,35 @@ const visibilityTrait = (superClass) => ); const [ + // Users banned by comment author + bannedByAuthor, // Users who banned comment author authorBannedBy, usersDisabledBans, ] = await Promise.all([ + this.getUserBansIds(commentAuthor), this.getUserIdsWhoBannedUser(commentAuthor), this.getUsersWithDisabledBansInGroups(postGroups), ]); + // Users who choose to see banned posts in any of post group const allWhoDisabledBans = usersDisabledBans.map((r) => r.user_id); + // Users who are admins of any post group and choose to see banned posts in it + const adminsWhoDisabledBans = usersDisabledBans + .filter((r) => r.is_admin) + .map((r) => r.user_id); return List.intersection( postViewers, - List.inverse(List.difference(authorBannedBy, allWhoDisabledBans)), + // List.inverse(List.difference(authorBannedBy, allWhoDisabledBans)), + List.inverse( + List.union( + // All who banned comment author, except those who disabled bans + List.difference(authorBannedBy, allWhoDisabledBans), + // All banned by comment author, except ADMINS who disabled bans + List.difference(bannedByAuthor, adminsWhoDisabledBans), + ), + ), ); } diff --git a/config/test.js b/config/test.js index e947c4860..c602f4384 100644 --- a/config/test.js +++ b/config/test.js @@ -52,7 +52,10 @@ module.exports = { defaults: { // User does't want to view banned comments by default (for compatibility // with old tests) - hideCommentsOfTypes: [2 /* Comment.HIDDEN_BANNED */], + hideCommentsOfTypes: [ + 2, // Comment.HIDDEN_AUTHOR_BANNED + 4, // Comment.HIDDEN_VIEWER_BANNED + ], }, }, diff --git a/doc/visibility-rules.md b/doc/visibility-rules.md index ce7dadabd..a5d610710 100644 --- a/doc/visibility-rules.md +++ b/doc/visibility-rules.md @@ -38,13 +38,14 @@ Comments, Likes and Comment likes (hereinafter "actions") shares the same logic. Actions on the given post is not visible for viewer if the post is not visible. -Action is visible when: +Action is visible when (AND-joined): * The action author is not banned by viewer OR post is published to a group -where the viewer had disabled bans. Thus, all actions in groups with disabled -bans are visible. + where the viewer had disabled bans. +* Viewer is not banned by the action author OR post is published to a group where + the viewer *is admin* and had disabled bans. If the post is visible but the comment is not, the comment may appear as a stub -(with hideType = HIDDEN_BANNED). It depends on *hideCommentsOfTypes* field of +(with hideType = HIDDEN_AUTHOR_BANNED). It depends on *hideCommentsOfTypes* field of viewer properties. Handling the visibility of comments is a bit special (see the @@ -57,13 +58,14 @@ to comment, the middleware acts as follows: ### In code The action visibility rules calculates in the following places: -* app/support/DbAdapter/visibility.js, notBannedSQLFabric function. This makes - SQL filter fabric to select non-banned actions. +* app/support/DbAdapter/visibility.js, bannedActionsSQLsFabric and + notBannedActionsSQLFabric functions. This functions makes SQL filter fabrics + to select (non-)banned actions. * app/support/DbAdapter/visibility.js, getUsersWhoCanSeeComment function. This function returns list of users (IDs) who can see the given comment. -* app/support/DbAdapter/visibility.js, isCommentBannedForViewer function. This - function returns true if comment is banned (and should be hidden) for the - given viewer. +* app/support/DbAdapter/visibility.js, isCommentBannedForViewer and + areCommentsBannedForViewerAssoc functions. This functions checks if comment(s) + is/are banned (and should be hidden) for the given viewer. * app/pubsub-listener.js, broadcastMessage function checks access for actions. * app/controllers/middlewares/comment-access-required.js, the 'commentAccessRequired' middleware. \ No newline at end of file diff --git a/test/functional/comment_likes.js b/test/functional/comment_likes.js index fca3c1b35..6287a4235 100644 --- a/test/functional/comment_likes.js +++ b/test/functional/comment_likes.js @@ -723,14 +723,14 @@ describe('Comment likes', () => { res = await getCommentLikes(jupiterComment2.id, pluto); const responseJson = await res.json(); + // Pluto, being banned by Luna, shouldn't be able to see Luna's like expect(responseJson, 'to satisfy', { - likes: expect.it('to be an array').and('to be non-empty').and('to have length', 3), + likes: expect.it('to be an array').and('to be non-empty').and('to have length', 2), users: expect.it('to be an array').and('to have items satisfying', schema.user), }); expect(responseJson.likes[0].userId, 'to be', pluto.user.id); - expect(responseJson.likes[1].userId, 'to be', luna.user.id); - expect(responseJson.likes[2].userId, 'to be', mars.user.id); + expect(responseJson.likes[1].userId, 'to be', mars.user.id); }); }); }); diff --git a/test/functional/comments.js b/test/functional/comments.js index f362ef719..6cf14bdd0 100644 --- a/test/functional/comments.js +++ b/test/functional/comments.js @@ -442,8 +442,8 @@ describe('CommentsController', () => { __httpCode: 200, comments: { id: commentIds[1], - body: Comment.hiddenBody(Comment.HIDDEN_BANNED), - hideType: Comment.HIDDEN_BANNED, + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), + hideType: Comment.HIDDEN_AUTHOR_BANNED, createdBy: null, }, }); @@ -558,8 +558,8 @@ describe('CommentsController', () => { __httpCode: 200, comments: { id: commentIds[1], - body: Comment.hiddenBody(Comment.HIDDEN_BANNED), - hideType: Comment.HIDDEN_BANNED, + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), + hideType: Comment.HIDDEN_AUTHOR_BANNED, createdBy: null, }, }); diff --git a/test/functional/events.js b/test/functional/events.js index fad8f57f0..ee0b2ae27 100644 --- a/test/functional/events.js +++ b/test/functional/events.js @@ -1567,18 +1567,10 @@ describe('EventService', () => { await expectMentionEvents(lunaUserModel, []); }); - it('should create mention_in_comment event for mentioned user banned by comment author', async () => { + it('should not create mention_in_comment event for mentioned user banned by comment author', async () => { await banUser(jupiter, mars); await createCommentAsync(jupiter, post.id, 'Mentioning @mars'); - await expectMentionEvents(marsUserModel, [ - { - user_id: marsUserModel.intId, - event_type: 'mention_in_comment', - created_by_user_id: jupiterUserModel.intId, - target_user_id: marsUserModel.intId, - post_author_id: lunaUserModel.intId, - }, - ]); + await expectMentionEvents(marsUserModel, []); await expectMentionEvents(lunaUserModel, []); await expectMentionEvents(jupiterUserModel, []); }); diff --git a/test/functional/groups-without-bans.js b/test/functional/groups-without-bans.js index 25989430b..327bf2757 100644 --- a/test/functional/groups-without-bans.js +++ b/test/functional/groups-without-bans.js @@ -415,22 +415,15 @@ describe('Groups without bans', () => { ]); }); - it(`should also see Venus comment in post to Celestials because of bans asymmetry`, async () => { + it(`should not see Venus comment in post to Celestials because of bans symmetry`, async () => { const resp = await shouldSeePost(postFromMarsToCelestials, luna); - expect(resp.comments, 'to satisfy', [ - { createdBy: venus.user.id }, - { createdBy: mars.user.id }, - ]); + expect(resp.comments, 'to satisfy', [{ createdBy: mars.user.id }]); }); - it(`should find all posts with 'in-comment:venus' because of bans asymmetry`, () => + it(`should not find solo Celestials posts with 'in-comment:venus' because of bans symmetry`, () => shouldFindPosts( 'in-comment:venus', - [ - postFromMarsToSelenites, - postFromMarsToSelenitesAndCelestials, - postFromMarsToCelestials, - ], + [postFromMarsToSelenites, postFromMarsToSelenitesAndCelestials], luna, )); }); diff --git a/test/functional/hidden-comments.js b/test/functional/hidden-comments.js index be34d693b..8d0197e59 100644 --- a/test/functional/hidden-comments.js +++ b/test/functional/hidden-comments.js @@ -54,7 +54,7 @@ describe('Hidden comments', () => { expect(reply.comments, 'to have length', 2); const venusComment = reply.comments.find((c) => c.id === reply.posts.comments[0]); const lunaComment = reply.comments.find((c) => c.id === reply.posts.comments[1]); - expect(venusComment, 'to satisfy', { hideType: Comment.HIDDEN_BANNED }); + expect(venusComment, 'to satisfy', { hideType: Comment.HIDDEN_AUTHOR_BANNED }); expect(lunaComment, 'to satisfy', { hideType: Comment.VISIBLE }); }); @@ -64,7 +64,7 @@ describe('Hidden comments', () => { expect(reply.comments, 'to have length', 2); const venusComment = reply.comments.find((c) => c.id === postInReply.comments[0]); const lunaComment = reply.comments.find((c) => c.id === postInReply.comments[1]); - expect(venusComment, 'to satisfy', { hideType: Comment.HIDDEN_BANNED }); + expect(venusComment, 'to satisfy', { hideType: Comment.HIDDEN_AUTHOR_BANNED }); expect(lunaComment, 'to satisfy', { hideType: Comment.VISIBLE }); }); @@ -85,8 +85,8 @@ describe('Hidden comments', () => { await expect(test, 'when fulfilled', 'to satisfy', { comments: { createdBy: null, - hideType: Comment.HIDDEN_BANNED, - body: Comment.hiddenBody(Comment.HIDDEN_BANNED), + hideType: Comment.HIDDEN_AUTHOR_BANNED, + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), }, }); }); @@ -96,7 +96,7 @@ describe('Hidden comments', () => { describe("Luna doesn't want to see comments from banned users", () => { beforeEach(async () => { await updateUserAsync(luna, { - preferences: { hideCommentsOfTypes: [Comment.HIDDEN_BANNED] }, + preferences: { hideCommentsOfTypes: [Comment.HIDDEN_AUTHOR_BANNED] }, }); }); @@ -123,7 +123,7 @@ describe('Hidden comments', () => { const reply1 = await fetchPost(post.id, mars); expect(reply1.comments, 'to have length', 2); const venusComment = reply1.comments.find((c) => c.id === reply1.posts.comments[0]); - expect(venusComment, 'to satisfy', { hideType: Comment.HIDDEN_BANNED }); + expect(venusComment, 'to satisfy', { hideType: Comment.HIDDEN_AUTHOR_BANNED }); const delReply = await removeCommentAsync(mars, venusComment.id); delReply.status.should.eql(200); diff --git a/test/functional/realtime.js b/test/functional/realtime.js index 0589cbac9..aeac9db85 100644 --- a/test/functional/realtime.js +++ b/test/functional/realtime.js @@ -425,7 +425,7 @@ describe('Realtime (Socket.io)', () => { ); }); - it("Jupiter gets notifications about comment likes to Mars' comment", async () => { + it("Jupiter doesn't get notifications about comment likes to Mars' comment", async () => { const { context: { commentLikeRealtimeMsg: msg }, } = await expect( @@ -434,14 +434,10 @@ describe('Realtime (Socket.io)', () => { lunaPost.id, 'with comment having id', marsComment.id, - 'to get comment_like:new event from', + 'not to get comment_like:new event from', lunaContext, ); - expect( - msg, - 'to satisfy', - commentHavingNLikesExpectation(1, false, lunaContext.user.id), - ); + expect(msg, 'to be', null); }); }); }); @@ -694,7 +690,7 @@ describe('Realtime (Socket.io)', () => { ); }); - it("Jupiter gets notifications about comment likes to Mars' comment", async () => { + it("Jupiter doesn't get notifications about comment likes to Mars' comment", async () => { const { context: { commentLikeRealtimeMsg: msg }, } = await expect( @@ -703,14 +699,10 @@ describe('Realtime (Socket.io)', () => { lunaTimeline, 'with comment having id', marsComment.id, - 'to get comment_like:new event from', + 'not to get comment_like:new event from', lunaContext, ); - expect( - msg, - 'to satisfy', - commentHavingNLikesExpectation(1, false, lunaContext.user.id), - ); + expect(msg, 'to be', null); }); }); }); @@ -1136,7 +1128,7 @@ describe('Realtime (Socket.io)', () => { ); }); - it("Jupiter gets notifications about comment likes to Mars' comment", async () => { + it("Jupiter doesn't notifications about comment likes to Mars' comment", async () => { const { context: { commentLikeRealtimeMsg: msg }, } = await expect( @@ -1145,14 +1137,10 @@ describe('Realtime (Socket.io)', () => { lunaCommentsFeed, 'with comment having id', marsComment.id, - 'to get comment_like:new event from', + 'not to get comment_like:new event from', lunaContext, ); - expect( - msg, - 'to satisfy', - commentHavingNLikesExpectation(1, false, lunaContext.user.id), - ); + expect(msg, 'to be', null); }); }); }); diff --git a/test/functional/symmetric-bans.js b/test/functional/symmetric-bans.js new file mode 100644 index 000000000..727b8c9e0 --- /dev/null +++ b/test/functional/symmetric-bans.js @@ -0,0 +1,308 @@ +/* eslint-env node, mocha */ +/* global $database */ + +import expect from 'unexpected'; + +import { dbAdapter, Comment, PubSub } from '../../app/models'; +import cleanDB from '../dbCleaner'; +import { getSingleton } from '../../app/app'; +import { PubSubAdapter, eventNames } from '../../app/support/PubSubAdapter'; + +import { + banUser, + createAndReturnPost, + createTestUsers, + performJSONRequest, + authHeaders, + updateUserAsync, + like, + likeComment, +} from './functional_test_helper'; +import Session from './realtime-session'; + +describe('Symmetric bans', () => { + beforeEach(() => cleanDB(dbAdapter.database)); + describe('Luna bans Mars, Venus wrote post', () => { + let luna; + let mars; + let venus; + let post; + beforeEach(async () => { + [luna, mars, venus] = await createTestUsers(['luna', 'mars', 'venus']); + post = await createAndReturnPost(venus, 'Post body'); + await banUser(luna, mars); + }); + + describe('Comments visibility', () => { + describe('Luna and Mars both commented the Venus post', () => { + beforeEach(async () => { + await createComment(luna, post.id, 'Comment from Luna'); + await createComment(mars, post.id, 'Comment from Mars'); + }); + + it('should show all comments to Venus', async () => { + const resp = await fetchPost(post.id, venus); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Luna', createdBy: luna.user.id }, + { body: 'Comment from Mars', createdBy: mars.user.id }, + ]); + }); + + it(`should not show Mars' comments to Luna`, async () => { + const resp = await fetchPost(post.id, luna); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Luna', createdBy: luna.user.id }, + ]); + }); + + it(`should not show Luna's comments to Mars`, async () => { + const resp = await fetchPost(post.id, mars); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Mars', createdBy: mars.user.id }, + ]); + }); + + describe('Luna and Mars wants to see all hidden comments', () => { + beforeEach(() => + Promise.all([ + updateUserAsync(luna, { preferences: { hideCommentsOfTypes: [] } }), + updateUserAsync(mars, { preferences: { hideCommentsOfTypes: [] } }), + ]), + ); + + it(`should show Mars' comments to Luna as placeholder`, async () => { + const resp = await fetchPost(post.id, luna); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Luna', createdBy: luna.user.id }, + { + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), + createdBy: null, + hideType: Comment.HIDDEN_AUTHOR_BANNED, + }, + ]); + }); + + it(`should show Luna's comments to Mars with _hideType`, async () => { + const resp = await fetchPost(post.id, mars); + expect(resp.comments, 'to satisfy', [ + { + body: 'Comment from Luna', + createdBy: luna.user.id, + hideType: Comment.VISIBLE, + _hideType: Comment.HIDDEN_VIEWER_BANNED, + }, + { body: 'Comment from Mars', createdBy: mars.user.id }, + ]); + }); + }); + + describe('Luna and Mars wants to see all comments except of HIDDEN_AUTHOR_BANNED', () => { + beforeEach(() => + Promise.all([ + updateUserAsync(luna, { + preferences: { hideCommentsOfTypes: [Comment.HIDDEN_AUTHOR_BANNED] }, + }), + updateUserAsync(mars, { + preferences: { hideCommentsOfTypes: [Comment.HIDDEN_AUTHOR_BANNED] }, + }), + ]), + ); + + it(`should not show Mars' comments to Luna`, async () => { + const resp = await fetchPost(post.id, luna); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Luna', createdBy: luna.user.id }, + ]); + }); + + it(`should show Luna's comments to Mars with _hideType`, async () => { + const resp = await fetchPost(post.id, mars); + expect(resp.comments, 'to satisfy', [ + { + body: 'Comment from Luna', + createdBy: luna.user.id, + hideType: Comment.VISIBLE, + _hideType: Comment.HIDDEN_VIEWER_BANNED, + }, + { body: 'Comment from Mars', createdBy: mars.user.id }, + ]); + }); + }); + + describe('Luna and Mars wants to see all comments except of HIDDEN_VIEWER_BANNED', () => { + beforeEach(() => + Promise.all([ + updateUserAsync(luna, { + preferences: { hideCommentsOfTypes: [Comment.HIDDEN_VIEWER_BANNED] }, + }), + updateUserAsync(mars, { + preferences: { hideCommentsOfTypes: [Comment.HIDDEN_VIEWER_BANNED] }, + }), + ]), + ); + + it(`should show Mars' comments to Luna as placeholder`, async () => { + const resp = await fetchPost(post.id, luna); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Luna', createdBy: luna.user.id }, + { + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), + createdBy: null, + hideType: Comment.HIDDEN_AUTHOR_BANNED, + }, + ]); + }); + + it(`should not show Luna's comments to Mars`, async () => { + const resp = await fetchPost(post.id, mars); + expect(resp.comments, 'to satisfy', [ + { body: 'Comment from Mars', createdBy: mars.user.id }, + ]); + }); + }); + }); + }); + + describe('Likes visibility', () => { + describe('Luna and Mars both liked the Venus post', () => { + beforeEach(async () => { + await like(post.id, luna.authToken); + await like(post.id, mars.authToken); + }); + + it('should show both likes to Venus', async () => { + const resp = await fetchPost(post.id, venus); + expect(resp.posts.likes, 'to equal', [mars.user.id, luna.user.id]); + }); + + it(`should show only Luna's like to Luna`, async () => { + const resp = await fetchPost(post.id, luna); + expect(resp.posts.likes, 'to equal', [luna.user.id]); + }); + + it(`should show only Mars' like to Mars`, async () => { + const resp = await fetchPost(post.id, mars); + expect(resp.posts.likes, 'to equal', [mars.user.id]); + }); + }); + }); + + describe('Comment likes visibility', () => { + describe('Luna and Mars both liked the Venus comment', () => { + let comment; + beforeEach(async () => { + ({ comments: comment } = await createComment(venus, post.id, 'Venus comment')); + await likeComment(comment.id, luna); + await likeComment(comment.id, mars); + }); + + it('should show both comment likes to Venus', async () => { + const resp = await fetchPost(post.id, venus); + expect(resp.comments, 'to satisfy', [{ likes: 2, hasOwnLike: false }]); + + const { likes } = await getCommentLikes(comment.id, venus); + expect(likes, 'to satisfy', [{ userId: mars.user.id }, { userId: luna.user.id }]); + }); + + it(`should show only Luna's comment like to Luna`, async () => { + const resp = await fetchPost(post.id, luna); + expect(resp.comments, 'to satisfy', [{ likes: 1, hasOwnLike: true }]); + + const { likes } = await getCommentLikes(comment.id, luna); + expect(likes, 'to satisfy', [{ userId: luna.user.id }]); + }); + + it(`should show only Mars' comment like to Mars`, async () => { + const resp = await fetchPost(post.id, mars); + expect(resp.comments, 'to satisfy', [{ likes: 1, hasOwnLike: true }]); + + const { likes } = await getCommentLikes(comment.id, mars); + expect(likes, 'to satisfy', [{ userId: mars.user.id }]); + }); + }); + }); + + describe('Realtime', () => { + let port; + before(async () => { + const app = await getSingleton(); + port = process.env.PEPYATKA_SERVER_PORT || app.context.config.port; + const pubsubAdapter = new PubSubAdapter($database); + PubSub.setPublisher(pubsubAdapter); + }); + + let lunaSession, marsSession; + beforeEach(async () => { + [lunaSession, marsSession] = await Promise.all([ + Session.create(port, 'Luna session'), + Session.create(port, 'Mars session'), + ]); + + await Promise.all([ + lunaSession.sendAsync('auth', { authToken: luna.authToken }), + marsSession.sendAsync('auth', { authToken: mars.authToken }), + ]); + + await Promise.all([ + lunaSession.sendAsync('subscribe', { post: [post.id] }), + marsSession.sendAsync('subscribe', { post: [post.id] }), + ]); + }); + + afterEach(() => [lunaSession, marsSession].forEach((s) => s.disconnect())); + + describe('Comment creation (Luna and Mars wants to see all hidden comments)', () => { + beforeEach(() => + Promise.all([ + updateUserAsync(luna, { preferences: { hideCommentsOfTypes: [] } }), + updateUserAsync(mars, { preferences: { hideCommentsOfTypes: [] } }), + ]), + ); + + it(`should deliver "${eventNames.COMMENT_CREATED}" to Luna when Mars creates comment`, async () => { + const test = lunaSession.receiveWhile(eventNames.COMMENT_CREATED, () => + createComment(mars, post.id, 'Comment from Mars'), + ); + await expect(test, 'when fulfilled', 'to satisfy', { + comments: { + hideType: Comment.HIDDEN_AUTHOR_BANNED, + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), + createdBy: null, + }, + }); + }); + + it(`should deliver "${eventNames.COMMENT_CREATED}" to Mars when Luna creates comment`, async () => { + const test = marsSession.receiveWhile(eventNames.COMMENT_CREATED, () => + createComment(luna, post.id, 'Comment from Luna'), + ); + await expect(test, 'when fulfilled', 'to satisfy', { + comments: { + hideType: Comment.VISIBLE, + _hideType: Comment.HIDDEN_VIEWER_BANNED, + body: 'Comment from Luna', + createdBy: luna.user.id, + }, + }); + }); + }); + }); + }); +}); + +function fetchPost(postId, userCtx) { + return performJSONRequest('GET', `/v2/posts/${postId}`, null, authHeaders(userCtx)); +} + +function createComment(userCtx, postId, body) { + return performJSONRequest( + 'POST', + `/v2/comments`, + { comment: { body, postId } }, + authHeaders(userCtx), + ); +} + +function getCommentLikes(commentId, userCtx) { + return performJSONRequest('GET', `/v2/comments/${commentId}/likes`, null, authHeaders(userCtx)); +} diff --git a/test/functional/usersV2.js b/test/functional/usersV2.js index 145ebd6d1..ed7672bc0 100644 --- a/test/functional/usersV2.js +++ b/test/functional/usersV2.js @@ -269,7 +269,7 @@ describe('UsersControllerV2', () => { }); it('should allow to update preferences with valid value', async () => { - const preferences = { hideCommentsOfTypes: [Comment.HIDDEN_BANNED] }; + const preferences = { hideCommentsOfTypes: [Comment.HIDDEN_AUTHOR_BANNED] }; const res = await updateUserAsync(luna, { preferences }); const data = await res.json(); expect(data, 'to satisfy', { users: { preferences } }); diff --git a/test/integration/controllers/comment-access-required.js b/test/integration/controllers/comment-access-required.js index 53efe6fd5..4b652fc67 100644 --- a/test/integration/controllers/comment-access-required.js +++ b/test/integration/controllers/comment-access-required.js @@ -11,9 +11,6 @@ import { ForbiddenException } from '../../../app/support/exceptions'; describe('commentAccessRequired', () => { beforeEach(() => cleanDB($pg_database)); - const handler = (ctx, mustBeVisible = true) => - commentAccessRequired({ mustBeVisible })(ctx, noop); - describe('Luna created post in Selenites group, Mars wrote comment', () => { let /** @type {User} */ luna, @@ -23,8 +20,9 @@ describe('commentAccessRequired', () => { venus, /** @type {Group} */ selenites; - let /** @type {Post} */ post, /** @type {Comment} */ comment; - let ctx; + let /** @type {Post} */ post, + /** @type {Comment} */ marsComment, + /** @type {Comment} */ venusComment; beforeEach(async () => { luna = new User({ username: 'Luna', password: 'password' }); @@ -41,35 +39,35 @@ describe('commentAccessRequired', () => { }); await post.create(); - comment = new Comment({ + marsComment = new Comment({ body: 'Comment body', userId: mars.id, postId: post.id, }); - await comment.create(); - - ctx = { - params: { commentId: comment.id }, - state: { user: null }, - }; + await marsComment.create(); + venusComment = new Comment({ + body: 'Comment body', + userId: venus.id, + postId: post.id, + }); + await venusComment.create(); }); it(`should show comment to anonymous`, async () => { - await expect(handler(ctx), 'to be fulfilled'); + const ctx = await checkCommentAccess(marsComment.id, null); expect(ctx.state, 'to satisfy', { post: { id: post.id, }, comment: { - id: comment.id, - body: comment.body, + id: marsComment.id, + body: marsComment.body, }, }); }); it(`should show comment to Venus`, async () => { - ctx.state.user = venus; - await expect(handler(ctx), 'to be fulfilled'); + await expect(checkCommentAccess(marsComment.id, venus), 'to be fulfilled'); }); // commentAccessRequired includes postAccessRequired, so we will check only @@ -78,22 +76,31 @@ describe('commentAccessRequired', () => { beforeEach(() => venus.ban(mars.username)); it(`should not show comment to Venus`, async () => { - ctx.state.user = venus; await expect( - handler(ctx), + checkCommentAccess(marsComment.id, venus), 'to be rejected with', new ForbiddenException('You have banned the author of this comment'), ); }); it(`should show comment with placeholder to Venus`, async () => { - ctx.state.user = venus; - await expect(handler(ctx, false), 'to be fulfilled'); + const ctx = await checkCommentAccess(marsComment.id, venus, false); + expect(ctx.state, 'to satisfy', { + comment: { + id: marsComment.id, + body: Comment.hiddenBody(Comment.HIDDEN_AUTHOR_BANNED), + hideType: Comment.HIDDEN_AUTHOR_BANNED, + }, + }); + }); + + it(`should show comment with placeholder to Mars`, async () => { + const ctx = await checkCommentAccess(venusComment.id, mars, false); expect(ctx.state, 'to satisfy', { comment: { - id: comment.id, - body: 'Hidden comment', - hideType: Comment.HIDDEN_BANNED, + id: venusComment.id, + body: Comment.hiddenBody(Comment.HIDDEN_VIEWER_BANNED), + hideType: Comment.HIDDEN_VIEWER_BANNED, }, }); }); @@ -102,10 +109,15 @@ describe('commentAccessRequired', () => { beforeEach(() => selenites.disableBansFor(venus.id)); it(`should show comment to Venus`, async () => { - ctx.state.user = venus; - await expect(handler(ctx), 'to be fulfilled'); + await expect(checkCommentAccess(marsComment.id, venus), 'to be fulfilled'); }); }); }); }); }); + +async function checkCommentAccess(commentId, user, mustBeVisible = true) { + const ctx = { params: { commentId }, state: { user } }; + await commentAccessRequired({ mustBeVisible })(ctx, noop); + return ctx; +} diff --git a/test/integration/support/events/backlinks.js b/test/integration/support/events/backlinks.js index 4bbe95f38..ef2c7f211 100644 --- a/test/integration/support/events/backlinks.js +++ b/test/integration/support/events/backlinks.js @@ -183,16 +183,20 @@ describe('EventService', () => { }); it('should not create backlink_in_comment event for mentioned user who banned post author', async () => { + // Mars banned Luna await mars.ban(luna.username); + // Luna shouldn't allow to see Mars' comment, so no event will be created for her comment = await createComment(luna, venusPost, `Mentioning ${marsPost.id}`); await expectBacklinkEvents(mars, []); await mars.unban(luna.username); }); - it('should create (!) backlink_in_comment event for banned mentioned user', async () => { + it('should not create backlink_in_comment event for banned mentioned user', async () => { + // Luna banned Mars await luna.ban(mars.username); + // Mars shouldn't allow to see Luna's comment, so no event will be created for him comment = await createComment(luna, venusPost, `Mentioning ${marsPost.id}`); - await expectBacklinkEvents(mars, [await backlinkInCommentEvent(comment, marsPost)]); + await expectBacklinkEvents(mars, []); await luna.unban(mars.username); }); @@ -451,10 +455,12 @@ describe('EventService', () => { await mars.unban(luna.username); }); - it('should create (!) backlink_in_comment event for banned mentioned user', async () => { + it('should not create backlink_in_comment event for banned mentioned user', async () => { + // Luna banned Mars await luna.ban(mars.username); + // Mars shouldn't allow to see Luna's comment, so no event will be created for him comment = await createComment(luna, venusPost, `Mentioning ${marsComment.id}`); - await expectBacklinkEvents(mars, [await backlinkInCommentEvent(comment, marsComment)]); + await expectBacklinkEvents(mars, []); await luna.unban(mars.username); });