diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index 2a611f324..c6f110ed1 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -39,6 +39,28 @@ export const createGroupMutation = gql` } ` +export const joinGroupMutation = gql` + mutation ($groupId: ID!, $userId: ID!) { + JoinGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } +` + +export const changeGroupMemberRoleMutation = gql` + mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { + ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { + id + name + slug + myRoleInGroup + } + } +` + // ------ queries export const groupQuery = gql` @@ -93,3 +115,14 @@ export const groupQuery = gql` } } ` + +export const groupMembersQuery = gql` + query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) { + GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) { + id + name + slug + myRoleInGroup + } + } +` diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index e41ef1abc..a71b11131 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -5,7 +5,11 @@ import createServer from '../server' import faker from '@faker-js/faker' import Factory from '../db/factories' import { getNeode, getDriver } from '../db/neo4j' -import { createGroupMutation } from './graphql/groups' +import { + createGroupMutation, + joinGroupMutation, + changeGroupMemberRoleMutation, +} from './graphql/groups' import { createPostMutation } from './graphql/posts' import { createCommentMutation } from './graphql/comments' @@ -400,6 +404,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }), ]) + await Promise.all([ + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g0', + userId: 'u2', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g0', + userId: 'u3', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g0', + userId: 'u4', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g0', + userId: 'u6', + }, + }), + ]) + await Promise.all([ + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u2', + roleInGroup: 'usual', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u4', + roleInGroup: 'admin', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u3', + roleInGroup: 'owner', + }, + }), + ]) authenticatedUser = await jennyRostock.toJson() await Promise.all([ @@ -416,6 +476,77 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }), ]) + await Promise.all([ + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g1', + userId: 'u1', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g1', + userId: 'u2', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g1', + userId: 'u5', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g1', + userId: 'u6', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g1', + userId: 'u7', + }, + }), + ]) + await Promise.all([ + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u1', + roleInGroup: 'usual', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u2', + roleInGroup: 'usual', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u5', + roleInGroup: 'admin', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u6', + roleInGroup: 'owner', + }, + }), + ]) authenticatedUser = await bobDerBaumeister.toJson() await Promise.all([ @@ -432,6 +563,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }), ]) + await Promise.all([ + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g2', + userId: 'u4', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g2', + userId: 'u5', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g2', + userId: 'u6', + }, + }), + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'g2', + userId: 'u7', + }, + }), + ]) + await Promise.all([ + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u4', + roleInGroup: 'usual', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u5', + roleInGroup: 'usual', + }, + }), + mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'g0', + userId: 'u6', + roleInGroup: 'usual', + }, + }), + ]) // Create Posts diff --git a/backend/src/helpers/jest.js b/backend/src/helpers/jest.js index ecfc1a042..e3f6a3c84 100644 --- a/backend/src/helpers/jest.js +++ b/backend/src/helpers/jest.js @@ -7,3 +7,12 @@ export function gql(strings) { return strings.join('') } + +// sometime we have to wait to check a db state by having a look into the db in a certain moment +// or we wait a bit to check if we missed to set an await somewhere +// see: https://www.sitepoint.com/delay-sleep-pause-wait/ +export function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} +// usage – 4 seconds for example +// await sleep(4 * 1000) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index c81b069d2..cf57aae85 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -52,6 +52,115 @@ const isMySocialMedia = rule({ return socialMedia.ownedBy.node.id === user.id }) +const isAllowedSeeingMembersOfGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { id: groupId } = args + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group {id: $groupId}) + OPTIONAL MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group) + RETURN group {.*}, member {.*, myRoleInGroup: membership.role} + `, + { groupId, userId: user.id }, + ) + return { + member: transactionResponse.records.map((record) => record.get('member'))[0], + group: transactionResponse.records.map((record) => record.get('group'))[0], + } + }) + try { + const { member, group } = await readTxPromise + return ( + !!group && + (group.groupType === 'public' || + (['closed', 'hidden'].includes(group.groupType) && + !!member && + ['usual', 'admin', 'owner'].includes(member.myRoleInGroup))) + ) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + +const isAllowedToChangeGroupMemberRole = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const adminId = user.id + const { groupId, userId, roleInGroup } = args + if (adminId === userId) return false + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (admin:User {id: $adminId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId}) + OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(member:User {id: $userId}) + RETURN group {.*}, admin {.*, myRoleInGroup: adminMembership.role}, member {.*, myRoleInGroup: userMembership.role} + `, + { groupId, adminId, userId }, + ) + return { + admin: transactionResponse.records.map((record) => record.get('admin'))[0], + group: transactionResponse.records.map((record) => record.get('group'))[0], + member: transactionResponse.records.map((record) => record.get('member'))[0], + } + }) + try { + const { admin, group, member } = await readTxPromise + return ( + !!group && + !!admin && + (!member || + (!!member && + (member.myRoleInGroup === roleInGroup || !['owner'].includes(member.myRoleInGroup)))) && + ((['admin'].includes(admin.myRoleInGroup) && + ['pending', 'usual', 'admin'].includes(roleInGroup)) || + (['owner'].includes(admin.myRoleInGroup) && + ['pending', 'usual', 'admin', 'owner'].includes(roleInGroup))) + ) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + +const isAllowedToJoinGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { groupId, userId } = args + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group {id: $groupId}) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(member:User {id: $userId}) + RETURN group {.*}, member {.*, myRoleInGroup: membership.role} + `, + { groupId, userId }, + ) + return { + group: transactionResponse.records.map((record) => record.get('group'))[0], + member: transactionResponse.records.map((record) => record.get('member'))[0], + } + }) + try { + const { group, member } = await readTxPromise + return !!group && (group.groupType !== 'hidden' || (!!member && !!member.myRoleInGroup)) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -78,7 +187,7 @@ const isAuthor = rule({ const isDeletingOwnAccount = rule({ cache: 'no_cache', -})(async (parent, args, context, info) => { +})(async (parent, args, context, _info) => { return context.user.id === args.id }) @@ -115,6 +224,7 @@ export default shield( statistics: allow, currentUser: allow, Group: isAuthenticated, + GroupMembers: isAllowedSeeingMembersOfGroup, Post: allow, profilePagePosts: allow, Comment: allow, @@ -142,6 +252,8 @@ export default shield( SignupVerification: allow, UpdateUser: onlyYourself, CreateGroup: isAuthenticated, + JoinGroup: isAllowedToJoinGroup, + ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 5737f5505..abaa1716f 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -46,6 +46,27 @@ export default { session.close() } }, + GroupMembers: async (_object, params, context, _resolveInfo) => { + const { id: groupId } = params + const session = context.driver.session() + const readTxResultPromise = session.readTransaction(async (txc) => { + const groupMemberCypher = ` + MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId}) + RETURN user {.*, myRoleInGroup: membership.role} + ` + const result = await txc.run(groupMemberCypher, { + groupId, + }) + return result.records.map((record) => record.get('user')) + }) + try { + return await readTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Mutation: { CreateGroup: async (_parent, params, context, _resolveInfo) => { @@ -86,9 +107,10 @@ export default { MATCH (owner:User {id: $userId}) MERGE (owner)-[:CREATED]->(group) MERGE (owner)-[membership:MEMBER_OF]->(group) - SET membership.createdAt = toString(datetime()) - SET membership.updatedAt = toString(datetime()) - SET membership.role = 'owner' + SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = 'owner' ${categoriesCypher} RETURN group {.*, myRole: membership.role} `, @@ -100,8 +122,7 @@ export default { return group }) try { - const group = await writeTxResultPromise - return group + return await writeTxResultPromise } catch (error) { if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') throw new UserInputError('Group with this slug already exists!') @@ -110,6 +131,63 @@ export default { session.close() } }, + JoinGroup: async (_parent, params, context, _resolveInfo) => { + const { groupId, userId } = params + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const joinGroupCypher = ` + MATCH (member:User {id: $userId}), (group:Group {id: $groupId}) + MERGE (member)-[membership:MEMBER_OF]->(group) + ON CREATE SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = + CASE WHEN group.groupType = 'public' + THEN 'usual' + ELSE 'pending' + END + RETURN member {.*, myRoleInGroup: membership.role} + ` + const result = await transaction.run(joinGroupCypher, { groupId, userId }) + const [member] = await result.records.map((record) => record.get('member')) + return member + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => { + const { groupId, userId, roleInGroup } = params + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const joinGroupCypher = ` + MATCH (member:User {id: $userId}), (group:Group {id: $groupId}) + MERGE (member)-[membership:MEMBER_OF]->(group) + ON CREATE SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = $roleInGroup + ON MATCH SET + membership.updatedAt = toString(datetime()), + membership.role = $roleInGroup + RETURN member {.*, myRoleInGroup: membership.role} + ` + const result = await transaction.run(joinGroupCypher, { groupId, userId, roleInGroup }) + const [member] = await result.records.map((record) => record.get('member')) + return member + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Group: { ...Resolver('Group', { diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 6890e9147..1d272de2b 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -1,6 +1,12 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { createGroupMutation, groupQuery } from '../../db/graphql/groups' +import { + createGroupMutation, + joinGroupMutation, + changeGroupMemberRoleMutation, + groupMembersQuery, + groupQuery, +} from '../../db/graphql/groups' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import CONFIG from '../../config' @@ -8,8 +14,6 @@ import CONFIG from '../../config' const driver = getDriver() const neode = getNeode() -let query -let mutate let authenticatedUser let user @@ -18,27 +22,18 @@ const descriptionAdditional100 = ' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789' let variables = {} -beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate +const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, }) +const { mutate, query } = createTestClient(server) -afterAll(async () => { - await cleanDatabase() -}) - -beforeEach(async () => { +const seedBasicsAndClearAuthentication = async () => { variables = {} user = await Factory.build( 'user', @@ -78,241 +73,1973 @@ beforeEach(async () => { }), ]) authenticatedUser = null -}) +} -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 -afterEach(async () => { +beforeAll(async () => { await cleanDatabase() }) -describe('Group', () => { - describe('unauthenticated', () => { - it('throws authorization error', async () => { - const { errors } = await query({ query: groupQuery, variables: {} }) - expect(errors[0]).toHaveProperty('message', 'Not Authorized!') - }) - }) +afterAll(async () => { + await cleanDatabase() +}) - describe('authenticated', () => { +describe('in mode', () => { + describe('clean db after each single test', () => { beforeEach(async () => { - authenticatedUser = await user.toJson() + await seedBasicsAndClearAuthentication() }) - let otherUser + // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 + afterEach(async () => { + await cleanDatabase() + }) - beforeEach(async () => { - otherUser = await Factory.build( - 'user', - { - id: 'other-user', - name: 'Other TestUser', - }, - { - email: 'test2@example.org', - password: '1234', - }, - ) - authenticatedUser = await otherUser.toJson() - await mutate({ - mutation: createGroupMutation, - variables: { - id: 'others-group', - name: 'Uninteresting Group', - about: 'We will change nothing!', - description: 'We love it like it is!?' + descriptionAdditional100, - groupType: 'closed', - actionRadius: 'global', - categoryIds, - }, - }) - authenticatedUser = await user.toJson() - await mutate({ - mutation: createGroupMutation, - variables: { - id: 'my-group', + describe('CreateGroup', () => { + beforeEach(() => { + variables = { + ...variables, + id: 'g589', name: 'The Best Group', + slug: 'the-group', about: 'We will change the world!', description: 'Some description' + descriptionAdditional100, groupType: 'public', actionRadius: 'regional', categoryIds, - }, + } }) - }) - describe('query groups', () => { - describe('without any filters', () => { - it('finds all groups', async () => { - const expected = { - data: { - Group: expect.arrayContaining([ - expect.objectContaining({ - id: 'my-group', - slug: 'the-best-group', - myRole: 'owner', - }), - expect.objectContaining({ - id: 'others-group', - slug: 'uninteresting-group', - myRole: null, - }), - ]), - }, - errors: undefined, - } - await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject(expected) + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const { errors } = await mutate({ mutation: createGroupMutation, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) - describe('isMember = true', () => { - it('finds only groups where user is member', async () => { - const expected = { - data: { - Group: [ - { - id: 'my-group', - slug: 'the-best-group', + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('creates a group', async () => { + await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject( + { + data: { + CreateGroup: { + name: 'The Best Group', + slug: 'the-group', + about: 'We will change the world!', + }, + }, + errors: undefined, + }, + ) + }) + + it('assigns the authenticated user as owner', async () => { + await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject( + { + data: { + CreateGroup: { + name: 'The Best Group', myRole: 'owner', }, - ], - }, - errors: undefined, - } - await expect( - query({ query: groupQuery, variables: { isMember: true } }), - ).resolves.toMatchObject(expected) - }) - }) - - describe('isMember = false', () => { - it('finds only groups where user is not(!) member', async () => { - const expected = { - data: { - Group: expect.arrayContaining([ - expect.objectContaining({ - id: 'others-group', - slug: 'uninteresting-group', - myRole: null, - }), - ]), - }, - errors: undefined, - } - await expect( - query({ query: groupQuery, variables: { isMember: false } }), - ).resolves.toMatchObject(expected) - }) - }) - }) - }) -}) - -describe('CreateGroup', () => { - beforeEach(() => { - variables = { - ...variables, - id: 'g589', - name: 'The Best Group', - slug: 'the-group', - about: 'We will change the world!', - description: 'Some description' + descriptionAdditional100, - groupType: 'public', - actionRadius: 'regional', - categoryIds, - } - }) - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - const { errors } = await mutate({ mutation: createGroupMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorized!') - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - authenticatedUser = await user.toJson() - }) - - it('creates a group', async () => { - const expected = { - data: { - CreateGroup: { - name: 'The Best Group', - slug: 'the-group', - about: 'We will change the world!', - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('assigns the authenticated user as owner', async () => { - const expected = { - data: { - CreateGroup: { - name: 'The Best Group', - myRole: 'owner', - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('has "disabled" and "deleted" default to "false"', async () => { - const expected = { data: { CreateGroup: { disabled: false, deleted: false } } } - await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - describe('description', () => { - describe('length without HTML', () => { - describe('less then 100 chars', () => { - it('throws error: "Too view categories!"', async () => { - const { errors } = await mutate({ - mutation: createGroupMutation, - variables: { - ...variables, - description: - '0123456789' + - '0123456789', }, + errors: undefined, + }, + ) + }) + + it('has "disabled" and "deleted" default to "false"', async () => { + await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject( + { + data: { CreateGroup: { disabled: false, deleted: false } }, + }, + ) + }) + + describe('description', () => { + describe('length without HTML', () => { + describe('less then 100 chars', () => { + it('throws error: "Too view categories!"', async () => { + const { errors } = await mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + description: + '0123456789' + + '0123456789', + }, + }) + expect(errors[0]).toHaveProperty('message', 'Description too short!') + }) + }) + }) + }) + + describe('categories', () => { + beforeEach(() => { + CONFIG.CATEGORIES_ACTIVE = true + }) + + describe('not even one', () => { + it('throws error: "Too view categories!"', async () => { + const { errors } = await mutate({ + mutation: createGroupMutation, + variables: { ...variables, categoryIds: null }, + }) + expect(errors[0]).toHaveProperty('message', 'Too view categories!') + }) + }) + + describe('four', () => { + it('throws error: "Too many categories!"', async () => { + const { errors } = await mutate({ + mutation: createGroupMutation, + variables: { ...variables, categoryIds: ['cat9', 'cat4', 'cat15', 'cat27'] }, + }) + expect(errors[0]).toHaveProperty('message', 'Too many categories!') + }) + }) + }) + }) + }) + }) + + describe('building up – clean db after each resolver', () => { + describe('Group', () => { + beforeAll(async () => { + await seedBasicsAndClearAuthentication() + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const { errors } = await query({ query: groupQuery, variables: {} }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('authenticated', () => { + let otherUser + + beforeAll(async () => { + otherUser = await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other TestUser', + }, + { + email: 'test2@example.org', + password: '1234', + }, + ) + authenticatedUser = await otherUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'others-group', + name: 'Uninteresting Group', + about: 'We will change nothing!', + description: 'We love it like it is!?' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'global', + categoryIds, + }, + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'my-group', + name: 'The Best Group', + about: 'We will change the world!', + description: 'Some description' + descriptionAdditional100, + groupType: 'public', + actionRadius: 'regional', + categoryIds, + }, + }) + }) + + describe('query groups', () => { + describe('without any filters', () => { + it('finds all groups', async () => { + await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }), + expect.objectContaining({ + id: 'others-group', + slug: 'uninteresting-group', + myRole: null, + }), + ]), + }, + errors: undefined, + }) + }) + }) + + describe('isMember = true', () => { + it('finds only groups where user is member', async () => { + await expect( + query({ query: groupQuery, variables: { isMember: true } }), + ).resolves.toMatchObject({ + data: { + Group: [ + { + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }, + ], + }, + errors: undefined, + }) + }) + }) + + describe('isMember = false', () => { + it('finds only groups where user is not(!) member', async () => { + await expect( + query({ query: groupQuery, variables: { isMember: false } }), + ).resolves.toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'others-group', + slug: 'uninteresting-group', + myRole: null, + }), + ]), + }, + errors: undefined, + }) }) - expect(errors[0]).toHaveProperty('message', 'Description too short!') }) }) }) }) - describe('categories', () => { - beforeEach(() => { - CONFIG.CATEGORIES_ACTIVE = true + describe('JoinGroup', () => { + beforeAll(async () => { + await seedBasicsAndClearAuthentication() }) - describe('not even one', () => { - it('throws error: "Too view categories!"', async () => { + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { const { errors } = await mutate({ - mutation: createGroupMutation, - variables: { ...variables, categoryIds: null }, + mutation: joinGroupMutation, + variables: { + groupId: 'not-existing-group', + userId: 'current-user', + }, }) - expect(errors[0]).toHaveProperty('message', 'Too view categories!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) - describe('four', () => { - it('throws error: "Too many categories!"', async () => { - const { errors } = await mutate({ + describe('authenticated', () => { + let ownerOfClosedGroupUser + let ownerOfHiddenGroupUser + + beforeAll(async () => { + // create users + ownerOfClosedGroupUser = await Factory.build( + 'user', + { + id: 'owner-of-closed-group', + name: 'Owner Of Closed Group', + }, + { + email: 'owner-of-closed-group@example.org', + password: '1234', + }, + ) + ownerOfHiddenGroupUser = await Factory.build( + 'user', + { + id: 'owner-of-hidden-group', + name: 'Owner Of Hidden Group', + }, + { + email: 'owner-of-hidden-group@example.org', + password: '1234', + }, + ) + // create groups + // public-group + authenticatedUser = await ownerOfClosedGroupUser.toJson() + await mutate({ mutation: createGroupMutation, - variables: { ...variables, categoryIds: ['cat9', 'cat4', 'cat15', 'cat27'] }, + variables: { + id: 'closed-group', + name: 'Uninteresting Group', + about: 'We will change nothing!', + description: 'We love it like it is!?' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + }, + }) + authenticatedUser = await ownerOfHiddenGroupUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'hidden-group', + name: 'Investigative Journalism Group', + about: 'We will change all.', + description: 'We research …' + descriptionAdditional100, + groupType: 'hidden', + actionRadius: 'global', + categoryIds, + }, + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'public-group', + name: 'The Best Group', + about: 'We will change the world!', + description: 'Some description' + descriptionAdditional100, + groupType: 'public', + actionRadius: 'regional', + categoryIds, + }, + }) + }) + + describe('public group', () => { + describe('joined by "owner-of-closed-group"', () => { + it('has "usual" as membership role', async () => { + await expect( + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'public-group', + userId: 'owner-of-closed-group', + }, + }), + ).resolves.toMatchObject({ + data: { + JoinGroup: { + id: 'owner-of-closed-group', + myRoleInGroup: 'usual', + }, + }, + errors: undefined, + }) + }) + }) + + describe('joined by its owner', () => { + describe('does not create additional "MEMBER_OF" relation and therefore', () => { + it('has still "owner" as membership role', async () => { + await expect( + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'public-group', + userId: 'current-user', + }, + }), + ).resolves.toMatchObject({ + data: { + JoinGroup: { + id: 'current-user', + myRoleInGroup: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + }) + }) + + describe('closed group', () => { + describe('joined by "current-user"', () => { + it('has "pending" as membership role', async () => { + await expect( + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'closed-group', + userId: 'current-user', + }, + }), + ).resolves.toMatchObject({ + data: { + JoinGroup: { + id: 'current-user', + myRoleInGroup: 'pending', + }, + }, + errors: undefined, + }) + }) + }) + + describe('joined by its owner', () => { + describe('does not create additional "MEMBER_OF" relation and therefore', () => { + it('has still "owner" as membership role', async () => { + await expect( + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'closed-group', + userId: 'owner-of-closed-group', + }, + }), + ).resolves.toMatchObject({ + data: { + JoinGroup: { + id: 'owner-of-closed-group', + myRoleInGroup: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + }) + }) + + describe('hidden group', () => { + describe('joined by "owner-of-closed-group"', () => { + it('throws authorization error', async () => { + const { errors } = await query({ + query: joinGroupMutation, + variables: { + groupId: 'hidden-group', + userId: 'owner-of-closed-group', + }, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('joined by its owner', () => { + describe('does not create additional "MEMBER_OF" relation and therefore', () => { + it('has still "owner" as membership role', async () => { + await expect( + mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'hidden-group', + userId: 'owner-of-hidden-group', + }, + }), + ).resolves.toMatchObject({ + data: { + JoinGroup: { + id: 'owner-of-hidden-group', + myRoleInGroup: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + }) + }) + }) + }) + + describe('GroupMembers', () => { + beforeAll(async () => { + await seedBasicsAndClearAuthentication() + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + variables = { + id: 'not-existing-group', + } + const { errors } = await query({ query: groupMembersQuery, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('authenticated', () => { + let otherUser + let pendingUser + let ownerOfClosedGroupUser + let ownerOfHiddenGroupUser + + beforeAll(async () => { + // create users + otherUser = await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other TestUser', + }, + { + email: 'other-user@example.org', + password: '1234', + }, + ) + pendingUser = await Factory.build( + 'user', + { + id: 'pending-user', + name: 'Pending TestUser', + }, + { + email: 'pending@example.org', + password: '1234', + }, + ) + ownerOfClosedGroupUser = await Factory.build( + 'user', + { + id: 'owner-of-closed-group', + name: 'Owner Of Closed Group', + }, + { + email: 'owner-of-closed-group@example.org', + password: '1234', + }, + ) + ownerOfHiddenGroupUser = await Factory.build( + 'user', + { + id: 'owner-of-hidden-group', + name: 'Owner Of Hidden Group', + }, + { + email: 'owner-of-hidden-group@example.org', + password: '1234', + }, + ) + // create groups + // public-group + authenticatedUser = await user.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'public-group', + name: 'The Best Group', + about: 'We will change the world!', + description: 'Some description' + descriptionAdditional100, + groupType: 'public', + actionRadius: 'regional', + categoryIds, + }, + }) + await mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'public-group', + userId: 'owner-of-closed-group', + }, + }) + await mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'public-group', + userId: 'owner-of-hidden-group', + }, + }) + // closed-group + authenticatedUser = await ownerOfClosedGroupUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'closed-group', + name: 'Uninteresting Group', + about: 'We will change nothing!', + description: 'We love it like it is!?' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + }, + }) + await mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'closed-group', + userId: 'current-user', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'closed-group', + userId: 'owner-of-hidden-group', + roleInGroup: 'usual', + }, + }) + // hidden-group + authenticatedUser = await ownerOfHiddenGroupUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'hidden-group', + name: 'Investigative Journalism Group', + about: 'We will change all.', + description: 'We research …' + descriptionAdditional100, + groupType: 'hidden', + actionRadius: 'global', + categoryIds, + }, + }) + // 'JoinGroup' mutation does not work in hidden groups so we join them by 'ChangeGroupMemberRole' through the owner + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'hidden-group', + userId: 'pending-user', + roleInGroup: 'pending', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'hidden-group', + userId: 'current-user', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'hidden-group', + userId: 'owner-of-closed-group', + roleInGroup: 'admin', + }, + }) + + authenticatedUser = null + }) + + describe('public group', () => { + beforeEach(async () => { + variables = { + id: 'public-group', + } + }) + + describe('query group members', () => { + describe('by owner "current-user"', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'owner', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'usual', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'usual', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(3) + }) + }) + + describe('by usual member "owner-of-closed-group"', () => { + beforeEach(async () => { + authenticatedUser = await ownerOfClosedGroupUser.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'owner', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'usual', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'usual', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(3) + }) + }) + + describe('by none member "other-user"', () => { + beforeEach(async () => { + authenticatedUser = await otherUser.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'owner', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'usual', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'usual', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(3) + }) + }) + }) + }) + + describe('closed group', () => { + beforeEach(async () => { + variables = { + id: 'closed-group', + } + }) + + describe('query group members', () => { + describe('by owner "owner-of-closed-group"', () => { + beforeEach(async () => { + authenticatedUser = await ownerOfClosedGroupUser.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'pending', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'owner', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'usual', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(3) + }) + }) + + describe('by usual member "owner-of-hidden-group"', () => { + beforeEach(async () => { + authenticatedUser = await ownerOfHiddenGroupUser.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'pending', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'owner', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'usual', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(3) + }) + }) + + describe('by pending member "current-user"', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('throws authorization error', async () => { + const { errors } = await query({ query: groupMembersQuery, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('by none member "other-user"', () => { + beforeEach(async () => { + authenticatedUser = await otherUser.toJson() + }) + + it('throws authorization error', async () => { + const { errors } = await query({ query: groupMembersQuery, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) + + describe('hidden group', () => { + beforeEach(async () => { + variables = { + id: 'hidden-group', + } + }) + + describe('query group members', () => { + describe('by owner "owner-of-hidden-group"', () => { + beforeEach(async () => { + authenticatedUser = await ownerOfHiddenGroupUser.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'pending-user', + myRoleInGroup: 'pending', + }), + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'usual', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'admin', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'owner', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(4) + }) + }) + + describe('by usual member "current-user"', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'pending-user', + myRoleInGroup: 'pending', + }), + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'usual', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'admin', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'owner', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(4) + }) + }) + + describe('by admin member "owner-of-closed-group"', () => { + beforeEach(async () => { + authenticatedUser = await ownerOfClosedGroupUser.toJson() + }) + + it('finds all members', async () => { + const result = await mutate({ + mutation: groupMembersQuery, + variables, + }) + expect(result).toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + expect.objectContaining({ + id: 'pending-user', + myRoleInGroup: 'pending', + }), + expect.objectContaining({ + id: 'current-user', + myRoleInGroup: 'usual', + }), + expect.objectContaining({ + id: 'owner-of-closed-group', + myRoleInGroup: 'admin', + }), + expect.objectContaining({ + id: 'owner-of-hidden-group', + myRoleInGroup: 'owner', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.GroupMembers.length).toBe(4) + }) + }) + + describe('by pending member "pending-user"', () => { + beforeEach(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('throws authorization error', async () => { + const { errors } = await query({ query: groupMembersQuery, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('by none member "other-user"', () => { + beforeEach(async () => { + authenticatedUser = await otherUser.toJson() + }) + + it('throws authorization error', async () => { + const { errors } = await query({ query: groupMembersQuery, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) + }) + }) + + describe('ChangeGroupMemberRole', () => { + let pendingMemberUser + let usualMemberUser + let adminMemberUser + let ownerMemberUser + let secondOwnerMemberUser + + beforeAll(async () => { + await seedBasicsAndClearAuthentication() + // create users + pendingMemberUser = await Factory.build( + 'user', + { + id: 'pending-member-user', + name: 'Pending Member TestUser', + }, + { + email: 'pending-member-user@example.org', + password: '1234', + }, + ) + usualMemberUser = await Factory.build( + 'user', + { + id: 'usual-member-user', + name: 'Usual Member TestUser', + }, + { + email: 'usual-member-user@example.org', + password: '1234', + }, + ) + adminMemberUser = await Factory.build( + 'user', + { + id: 'admin-member-user', + name: 'Admin Member TestUser', + }, + { + email: 'admin-member-user@example.org', + password: '1234', + }, + ) + ownerMemberUser = await Factory.build( + 'user', + { + id: 'owner-member-user', + name: 'Owner Member TestUser', + }, + { + email: 'owner-member-user@example.org', + password: '1234', + }, + ) + secondOwnerMemberUser = await Factory.build( + 'user', + { + id: 'second-owner-member-user', + name: 'Second Owner Member TestUser', + }, + { + email: 'second-owner-member-user@example.org', + password: '1234', + }, + ) + // create groups + // public-group + authenticatedUser = await usualMemberUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'public-group', + name: 'The Best Group', + about: 'We will change the world!', + description: 'Some description' + descriptionAdditional100, + groupType: 'public', + actionRadius: 'regional', + categoryIds, + }, + }) + await mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'public-group', + userId: 'owner-of-closed-group', + }, + }) + await mutate({ + mutation: joinGroupMutation, + variables: { + groupId: 'public-group', + userId: 'owner-of-hidden-group', + }, + }) + // closed-group + authenticatedUser = await ownerMemberUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'closed-group', + name: 'Uninteresting Group', + about: 'We will change nothing!', + description: 'We love it like it is!?' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + }, + }) + // hidden-group + authenticatedUser = await adminMemberUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'hidden-group', + name: 'Investigative Journalism Group', + about: 'We will change all.', + description: 'We research …' + descriptionAdditional100, + groupType: 'hidden', + actionRadius: 'global', + categoryIds, + }, + }) + // 'JoinGroup' mutation does not work in hidden groups so we join them by 'ChangeGroupMemberRole' through the owner + 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', + }, + }) + 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', + }, + }) + + authenticatedUser = null + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables: { + groupId: 'not-existing-group', + userId: 'current-user', + roleInGroup: 'pending', + }, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('authenticated', () => { + describe('in all group types – here "closed-group" for example', () => { + beforeEach(async () => { + variables = { + groupId: 'closed-group', + } + }) + + describe('join the members and give them their prospective roles', () => { + describe('by owner "owner-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + describe('for "usual-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'usual-member-user', + } + }) + + describe('as usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('has role usual', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'usual-member-user', + myRoleInGroup: 'usual', + }, + }, + errors: undefined, + }) + }) + + // the GQL mutation needs this fields in the result for testing + it.todo('has "updatedAt" newer as "createdAt"') + }) + }) + + describe('for "admin-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'admin-member-user', + } + }) + + describe('as admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('has role admin', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'admin-member-user', + myRoleInGroup: 'admin', + }, + }, + errors: undefined, + }) + }) + }) + }) + + describe('for "second-owner-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'second-owner-member-user', + } + }) + + describe('as owner', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'owner', + } + }) + + it('has role owner', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'second-owner-member-user', + myRoleInGroup: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + }) + }) + }) + + describe('switch role', () => { + describe('of owner "owner-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'owner-member-user', + } + }) + + describe('by owner themself "owner-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + describe('to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + // shall this be possible in the future? + // or shall only an owner who gave the second owner the owner role downgrade themself for savety? + // otherwise the first owner who downgrades the other one has the victory over the group! + describe('by second owner "second-owner-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await secondOwnerMemberUser.toJson() + }) + + describe('to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('to same role owner', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'owner', + } + }) + + it('has role owner still', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'owner-member-user', + myRoleInGroup: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + }) + + describe('by admin "admin-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await adminMemberUser.toJson() + }) + + describe('to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by usual member "usual-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await usualMemberUser.toJson() + }) + + describe('to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by still pending member "pending-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await pendingMemberUser.toJson() + }) + + describe('to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) + + describe('of admin "admin-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'admin-member-user', + } + }) + + describe('by owner "owner-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + describe('to owner', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'owner', + } + }) + + it('has role owner', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'admin-member-user', + myRoleInGroup: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + + describe('back to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by usual member "usual-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await usualMemberUser.toJson() + }) + + describe('upgrade to owner', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'owner', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('degrade to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by still pending member "pending-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await pendingMemberUser.toJson() + }) + + describe('upgrade to owner', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'owner', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('degrade to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by none member "current-user"', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + describe('upgrade to owner', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'owner', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('degrade to pending again', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'pending', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) + + describe('of usual member "usual-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'usual-member-user', + } + }) + + describe('by owner "owner-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + describe('to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('has role admin', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'usual-member-user', + myRoleInGroup: 'admin', + }, + }, + errors: undefined, + }) + }) + }) + + describe('back to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('has role usual again', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'usual-member-user', + myRoleInGroup: 'usual', + }, + }, + errors: undefined, + }) + }) + }) + }) + + describe('by usual member "usual-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await usualMemberUser.toJson() + }) + + describe('upgrade to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('degrade to pending', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'pending', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by still pending member "pending-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await pendingMemberUser.toJson() + }) + + describe('upgrade to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('degrade to pending', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'pending', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by none member "current-user"', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + describe('upgrade to admin', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'admin', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('degrade to pending again', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'pending', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) + + describe('of still pending member "pending-member-user"', () => { + beforeEach(async () => { + variables = { + ...variables, + userId: 'pending-member-user', + } + }) + + describe('by owner "owner-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + describe('to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('has role usual', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'pending-member-user', + myRoleInGroup: 'usual', + }, + }, + errors: undefined, + }) + }) + }) + + describe('back to pending', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'pending', + } + }) + + it('has role usual again', async () => { + await expect( + mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + ChangeGroupMemberRole: { + id: 'pending-member-user', + myRoleInGroup: 'pending', + }, + }, + errors: undefined, + }) + }) + }) + }) + + describe('by usual member "usual-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await usualMemberUser.toJson() + }) + + describe('upgrade to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by still pending member "pending-member-user"', () => { + beforeEach(async () => { + authenticatedUser = await pendingMemberUser.toJson() + }) + + describe('upgrade to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + + describe('by none member "current-user"', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + describe('upgrade to usual', () => { + beforeEach(async () => { + variables = { + ...variables, + roleInGroup: 'usual', + } + }) + + it('throws authorization error', async () => { + const { errors } = await mutate({ + mutation: changeGroupMemberRoleMutation, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) }) - expect(errors[0]).toHaveProperty('message', 'Too many categories!') }) }) }) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index 3165b4a44..e254e5086 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -59,7 +59,7 @@ input _GroupFilter { type Query { Group( - isMember: Boolean # if 'undefined' or 'null' then all groups + isMember: Boolean # if 'undefined' or 'null' then get all groups id: ID name: String slug: String @@ -71,14 +71,21 @@ type Query { first: Int offset: Int orderBy: [_GroupOrdering] - filter: _GroupFilter ): [Group] - AvailableGroupTypes: [GroupType]! + GroupMembers( + id: ID! + first: Int + offset: Int + orderBy: [_UserOrdering] + filter: _UserFilter + ): [User] - AvailableGroupActionRadii: [GroupActionRadius]! + # AvailableGroupTypes: [GroupType]! - AvailableGroupMemberRoles: [GroupMemberRole]! + # AvailableGroupActionRadii: [GroupActionRadius]! + + # AvailableGroupMemberRoles: [GroupMemberRole]! } type Mutation { @@ -95,15 +102,26 @@ type Mutation { locationName: String ): Group - UpdateGroup( - id: ID! - name: String - slug: String - avatar: ImageInput - locationName: String - about: String - description: String - ): Group + # UpdateGroup( + # id: ID! + # name: String + # slug: String + # avatar: ImageInput + # locationName: String + # about: String + # description: String + # ): Group - DeleteGroup(id: ID!): Group + # DeleteGroup(id: ID!): Group + + JoinGroup( + groupId: ID! + userId: ID! + ): User + + ChangeGroupMemberRole( + groupId: ID! + userId: ID! + roleInGroup: GroupMemberRole! + ): User } diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index a25e51079..4219cd00e 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -114,6 +114,8 @@ type User { badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") emotions: [EMOTED] + + myRoleInGroup: GroupMemberRole }