From 90e1696f8669992004a3b3342d6b1dbe0f1472e1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Sep 2022 18:53:00 +0200 Subject: [PATCH 01/33] add group to post in schema --- backend/src/schema/types/type/Group.gql | 2 ++ backend/src/schema/types/type/Post.gql | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index d586f6b53f..ef94537fb8 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -38,6 +38,8 @@ type Group { categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") myRole: GroupMemberRole # if 'null' then the current user is no member + + posts: [Post] @relation(name: "IN", direction: "IN") } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 2f9221dd12..434b44772e 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -81,6 +81,7 @@ input _PostFilter { emotions_none: _PostEMOTEDFilter emotions_single: _PostEMOTEDFilter emotions_every: _PostEMOTEDFilter + group: ID } enum _PostOrdering { @@ -167,6 +168,8 @@ type Post { emotions: [EMOTED] emotionsCount: Int! @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") + + group: Group @relation(name: "IN", direction: "OUT") } input _PostInput { @@ -184,6 +187,7 @@ type Mutation { language: String categoryIds: [ID] contentExcerpt: String + groupId: ID ): Post UpdatePost( id: ID! From f3318135721693a48308b51ea0736d1c239789ea Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Sep 2022 19:08:54 +0200 Subject: [PATCH 02/33] create post in group --- backend/src/schema/resolvers/posts.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 97230715f0..8e97e0b87e 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -77,10 +77,11 @@ export default { }, Mutation: { CreatePost: async (_parent, params, context, _resolveInfo) => { - const { categoryIds } = params + const { categoryIds, groupId } = params const { image: imageInput } = params delete params.categoryIds delete params.image + delete params.groupId params.id = params.id || uuid() const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -91,6 +92,10 @@ export default { MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category)` : '' + const groupCypher = groupId + ? `WITH post MATCH (group:Group { id: $groupId }) + MERGE (post)-[:IN]-(group)` + : '' const createPostTransactionResponse = await transaction.run( ` CREATE (post:Post) @@ -103,9 +108,10 @@ export default { MATCH (author:User {id: $userId}) MERGE (post)<-[:WROTE]-(author) ${categoriesCypher} + ${groupCypher} RETURN post {.*} `, - { userId: context.user.id, params, categoryIds }, + { userId: context.user.id, params, categoryIds, groupId }, ) const [post] = createPostTransactionResponse.records.map((record) => record.get('post')) if (imageInput) { @@ -367,6 +373,7 @@ export default { author: '<-[:WROTE]-(related:User)', pinnedBy: '<-[:PINNED]-(related:User)', image: '-[:HERO_IMAGE]->(related:Image)', + group: '-[:IN]->(related:Group)', }, count: { commentsCount: From 0c32fd8023503606f33fd2150763cf182272bc16 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Sep 2022 20:20:55 +0200 Subject: [PATCH 03/33] add post access filter for all post queries --- backend/src/schema/resolvers/posts.js | 23 +++++++++++++++++++++++ backend/src/schema/types/type/Post.gql | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 8e97e0b87e..733ad57102 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -17,19 +17,42 @@ const maintainPinnedPosts = (params) => { return params } +const postAccessFilter = (params) => { + const groupFilter = { + group: { + OR: [{ groupType_in: 'public' }, { myRole_in: ['usual', 'admin', 'owner'] }], + }, + } + if (isEmpty(params.filter)) { + params.filter = { OR: [groupFilter, {}] } + } else { + if (isEmpty(params.filter.group)) { + params.filter = { OR: [groupFilter, { ...params.filter }] } + } else { + params.filter.group = { + AND: [{ ...groupFilter.group }, { ...params.filter.group }], + } + } + } + return params +} + export default { Query: { Post: async (object, params, context, resolveInfo) => { params = await filterForMutedUsers(params, context) params = await maintainPinnedPosts(params) + params = await postAccessFilter(params) return neo4jgraphql(object, params, context, resolveInfo) }, findPosts: async (object, params, context, resolveInfo) => { params = await filterForMutedUsers(params, context) + params = await postAccessFilter(params) return neo4jgraphql(object, params, context, resolveInfo) }, profilePagePosts: async (object, params, context, resolveInfo) => { params = await filterForMutedUsers(params, context) + params = await postAccessFilter(params) return neo4jgraphql(object, params, context, resolveInfo) }, PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 434b44772e..8ea8b5cdb0 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -81,7 +81,7 @@ input _PostFilter { emotions_none: _PostEMOTEDFilter emotions_single: _PostEMOTEDFilter emotions_every: _PostEMOTEDFilter - group: ID + group: _GroupFilter } enum _PostOrdering { From 5e30bfe2ed5bf8200bb347f14aae104735dcede1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Sep 2022 20:30:00 +0200 Subject: [PATCH 04/33] add direction of Post in Group --- backend/src/schema/resolvers/posts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 733ad57102..74eca02bd0 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -117,7 +117,7 @@ export default { : '' const groupCypher = groupId ? `WITH post MATCH (group:Group { id: $groupId }) - MERGE (post)-[:IN]-(group)` + MERGE (post)-[:IN]->(group)` : '' const createPostTransactionResponse = await transaction.run( ` From 4d480d3130abc443340417d3497b4c6ef7a07789 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 22 Sep 2022 08:16:06 +0200 Subject: [PATCH 05/33] add post query --- backend/src/db/graphql/posts.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/db/graphql/posts.js b/backend/src/db/graphql/posts.js index 237446d41b..83f6cfb1ae 100644 --- a/backend/src/db/graphql/posts.js +++ b/backend/src/db/graphql/posts.js @@ -3,14 +3,42 @@ import gql from 'graphql-tag' // ------ mutations export const createPostMutation = gql` - mutation ($id: ID, $title: String!, $slug: String, $content: String!, $categoryIds: [ID]!) { - CreatePost(id: $id, title: $title, slug: $slug, content: $content, categoryIds: $categoryIds) { + mutation ( + $id: ID + $title: String! + $slug: String + $content: String! + $categoryIds: [ID] + $groupId: ID + ) { + CreatePost( + id: $id + title: $title + slug: $slug + content: $content + categoryIds: $categoryIds + groupId: $groupId + ) { id slug + title + content } } ` // ------ queries +export const postQuery = () => { + return gql` + query Post($id: ID!) { + Post(id: $id) { + id + title + content + } + } + ` +} + // fill queries in here From e748fcc6233028fbc5525dd298570c620aa28bb3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 22 Sep 2022 08:16:44 +0200 Subject: [PATCH 06/33] add is member of group check to permission of create post --- .../src/middleware/permissionsMiddleware.js | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 8f3d6947ac..df2326db5c 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, deny, allow, or } from 'graphql-shield' +import { rule, shield, deny, allow, or, and } from 'graphql-shield' import { getNeode } from '../db/neo4j' import CONFIG from '../config' import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes' @@ -221,6 +221,34 @@ const isAllowedToLeaveGroup = rule({ } }) +const isMemberOfGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { groupId } = args + if (!groupId) return true + const userId = user.id + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (User {id: $userId})-[membership:MEMBER_OF]->(Group {id: $groupId}) + RETURN membership.role AS role + `, + { groupId, userId }, + ) + return transactionResponse.records.map((record) => record.get('role'))[0] + }) + try { + const role = await readTxPromise + return ['usual', 'admin', 'owner'].includes(role) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -316,7 +344,7 @@ export default shield( JoinGroup: isAllowedToJoinGroup, LeaveGroup: isAllowedToLeaveGroup, ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, - CreatePost: isAuthenticated, + CreatePost: and(isAuthenticated, isMemberOfGroup), UpdatePost: isAuthor, DeletePost: isAuthor, fileReport: isAuthenticated, From a9cd661e63181dc5ea207b2e1e656f720e0b10d0 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 22 Sep 2022 08:17:09 +0200 Subject: [PATCH 07/33] add tets for posts in groups --- backend/src/schema/resolvers/groups.js | 1 + backend/src/schema/resolvers/posts.js | 14 +- .../schema/resolvers/postsInGroups.spec.js | 465 ++++++++++++++++++ backend/src/schema/types/type/Group.gql | 8 + 4 files changed, 483 insertions(+), 5 deletions(-) create mode 100644 backend/src/schema/resolvers/postsInGroups.spec.js diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index babef1d519..8d750284d9 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -302,6 +302,7 @@ export default { ...Resolver('Group', { hasMany: { categories: '-[:CATEGORIZED]->(related:Category)', + posts: '<-[:IN]-(related:Post)', }, hasOne: { avatar: '-[:AVATAR_IMAGE]->(related:Image)', diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 74eca02bd0..5bdda96abc 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -17,23 +17,27 @@ const maintainPinnedPosts = (params) => { return params } -const postAccessFilter = (params) => { +const postAccessFilter = (params, user) => { + const { id } = user const groupFilter = { group: { - OR: [{ groupType_in: 'public' }, { myRole_in: ['usual', 'admin', 'owner'] }], + OR: [{ groupType_in: 'public' }, { memberIds_includes: id }], }, } if (isEmpty(params.filter)) { - params.filter = { OR: [groupFilter, {}] } + params.filter = groupFilter } else { if (isEmpty(params.filter.group)) { - params.filter = { OR: [groupFilter, { ...params.filter }] } + // console.log(params.filter) + params.filter = { AND: [groupFilter, { ...params.filter }] } + // console.log(params.filter) } else { params.filter.group = { AND: [{ ...groupFilter.group }, { ...params.filter.group }], } } } + // console.log(params.filter.group) return params } @@ -42,7 +46,7 @@ export default { Post: async (object, params, context, resolveInfo) => { params = await filterForMutedUsers(params, context) params = await maintainPinnedPosts(params) - params = await postAccessFilter(params) + params = await postAccessFilter(params, context.user) return neo4jgraphql(object, params, context, resolveInfo) }, findPosts: async (object, params, context, resolveInfo) => { diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js new file mode 100644 index 0000000000..51248a0247 --- /dev/null +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -0,0 +1,465 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory, { cleanDatabase } from '../../db/factories' +import { getNeode, getDriver } from '../../db/neo4j' +import createServer from '../../server' +import { createGroupMutation, changeGroupMemberRoleMutation } from '../../db/graphql/groups' +import { createPostMutation, postQuery } from '../../db/graphql/posts' +// eslint-disable-next-line no-unused-vars +import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' +import CONFIG from '../../config' + +CONFIG.CATEGORIES_ACTIVE = false + +jest.mock('../../constants/groups', () => { + return { + __esModule: true, + DESCRIPTION_WITHOUT_HTML_LENGTH_MIN: 5, + } +}) + +const driver = getDriver() +const neode = getNeode() + +let query +let mutate +let anyUser +let allGroupsUser +let pendingUser +let publicUser +let closedUser +let hiddenUser +let authenticatedUser + +beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) + +afterAll(async () => { + // await cleanDatabase() +}) + +describe('Posts in Groups', () => { + beforeAll(async () => { + anyUser = await Factory.build('user', { + id: 'any-user', + name: 'Any User', + about: 'I am just an ordinary user and do not belong to any group.', + }) + + allGroupsUser = await Factory.build('user', { + id: 'all-groups-user', + name: 'All Groups User', + about: 'I am a member of all groups.', + }) + pendingUser = await Factory.build('user', { + id: 'pending-user', + name: 'Pending User', + about: 'I am a pending member of all groups.', + }) + publicUser = await Factory.build('user', { + id: 'public-user', + name: 'Public User', + about: 'I am the owner of the public group.', + }) + + closedUser = await Factory.build('user', { + id: 'closed-user', + name: 'Private User', + about: 'I am the owner of the closed group.', + }) + + hiddenUser = await Factory.build('user', { + id: 'hidden-user', + name: 'Secret User', + about: 'I am the owner of the hidden group.', + }) + + authenticatedUser = await publicUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'public-group', + name: 'The Public Group', + about: 'The public group!', + description: 'Anyone can see the posts of this group.', + groupType: 'public', + actionRadius: 'regional', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'public-group', + userId: 'pending-user', + roleInGroup: 'pending', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'public-group', + userId: 'all-groups-user', + roleInGroup: 'usual', + }, + }) + authenticatedUser = await closedUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'closed-group', + name: 'The Closed Group', + about: 'The closed group!', + description: 'Only members of this group can see the posts of this group.', + groupType: 'closed', + actionRadius: 'regional', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'closed-group', + userId: 'pending-user', + roleInGroup: 'pending', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'closed-group', + userId: 'all-groups-user', + roleInGroup: 'usual', + }, + }) + authenticatedUser = await hiddenUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'hidden-group', + name: 'The Hidden Group', + about: 'The hidden group!', + description: 'Only members of this group can see the posts of this group.', + groupType: 'hidden', + actionRadius: 'regional', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'hidden-group', + userId: 'pending-user', + roleInGroup: 'pending', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'hidden-group', + userId: 'all-groups-user', + roleInGroup: 'usual', + }, + }) + }) + + describe('creating posts in groups', () => { + describe('without membership of group', () => { + beforeEach(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('throws an error for public groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'p2', + title: 'A post to a pubic group', + content: 'I am posting into a public group without being a member of the group', + groupId: 'public-group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for closed groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'p2', + title: 'A post to a closed group', + content: 'I am posting into a closed group without being a member of the group', + groupId: 'closed-group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for hidden groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'p2', + title: 'A post to a closed group', + content: 'I am posting into a hidden group without being a member of the group', + groupId: 'hidden-group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + }) + + describe('as a pending member of group', () => { + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('throws an error for public groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'p2', + title: 'A post to a pubic group', + content: 'I am posting into a public group with a pending membership', + groupId: 'public-group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for closed groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'p2', + title: 'A post to a closed group', + content: 'I am posting into a closed group with a pending membership', + groupId: 'closed-group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for hidden groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'p2', + title: 'A post to a closed group', + content: 'I am posting into a hidden group with a pending membership', + groupId: 'hidden-group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + }) + + describe('as a member of group', () => { + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('creates a post for public groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + groupId: 'public-group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreatePost: { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + }, + errors: undefined, + }) + }) + + it('creates a post for closed groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + groupId: 'closed-group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreatePost: { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + }, + errors: undefined, + }) + }) + + it('creates a post for hidden groups', async () => { + await expect( + mutate({ + mutation: createPostMutation, + variables: { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + groupId: 'hidden-group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreatePost: { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + }, + errors: undefined, + }) + }) + }) + }) + + describe('visibility of posts', () => { + describe('query post by ID', () => { + describe('without membership of group', () => { + beforeEach(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('shows a post of the public group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-public-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + + it('does not show a post of a closed group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-closed-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + + it('does not show a post of a hidden group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-hidden-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + }) + + describe('as member of group', () => { + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('shows post of the public group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-public-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + + it('shows post of a closed group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-closed-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + + it('shows post of a hidden group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-hidden-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index ef94537fb8..bab19307ee 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -40,6 +40,13 @@ type Group { myRole: GroupMemberRole # if 'null' then the current user is no member posts: [Post] @relation(name: "IN", direction: "IN") + memberIds: [String] @cypher( + statement: """ + MATCH (this)<-[membership:MEMBER_OF]-(member:User) + WHERE membership.role IN ['usual', 'admin', 'owner'] + RETURN collect(member.id) + """ + ) } @@ -53,6 +60,7 @@ input _GroupFilter { groupType_in: [GroupType!] actionRadius_in: [GroupActionRadius!] myRole_in: [GroupMemberRole!] + memberIds_includes: String id: ID id_not: ID id_in: [ID!] From 1870efc3d0c3d316b3db03cf023752d09a8e3613 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 3 Oct 2022 22:36:46 +0200 Subject: [PATCH 08/33] add CANNOT_SEE relation to posts in groups --- backend/src/schema/resolvers/posts.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 5bdda96abc..65f7a1b336 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -112,6 +112,29 @@ export default { params.id = params.id || uuid() const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { + let groupCypher = '' + if (groupId) { + groupCypher = ` + WITH post MATCH (group:Group { id: $groupId }) + MERGE (post)-[:IN]->(group)` + const groupTypeResponse = await transaction.run( + ` + MATCH (group:Group { id: $groupId }) RETURN group.groupType AS groupType`, + { groupId }, + ) + const [groupType] = groupTypeResponse.records.map((record) => record.get('groupType')) + if (groupType !== 'public') + groupCypher += ` + WITH post, group + MATCH (user:User)-[membership:MEMBER_OF]->(group) + WHERE group.groupType IN ['closed', 'hidden'] + AND membership.role IN ['usual', 'admin', 'owner'] + WITH post, collect(user.id) AS userIds + OPTIONAL MATCH path =(blocked:User) WHERE NOT blocked.id IN userIds + FOREACH (user IN nodes(path) | + MERGE (user)-[:CANNOT_SEE]->(post) + )` + } const categoriesCypher = CONFIG.CATEGORIES_ACTIVE && categoryIds ? `WITH post @@ -119,10 +142,6 @@ export default { MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category)` : '' - const groupCypher = groupId - ? `WITH post MATCH (group:Group { id: $groupId }) - MERGE (post)-[:IN]->(group)` - : '' const createPostTransactionResponse = await transaction.run( ` CREATE (post:Post) From 5f4d7c2bf31fe70f135bc9c9dbed4607e0653ff1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 3 Oct 2022 22:40:54 +0200 Subject: [PATCH 09/33] add notVisibleFor statement --- backend/src/schema/types/type/Post.gql | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 8ea8b5cdb0..dbfa0676d1 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -170,6 +170,7 @@ type Post { @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") group: Group @relation(name: "IN", direction: "OUT") + notVisibleFor: [User]! @cypher("MATCH (this)<-[:CANNOT_SEE]-(user:User) RETURN user") } input _PostInput { From abcfe259a09d51b26076ce0252807db04119d447 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 3 Oct 2022 22:41:25 +0200 Subject: [PATCH 10/33] query and mutations are functions --- backend/src/db/graphql/posts.js | 46 +++++++++++++++++---------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/backend/src/db/graphql/posts.js b/backend/src/db/graphql/posts.js index 83f6cfb1ae..bb7402a9f5 100644 --- a/backend/src/db/graphql/posts.js +++ b/backend/src/db/graphql/posts.js @@ -2,30 +2,32 @@ import gql from 'graphql-tag' // ------ mutations -export const createPostMutation = gql` - mutation ( - $id: ID - $title: String! - $slug: String - $content: String! - $categoryIds: [ID] - $groupId: ID - ) { - CreatePost( - id: $id - title: $title - slug: $slug - content: $content - categoryIds: $categoryIds - groupId: $groupId +export const createPostMutation = () => { + return gql` + mutation ( + $id: ID + $title: String! + $slug: String + $content: String! + $categoryIds: [ID] + $groupId: ID ) { - id - slug - title - content + CreatePost( + id: $id + title: $title + slug: $slug + content: $content + categoryIds: $categoryIds + groupId: $groupId + ) { + id + slug + title + content + } } - } -` + ` +} // ------ queries From 4a63e1449e58b33a542895e6e6a93d524410200d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 3 Oct 2022 22:42:08 +0200 Subject: [PATCH 11/33] calls to query and mutate are functions --- .../schema/resolvers/postsInGroups.spec.js | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 51248a0247..9760df4e9f 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -88,7 +88,7 @@ describe('Posts in Groups', () => { authenticatedUser = await publicUser.toJson() await mutate({ - mutation: createGroupMutation, + mutation: createGroupMutation(), variables: { id: 'public-group', name: 'The Public Group', @@ -99,7 +99,7 @@ describe('Posts in Groups', () => { }, }) await mutate({ - mutation: changeGroupMemberRoleMutation, + mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'public-group', userId: 'pending-user', @@ -107,7 +107,7 @@ describe('Posts in Groups', () => { }, }) await mutate({ - mutation: changeGroupMemberRoleMutation, + mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'public-group', userId: 'all-groups-user', @@ -116,7 +116,7 @@ describe('Posts in Groups', () => { }) authenticatedUser = await closedUser.toJson() await mutate({ - mutation: createGroupMutation, + mutation: createGroupMutation(), variables: { id: 'closed-group', name: 'The Closed Group', @@ -127,7 +127,7 @@ describe('Posts in Groups', () => { }, }) await mutate({ - mutation: changeGroupMemberRoleMutation, + mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'closed-group', userId: 'pending-user', @@ -135,7 +135,7 @@ describe('Posts in Groups', () => { }, }) await mutate({ - mutation: changeGroupMemberRoleMutation, + mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'closed-group', userId: 'all-groups-user', @@ -144,7 +144,7 @@ describe('Posts in Groups', () => { }) authenticatedUser = await hiddenUser.toJson() await mutate({ - mutation: createGroupMutation, + mutation: createGroupMutation(), variables: { id: 'hidden-group', name: 'The Hidden Group', @@ -155,7 +155,7 @@ describe('Posts in Groups', () => { }, }) await mutate({ - mutation: changeGroupMemberRoleMutation, + mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', userId: 'pending-user', @@ -163,7 +163,7 @@ describe('Posts in Groups', () => { }, }) await mutate({ - mutation: changeGroupMemberRoleMutation, + mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', userId: 'all-groups-user', @@ -181,7 +181,7 @@ describe('Posts in Groups', () => { it('throws an error for public groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: 'A post to a pubic group', @@ -197,7 +197,7 @@ describe('Posts in Groups', () => { it('throws an error for closed groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: 'A post to a closed group', @@ -213,7 +213,7 @@ describe('Posts in Groups', () => { it('throws an error for hidden groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: 'A post to a closed group', @@ -235,7 +235,7 @@ describe('Posts in Groups', () => { it('throws an error for public groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: 'A post to a pubic group', @@ -251,7 +251,7 @@ describe('Posts in Groups', () => { it('throws an error for closed groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: 'A post to a closed group', @@ -267,7 +267,7 @@ describe('Posts in Groups', () => { it('throws an error for hidden groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: 'A post to a closed group', @@ -289,7 +289,7 @@ describe('Posts in Groups', () => { it('creates a post for public groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'post-to-public-group', title: 'A post to a public group', @@ -312,7 +312,7 @@ describe('Posts in Groups', () => { it('creates a post for closed groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'post-to-closed-group', title: 'A post to a closed group', @@ -335,7 +335,7 @@ describe('Posts in Groups', () => { it('creates a post for hidden groups', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'post-to-hidden-group', title: 'A post to a hidden group', From 4a12cdf9e24c282f5bddbad41dfc030111d1fa68 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 3 Oct 2022 22:43:46 +0200 Subject: [PATCH 12/33] remove unused memberIds statement --- backend/src/schema/types/type/Group.gql | 8 -------- 1 file changed, 8 deletions(-) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index 15b980345b..c4890fdce9 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -41,13 +41,6 @@ type Group { myRole: GroupMemberRole # if 'null' then the current user is no member posts: [Post] @relation(name: "IN", direction: "IN") - memberIds: [String] @cypher( - statement: """ - MATCH (this)<-[membership:MEMBER_OF]-(member:User) - WHERE membership.role IN ['usual', 'admin', 'owner'] - RETURN collect(member.id) - """ - ) } @@ -61,7 +54,6 @@ input _GroupFilter { groupType_in: [GroupType!] actionRadius_in: [GroupActionRadius!] myRole_in: [GroupMemberRole!] - memberIds_includes: String id: ID id_not: ID id_in: [ID!] From 069eefde17056c10a35f229efcc2501b3a4ba5c6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 01:59:32 +0200 Subject: [PATCH 13/33] add helper function to filter invisible posts --- .../resolvers/helpers/filterInvisiblePosts.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 backend/src/schema/resolvers/helpers/filterInvisiblePosts.js diff --git a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js new file mode 100644 index 0000000000..ba44850e85 --- /dev/null +++ b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js @@ -0,0 +1,44 @@ +import { mergeWith, isArray } from 'lodash' + +const getInvisiblePosts = async (context) => { + const session = context.driver.session() + const readTxResultPromise = await session.readTransaction(async (transaction) => { + let cypher = '' + if (context.user) { + cypher = ` + MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) + RETURN collect(post.id) AS invisiblePostIds` + } else { + cypher = ` + MATCH (post:Post)-[:IN]->(group:Group) + WHERE NOT group.groupType = 'public' + RETURN collect(post.id) AS invisiblePostIds` + } + const invisiblePostIdsResponse = await transaction.run(cypher, { userId: context.user.id }) + return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds')) + }) + try { + const [invisiblePostIds] = readTxResultPromise + return invisiblePostIds + } finally { + session.close() + } +} + +export const filterInvisiblePosts = async (params, context) => { + const invisiblePostIds = await getInvisiblePosts(context) + if (!invisiblePostIds.length) return params + + params.filter = mergeWith( + params.filter, + { + id_not_in: invisiblePostIds, + }, + (objValue, srcValue) => { + if (isArray(objValue)) { + return objValue.concat(srcValue) + } + }, + ) + return params +} From 833bd5ff77e0bc997e942fc5aa5cd5f9e66b4bd4 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 02:01:59 +0200 Subject: [PATCH 14/33] use invisible post filter --- backend/src/schema/resolvers/posts.js | 31 ++++----------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 65f7a1b336..aa861f2b9f 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -5,6 +5,7 @@ import { UserInputError } from 'apollo-server' import { mergeImage, deleteImage } from './images/images' import Resolver from './helpers/Resolver' import { filterForMutedUsers } from './helpers/filterForMutedUsers' +import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' import CONFIG from '../../config' const maintainPinnedPosts = (params) => { @@ -17,46 +18,22 @@ const maintainPinnedPosts = (params) => { return params } -const postAccessFilter = (params, user) => { - const { id } = user - const groupFilter = { - group: { - OR: [{ groupType_in: 'public' }, { memberIds_includes: id }], - }, - } - if (isEmpty(params.filter)) { - params.filter = groupFilter - } else { - if (isEmpty(params.filter.group)) { - // console.log(params.filter) - params.filter = { AND: [groupFilter, { ...params.filter }] } - // console.log(params.filter) - } else { - params.filter.group = { - AND: [{ ...groupFilter.group }, { ...params.filter.group }], - } - } - } - // console.log(params.filter.group) - return params -} - export default { Query: { Post: async (object, params, context, resolveInfo) => { + params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) params = await maintainPinnedPosts(params) - params = await postAccessFilter(params, context.user) return neo4jgraphql(object, params, context, resolveInfo) }, findPosts: async (object, params, context, resolveInfo) => { + params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) - params = await postAccessFilter(params) return neo4jgraphql(object, params, context, resolveInfo) }, profilePagePosts: async (object, params, context, resolveInfo) => { + params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) - params = await postAccessFilter(params) return neo4jgraphql(object, params, context, resolveInfo) }, PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { From 16a24bfddfcfa46670ebdddb1da1bb01ff6b5a5b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 02:02:59 +0200 Subject: [PATCH 15/33] remove not working filters from schema --- backend/src/schema/types/type/Post.gql | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index dbfa0676d1..8ea8b5cdb0 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -170,7 +170,6 @@ type Post { @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") group: Group @relation(name: "IN", direction: "OUT") - notVisibleFor: [User]! @cypher("MATCH (this)<-[:CANNOT_SEE]-(user:User) RETURN user") } input _PostInput { From 32d3c5e9047e262b40e201d5e47203892704c410 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 02:27:45 +0200 Subject: [PATCH 16/33] insure that user context is present as posts can be queried without authentication --- backend/src/middleware/userInteractions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/userInteractions.js b/backend/src/middleware/userInteractions.js index 553aefe785..62e8e47f71 100644 --- a/backend/src/middleware/userInteractions.js +++ b/backend/src/middleware/userInteractions.js @@ -31,7 +31,7 @@ const setPostCounter = async (postId, relation, context) => { } const userClickedPost = async (resolve, root, args, context, info) => { - if (args.id) { + if (args.id && context.user) { await setPostCounter(args.id, 'CLICKED', context) } return resolve(root, args, context, info) From 4331c7341479328256eb38211f53ad87c782d39c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 02:28:42 +0200 Subject: [PATCH 17/33] test post invisibility for unauthenticated users --- .../resolvers/helpers/filterInvisiblePosts.js | 7 ++- .../schema/resolvers/postsInGroups.spec.js | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js index ba44850e85..108f9c007c 100644 --- a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js +++ b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js @@ -4,7 +4,8 @@ const getInvisiblePosts = async (context) => { const session = context.driver.session() const readTxResultPromise = await session.readTransaction(async (transaction) => { let cypher = '' - if (context.user) { + const { user } = context + if (user && user.id) { cypher = ` MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) RETURN collect(post.id) AS invisiblePostIds` @@ -14,7 +15,9 @@ const getInvisiblePosts = async (context) => { WHERE NOT group.groupType = 'public' RETURN collect(post.id) AS invisiblePostIds` } - const invisiblePostIdsResponse = await transaction.run(cypher, { userId: context.user.id }) + const invisiblePostIdsResponse = await transaction.run(cypher, { + userId: user ? user.id : null, + }) return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds')) }) try { diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 9760df4e9f..87f3a13ead 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -359,6 +359,51 @@ describe('Posts in Groups', () => { describe('visibility of posts', () => { describe('query post by ID', () => { + describe('without authentication', () => { + beforeEach(async () => { + authenticatedUser = null + }) + + it('shows a post of the public group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-public-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + + it('does not show a post of a closed group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-closed-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + + it('does not show a post of a hidden group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-hidden-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + }) + describe('without membership of group', () => { beforeEach(async () => { authenticatedUser = await anyUser.toJson() From 77125e404f523eba2dd52687650d6ebe2f56d0e4 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 05:03:02 +0200 Subject: [PATCH 18/33] createPostMutation as function --- backend/src/db/seed.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index d693f9905e..242b3a856e 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -709,7 +709,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await Promise.all([ mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p2', title: `Nature Philosophy Yoga`, @@ -718,7 +718,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }), mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p7', title: 'This is post #7', @@ -727,7 +727,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }), mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p8', image: faker.image.unsplash.nature(), @@ -737,7 +737,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }), mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { id: 'p12', title: 'This is post #12', From 068d622c443f1fc0864e99a02de62dfdd26ed4e5 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 05:03:39 +0200 Subject: [PATCH 19/33] createPostMutation as function --- backend/src/middleware/slugifyMiddleware.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index c600d0e526..0b022fb534 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -366,7 +366,7 @@ describe('slugifyMiddleware', () => { it('generates a slug based on title', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables, }), ).resolves.toMatchObject({ @@ -382,7 +382,7 @@ describe('slugifyMiddleware', () => { it('generates a slug based on given slug', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { ...variables, slug: 'the-post', @@ -417,7 +417,7 @@ describe('slugifyMiddleware', () => { it('chooses another slug', async () => { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { ...variables, title: 'Pre-existing post', @@ -440,7 +440,7 @@ describe('slugifyMiddleware', () => { try { await expect( mutate({ - mutation: createPostMutation, + mutation: createPostMutation(), variables: { ...variables, title: 'Pre-existing post', From 275d196cb69143ed9544dfaaace9e4d42091f6b6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 05:04:42 +0200 Subject: [PATCH 20/33] further testting for post visibility --- .../schema/resolvers/postsInGroups.spec.js | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 87f3a13ead..ab070d7e46 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -7,6 +7,7 @@ import { createPostMutation, postQuery } from '../../db/graphql/posts' // eslint-disable-next-line no-unused-vars import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import CONFIG from '../../config' +import { signupVerificationMutation } from '../../db/graphql/authentications' CONFIG.CATEGORIES_ACTIVE = false @@ -29,6 +30,7 @@ let publicUser let closedUser let hiddenUser let authenticatedUser +let newUser beforeAll(async () => { await cleanDatabase() @@ -404,6 +406,72 @@ describe('Posts in Groups', () => { }) }) + describe('as new user', () => { + beforeAll(async () => { + await Factory.build('emailAddress', { + email: 'new-user@example.org', + nonce: '12345', + verifiedAt: null, + }) + const result = await mutate({ + mutation: signupVerificationMutation, + variables: { + name: 'New User', + slug: 'new-user', + nonce: '12345', + password: '1234', + about: 'I am a new user!', + email: 'new-user@example.org', + termsAndConditionsAgreedVersion: '0.0.1', + }, + }) + newUser = result.data.SignupVerification + }) + + beforeEach(async () => { + authenticatedUser = newUser + }) + + it('shows a post of the public group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-public-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + + it('does not show a post of a closed group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-closed-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + + it('does not show a post of a hidden group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-hidden-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + }) + describe('without membership of group', () => { beforeEach(async () => { authenticatedUser = await anyUser.toJson() @@ -449,6 +517,51 @@ describe('Posts in Groups', () => { }) }) + describe('with pending membership of group', () => { + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('shows a post of the public group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-public-group' } }), + ).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + + it('does not show a post of a closed group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-closed-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + + it('does not show a post of a hidden group', async () => { + await expect( + query({ query: postQuery(), variables: { id: 'post-to-hidden-group' } }), + ).resolves.toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + }) + describe('as member of group', () => { beforeEach(async () => { authenticatedUser = await allGroupsUser.toJson() From 03434071b42b32a84d29503d709d4c920d1e9108 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 4 Oct 2022 05:44:11 +0200 Subject: [PATCH 21/33] fix cypher to have signup verification to block posts of groups for new users --- backend/src/db/graphql/authentications.js | 1 + backend/src/schema/resolvers/registration.js | 21 +++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/db/graphql/authentications.js b/backend/src/db/graphql/authentications.js index f059706508..91605ec9fe 100644 --- a/backend/src/db/graphql/authentications.js +++ b/backend/src/db/graphql/authentications.js @@ -19,6 +19,7 @@ export const signupVerificationMutation = gql` nonce: $nonce termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion ) { + id slug } } diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index ea420bc2ae..52c92b0335 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -72,19 +72,19 @@ const signupCypher = (inviteCode) => { (inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User) ` optionalMerge = ` - MERGE(user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode) - MERGE(host)-[:INVITED { createdAt: toString(datetime()) }]->(user) - MERGE(user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host) - MERGE(host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user) + MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode) + MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user) + MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host) + MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user) ` } const cypher = ` - MATCH(email:EmailAddress {nonce: $nonce, email: $email}) + MATCH (email:EmailAddress {nonce: $nonce, email: $email}) WHERE NOT (email)-[:BELONGS_TO]->() ${optionalMatch} CREATE (user:User) - MERGE(user)-[:PRIMARY_EMAIL]->(email) - MERGE(user)<-[:BELONGS_TO]-(email) + MERGE (user)-[:PRIMARY_EMAIL]->(email) + MERGE (user)<-[:BELONGS_TO]-(email) ${optionalMerge} SET user += $args SET user.id = randomUUID() @@ -95,6 +95,13 @@ const signupCypher = (inviteCode) => { SET user.showShoutsPublicly = false SET user.sendNotificationEmails = true SET email.verifiedAt = toString(datetime()) + WITH user + OPTIONAL MATCH (post:Post)-[:IN]->(group:Group) + WHERE NOT group.groupType = 'public' + WITH user, collect(post) AS invisiblePosts + FOREACH (invisiblePost IN invisiblePosts | + MERGE (user)-[:CANNOT_SEE]->(invisiblePost) + ) RETURN user {.*} ` return cypher From 67cba104b8d14b342c8fb5827fffc2cd08608caf Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 08:09:21 +0200 Subject: [PATCH 22/33] remove findPosts and findUsers as they are never used. --- backend/src/middleware/permissionsMiddleware.js | 2 -- backend/src/schema/resolvers/posts.js | 5 ----- backend/src/schema/types/type/Post.gql | 14 -------------- backend/src/schema/types/type/User.gql | 12 ------------ 4 files changed, 33 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index b73ba166f8..d77363c292 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -299,8 +299,6 @@ export default shield( { Query: { '*': deny, - findPosts: allow, - findUsers: allow, searchResults: allow, searchPosts: allow, searchUsers: allow, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index aa861f2b9f..86dc22f441 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -26,11 +26,6 @@ export default { params = await maintainPinnedPosts(params) return neo4jgraphql(object, params, context, resolveInfo) }, - findPosts: async (object, params, context, resolveInfo) => { - params = await filterInvisiblePosts(params, context) - params = await filterForMutedUsers(params, context) - return neo4jgraphql(object, params, context, resolveInfo) - }, profilePagePosts: async (object, params, context, resolveInfo) => { params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 8ea8b5cdb0..9eac00b0ba 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -229,18 +229,4 @@ type Query { PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! PostsEmotionsByCurrentUser(postId: ID!): [String] profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post] - findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]! - @cypher( - statement: """ - CALL db.index.fulltext.queryNodes('post_fulltext_search', $query) - YIELD node as post, score - MATCH (post)<-[:WROTE]-(user:User) - WHERE score >= 0.2 - AND NOT user.deleted = true AND NOT user.disabled = true - AND NOT post.deleted = true AND NOT post.disabled = true - AND NOT user.id in COALESCE($filter.author_not.id_in, []) - RETURN post - LIMIT $limit - """ - ) } diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index fdab73d174..fe1ff43f0c 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -186,18 +186,6 @@ type Query { blockedUsers: [User] isLoggedIn: Boolean! currentUser: User - findUsers(query: String!,limit: Int = 10, filter: _UserFilter): [User]! - @cypher( - statement: """ - CALL db.index.fulltext.queryNodes('user_fulltext_search', $query) - YIELD node as post, score - MATCH (user) - WHERE score >= 0.2 - AND NOT user.deleted = true AND NOT user.disabled = true - RETURN user - LIMIT $limit - """ - ) } enum Deletable { From 570d6c099dcca3c36dae332ae9a1e1393823c18b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 08:27:35 +0200 Subject: [PATCH 23/33] test filter posts --- backend/src/db/graphql/posts.js | 12 ++ .../schema/resolvers/postsInGroups.spec.js | 166 +++++++++++++++++- 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/backend/src/db/graphql/posts.js b/backend/src/db/graphql/posts.js index bb7402a9f5..571f853ce6 100644 --- a/backend/src/db/graphql/posts.js +++ b/backend/src/db/graphql/posts.js @@ -43,4 +43,16 @@ export const postQuery = () => { ` } +export const filterPosts = () => { + return gql` + query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { + Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + } + } + ` +} + // fill queries in here diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index ab070d7e46..530f2ca0f1 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -3,7 +3,7 @@ import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createGroupMutation, changeGroupMemberRoleMutation } from '../../db/graphql/groups' -import { createPostMutation, postQuery } from '../../db/graphql/posts' +import { createPostMutation, postQuery, filterPosts } from '../../db/graphql/posts' // eslint-disable-next-line no-unused-vars import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import CONFIG from '../../config' @@ -619,5 +619,169 @@ describe('Posts in Groups', () => { }) }) }) + + describe('filter posts', () => { + beforeAll(async () => { + authenticatedUser = newUser + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + }) + }) + + describe('without authentication', () => { + beforeEach(async () => { + authenticatedUser = null + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('as new user', () => { + beforeEach(async () => { + authenticatedUser = newUser + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('without membership of group', () => { + beforeEach(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('with pending membership of group', () => { + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('as member of group', () => { + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('shows all posts', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(4) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) }) }) From ab5d308caaaf9a492d54a625c3051416740c9c97 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 08:38:05 +0200 Subject: [PATCH 24/33] test profile page posts --- backend/src/db/graphql/posts.js | 17 +- .../schema/resolvers/postsInGroups.spec.js | 159 +++++++++++++++++- 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/backend/src/db/graphql/posts.js b/backend/src/db/graphql/posts.js index 571f853ce6..e758c523e9 100644 --- a/backend/src/db/graphql/posts.js +++ b/backend/src/db/graphql/posts.js @@ -55,4 +55,19 @@ export const filterPosts = () => { ` } -// fill queries in here +export const profilePagePosts = () => { + return gql` + query profilePagePosts( + $filter: _PostFilter + $first: Int + $offset: Int + $orderBy: [_PostOrdering] + ) { + profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + } + } + ` +} diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 530f2ca0f1..5505feece0 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -3,7 +3,12 @@ import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createGroupMutation, changeGroupMemberRoleMutation } from '../../db/graphql/groups' -import { createPostMutation, postQuery, filterPosts } from '../../db/graphql/posts' +import { + createPostMutation, + postQuery, + filterPosts, + profilePagePosts, +} from '../../db/graphql/posts' // eslint-disable-next-line no-unused-vars import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import CONFIG from '../../config' @@ -783,5 +788,157 @@ describe('Posts in Groups', () => { }) }) }) + + describe('profile page posts', () => { + describe('without authentication', () => { + beforeEach(async () => { + authenticatedUser = null + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: profilePagePosts(), variables: {} }) + expect(result.data.profilePagePosts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + profilePagePosts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('as new user', () => { + beforeEach(async () => { + authenticatedUser = newUser + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: profilePagePosts(), variables: {} }) + expect(result.data.profilePagePosts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + profilePagePosts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('without membership of group', () => { + beforeEach(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: profilePagePosts(), variables: {} }) + expect(result.data.profilePagePosts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + profilePagePosts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('with pending membership of group', () => { + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('shows a the post of the public group and the post without group', async () => { + const result = await query({ query: profilePagePosts(), variables: {} }) + expect(result.data.profilePagePosts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + profilePagePosts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('as member of group', () => { + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('shows all posts', async () => { + const result = await query({ query: profilePagePosts(), variables: {} }) + expect(result.data.profilePagePosts).toHaveLength(4) + expect(result).toMatchObject({ + data: { + profilePagePosts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) }) }) From aa870b5020a0c625d34b3d1522cacbc8aaebc454 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 08:57:13 +0200 Subject: [PATCH 25/33] searches need authorization as they are not working without user id in context --- backend/src/middleware/permissionsMiddleware.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index d77363c292..96bffbedba 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -299,10 +299,10 @@ export default shield( { Query: { '*': deny, - searchResults: allow, - searchPosts: allow, - searchUsers: allow, - searchHashtags: allow, + searchResults: isAuthenticated, + searchPosts: isAuthenticated, + searchUsers: isAuthenticated, + searchHashtags: isAuthenticated, embed: allow, Category: allow, Tag: allow, From 76bfe484768cf9b20b2dced865d5d3e3eb999235 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 11:20:27 +0200 Subject: [PATCH 26/33] implement and test post visibilty when leaving or changing the role in a group --- backend/src/schema/resolvers/groups.js | 17 + .../schema/resolvers/postsInGroups.spec.js | 366 +++++++++++++++++- 2 files changed, 382 insertions(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 7a62980479..a014449b84 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -260,9 +260,12 @@ export default { const writeTxResultPromise = session.writeTransaction(async (transaction) => { const leaveGroupCypher = ` MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + OPTIONAL MATCH (post:Post)-[:IN]->(group) + MERGE (member)-[:CANNOT_SEE]->(post) DELETE membership RETURN member {.*, myRoleInGroup: NULL} ` + const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId }) const [member] = await transactionResponse.records.map((record) => record.get('member')) return member @@ -279,8 +282,21 @@ export default { const { groupId, userId, roleInGroup } = params const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { + let postRestrictionCypher = '' + if (['owner', 'admin', 'usual'].includes(roleInGroup)) { + postRestrictionCypher = ` + OPTIONAL MATCH (member)-[restriction:CANNOT_SEE]->(post:Post)-[:IN]->(group) + DELETE restriction` + } else { + // user becomes pending member + postRestrictionCypher = ` + OPTIONAL MATCH (post:Post)-[:IN]->(group) + MERGE (member)-[:CANNOT_SEE]->(post)` + } + const joinGroupCypher = ` MATCH (member:User {id: $userId}), (group:Group {id: $groupId}) + ${postRestrictionCypher} MERGE (member)-[membership:MEMBER_OF]->(group) ON CREATE SET membership.createdAt = toString(datetime()), @@ -291,6 +307,7 @@ export default { membership.role = $roleInGroup RETURN member {.*, myRoleInGroup: membership.role} ` + const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId, diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 5505feece0..5617a13a48 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -2,7 +2,11 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' -import { createGroupMutation, changeGroupMemberRoleMutation } from '../../db/graphql/groups' +import { + createGroupMutation, + changeGroupMemberRoleMutation, + leaveGroupMutation, +} from '../../db/graphql/groups' import { createPostMutation, postQuery, @@ -941,4 +945,364 @@ describe('Posts in Groups', () => { }) }) }) + + describe('changes of group membership', () => { + describe('pending member becomes usual member', () => { + describe('of closed group', () => { + beforeAll(async () => { + authenticatedUser = await closedUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'pending-user', + roleInGroup: 'usual', + }, + }) + }) + + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('shows the posts of the closed group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(3) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('of hidden group', () => { + beforeAll(async () => { + authenticatedUser = await hiddenUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'pending-user', + roleInGroup: 'usual', + }, + }) + }) + + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('shows all the posts', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(4) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + + describe('usual member becomes pending', () => { + describe('of closed group', () => { + beforeAll(async () => { + authenticatedUser = await closedUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'pending-user', + roleInGroup: 'pending', + }, + }) + }) + + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('does not shows the posts of the closed group anymore', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(3) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('of hidden group', () => { + beforeAll(async () => { + authenticatedUser = await hiddenUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'pending-user', + roleInGroup: 'pending', + }, + }) + }) + + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('shows only the public posts', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + + describe('usual member leaves', () => { + describe('closed group', () => { + beforeAll(async () => { + authenticatedUser = await allGroupsUser.toJson() + await mutate({ + mutation: leaveGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'all-groups-user', + }, + }) + }) + + it('does not shows the posts of the closed group anymore', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(3) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('hidden group', () => { + beforeAll(async () => { + authenticatedUser = await allGroupsUser.toJson() + await mutate({ + mutation: leaveGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'all-groups-user', + }, + }) + }) + + it('does only show the public posts', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + + describe('any user joins', () => { + describe('closed group', () => { + beforeAll(async () => { + authenticatedUser = await closedUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'all-groups-user', + roleInGroup: 'usual', + }, + }) + }) + + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('does not shows the posts of the closed group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(3) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('hidden group', () => { + beforeAll(async () => { + authenticatedUser = await hiddenUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'all-groups-user', + roleInGroup: 'usual', + }, + }) + }) + + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('shows all posts', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(4) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) }) From a924357a13d270dc7723558f2eba5f4a3f30caae Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 13:10:18 +0200 Subject: [PATCH 27/33] fix cypher when posts do not exist --- backend/src/schema/resolvers/groups.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index a014449b84..259a7c8183 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -260,9 +260,10 @@ export default { const writeTxResultPromise = session.writeTransaction(async (transaction) => { const leaveGroupCypher = ` MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) - OPTIONAL MATCH (post:Post)-[:IN]->(group) - MERGE (member)-[:CANNOT_SEE]->(post) DELETE membership + WITH member, group + FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | + MERGE (member)-[:CANNOT_SEE]->(post)) RETURN member {.*, myRoleInGroup: NULL} ` @@ -285,18 +286,20 @@ export default { let postRestrictionCypher = '' if (['owner', 'admin', 'usual'].includes(roleInGroup)) { postRestrictionCypher = ` - OPTIONAL MATCH (member)-[restriction:CANNOT_SEE]->(post:Post)-[:IN]->(group) - DELETE restriction` + WITH group, member, membership + FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | + DELETE restriction)` } else { // user becomes pending member postRestrictionCypher = ` - OPTIONAL MATCH (post:Post)-[:IN]->(group) - MERGE (member)-[:CANNOT_SEE]->(post)` + WITH group, member, membership + FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | + MERGE (member)-[:CANNOT_SEE]->(post))` } const joinGroupCypher = ` - MATCH (member:User {id: $userId}), (group:Group {id: $groupId}) - ${postRestrictionCypher} + MATCH (member:User {id: $userId}) + MATCH (group:Group {id: $groupId}) MERGE (member)-[membership:MEMBER_OF]->(group) ON CREATE SET membership.createdAt = toString(datetime()), @@ -305,6 +308,7 @@ export default { ON MATCH SET membership.updatedAt = toString(datetime()), membership.role = $roleInGroup + ${postRestrictionCypher} RETURN member {.*, myRoleInGroup: membership.role} ` From 74505a11c5128dc6c4ea18abfe523746199c5bd2 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 17:57:01 +0200 Subject: [PATCH 28/33] filter posts for group visibility on search posts --- backend/src/schema/resolvers/searches.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 60fd4318fc..aba89fd4ce 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -23,12 +23,15 @@ const postWhereClause = `WHERE score >= 0.0 AND NOT ( author.deleted = true OR author.disabled = true OR resource.deleted = true OR resource.disabled = true - OR (:User {id: $userId})-[:MUTED]->(author) - )` + ) AND block IS NULL AND restriction IS NULL` const searchPostsSetup = { fulltextIndex: 'post_fulltext_search', - match: 'MATCH (resource:Post)<-[:WROTE]-(author:User)', + match: `MATCH (resource:Post)<-[:WROTE]-(author:User) + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[block:MUTED]->(author) + OPTIONAL MATCH (user)-[restriction:CANNOT_SEE]->(resource) + WITH user, resource, author, block, restriction`, whereClause: postWhereClause, withClause: `WITH resource, author, [(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments, @@ -117,7 +120,6 @@ export default { searchPosts: async (_parent, args, context, _resolveInfo) => { const { query, postsOffset, firstPosts } = args const { id: userId } = context.user - return { postCount: getSearchResults( context, From a4cd7a86982c6db209309828b67a93a5f854e4a6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 5 Oct 2022 21:54:52 +0200 Subject: [PATCH 29/33] test search posts with groups --- backend/src/db/graphql/posts.js | 15 ++ .../src/middleware/permissionsMiddleware.js | 8 +- .../schema/resolvers/postsInGroups.spec.js | 187 ++++++++++++++++++ backend/src/schema/resolvers/searches.js | 6 +- 4 files changed, 210 insertions(+), 6 deletions(-) diff --git a/backend/src/db/graphql/posts.js b/backend/src/db/graphql/posts.js index e758c523e9..2669d6f242 100644 --- a/backend/src/db/graphql/posts.js +++ b/backend/src/db/graphql/posts.js @@ -71,3 +71,18 @@ export const profilePagePosts = () => { } ` } + +export const searchPosts = () => { + return gql` + query ($query: String!, $firstPosts: Int, $postsOffset: Int) { + searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { + postCount + posts { + id + title + content + } + } + } + ` +} diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 96bffbedba..d77363c292 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -299,10 +299,10 @@ export default shield( { Query: { '*': deny, - searchResults: isAuthenticated, - searchPosts: isAuthenticated, - searchUsers: isAuthenticated, - searchHashtags: isAuthenticated, + searchResults: allow, + searchPosts: allow, + searchUsers: allow, + searchHashtags: allow, embed: allow, Category: allow, Tag: allow, diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 5617a13a48..48178ae93e 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -12,6 +12,7 @@ import { postQuery, filterPosts, profilePagePosts, + searchPosts, } from '../../db/graphql/posts' // eslint-disable-next-line no-unused-vars import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' @@ -943,6 +944,192 @@ describe('Posts in Groups', () => { }) }) }) + + describe('searchPosts', () => { + describe('without authentication', () => { + beforeEach(async () => { + authenticatedUser = null + }) + + it('finds nothing', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(0) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 0, + posts: [], + }, + }, + }) + }) + }) + + describe('as new user', () => { + beforeEach(async () => { + authenticatedUser = newUser + }) + + it('finds the post of the public group and the post without group', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 2, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + }, + }) + }) + }) + + describe('without membership of group', () => { + beforeEach(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('finds the post of the public group and the post without group', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 2, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + }, + }) + }) + }) + + describe('with pending membership of group', () => { + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('finds the post of the public group and the post without group', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 2, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + ]), + }, + }, + }) + }) + }) + + describe('as member of group', () => { + beforeEach(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('finds all posts', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(4) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 4, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'As a new user, I do not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + }, + }) + }) + }) + }) }) }) diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index aba89fd4ce..63279b4bf5 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -119,7 +119,8 @@ export default { Query: { searchPosts: async (_parent, args, context, _resolveInfo) => { const { query, postsOffset, firstPosts } = args - const { id: userId } = context.user + let userId = null + if (context.user) userId = context.user.id return { postCount: getSearchResults( context, @@ -179,7 +180,8 @@ export default { }, searchResults: async (_parent, args, context, _resolveInfo) => { const { query, limit } = args - const { id: userId } = context.user + let userId = null + if (context.user) userId = context.user.id const searchType = query.replace(/^([!@#]?).*$/, '$1') const searchString = query.replace(/^([!@#])/, '') From 4fdaa0da4ed168d7322de066d3ca230d3382a3e3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 6 Oct 2022 13:13:23 +0200 Subject: [PATCH 30/33] Update backend/src/schema/resolvers/helpers/filterInvisiblePosts.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Wolfgang Huß --- backend/src/schema/resolvers/helpers/filterInvisiblePosts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js index 108f9c007c..73dfaad91c 100644 --- a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js +++ b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.js @@ -13,7 +13,7 @@ const getInvisiblePosts = async (context) => { cypher = ` MATCH (post:Post)-[:IN]->(group:Group) WHERE NOT group.groupType = 'public' - RETURN collect(post.id) AS invisiblePostIds` + RETURN collect(post.id) AS invisiblePostIds` } const invisiblePostIdsResponse = await transaction.run(cypher, { userId: user ? user.id : null, From 45562763512d988ac85fe35a0e3a7da8bcc7b27d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 10 Oct 2022 20:57:42 +0200 Subject: [PATCH 31/33] fix: leaving public group keeps the posts in public group visible. Test leaving public group and visibility of posts in public group --- backend/src/schema/resolvers/groups.js | 5 +- .../schema/resolvers/postsInGroups.spec.js | 149 +++++++++++------- 2 files changed, 98 insertions(+), 56 deletions(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 259a7c8183..55dd605ad4 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -262,7 +262,10 @@ export default { MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) DELETE membership WITH member, group - FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | + OPTIONAL MATCH (p:Post)-[:IN]->(group) + WHERE NOT group.groupType = 'public' + WITH member, group, collect(p) AS posts + FOREACH (post IN posts | MERGE (member)-[:CANNOT_SEE]->(post)) RETURN member {.*, myRoleInGroup: NULL} ` diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 48178ae93e..55174c39da 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -182,6 +182,33 @@ describe('Posts in Groups', () => { roleInGroup: 'usual', }, }) + await Factory.build('emailAddress', { + email: 'new-user@example.org', + nonce: '12345', + verifiedAt: null, + }) + const result = await mutate({ + mutation: signupVerificationMutation, + variables: { + name: 'New User', + slug: 'new-user', + nonce: '12345', + password: '1234', + about: 'I am a new user!', + email: 'new-user@example.org', + termsAndConditionsAgreedVersion: '0.0.1', + }, + }) + newUser = result.data.SignupVerification + authenticatedUser = await anyUser.toJson() + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'post-without-group', + title: 'A post without a group', + content: 'I am a user who does not belong to a group yet.', + }, + }) }) describe('creating posts in groups', () => { @@ -417,27 +444,6 @@ describe('Posts in Groups', () => { }) describe('as new user', () => { - beforeAll(async () => { - await Factory.build('emailAddress', { - email: 'new-user@example.org', - nonce: '12345', - verifiedAt: null, - }) - const result = await mutate({ - mutation: signupVerificationMutation, - variables: { - name: 'New User', - slug: 'new-user', - nonce: '12345', - password: '1234', - about: 'I am a new user!', - email: 'new-user@example.org', - termsAndConditionsAgreedVersion: '0.0.1', - }, - }) - newUser = result.data.SignupVerification - }) - beforeEach(async () => { authenticatedUser = newUser }) @@ -631,18 +637,6 @@ describe('Posts in Groups', () => { }) describe('filter posts', () => { - beforeAll(async () => { - authenticatedUser = newUser - await mutate({ - mutation: createPostMutation(), - variables: { - id: 'post-without-group', - title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', - }, - }) - }) - describe('without authentication', () => { beforeEach(async () => { authenticatedUser = null @@ -662,7 +656,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -690,7 +684,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -718,7 +712,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -746,7 +740,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -774,7 +768,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', @@ -814,7 +808,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -842,7 +836,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -870,7 +864,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -898,7 +892,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -926,7 +920,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', @@ -1000,7 +994,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -1037,7 +1031,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -1074,7 +1068,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -1111,7 +1105,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', @@ -1166,7 +1160,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', @@ -1211,7 +1205,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', @@ -1263,7 +1257,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-hidden-group', @@ -1308,7 +1302,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -1319,6 +1313,51 @@ describe('Posts in Groups', () => { }) describe('usual member leaves', () => { + describe('public group', () => { + beforeAll(async () => { + authenticatedUser = await allGroupsUser.toJson() + await mutate({ + mutation: leaveGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'all-groups-user', + }, + }) + }) + + it('still shows the posts of the public group', async () => { + const result = await query({ query: filterPosts(), variables: {} }) + expect(result.data.Post).toHaveLength(4) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'I am a user who does not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + describe('closed group', () => { beforeAll(async () => { authenticatedUser = await allGroupsUser.toJson() @@ -1345,7 +1384,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-hidden-group', @@ -1385,7 +1424,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, ]), }, @@ -1427,7 +1466,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', @@ -1472,7 +1511,7 @@ describe('Posts in Groups', () => { { id: 'post-without-group', title: 'A post without a group', - content: 'As a new user, I do not belong to a group yet.', + content: 'I am a user who does not belong to a group yet.', }, { id: 'post-to-closed-group', From 631f34a2e5224d68279337a92e7535794b670d70 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 10 Oct 2022 21:21:45 +0200 Subject: [PATCH 32/33] improved code and tests as suggested by @tirokk, thanks for the great review! --- backend/src/schema/resolvers/groups.js | 11 +- backend/src/schema/resolvers/posts.js | 4 +- .../schema/resolvers/postsInGroups.spec.js | 434 +++++++++--------- 3 files changed, 215 insertions(+), 234 deletions(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 55dd605ad4..76564cdb36 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -287,17 +287,16 @@ export default { const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { let postRestrictionCypher = '' - if (['owner', 'admin', 'usual'].includes(roleInGroup)) { + if (roleInGroup === 'pending') { postRestrictionCypher = ` WITH group, member, membership - FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | - DELETE restriction)` + FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | + MERGE (member)-[:CANNOT_SEE]->(post))` } else { - // user becomes pending member postRestrictionCypher = ` WITH group, member, membership - FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | - MERGE (member)-[:CANNOT_SEE]->(post))` + FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | + DELETE restriction)` } const joinGroupCypher = ` diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 86dc22f441..78515e6418 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -102,7 +102,7 @@ export default { WHERE group.groupType IN ['closed', 'hidden'] AND membership.role IN ['usual', 'admin', 'owner'] WITH post, collect(user.id) AS userIds - OPTIONAL MATCH path =(blocked:User) WHERE NOT blocked.id IN userIds + OPTIONAL MATCH path =(restricted:User) WHERE NOT restricted.id IN userIds FOREACH (user IN nodes(path) | MERGE (user)-[:CANNOT_SEE]->(post) )` @@ -129,7 +129,7 @@ export default { ${groupCypher} RETURN post {.*} `, - { userId: context.user.id, params, categoryIds, groupId }, + { userId: context.user.id, categoryIds, groupId, params }, ) const [post] = createPostTransactionResponse.records.map((record) => record.get('post')) if (imageInput) { diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 55174c39da..d17c928ecc 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -59,7 +59,7 @@ beforeAll(async () => { }) afterAll(async () => { - // await cleanDatabase() + await cleanDatabase() }) describe('Posts in Groups', () => { @@ -182,24 +182,6 @@ describe('Posts in Groups', () => { roleInGroup: 'usual', }, }) - await Factory.build('emailAddress', { - email: 'new-user@example.org', - nonce: '12345', - verifiedAt: null, - }) - const result = await mutate({ - mutation: signupVerificationMutation, - variables: { - name: 'New User', - slug: 'new-user', - nonce: '12345', - password: '1234', - about: 'I am a new user!', - email: 'new-user@example.org', - termsAndConditionsAgreedVersion: '0.0.1', - }, - }) - newUser = result.data.SignupVerification authenticatedUser = await anyUser.toJson() await mutate({ mutation: createPostMutation(), @@ -213,7 +195,7 @@ describe('Posts in Groups', () => { describe('creating posts in groups', () => { describe('without membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await anyUser.toJson() }) @@ -267,7 +249,7 @@ describe('Posts in Groups', () => { }) describe('as a pending member of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await pendingUser.toJson() }) @@ -321,7 +303,7 @@ describe('Posts in Groups', () => { }) describe('as a member of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await allGroupsUser.toJson() }) @@ -399,7 +381,7 @@ describe('Posts in Groups', () => { describe('visibility of posts', () => { describe('query post by ID', () => { describe('without authentication', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = null }) @@ -444,7 +426,25 @@ describe('Posts in Groups', () => { }) describe('as new user', () => { - beforeEach(async () => { + beforeAll(async () => { + await Factory.build('emailAddress', { + email: 'new-user@example.org', + nonce: '12345', + verifiedAt: null, + }) + const result = await mutate({ + mutation: signupVerificationMutation, + variables: { + name: 'New User', + slug: 'new-user', + nonce: '12345', + password: '1234', + about: 'I am a new user!', + email: 'new-user@example.org', + termsAndConditionsAgreedVersion: '0.0.1', + }, + }) + newUser = result.data.SignupVerification authenticatedUser = newUser }) @@ -489,7 +489,7 @@ describe('Posts in Groups', () => { }) describe('without membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await anyUser.toJson() }) @@ -534,7 +534,7 @@ describe('Posts in Groups', () => { }) describe('with pending membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await pendingUser.toJson() }) @@ -579,7 +579,7 @@ describe('Posts in Groups', () => { }) describe('as member of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await allGroupsUser.toJson() }) @@ -638,11 +638,11 @@ describe('Posts in Groups', () => { describe('filter posts', () => { describe('without authentication', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = null }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(2) expect(result).toMatchObject({ @@ -666,11 +666,11 @@ describe('Posts in Groups', () => { }) describe('as new user', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = newUser }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(2) expect(result).toMatchObject({ @@ -694,11 +694,11 @@ describe('Posts in Groups', () => { }) describe('without membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await anyUser.toJson() }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(2) expect(result).toMatchObject({ @@ -722,11 +722,11 @@ describe('Posts in Groups', () => { }) describe('with pending membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await pendingUser.toJson() }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(2) expect(result).toMatchObject({ @@ -750,7 +750,7 @@ describe('Posts in Groups', () => { }) describe('as member of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await allGroupsUser.toJson() }) @@ -790,11 +790,11 @@ describe('Posts in Groups', () => { describe('profile page posts', () => { describe('without authentication', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = null }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: profilePagePosts(), variables: {} }) expect(result.data.profilePagePosts).toHaveLength(2) expect(result).toMatchObject({ @@ -818,11 +818,11 @@ describe('Posts in Groups', () => { }) describe('as new user', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = newUser }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: profilePagePosts(), variables: {} }) expect(result.data.profilePagePosts).toHaveLength(2) expect(result).toMatchObject({ @@ -846,11 +846,11 @@ describe('Posts in Groups', () => { }) describe('without membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await anyUser.toJson() }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: profilePagePosts(), variables: {} }) expect(result.data.profilePagePosts).toHaveLength(2) expect(result).toMatchObject({ @@ -874,11 +874,11 @@ describe('Posts in Groups', () => { }) describe('with pending membership of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await pendingUser.toJson() }) - it('shows a the post of the public group and the post without group', async () => { + it('shows the post of the public group and the post without group', async () => { const result = await query({ query: profilePagePosts(), variables: {} }) expect(result.data.profilePagePosts).toHaveLength(2) expect(result).toMatchObject({ @@ -902,7 +902,7 @@ describe('Posts in Groups', () => { }) describe('as member of group', () => { - beforeEach(async () => { + beforeAll(async () => { authenticatedUser = await allGroupsUser.toJson() }) @@ -938,189 +938,189 @@ describe('Posts in Groups', () => { }) }) }) + }) - describe('searchPosts', () => { - describe('without authentication', () => { - beforeEach(async () => { - authenticatedUser = null - }) + describe('searchPosts', () => { + describe('without authentication', () => { + beforeAll(async () => { + authenticatedUser = null + }) - it('finds nothing', async () => { - const result = await query({ - query: searchPosts(), - variables: { - query: 'post', - postsOffset: 0, - firstPosts: 25, - }, - }) - expect(result.data.searchPosts.posts).toHaveLength(0) - expect(result).toMatchObject({ - data: { - searchPosts: { - postCount: 0, - posts: [], - }, + it('finds nothing', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(0) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 0, + posts: [], }, - }) + }, }) }) + }) - describe('as new user', () => { - beforeEach(async () => { - authenticatedUser = newUser - }) + describe('as new user', () => { + beforeAll(async () => { + authenticatedUser = newUser + }) - it('finds the post of the public group and the post without group', async () => { - const result = await query({ - query: searchPosts(), - variables: { - query: 'post', - postsOffset: 0, - firstPosts: 25, - }, - }) - expect(result.data.searchPosts.posts).toHaveLength(2) - expect(result).toMatchObject({ - data: { - searchPosts: { - postCount: 2, - posts: expect.arrayContaining([ - { - id: 'post-to-public-group', - title: 'A post to a public group', - content: 'I am posting into a public group as a member of the group', - }, - { - id: 'post-without-group', - title: 'A post without a group', - content: 'I am a user who does not belong to a group yet.', - }, - ]), - }, + it('finds the post of the public group and the post without group', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 2, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'I am a user who does not belong to a group yet.', + }, + ]), }, - }) + }, }) }) + }) + + describe('without membership of group', () => { + beforeAll(async () => { + authenticatedUser = await anyUser.toJson() + }) - describe('without membership of group', () => { - beforeEach(async () => { - authenticatedUser = await anyUser.toJson() + it('finds the post of the public group and the post without group', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, }) - - it('finds the post of the public group and the post without group', async () => { - const result = await query({ - query: searchPosts(), - variables: { - query: 'post', - postsOffset: 0, - firstPosts: 25, - }, - }) - expect(result.data.searchPosts.posts).toHaveLength(2) - expect(result).toMatchObject({ - data: { - searchPosts: { - postCount: 2, - posts: expect.arrayContaining([ - { - id: 'post-to-public-group', - title: 'A post to a public group', - content: 'I am posting into a public group as a member of the group', - }, - { - id: 'post-without-group', - title: 'A post without a group', - content: 'I am a user who does not belong to a group yet.', - }, - ]), - }, + expect(result.data.searchPosts.posts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 2, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'I am a user who does not belong to a group yet.', + }, + ]), }, - }) + }, }) }) + }) - describe('with pending membership of group', () => { - beforeEach(async () => { - authenticatedUser = await pendingUser.toJson() - }) + describe('with pending membership of group', () => { + beforeAll(async () => { + authenticatedUser = await pendingUser.toJson() + }) - it('finds the post of the public group and the post without group', async () => { - const result = await query({ - query: searchPosts(), - variables: { - query: 'post', - postsOffset: 0, - firstPosts: 25, - }, - }) - expect(result.data.searchPosts.posts).toHaveLength(2) - expect(result).toMatchObject({ - data: { - searchPosts: { - postCount: 2, - posts: expect.arrayContaining([ - { - id: 'post-to-public-group', - title: 'A post to a public group', - content: 'I am posting into a public group as a member of the group', - }, - { - id: 'post-without-group', - title: 'A post without a group', - content: 'I am a user who does not belong to a group yet.', - }, - ]), - }, + it('finds the post of the public group and the post without group', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(2) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 2, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'I am a user who does not belong to a group yet.', + }, + ]), }, - }) + }, }) }) + }) - describe('as member of group', () => { - beforeEach(async () => { - authenticatedUser = await allGroupsUser.toJson() - }) + describe('as member of group', () => { + beforeAll(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) - it('finds all posts', async () => { - const result = await query({ - query: searchPosts(), - variables: { - query: 'post', - postsOffset: 0, - firstPosts: 25, - }, - }) - expect(result.data.searchPosts.posts).toHaveLength(4) - expect(result).toMatchObject({ - data: { - searchPosts: { - postCount: 4, - posts: expect.arrayContaining([ - { - id: 'post-to-public-group', - title: 'A post to a public group', - content: 'I am posting into a public group as a member of the group', - }, - { - id: 'post-without-group', - title: 'A post without a group', - content: 'I am a user who does not belong to a group yet.', - }, - { - id: 'post-to-closed-group', - title: 'A post to a closed group', - content: 'I am posting into a closed group as a member of the group', - }, - { - id: 'post-to-hidden-group', - title: 'A post to a hidden group', - content: 'I am posting into a hidden group as a member of the group', - }, - ]), - }, + it('finds all posts', async () => { + const result = await query({ + query: searchPosts(), + variables: { + query: 'post', + postsOffset: 0, + firstPosts: 25, + }, + }) + expect(result.data.searchPosts.posts).toHaveLength(4) + expect(result).toMatchObject({ + data: { + searchPosts: { + postCount: 4, + posts: expect.arrayContaining([ + { + id: 'post-to-public-group', + title: 'A post to a public group', + content: 'I am posting into a public group as a member of the group', + }, + { + id: 'post-without-group', + title: 'A post without a group', + content: 'I am a user who does not belong to a group yet.', + }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), }, - }) + }, }) }) }) @@ -1140,9 +1140,6 @@ describe('Posts in Groups', () => { roleInGroup: 'usual', }, }) - }) - - beforeEach(async () => { authenticatedUser = await pendingUser.toJson() }) @@ -1185,9 +1182,6 @@ describe('Posts in Groups', () => { roleInGroup: 'usual', }, }) - }) - - beforeEach(async () => { authenticatedUser = await pendingUser.toJson() }) @@ -1237,13 +1231,10 @@ describe('Posts in Groups', () => { roleInGroup: 'pending', }, }) - }) - - beforeEach(async () => { authenticatedUser = await pendingUser.toJson() }) - it('does not shows the posts of the closed group anymore', async () => { + it('does not show the posts of the closed group anymore', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(3) expect(result).toMatchObject({ @@ -1282,9 +1273,6 @@ describe('Posts in Groups', () => { roleInGroup: 'pending', }, }) - }) - - beforeEach(async () => { authenticatedUser = await pendingUser.toJson() }) @@ -1370,7 +1358,7 @@ describe('Posts in Groups', () => { }) }) - it('does not shows the posts of the closed group anymore', async () => { + it('does not show the posts of the closed group anymore', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(3) expect(result).toMatchObject({ @@ -1446,13 +1434,10 @@ describe('Posts in Groups', () => { roleInGroup: 'usual', }, }) - }) - - beforeEach(async () => { authenticatedUser = await allGroupsUser.toJson() }) - it('does not shows the posts of the closed group', async () => { + it('does not show the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) expect(result.data.Post).toHaveLength(3) expect(result).toMatchObject({ @@ -1491,9 +1476,6 @@ describe('Posts in Groups', () => { roleInGroup: 'usual', }, }) - }) - - beforeEach(async () => { authenticatedUser = await allGroupsUser.toJson() }) From 7cb1663be75810cbebc490106503b33483e2392f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 11 Oct 2022 14:35:27 +0200 Subject: [PATCH 33/33] change from role eq pending to usual, admin, owner includes role --- backend/src/schema/resolvers/groups.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 76564cdb36..5e22bd7437 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -287,16 +287,16 @@ export default { const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { let postRestrictionCypher = '' - if (roleInGroup === 'pending') { + if (['usual', 'admin', 'owner'].includes(roleInGroup)) { postRestrictionCypher = ` WITH group, member, membership - FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | - MERGE (member)-[:CANNOT_SEE]->(post))` + FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | + DELETE restriction)` } else { postRestrictionCypher = ` WITH group, member, membership - FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] | - DELETE restriction)` + FOREACH (post IN [(p:Post)-[:IN]->(group) | p] | + MERGE (member)-[:CANNOT_SEE]->(post))` } const joinGroupCypher = `