From 6703e4a311e8d18048b20ca137c644fee9672d54 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 16:41:59 +0100 Subject: [PATCH 01/13] add RemoveUserFromGroup mutation --- backend/src/schema/types/type/Group.gql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index ce90fad1d..acf585f71 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -132,4 +132,9 @@ type Mutation { userId: ID! roleInGroup: GroupMemberRole! ): User + + RemoveUserFromGroup( + groupId: ID! + userId: ID! + ): User } From de7dded19763bbf5c3a17d13ece6d1ed4fe598d9 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 16:42:49 +0100 Subject: [PATCH 02/13] add permisson for removeUserFromGroup --- .../src/middleware/permissionsMiddleware.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 9aef8646b..7e9f40246 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -253,6 +253,39 @@ const isMemberOfGroup = rule({ } }) +const canRemoveUserFromGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { groupId, userId } = args + const currentUserId = user.id + if (currentUserId === userId) return false + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (User {id: $currentUserId})-[currentUserMembership:MEMBER_OF]->(group:Group {id: $groupId}) + OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(user:User { id: $userId }) + RETURN currentUserMembership.role AS currentUserRole, userMembership.role AS userRole + `, + { currentUserId, groupId, userId }, + ) + return { + currentUserRole: transactionResponse.records.map((record) => record.get('currentUserRole'))[0], + userRole: transactionResponse.records.map((record) => record.get('userRole'))[0], + } + }) + try { + const { currentUserRole, userRole } = await readTxPromise + return currentUserRole && ['admin', 'owner'].includes(currentUserRole) + && userRole && userRole !== 'owner' + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const canCommentPost = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -382,6 +415,7 @@ export default shield( JoinGroup: isAllowedToJoinGroup, LeaveGroup: isAllowedToLeaveGroup, ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, + RemoveUserFromGroup: canRemoveUserFromGroup, CreatePost: and(isAuthenticated, isMemberOfGroup), UpdatePost: isAuthor, DeletePost: isAuthor, From 608ebb0a62b24397af310bba520482dd24b1daa2 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 16:43:24 +0100 Subject: [PATCH 03/13] remove user from group mutations --- backend/src/schema/resolvers/groups.js | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 4ea588d28..8ec0e9a4b 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -368,6 +368,37 @@ export default { session.close() } }, + RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { + const { groupId, userId } = params + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const removeUserFromGroupCypher = ` + MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + DELETE membership + WITH member AS user, group + OPTIONAL MATCH (u:User)-[:WROTE]->(p:Post)-[:IN]->(group) + WHERE NOT u.id = $userId + WITH user, collect(p) AS posts + FOREACH (post IN posts | + MERGE (user)-[:CANNOT_SEE]->(post)) + RETURN user {.*, myRoleInGroup: null}` + + const transactionResponse = await transaction.run(removeUserFromGroupCypher, { + groupId, + userId, + }) + + const [user] = await transactionResponse.records.map((record) => record.get('user')) + return user + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Group: { ...Resolver('Group', { From cd3e2ee8ad2a1a6a0f78589c7bb969dfbeaa76cb Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 17:29:48 +0100 Subject: [PATCH 04/13] test remove user from group mutation --- backend/src/graphql/groups.js | 13 ++ .../src/middleware/permissionsMiddleware.js | 14 +- backend/src/schema/resolvers/groups.spec.js | 192 ++++++++++++++++-- 3 files changed, 195 insertions(+), 24 deletions(-) diff --git a/backend/src/graphql/groups.js b/backend/src/graphql/groups.js index e388b2cd9..a7cfc3351 100644 --- a/backend/src/graphql/groups.js +++ b/backend/src/graphql/groups.js @@ -150,6 +150,19 @@ export const changeGroupMemberRoleMutation = () => { ` } +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} + // ------ queries export const groupQuery = () => { diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 7e9f40246..2e04dd4a0 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -271,19 +271,25 @@ const canRemoveUserFromGroup = rule({ { currentUserId, groupId, userId }, ) return { - currentUserRole: transactionResponse.records.map((record) => record.get('currentUserRole'))[0], + currentUserRole: transactionResponse.records.map((record) => + record.get('currentUserRole'), + )[0], userRole: transactionResponse.records.map((record) => record.get('userRole'))[0], } }) try { const { currentUserRole, userRole } = await readTxPromise - return currentUserRole && ['admin', 'owner'].includes(currentUserRole) - && userRole && userRole !== 'owner' + return ( + currentUserRole && + ['admin', 'owner'].includes(currentUserRole) && + userRole && + userRole !== 'owner' + ) } catch (error) { throw new Error(error) } finally { session.close() - } + } }) const canCommentPost = rule({ diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 3b84f4b42..e786756ea 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -6,6 +6,7 @@ import { joinGroupMutation, leaveGroupMutation, changeGroupMemberRoleMutation, + removeUserFromGroupMutation, groupMembersQuery, groupQuery, } from '../../graphql/groups' @@ -196,7 +197,6 @@ const seedComplexScenarioAndClearAuthentication = async () => { }, }) // hidden-group - authenticatedUser = await adminMemberUser.toJson() await mutate({ mutation: createGroupMutation(), variables: { @@ -214,32 +214,17 @@ const seedComplexScenarioAndClearAuthentication = async () => { mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', - userId: 'admin-member-user', - roleInGroup: 'usual', - }, - }) - await mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'hidden-group', - userId: 'second-owner-member-user', + userId: 'usual-member-user', roleInGroup: 'usual', }, }) + await mutate({ mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', userId: 'admin-member-user', - roleInGroup: 'usual', - }, - }) - await mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'hidden-group', - userId: 'second-owner-member-user', - roleInGroup: 'usual', + roleInGroup: 'admin', }, }) @@ -251,7 +236,7 @@ beforeAll(async () => { }) afterAll(async () => { - await cleanDatabase() + // await cleanDatabase() driver.close() }) @@ -2982,4 +2967,171 @@ describe('in mode', () => { }) }) }) + + describe('RemoveUserFromGroup', () => { + beforeAll(async () => { + await seedComplexScenarioAndClearAuthentication() + }) + + afterEach(async () => { + // await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('authenticated', () => { + describe('as usual member', () => { + it('throws an error', async () => { + authenticatedUser = await usualMemberUser.toJson() + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('as owner', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + it('removes the user from the group', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + data: { + RemoveUserFromGroup: expect.objectContaining({ + id: 'usual-member-user', + myRoleInGroup: null, + }), + }, + errors: undefined, + }) + }) + + it('cannot remove self', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'owner-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('as admin', () => { + beforeEach(async () => { + authenticatedUser = await adminMemberUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + roleInGroup: 'usual', + }, + }) + }) + + it('removes the user from the group', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + data: { + RemoveUserFromGroup: expect.objectContaining({ + id: 'usual-member-user', + myRoleInGroup: null, + }), + }, + errors: undefined, + }) + }) + + it('cannot remove self', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + + it('cannot remove owner', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'owner-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + }) + }) }) From 640f2519f386cf686fbdc975a85114aa816e1001 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 17:43:01 +0100 Subject: [PATCH 05/13] clean database after all --- backend/src/schema/resolvers/groups.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index e786756ea..1142b0b32 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -236,7 +236,7 @@ beforeAll(async () => { }) afterAll(async () => { - // await cleanDatabase() + await cleanDatabase() driver.close() }) @@ -2973,8 +2973,8 @@ describe('in mode', () => { await seedComplexScenarioAndClearAuthentication() }) - afterEach(async () => { - // await cleanDatabase() + afterAll(async () => { + await cleanDatabase() }) describe('unauthenticated', () => { From e384ec5ea6adb5e409502017ebbd309dda45d363 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 17:43:41 +0100 Subject: [PATCH 06/13] message for removal og group members --- webapp/locales/de.json | 1 + webapp/locales/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 3e0e7369d..aebf8d5cd 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -452,6 +452,7 @@ "message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!
Gruppe „{name}“ verlassen!", "title": "Möchtest du wirklich die Gruppe verlassen?" }, + "memberRemoved": "Nutzer „{name}“ wurde aus der Gruppe entfernt", "members": "Mitglieder", "membersAdministrationList": { "avatar": "Avatar", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 9e45396b8..8f82b4578 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -452,6 +452,7 @@ "message": "Leaving a group may be irreversible!
Leave group “{name}”!", "title": "Do you really want to leave the group?" }, + "memberRemoved": "User “{name}” was removed from group", "members": "Members", "membersAdministrationList": { "avatar": "Avatar", From 6a84a020273868431c841099c6916c62d537afc9 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 17:46:20 +0100 Subject: [PATCH 07/13] add remove user from group mutation --- webapp/graphql/groups.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index cf64c21b3..bb5292675 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -143,6 +143,19 @@ export const changeGroupMemberRoleMutation = () => { ` } +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} + // ------ queries export const groupQuery = (i18n) => { From c4d7c20b27848f32592a1d6a82007fc1e48f0bca Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 17:55:22 +0100 Subject: [PATCH 08/13] remove user from group implemented --- webapp/components/Group/GroupMember.vue | 58 +++++++++++++++++-------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/webapp/components/Group/GroupMember.vue b/webapp/components/Group/GroupMember.vue index bacd0259d..c0cdc67ea 100644 --- a/webapp/components/Group/GroupMember.vue +++ b/webapp/components/Group/GroupMember.vue @@ -53,30 +53,33 @@ - - - + From e298a29d8b7b141a249acb729e426ea253d3ee9b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 3 Mar 2023 18:38:41 +0100 Subject: [PATCH 09/13] test remove user from group --- webapp/components/Group/GroupMember.spec.js | 162 +++++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/webapp/components/Group/GroupMember.spec.js b/webapp/components/Group/GroupMember.spec.js index 0c0b46e43..ef8b96568 100644 --- a/webapp/components/Group/GroupMember.spec.js +++ b/webapp/components/Group/GroupMember.spec.js @@ -1,26 +1,65 @@ import { mount } from '@vue/test-utils' import GroupMember from './GroupMember.vue' +import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js' const localVue = global.localVue const propsData = { - groupId: '', - groupMembers: [], + groupId: 'group-id', + groupMembers: [ + { + slug: 'owner', + id: 'owner', + myRoleInGroup: 'owner', + }, + { + slug: 'user', + id: 'user', + myRoleInGroup: 'usual', + }, + ], } +const stubs = { + 'nuxt-link': true, +} + +const apolloMock = jest + .fn() + .mockRejectedValueOnce({ message: 'Oh no!' }) + .mockResolvedValue({ + data: { + ChangeGroupMemberRole: { + slug: 'user', + id: 'user', + myRoleInGroup: 'admin', + }, + }, + }) + +const toastErrorMock = jest.fn() +const toastSuccessMock = jest.fn() + describe('GroupMember', () => { let wrapper let mocks beforeEach(() => { mocks = { - $t: jest.fn(), + $t: jest.fn((t) => t), + $apollo: { + mutate: apolloMock, + }, + $toast: { + error: toastErrorMock, + success: toastSuccessMock, + }, } }) describe('mount', () => { const Wrapper = () => { - return mount(GroupMember, { propsData, mocks, localVue }) + return mount(GroupMember, { propsData, mocks, localVue, stubs }) } beforeEach(() => { @@ -30,5 +69,120 @@ describe('GroupMember', () => { it('renders', () => { expect(wrapper.findAll('.group-member')).toHaveLength(1) }) + + it('has two users in table', () => { + expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2) + }) + + it('has no modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + + describe('change user role', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper + .find('tbody') + .findAll('tr') + .at(1) + .find('select') + .findAll('option') + .at(2) + .setSelected() + wrapper.find('tbody').findAll('tr').at(1).find('select').trigger('change') + }) + + describe('with server error', () => { + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Oh no!') + }) + }) + + describe('with server success', () => { + it('calls the API', () => { + expect(apolloMock).toBeCalledWith({ + mutation: changeGroupMemberRoleMutation(), + variables: { groupId: 'group-id', userId: 'user', roleInGroup: 'admin' }, + }) + }) + + it('toasts a success message', () => { + expect(toastSuccessMock).toBeCalledWith('group.changeMemberRole') + }) + }) + }) + + describe('click remove user', () => { + beforeAll(() => { + apolloMock.mockRejectedValueOnce({ message: 'Oh no!!' }).mockResolvedValue({ + data: { + RemoveUserFromGroup: { + slug: 'user', + id: 'user', + myRoleInGroup: null, + }, + }, + }) + }) + + beforeEach(() => { + wrapper = Wrapper() + wrapper.find('tbody').findAll('tr').at(1).find('button').trigger('click') + }) + + it('opens the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').isVisible()).toBe(true) + }) + + describe('click on cancel', () => { + beforeEach(() => { + wrapper.find('div.ds-modal-wrapper').find('button.ds-button-ghost').trigger('click') + }) + + it('closes the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + }) + + describe('click on confirm with server error', () => { + beforeEach(() => { + wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Oh no!!') + }) + + it('closes the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + }) + + describe('click on confirm with success', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click') + }) + + it('calls the API', () => { + expect(apolloMock).toBeCalledWith({ + mutation: removeUserFromGroupMutation(), + variables: { groupId: 'group-id', userId: 'user' }, + }) + }) + + it('emits load group members', () => { + expect(wrapper.emitted('loadGroupMembers')).toBeTruthy() + }) + + it('toasts a success message', () => { + expect(toastSuccessMock).toBeCalledWith('group.memberRemoved') + }) + + it('closes the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + }) + }) }) }) From 6d09dbcf08c4d6be28bb7eae443eb64389dfbe4d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 6 Mar 2023 13:47:14 +0100 Subject: [PATCH 10/13] Update webapp/locales/de.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Wolfgang Huß --- webapp/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index aebf8d5cd..67ba5aaa4 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -452,7 +452,7 @@ "message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!
Gruppe „{name}“ verlassen!", "title": "Möchtest du wirklich die Gruppe verlassen?" }, - "memberRemoved": "Nutzer „{name}“ wurde aus der Gruppe entfernt", + "memberRemoved": "Nutzer „{name}“ wurde aus der Gruppe entfernt!", "members": "Mitglieder", "membersAdministrationList": { "avatar": "Avatar", From eb0bc971ecbe6129dc945ae7d6ce99073b77e521 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 6 Mar 2023 13:47:28 +0100 Subject: [PATCH 11/13] Update webapp/locales/en.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Wolfgang Huß --- webapp/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 8f82b4578..31784ac2e 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -452,7 +452,7 @@ "message": "Leaving a group may be irreversible!
Leave group “{name}”!", "title": "Do you really want to leave the group?" }, - "memberRemoved": "User “{name}” was removed from group", + "memberRemoved": "User “{name}” was removed from group!", "members": "Members", "membersAdministrationList": { "avatar": "Avatar", From db8ad8897e542aad48a461fc26baa5645f6495e4 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 6 Mar 2023 14:11:55 +0100 Subject: [PATCH 12/13] same cypher for removeUserFromGroup and leaveGroup, adjust tests, admin cannot remove users fro group --- .../src/middleware/permissionsMiddleware.js | 5 +- backend/src/schema/resolvers/groups.js | 65 ++++++++----------- backend/src/schema/resolvers/groups.spec.js | 21 ++++++ .../schema/resolvers/postsInGroups.spec.js | 32 +++++++-- 4 files changed, 74 insertions(+), 49 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 2e04dd4a0..00a34f9ab 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -280,10 +280,7 @@ const canRemoveUserFromGroup = rule({ try { const { currentUserRole, userRole } = await readTxPromise return ( - currentUserRole && - ['admin', 'owner'].includes(currentUserRole) && - userRole && - userRole !== 'owner' + currentUserRole && ['owner'].includes(currentUserRole) && userRole && userRole !== 'owner' ) } catch (error) { throw new Error(error) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 8ec0e9a4b..4a13dcc88 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -295,25 +295,8 @@ export default { LeaveGroup: async (_parent, params, context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const leaveGroupCypher = ` - MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) - DELETE membership - WITH member, group - 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} - ` - - const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) - return member - }) try { - return await writeTxResultPromise + return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) } catch (error) { throw new Error(error) } finally { @@ -371,28 +354,8 @@ export default { RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const removeUserFromGroupCypher = ` - MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) - DELETE membership - WITH member AS user, group - OPTIONAL MATCH (u:User)-[:WROTE]->(p:Post)-[:IN]->(group) - WHERE NOT u.id = $userId - WITH user, collect(p) AS posts - FOREACH (post IN posts | - MERGE (user)-[:CANNOT_SEE]->(post)) - RETURN user {.*, myRoleInGroup: null}` - - const transactionResponse = await transaction.run(removeUserFromGroupCypher, { - groupId, - userId, - }) - - const [user] = await transactionResponse.records.map((record) => record.get('user')) - return user - }) try { - return await writeTxResultPromise + return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) } catch (error) { throw new Error(error) } finally { @@ -414,3 +377,27 @@ export default { }), }, } + +const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) => { + return session.writeTransaction(async (transaction) => { + const removeUserFromGroupCypher = ` + MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + DELETE membership + WITH user, group + OPTIONAL MATCH (author:User)-[:WROTE]->(p:Post)-[:IN]->(group) + WHERE NOT group.groupType = 'public' + AND NOT author.id = $userId + WITH user, collect(p) AS posts + FOREACH (post IN posts | + MERGE (user)-[:CANNOT_SEE]->(post)) + RETURN user {.*, myRoleInGroup: NULL} + ` + + const transactionResponse = await transaction.run(removeUserFromGroupCypher, { + groupId, + userId, + }) + const [user] = await transactionResponse.records.map((record) => record.get('user')) + return user + }) +} diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 1142b0b32..13291383d 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -3076,6 +3076,26 @@ describe('in mode', () => { }) }) + it('throws an error', async () => { + authenticatedUser = await usualMemberUser.toJson() + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + + /* it('removes the user from the group', async () => { await expect( mutate({ @@ -3131,6 +3151,7 @@ describe('in mode', () => { ]), }) }) + */ }) }) }) diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 5bf5820f0..404c3f25f 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -1524,9 +1524,9 @@ describe('Posts in Groups', () => { }) }) - it('does not show the posts of the closed group anymore', async () => { + it('stil shows the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(3) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1540,6 +1540,11 @@ describe('Posts in Groups', () => { 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', @@ -1564,9 +1569,9 @@ describe('Posts in Groups', () => { }) }) - it('does only show the public posts', async () => { + it('still shows the post of the hidden group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(2) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1580,6 +1585,16 @@ describe('Posts in Groups', () => { 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, @@ -1603,9 +1618,9 @@ describe('Posts in Groups', () => { authenticatedUser = await allGroupsUser.toJson() }) - it('does not show the posts of the closed group', async () => { + it('shows the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(3) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1624,6 +1639,11 @@ describe('Posts in Groups', () => { 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 7fcb9a0ff6c9aec01858d90420aeea48e7faed93 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 7 Mar 2023 22:33:14 +0100 Subject: [PATCH 13/13] use base button --- webapp/components/Group/GroupMember.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/components/Group/GroupMember.vue b/webapp/components/Group/GroupMember.vue index c0cdc67ea..d6c09746e 100644 --- a/webapp/components/Group/GroupMember.vue +++ b/webapp/components/Group/GroupMember.vue @@ -53,7 +53,7 @@