From d969141fc6d6eb7d641c537ae13116061ddce19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 30 Aug 2022 06:58:39 +0200 Subject: [PATCH 01/15] Implement 'UpdateGroup' resolver, first step --- backend/src/constants/groups.js | 1 + backend/src/db/graphql/groups.js | 38 +++++++++++++++++++ backend/src/middleware/excerptMiddleware.js | 7 +++- .../src/middleware/permissionsMiddleware.js | 31 +++++++++++++++ backend/src/middleware/sluggifyMiddleware.js | 4 ++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/backend/src/constants/groups.js b/backend/src/constants/groups.js index b4a6063f1..64ffeaa59 100644 --- a/backend/src/constants/groups.js +++ b/backend/src/constants/groups.js @@ -1,2 +1,3 @@ // this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js` export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 100 // with removed HTML tags +export const DESCRIPTION_EXCERPT_HTML_LENGTH = 120 // with removed HTML tags diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index 2a611f324..4d6bad5fd 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -39,6 +39,44 @@ export const createGroupMutation = gql` } ` +export const updateGroupMutation = gql` + mutation ( + $id: ID! + $name: String + $slug: String + $avatar: ImageInput + $about: String + $description: String + $actionRadius: GroupActionRadius + $categoryIds: [ID] + ) { + UpdateGroup( + id: $id + name: $name + slug: $slug + avatar: $avatar + about: $about + description: $description + actionRadius: $actionRadius + categoryIds: $categoryIds + ) { + id + name + slug + avatar + createdAt + updatedAt + disabled + deleted + about + description + groupType + actionRadius + myRole + } + } +` + // ------ queries export const groupQuery = gql` diff --git a/backend/src/middleware/excerptMiddleware.js b/backend/src/middleware/excerptMiddleware.js index ca061609a..68eea9a74 100644 --- a/backend/src/middleware/excerptMiddleware.js +++ b/backend/src/middleware/excerptMiddleware.js @@ -1,9 +1,14 @@ import trunc from 'trunc-html' +import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups' export default { Mutation: { CreateGroup: async (resolve, root, args, context, info) => { - args.descriptionExcerpt = trunc(args.description, 120).html + args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html + return resolve(root, args, context, info) + }, + UpdateGroup: async (resolve, root, args, context, info) => { + args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html return resolve(root, args, context, info) }, CreatePost: async (resolve, root, args, context, info) => { diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index c81b069d2..9909f542d 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -52,6 +52,36 @@ const isMySocialMedia = rule({ return socialMedia.ownedBy.node.id === user.id }) +const isAllowedToChangeGroupSettings = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const ownerId = user.id + const { id: groupId } = args + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (owner:User {id: $ownerId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId}) + RETURN group {.*}, owner {.*, myRoleInGroup: adminMembership.role} + `, + { groupId, ownerId }, + ) + return { + owner: transactionResponse.records.map((record) => record.get('owner'))[0], + group: transactionResponse.records.map((record) => record.get('group'))[0], + } + }) + try { + const { owner, group } = await readTxPromise + return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -142,6 +172,7 @@ export default shield( SignupVerification: allow, UpdateUser: onlyYourself, CreateGroup: isAuthenticated, + UpdateGroup: isAllowedToChangeGroupSettings, CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 2a965c87f..8fd200e8f 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -30,6 +30,10 @@ export default { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) return resolve(root, args, context, info) }, + UpdateGroup: async (resolve, root, args, context, info) => { + args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) + return resolve(root, args, context, info) + }, CreatePost: async (resolve, root, args, context, info) => { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) From fa2e92a363045d8ab630119c4ed0719cad48e19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 30 Aug 2022 08:20:28 +0200 Subject: [PATCH 02/15] Refine tests in 'slugifyMiddleware.spec.js' --- .../src/middleware/slugifyMiddleware.spec.js | 190 +++++++++++++++--- 1 file changed, 158 insertions(+), 32 deletions(-) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 3fea526ee..62bfcf76d 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -8,6 +8,7 @@ import { signupVerificationMutation } from '../db/graphql/authentications' let authenticatedUser let variables +const categoryIds = ['cat9'] const driver = getDriver() const neode = getNeode() @@ -62,8 +63,6 @@ afterEach(async () => { describe('slugifyMiddleware', () => { describe('CreateGroup', () => { - const categoryIds = ['cat9'] - beforeEach(() => { variables = { ...variables, @@ -130,15 +129,14 @@ describe('slugifyMiddleware', () => { }) it('chooses another slug', async () => { - variables = { - ...variables, - name: 'Pre-Existing Group', - about: 'As an about', - } await expect( mutate({ mutation: createGroupMutation, - variables, + variables: { + ...variables, + name: 'Pre-Existing Group', + about: 'As an about', + }, }), ).resolves.toMatchObject({ data: { @@ -151,15 +149,144 @@ describe('slugifyMiddleware', () => { describe('but if the client specifies a slug', () => { it('rejects CreateGroup', async (done) => { - variables = { - ...variables, - name: 'Pre-Existing Group', - about: 'As an about', - slug: 'pre-existing-group', - } try { await expect( - mutate({ mutation: createGroupMutation, variables }), + mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + name: 'Pre-Existing Group', + about: 'As an about', + slug: 'pre-existing-group', + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Group with this slug already exists!', + }, + ], + }) + done() + } catch (error) { + throw new Error(` + ${error} + + Probably your database has no unique constraints! + + To see all constraints go to http://localhost:7474/browser/ and + paste the following: + \`\`\` + CALL db.constraints(); + \`\`\` + + Learn how to setup the database here: + https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints + `) + } + }) + }) + }) + }) + + describe('UpdateGroup', () => { + beforeEach(() => { + variables = { + ...variables, + name: 'The Best Group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + } + }) + + describe('if slug not exists', () => { + it('generates a slug based on name', async () => { + await expect( + mutate({ + mutation: createGroupMutation, + variables, + }), + ).resolves.toMatchObject({ + data: { + CreateGroup: { + name: 'The Best Group', + slug: 'the-best-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + }, + }, + }) + }) + + it('generates a slug based on given slug', async () => { + await expect( + mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + slug: 'the-group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateGroup: { + slug: 'the-group', + }, + }, + }) + }) + }) + + describe('if slug exists', () => { + beforeEach(async () => { + await mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + name: 'Pre-Existing Group', + slug: 'pre-existing-group', + about: 'As an about', + }, + }) + }) + + it('chooses another slug', async () => { + await expect( + mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + name: 'Pre-Existing Group', + about: 'As an about', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateGroup: { + slug: 'pre-existing-group-1', + }, + }, + }) + }) + + describe('but if the client specifies a slug', () => { + it('rejects CreateGroup', async (done) => { + try { + await expect( + mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + name: 'Pre-Existing Group', + about: 'As an about', + slug: 'pre-existing-group', + }, + }), ).resolves.toMatchObject({ errors: [ { @@ -190,8 +317,6 @@ describe('slugifyMiddleware', () => { }) describe('CreatePost', () => { - const categoryIds = ['cat9'] - beforeEach(() => { variables = { ...variables, @@ -252,16 +377,15 @@ describe('slugifyMiddleware', () => { }) it('chooses another slug', async () => { - variables = { - ...variables, - title: 'Pre-existing post', - content: 'Some content', - categoryIds, - } await expect( mutate({ mutation: createPostMutation, - variables, + variables: { + ...variables, + title: 'Pre-existing post', + content: 'Some content', + categoryIds, + }, }), ).resolves.toMatchObject({ data: { @@ -274,16 +398,18 @@ describe('slugifyMiddleware', () => { describe('but if the client specifies a slug', () => { it('rejects CreatePost', async (done) => { - variables = { - ...variables, - title: 'Pre-existing post', - content: 'Some content', - slug: 'pre-existing-post', - categoryIds, - } try { await expect( - mutate({ mutation: createPostMutation, variables }), + mutate({ + mutation: createPostMutation, + variables: { + ...variables, + title: 'Pre-existing post', + content: 'Some content', + slug: 'pre-existing-post', + categoryIds, + }, + }), ).resolves.toMatchObject({ errors: [ { From 422d818133de538d5a6c67ad340319760b4660dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 31 Aug 2022 08:51:01 +0200 Subject: [PATCH 03/15] !!! Temp !!! --- backend/src/middleware/slugifyMiddleware.spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 62bfcf76d..3a8f380d9 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -189,7 +189,7 @@ describe('slugifyMiddleware', () => { }) }) - describe('UpdateGroup', () => { + describe.only('UpdateGroup', () => { beforeEach(() => { variables = { ...variables, @@ -200,6 +200,10 @@ describe('slugifyMiddleware', () => { actionRadius: 'national', categoryIds, } + await mutate({ + mutation: createGroupMutation, + variables, + }) }) describe('if slug not exists', () => { From 44eb9d0bde3173e127e929367cc2a55f717c9cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 1 Sep 2022 09:31:50 +0200 Subject: [PATCH 04/15] Implement 'UpdateGroup' resolver, not working --- backend/src/middleware/excerptMiddleware.js | 1 + .../src/middleware/permissionsMiddleware.js | 3 + backend/src/middleware/sluggifyMiddleware.js | 1 + .../src/middleware/slugifyMiddleware.spec.js | 111 ++++++------------ backend/src/schema/resolvers/groups.js | 86 ++++++++++++-- backend/src/schema/resolvers/posts.js | 10 +- backend/src/schema/types/type/Group.gql | 18 +-- 7 files changed, 134 insertions(+), 96 deletions(-) diff --git a/backend/src/middleware/excerptMiddleware.js b/backend/src/middleware/excerptMiddleware.js index 68eea9a74..597b97714 100644 --- a/backend/src/middleware/excerptMiddleware.js +++ b/backend/src/middleware/excerptMiddleware.js @@ -8,6 +8,7 @@ export default { return resolve(root, args, context, info) }, UpdateGroup: async (resolve, root, args, context, info) => { + console.log('excerptMiddleware - UpdateGroup !!!') args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html return resolve(root, args, context, info) }, diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 40f336d2f..f0723e50b 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -55,9 +55,12 @@ const isMySocialMedia = rule({ const isAllowedToChangeGroupSettings = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { + console.log('isAllowedToChangeGroupSettings !!!') if (!(user && user.id)) return false const ownerId = user.id const { id: groupId } = args + console.log('ownerId: ', ownerId) + console.log('groupId: ', groupId) const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { const transactionResponse = await transaction.run( diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 8fd200e8f..d5f75adb0 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -31,6 +31,7 @@ export default { return resolve(root, args, context, info) }, UpdateGroup: async (resolve, root, args, context, info) => { + console.log('sluggifyMiddleware - UpdateGroup !!!') args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) return resolve(root, args, context, info) }, diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 4f0401cdc..9baef1fae 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -2,7 +2,8 @@ import { getNeode, getDriver } from '../db/neo4j' import createServer from '../server' import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../db/factories' -import { createGroupMutation } from '../db/graphql/groups' +import { sleep } from '../helpers/jest.js' +import { createGroupMutation, updateGroupMutation } from '../db/graphql/groups' import { createPostMutation } from '../db/graphql/posts' import { signupVerificationMutation } from '../db/graphql/authentications' @@ -189,101 +190,61 @@ describe('slugifyMiddleware', () => { }) }) - describe.only('UpdateGroup', () => { + describe('UpdateGroup', () => { beforeEach(async () => { variables = { ...variables, - name: 'The Best Group', + name: 'Pre-Existing Group', + slug: 'pre-existing-group', about: 'Some about', description: 'Some description' + descriptionAdditional100, groupType: 'closed', actionRadius: 'national', categoryIds, } + console.log('createGroupMutation: ', createGroupMutation) await mutate({ mutation: createGroupMutation, variables, }) + // Wolle: console.log('sleep !!!') + // await sleep(4 * 1000) }) - describe('if slug not exists', () => { - it('generates a slug based on name', async () => { - await expect( - mutate({ - mutation: createGroupMutation, - variables, - }), - ).resolves.toMatchObject({ - data: { - CreateGroup: { - name: 'The Best Group', - slug: 'the-best-group', - about: 'Some about', - description: 'Some description' + descriptionAdditional100, - groupType: 'closed', - actionRadius: 'national', + describe('if group exists', () => { + describe('if new slug not(!) exists', () => { + it.only('has the new slug', async () => { + console.log('updateGroupMutation: ', updateGroupMutation) + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + ...variables, + slug: 'my-best-group', + }, + }), + ).resolves.toMatchObject({ + data: { + UpdateGroup: { + name: 'The Best Group', + slug: 'my-best-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + myRole: 'owner', + }, }, - }, + }) }) }) - it('generates a slug based on given slug', async () => { - await expect( - mutate({ - mutation: createGroupMutation, - variables: { - ...variables, - slug: 'the-group', - }, - }), - ).resolves.toMatchObject({ - data: { - CreateGroup: { - slug: 'the-group', - }, - }, - }) - }) - }) - - describe('if slug exists', () => { - beforeEach(async () => { - await mutate({ - mutation: createGroupMutation, - variables: { - ...variables, - name: 'Pre-Existing Group', - slug: 'pre-existing-group', - about: 'As an about', - }, - }) - }) - - it('chooses another slug', async () => { - await expect( - mutate({ - mutation: createGroupMutation, - variables: { - ...variables, - name: 'Pre-Existing Group', - about: 'As an about', - }, - }), - ).resolves.toMatchObject({ - data: { - CreateGroup: { - slug: 'pre-existing-group-1', - }, - }, - }) - }) - - describe('but if the client specifies a slug', () => { - it('rejects CreateGroup', async (done) => { + describe('if new slug exists', () => { + it('rejects UpdateGroup', async (done) => { try { await expect( mutate({ - mutation: createGroupMutation, + mutation: updateGroupMutation, variables: { ...variables, name: 'Pre-Existing Group', @@ -443,6 +404,8 @@ describe('slugifyMiddleware', () => { }) }) + it.todo('UpdatePost') + describe('SignupVerification', () => { beforeEach(() => { variables = { diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index abaa1716f..312c37e69 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -33,10 +33,10 @@ export default { ` } } - const result = await txc.run(groupCypher, { + const transactionResponse = await txc.run(groupCypher, { userId: context.user.id, }) - return result.records.map((record) => record.get('group')) + return transactionResponse.records.map((record) => record.get('group')) }) try { return await readTxResultPromise @@ -54,10 +54,10 @@ export default { MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId}) RETURN user {.*, myRoleInGroup: membership.role} ` - const result = await txc.run(groupMemberCypher, { + const transactionResponse = await txc.run(groupMemberCypher, { groupId, }) - return result.records.map((record) => record.get('user')) + return transactionResponse.records.map((record) => record.get('user')) }) try { return await readTxResultPromise @@ -131,6 +131,72 @@ export default { session.close() } }, + UpdateGroup: async (_parent, params, context, _resolveInfo) => { + console.log('UpdateGroup !!!') + const { categoryIds } = params + const { id: groupId } = params + console.log('categoryIds: ', categoryIds) + console.log('groupId: ', groupId) + delete params.categoryIds + if (CONFIG.CATEGORIES_ACTIVE && (!categoryIds || categoryIds.length < CATEGORIES_MIN)) { + throw new UserInputError('Too view categories!') + } + if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > CATEGORIES_MAX) { + throw new UserInputError('Too many categories!') + } + if ( + params.description === undefined || + params.description === null || + removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN + ) { + throw new UserInputError('Description too short!') + } + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + let updateGroupCypher = ` + MATCH (group:Group {id: $groupId}) + SET group += $params + SET group.updatedAt = toString(datetime()) + WITH group + ` + if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { + const cypherDeletePreviousRelations = ` + MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category) + DELETE previousRelations + RETURN group, category + ` + await session.writeTransaction((transaction) => { + return transaction.run(cypherDeletePreviousRelations, { groupId }) + }) + updateGroupCypher += ` + UNWIND $categoryIds AS categoryId + MATCH (category:Category {id: categoryId}) + MERGE (group)-[:CATEGORIZED]->(category) + WITH group + OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) + WITH group, membership # Wolle: is not needed in my eyes + ` + } + updateGroupCypher += `RETURN group {.*, myRole: membership.role}` + const transactionResponse = await transaction.run(updateGroupCypher, { + groupId, + userId: context.user.id, + categoryIds, + params, + }) + const [group] = await transactionResponse.records.map((record) => record.get('group')) + return group + }) + try { + return await writeTxResultPromise + } catch (error) { + if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') + throw new UserInputError('Group with this slug already exists!') + throw new Error(error) + } finally { + session.close() + } + }, JoinGroup: async (_parent, params, context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() @@ -148,8 +214,8 @@ export default { END RETURN member {.*, myRoleInGroup: membership.role} ` - const result = await transaction.run(joinGroupCypher, { groupId, userId }) - const [member] = await result.records.map((record) => record.get('member')) + const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId }) + const [member] = await transactionResponse.records.map((record) => record.get('member')) return member }) try { @@ -176,8 +242,12 @@ export default { 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')) + const transactionResponse = await transaction.run(joinGroupCypher, { + groupId, + userId, + roleInGroup, + }) + const [member] = await transactionResponse.records.map((record) => record.get('member')) return member }) try { diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index d9a04732c..97230715f 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -131,11 +131,11 @@ export default { delete params.image const session = context.driver.session() let updatePostCypher = ` - MATCH (post:Post {id: $params.id}) - SET post += $params - SET post.updatedAt = toString(datetime()) - WITH post - ` + MATCH (post:Post {id: $params.id}) + SET post += $params + SET post.updatedAt = toString(datetime()) + WITH post + ` if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { const cypherDeletePreviousRelations = ` diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index e254e5086..aa28eccd1 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -102,15 +102,15 @@ 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 From 4f7ce5a6c1adaa5cc38ca80da890179eb6dbfe44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 1 Sep 2022 15:04:49 +0200 Subject: [PATCH 05/15] Fix slugification tests of 'UpdateGroup' --- backend/src/db/graphql/groups.js | 25 ++++++++- backend/src/middleware/excerptMiddleware.js | 1 - .../src/middleware/permissionsMiddleware.js | 12 +++-- backend/src/middleware/sluggifyMiddleware.js | 1 - .../src/middleware/slugifyMiddleware.spec.js | 52 +++++++++++-------- backend/src/schema/resolvers/groups.js | 31 ++++++----- backend/src/schema/types/type/Group.gql | 6 ++- 7 files changed, 80 insertions(+), 48 deletions(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index cbfebe1b2..277ae356f 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -12,6 +12,8 @@ export const createGroupMutation = gql` $groupType: GroupType! $actionRadius: GroupActionRadius! $categoryIds: [ID] + $avatar: ImageInput + $locationName: String ) { CreateGroup( id: $id @@ -22,6 +24,8 @@ export const createGroupMutation = gql` groupType: $groupType actionRadius: $actionRadius categoryIds: $categoryIds + avatar: $avatar + locationName: $locationName ) { id name @@ -34,6 +38,14 @@ export const createGroupMutation = gql` description groupType actionRadius + # categories { + # id + # slug + # name + # icon + # } + # avatar + # locationName myRole } } @@ -44,11 +56,12 @@ export const updateGroupMutation = gql` $id: ID! $name: String $slug: String - $avatar: ImageInput $about: String $description: String $actionRadius: GroupActionRadius $categoryIds: [ID] + $avatar: ImageInput + $locationName: String ) { UpdateGroup( id: $id @@ -59,11 +72,11 @@ export const updateGroupMutation = gql` description: $description actionRadius: $actionRadius categoryIds: $categoryIds + locationName: $locationName ) { id name slug - avatar createdAt updatedAt disabled @@ -72,6 +85,14 @@ export const updateGroupMutation = gql` description groupType actionRadius + # categories { + # id + # slug + # name + # icon + # } + # avatar + # locationName myRole } } diff --git a/backend/src/middleware/excerptMiddleware.js b/backend/src/middleware/excerptMiddleware.js index 597b97714..68eea9a74 100644 --- a/backend/src/middleware/excerptMiddleware.js +++ b/backend/src/middleware/excerptMiddleware.js @@ -8,7 +8,6 @@ export default { return resolve(root, args, context, info) }, UpdateGroup: async (resolve, root, args, context, info) => { - console.log('excerptMiddleware - UpdateGroup !!!') args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html return resolve(root, args, context, info) }, diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index f0723e50b..1752d2bdf 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -55,18 +55,18 @@ const isMySocialMedia = rule({ const isAllowedToChangeGroupSettings = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - console.log('isAllowedToChangeGroupSettings !!!') + // Wolle: console.log('isAllowedToChangeGroupSettings !!!') if (!(user && user.id)) return false const ownerId = user.id const { id: groupId } = args - console.log('ownerId: ', ownerId) - console.log('groupId: ', groupId) + // Wolle: console.log('ownerId: ', ownerId) + // Wolle: console.log('groupId: ', groupId) const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { const transactionResponse = await transaction.run( ` - MATCH (owner:User {id: $ownerId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId}) - RETURN group {.*}, owner {.*, myRoleInGroup: adminMembership.role} + MATCH (owner:User {id: $ownerId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + RETURN group {.*}, owner {.*, myRoleInGroup: membership.role} `, { groupId, ownerId }, ) @@ -77,6 +77,8 @@ const isAllowedToChangeGroupSettings = rule({ }) try { const { owner, group } = await readTxPromise + // Wolle: console.log('owner: ', owner) + // Wolle: console.log('group: ', group) return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup) } catch (error) { throw new Error(error) diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index d5f75adb0..8fd200e8f 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -31,7 +31,6 @@ export default { return resolve(root, args, context, info) }, UpdateGroup: async (resolve, root, args, context, info) => { - console.log('sluggifyMiddleware - UpdateGroup !!!') args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) return resolve(root, args, context, info) }, diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 9baef1fae..628d20fe6 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -2,7 +2,6 @@ import { getNeode, getDriver } from '../db/neo4j' import createServer from '../server' import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../db/factories' -import { sleep } from '../helpers/jest.js' import { createGroupMutation, updateGroupMutation } from '../db/graphql/groups' import { createPostMutation } from '../db/graphql/posts' import { signupVerificationMutation } from '../db/graphql/authentications' @@ -191,35 +190,32 @@ describe('slugifyMiddleware', () => { }) describe('UpdateGroup', () => { + let createGroupResult + beforeEach(async () => { - variables = { - ...variables, - name: 'Pre-Existing Group', - slug: 'pre-existing-group', - about: 'Some about', - description: 'Some description' + descriptionAdditional100, - groupType: 'closed', - actionRadius: 'national', - categoryIds, - } - console.log('createGroupMutation: ', createGroupMutation) - await mutate({ + createGroupResult = await mutate({ mutation: createGroupMutation, - variables, + variables: { + name: 'The Best Group', + slug: 'the-best-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + }, }) - // Wolle: console.log('sleep !!!') - // await sleep(4 * 1000) }) describe('if group exists', () => { describe('if new slug not(!) exists', () => { - it.only('has the new slug', async () => { - console.log('updateGroupMutation: ', updateGroupMutation) + it('has the new slug', async () => { + // Wolle: console.log('createGroupResult: ', createGroupResult) await expect( mutate({ mutation: updateGroupMutation, variables: { - ...variables, + id: createGroupResult.data.CreateGroup.id, slug: 'my-best-group', }, }), @@ -239,16 +235,26 @@ describe('slugifyMiddleware', () => { }) }) - describe('if new slug exists', () => { + describe('if new slug exists in another group', () => { it('rejects UpdateGroup', async (done) => { + await mutate({ + mutation: createGroupMutation, + variables: { + name: 'Pre-Existing Group', + slug: 'pre-existing-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + }, + }) try { await expect( mutate({ mutation: updateGroupMutation, variables: { - ...variables, - name: 'Pre-Existing Group', - about: 'As an about', + id: createGroupResult.data.CreateGroup.id, slug: 'pre-existing-group', }, }), diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 312c37e69..c8a59f0cb 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -132,21 +132,22 @@ export default { } }, UpdateGroup: async (_parent, params, context, _resolveInfo) => { - console.log('UpdateGroup !!!') + // Wolle: console.log('UpdateGroup !!!') const { categoryIds } = params const { id: groupId } = params - console.log('categoryIds: ', categoryIds) - console.log('groupId: ', groupId) + // Wolle: console.log('categoryIds: ', categoryIds) + // Wolle: console.log('groupId: ', groupId) delete params.categoryIds - if (CONFIG.CATEGORIES_ACTIVE && (!categoryIds || categoryIds.length < CATEGORIES_MIN)) { - throw new UserInputError('Too view categories!') - } - if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > CATEGORIES_MAX) { - throw new UserInputError('Too many categories!') + if (CONFIG.CATEGORIES_ACTIVE && categoryIds) { + if (categoryIds.length < CATEGORIES_MIN) { + throw new UserInputError('Too view categories!') + } + if (categoryIds.length > CATEGORIES_MAX) { + throw new UserInputError('Too many categories!') + } } if ( - params.description === undefined || - params.description === null || + params.description && removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN ) { throw new UserInputError('Description too short!') @@ -164,7 +165,7 @@ export default { MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category) DELETE previousRelations RETURN group, category - ` + ` await session.writeTransaction((transaction) => { return transaction.run(cypherDeletePreviousRelations, { groupId }) }) @@ -173,11 +174,13 @@ export default { MATCH (category:Category {id: categoryId}) MERGE (group)-[:CATEGORIZED]->(category) WITH group - OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) - WITH group, membership # Wolle: is not needed in my eyes ` } - updateGroupCypher += `RETURN group {.*, myRole: membership.role}` + updateGroupCypher += ` + OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) + RETURN group {.*, myRole: membership.role} + ` + // Wolle: console.log('updateGroupCypher: ', updateGroupCypher) const transactionResponse = await transaction.run(updateGroupCypher, { groupId, userId: context.user.id, diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index aa28eccd1..70f9ce16f 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -106,10 +106,12 @@ type Mutation { id: ID! name: String slug: String - avatar: ImageInput - locationName: String about: String description: String + actionRadius: GroupActionRadius + categoryIds: [ID] + avatar: ImageInput + locationName: String ): Group # DeleteGroup(id: ID!): Group From da66aa4852b036fb1e88382f6f66fcf54555fe20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 1 Sep 2022 15:33:28 +0200 Subject: [PATCH 06/15] Test 'UpdateGroup' resolver, a start --- backend/src/schema/resolvers/groups.spec.js | 130 ++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 1d272de2b..ddf003659 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -2,6 +2,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' import { createGroupMutation, + updateGroupMutation, joinGroupMutation, changeGroupMemberRoleMutation, groupMembersQuery, @@ -2043,5 +2044,134 @@ describe('in mode', () => { }) }) }) + + describe('UpdateGroup', () => { + beforeAll(async () => { + await seedBasicsAndClearAuthentication() + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it.only('throws authorization error', async () => { + const { errors } = await mutate({ + query: updateGroupMutation, + variables: { + id: 'my-group', + slug: 'my-best-group', + }, + }) + 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, + }) + }) + }) + }) + }) + }) }) }) From b82dcd45aa6727b5c556975af77d001671231e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 2 Sep 2022 05:35:52 +0200 Subject: [PATCH 07/15] Add a missing await in 'posts.spec.js' --- backend/src/schema/resolvers/posts.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 52bd8fcd0..6fc9b5722 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -368,7 +368,7 @@ describe('UpdatePost', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { authenticatedUser = null - expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({ + await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({ errors: [{ message: 'Not Authorized!' }], data: { UpdatePost: null }, }) From 7ec608691582a0fd4326f520971beb2ea9aa3758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 2 Sep 2022 08:10:46 +0200 Subject: [PATCH 08/15] Test 'UpdateGroup' in general resolver and 'Group' resolver with 'id' --- backend/src/db/graphql/groups.js | 35 +-- backend/src/schema/resolvers/groups.js | 27 ++- backend/src/schema/resolvers/groups.spec.js | 239 +++++++++++++++----- backend/src/schema/types/type/Group.gql | 16 +- 4 files changed, 231 insertions(+), 86 deletions(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index 277ae356f..fe3153fa2 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -38,14 +38,15 @@ export const createGroupMutation = gql` description groupType actionRadius - # categories { - # id - # slug - # name - # icon - # } - # avatar - # locationName + categories { + # test this as result + id + slug + name + icon + } + # avatar # test this as result + # locationName # test this as result myRole } } @@ -85,14 +86,15 @@ export const updateGroupMutation = gql` description groupType actionRadius - # categories { - # id - # slug - # name - # icon - # } - # avatar - # locationName + categories { + # test this as result + id + slug + name + icon + } + # avatar # test this as result + # locationName # test this as result myRole } } @@ -166,6 +168,7 @@ export const groupQuery = gql` actionRadius myRole categories { + # test this as result id slug name diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index c8a59f0cb..2a3f2fc11 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -9,25 +9,26 @@ import Resolver from './helpers/Resolver' export default { Query: { Group: async (_object, params, context, _resolveInfo) => { - const { isMember } = params + const { id: groupId, isMember } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { + const groupIdCypher = groupId ? ` {id: "${groupId}"}` : '' let groupCypher if (isMember === true) { groupCypher = ` - MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group) + MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher}) RETURN group {.*, myRole: membership.role} ` } else { if (isMember === false) { groupCypher = ` - MATCH (group:Group) + MATCH (group:Group${groupIdCypher}) WHERE NOT (:User {id: $userId})-[:MEMBER_OF]->(group) RETURN group {.*, myRole: NULL} ` } else { groupCypher = ` - MATCH (group:Group) + MATCH (group:Group${groupIdCypher}) OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) RETURN group {.*, myRole: membership.role} ` @@ -153,6 +154,16 @@ export default { throw new UserInputError('Description too short!') } const session = context.driver.session() + if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { + const cypherDeletePreviousRelations = ` + MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category) + DELETE previousRelations + RETURN group, category + ` + await session.writeTransaction((transaction) => { + return transaction.run(cypherDeletePreviousRelations, { groupId }) + }) + } const writeTxResultPromise = session.writeTransaction(async (transaction) => { let updateGroupCypher = ` MATCH (group:Group {id: $groupId}) @@ -161,14 +172,6 @@ export default { WITH group ` if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { - const cypherDeletePreviousRelations = ` - MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category) - DELETE previousRelations - RETURN group, category - ` - await session.writeTransaction((transaction) => { - return transaction.run(cypherDeletePreviousRelations, { groupId }) - }) updateGroupCypher += ` UNWIND $categoryIds AS categoryId MATCH (category:Category {id: categoryId}) diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index ddf003659..2616cd606 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -107,6 +107,8 @@ describe('in mode', () => { groupType: 'public', actionRadius: 'regional', categoryIds, + // avatar, // test this as result + // locationName, // test this as result } }) @@ -183,13 +185,50 @@ describe('in mode', () => { 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 }, + describe('with matching amount of categories', () => { + it('has new categories', async () => { + await expect( + mutate({ + mutation: createGroupMutation, + variables: { + ...variables, + categoryIds: ['cat4', 'cat27'], + }, + }), + ).resolves.toMatchObject({ + data: { + CreateGroup: { + categories: expect.arrayContaining([ + expect.objectContaining({ id: 'cat4' }), + expect.objectContaining({ id: 'cat27' }), + ]), + myRole: 'owner', + }, + }, + errors: undefined, + }) + }) + }) + + describe('not even one', () => { + describe('by "categoryIds: null"', () => { + 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('by "categoryIds: []"', () => { + it('throws error: "Too view categories!"', async () => { + const { errors } = await mutate({ + mutation: createGroupMutation, + variables: { ...variables, categoryIds: [] }, + }) + expect(errors[0]).toHaveProperty('message', 'Too view categories!') }) - expect(errors[0]).toHaveProperty('message', 'Too view categories!') }) }) @@ -290,6 +329,25 @@ describe('in mode', () => { }) }) + describe("id = 'my-group'", () => { + it('finds only the group with this id', async () => { + await expect( + query({ query: groupQuery, variables: { id: 'my-group' } }), + ).resolves.toMatchObject({ + data: { + Group: [ + expect.objectContaining({ + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }), + ], + }, + errors: undefined, + }) + }) + }) + describe('isMember = true', () => { it('finds only groups where user is member', async () => { await expect( @@ -2055,7 +2113,7 @@ describe('in mode', () => { }) describe('unauthenticated', () => { - it.only('throws authorization error', async () => { + it('throws authorization error', async () => { const { errors } = await mutate({ query: updateGroupMutation, variables: { @@ -2110,64 +2168,141 @@ describe('in mode', () => { }) }) - describe('query groups', () => { - describe('without any filters', () => { - it('finds all groups', async () => { - await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({ + describe('change group settings', () => { + describe('as owner', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('has set the settings', async () => { + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + id: 'my-group', + name: 'The New Group For Our Coutry', + about: 'We will change the land!', + description: 'Some country relevant description' + descriptionAdditional100, + actionRadius: 'national', + // avatar, // test this as result + // locationName, // test this as result + }, + }), + ).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, - }), - ]), + UpdateGroup: { + id: 'my-group', + name: 'The New Group For Our Coutry', + slug: 'the-new-group-for-our-coutry', // changing the slug is tested in the slugifyMiddleware + about: 'We will change the land!', + description: 'Some country relevant description' + descriptionAdditional100, + actionRadius: 'national', + // avatar, // test this as result + // locationName, // test this as result + myRole: 'owner', + }, }, 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', + describe('description', () => { + describe('length without HTML', () => { + describe('less then 100 chars', () => { + it('throws error: "Too view categories!"', async () => { + const { errors } = await mutate({ + mutation: updateGroupMutation, + variables: { + id: 'my-group', + description: + '0123456789' + + '0123456789', + }, + }) + expect(errors[0]).toHaveProperty('message', 'Description too short!') + }) + }) + }) + }) + + describe('categories', () => { + beforeEach(async () => { + CONFIG.CATEGORIES_ACTIVE = true + }) + + describe('with matching amount of categories', () => { + it('has new categories', async () => { + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + id: 'my-group', + categoryIds: ['cat4', 'cat27'], + }, + }), + ).resolves.toMatchObject({ + data: { + UpdateGroup: { + id: 'my-group', + categories: expect.arrayContaining([ + expect.objectContaining({ id: 'cat4' }), + expect.objectContaining({ id: 'cat27' }), + ]), + myRole: 'owner', + }, }, - ], - }, - errors: undefined, + errors: undefined, + }) + }) + }) + + describe('not even one', () => { + describe('by "categoryIds: []"', () => { + it('throws error: "Too view categories!"', async () => { + const { errors } = await mutate({ + mutation: updateGroupMutation, + variables: { + id: 'my-group', + categoryIds: [], + }, + }) + expect(errors[0]).toHaveProperty('message', 'Too view categories!') + }) + }) + }) + + describe('four', () => { + it('throws error: "Too many categories!"', async () => { + const { errors } = await mutate({ + mutation: updateGroupMutation, + variables: { + id: 'my-group', + categoryIds: ['cat9', 'cat4', 'cat15', 'cat27'], + }, + }) + expect(errors[0]).toHaveProperty('message', 'Too many categories!') + }) }) }) }) - 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, - }), - ]), + describe('as no(!) owner', () => { + it('throws authorization error', async () => { + authenticatedUser = await otherUser.toJson() + const { errors } = await mutate({ + mutation: updateGroupMutation, + variables: { + id: 'my-group', + name: 'The New Group For Our Coutry', + about: 'We will change the land!', + description: 'Some country relevant description' + descriptionAdditional100, + actionRadius: 'national', + categoryIds: ['cat4', 'cat27'], // test this as result + // avatar, // test this as result + // locationName, // test this as result }, - errors: undefined, }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index 70f9ce16f..4dc466e53 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -67,6 +67,9 @@ type Query { updatedAt: String about: String description: String + # groupType: GroupType # test this + # actionRadius: GroupActionRadius # test this + # avatar: ImageInput # test this locationName: String first: Int offset: Int @@ -93,13 +96,13 @@ type Mutation { id: ID name: String! slug: String - avatar: ImageInput about: String description: String! groupType: GroupType! actionRadius: GroupActionRadius! - categoryIds: [ID] - locationName: String + categoryIds: [ID] # test this as result + avatar: ImageInput # test this as result + locationName: String # test this as result ): Group UpdateGroup( @@ -108,10 +111,11 @@ type Mutation { slug: String about: String description: String + # groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden actionRadius: GroupActionRadius - categoryIds: [ID] - avatar: ImageInput - locationName: String + categoryIds: [ID] # test this as result + avatar: ImageInput # test this as result + locationName: String # test this as result ): Group # DeleteGroup(id: ID!): Group From 999bbd85c1f72f159f8ba53eb026bfa1fcff41f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 2 Sep 2022 08:11:40 +0200 Subject: [PATCH 09/15] Add more slugification tests for 'CreateGroup' and 'UpdateGroup' --- .../src/middleware/slugifyMiddleware.spec.js | 140 +++++++++++++----- 1 file changed, 99 insertions(+), 41 deletions(-) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 628d20fe6..edb6b64eb 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -209,34 +209,61 @@ describe('slugifyMiddleware', () => { describe('if group exists', () => { describe('if new slug not(!) exists', () => { - it('has the new slug', async () => { - // Wolle: console.log('createGroupResult: ', createGroupResult) - await expect( - mutate({ - mutation: updateGroupMutation, - variables: { - id: createGroupResult.data.CreateGroup.id, - slug: 'my-best-group', + describe('setting slug by group name', () => { + it('has the new slug', async () => { + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + id: createGroupResult.data.CreateGroup.id, + name: 'My Best Group', + }, + }), + ).resolves.toMatchObject({ + data: { + UpdateGroup: { + name: 'My Best Group', + slug: 'my-best-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + myRole: 'owner', + }, }, - }), - ).resolves.toMatchObject({ - data: { - UpdateGroup: { - name: 'The Best Group', - slug: 'my-best-group', - about: 'Some about', - description: 'Some description' + descriptionAdditional100, - groupType: 'closed', - actionRadius: 'national', - myRole: 'owner', + }) + }) + }) + + describe('setting slug explicitly', () => { + it('has the new slug', async () => { + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + id: createGroupResult.data.CreateGroup.id, + slug: 'my-best-group', + }, + }), + ).resolves.toMatchObject({ + data: { + UpdateGroup: { + name: 'The Best Group', + slug: 'my-best-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + myRole: 'owner', + }, }, - }, + }) }) }) }) describe('if new slug exists in another group', () => { - it('rejects UpdateGroup', async (done) => { + beforeEach(async () => { await mutate({ mutation: createGroupMutation, variables: { @@ -249,39 +276,70 @@ describe('slugifyMiddleware', () => { categoryIds, }, }) - try { + }) + + describe('setting slug by group name', () => { + it('has unique slug "*-1"', async () => { await expect( mutate({ mutation: updateGroupMutation, variables: { id: createGroupResult.data.CreateGroup.id, - slug: 'pre-existing-group', + name: 'Pre-Existing Group', }, }), ).resolves.toMatchObject({ - errors: [ - { - message: 'Group with this slug already exists!', + data: { + UpdateGroup: { + name: 'Pre-Existing Group', + slug: 'pre-existing-group-1', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + myRole: 'owner', }, - ], + }, }) - done() - } catch (error) { - throw new Error(` - ${error} + }) + }) - Probably your database has no unique constraints! + describe('setting slug explicitly', () => { + it('rejects UpdateGroup', async (done) => { + try { + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + id: createGroupResult.data.CreateGroup.id, + slug: 'pre-existing-group', + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Group with this slug already exists!', + }, + ], + }) + done() + } catch (error) { + throw new Error(` + ${error} - To see all constraints go to http://localhost:7474/browser/ and - paste the following: - \`\`\` - CALL db.constraints(); - \`\`\` + Probably your database has no unique constraints! - Learn how to setup the database here: - https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints - `) - } + To see all constraints go to http://localhost:7474/browser/ and + paste the following: + \`\`\` + CALL db.constraints(); + \`\`\` + + Learn how to setup the database here: + https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints + `) + } + }) }) }) }) From bfc60bcbf051176bb41e9dd666223eb0a4e7bbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 2 Sep 2022 08:13:53 +0200 Subject: [PATCH 10/15] Test 'UpdateGroup' with 'categoryIds', remainders --- backend/src/schema/types/type/Group.gql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index 4dc466e53..c7473d6ea 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -100,7 +100,7 @@ type Mutation { description: String! groupType: GroupType! actionRadius: GroupActionRadius! - categoryIds: [ID] # test this as result + categoryIds: [ID] avatar: ImageInput # test this as result locationName: String # test this as result ): Group @@ -113,7 +113,7 @@ type Mutation { description: String # groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden actionRadius: GroupActionRadius - categoryIds: [ID] # test this as result + categoryIds: [ID] avatar: ImageInput # test this as result locationName: String # test this as result ): Group From e713f65522da23c52447eef3d44963ee84d65f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 5 Sep 2022 09:01:42 +0200 Subject: [PATCH 11/15] Refine groups testing --- backend/src/db/graphql/groups.js | 5 +---- backend/src/schema/resolvers/groups.js | 6 +++++- backend/src/schema/resolvers/groups.spec.js | 8 +++++++- backend/src/schema/types/type/Group.gql | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index fe3153fa2..ddf179572 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -12,7 +12,6 @@ export const createGroupMutation = gql` $groupType: GroupType! $actionRadius: GroupActionRadius! $categoryIds: [ID] - $avatar: ImageInput $locationName: String ) { CreateGroup( @@ -24,7 +23,6 @@ export const createGroupMutation = gql` groupType: $groupType actionRadius: $actionRadius categoryIds: $categoryIds - avatar: $avatar locationName: $locationName ) { id @@ -45,7 +43,6 @@ export const createGroupMutation = gql` name icon } - # avatar # test this as result # locationName # test this as result myRole } @@ -68,11 +65,11 @@ export const updateGroupMutation = gql` id: $id name: $name slug: $slug - avatar: $avatar about: $about description: $description actionRadius: $actionRadius categoryIds: $categoryIds + avatar: $avatar locationName: $locationName ) { id diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 2a3f2fc11..4d97a2c9f 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -5,6 +5,7 @@ import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories' import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import { removeHtmlTags } from '../../middleware/helpers/cleanHtml.js' import Resolver from './helpers/Resolver' +import { mergeImage } from './images/images' export default { Query: { @@ -135,7 +136,7 @@ export default { UpdateGroup: async (_parent, params, context, _resolveInfo) => { // Wolle: console.log('UpdateGroup !!!') const { categoryIds } = params - const { id: groupId } = params + const { id: groupId, avatar: avatarInput } = params // Wolle: console.log('categoryIds: ', categoryIds) // Wolle: console.log('groupId: ', groupId) delete params.categoryIds @@ -191,6 +192,9 @@ export default { params, }) const [group] = await transactionResponse.records.map((record) => record.get('group')) + if (avatarInput) { + await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction }) + } return group }) try { diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 2616cd606..068bf5c73 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -17,6 +17,7 @@ const neode = getNeode() let authenticatedUser let user +// Wolle: let groupAvartar const categoryIds = ['cat9', 'cat4', 'cat15'] const descriptionAdditional100 = @@ -73,6 +74,9 @@ const seedBasicsAndClearAuthentication = async () => { icon: 'paw', }), ]) + // Wolle: groupAvartar = await Factory.build('image', { + // url: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', + // }) authenticatedUser = null } @@ -107,7 +111,6 @@ describe('in mode', () => { groupType: 'public', actionRadius: 'regional', categoryIds, - // avatar, // test this as result // locationName, // test this as result } }) @@ -132,6 +135,9 @@ describe('in mode', () => { name: 'The Best Group', slug: 'the-group', about: 'We will change the world!', + description: 'Some description' + descriptionAdditional100, + groupType: 'public', + actionRadius: 'regional', }, }, errors: undefined, diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index c7473d6ea..c1b097857 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -101,7 +101,7 @@ type Mutation { groupType: GroupType! actionRadius: GroupActionRadius! categoryIds: [ID] - avatar: ImageInput # test this as result + # avatar: ImageInput # a group can not be created with an avatar locationName: String # test this as result ): Group From 92e6cc3f1ba1c519705d2b3e0b4b06d62a18a5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 5 Sep 2022 09:13:47 +0200 Subject: [PATCH 12/15] Cleanup --- backend/src/middleware/permissionsMiddleware.js | 5 ----- backend/src/schema/resolvers/groups.js | 4 ---- backend/src/schema/resolvers/groups.spec.js | 4 ---- 3 files changed, 13 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 1752d2bdf..f6f675008 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -55,12 +55,9 @@ const isMySocialMedia = rule({ const isAllowedToChangeGroupSettings = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - // Wolle: console.log('isAllowedToChangeGroupSettings !!!') if (!(user && user.id)) return false const ownerId = user.id const { id: groupId } = args - // Wolle: console.log('ownerId: ', ownerId) - // Wolle: console.log('groupId: ', groupId) const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { const transactionResponse = await transaction.run( @@ -77,8 +74,6 @@ const isAllowedToChangeGroupSettings = rule({ }) try { const { owner, group } = await readTxPromise - // Wolle: console.log('owner: ', owner) - // Wolle: console.log('group: ', group) return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup) } catch (error) { throw new Error(error) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 4d97a2c9f..2111aa54a 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -134,11 +134,8 @@ export default { } }, UpdateGroup: async (_parent, params, context, _resolveInfo) => { - // Wolle: console.log('UpdateGroup !!!') const { categoryIds } = params const { id: groupId, avatar: avatarInput } = params - // Wolle: console.log('categoryIds: ', categoryIds) - // Wolle: console.log('groupId: ', groupId) delete params.categoryIds if (CONFIG.CATEGORIES_ACTIVE && categoryIds) { if (categoryIds.length < CATEGORIES_MIN) { @@ -184,7 +181,6 @@ export default { OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) RETURN group {.*, myRole: membership.role} ` - // Wolle: console.log('updateGroupCypher: ', updateGroupCypher) const transactionResponse = await transaction.run(updateGroupCypher, { groupId, userId: context.user.id, diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 068bf5c73..4eb713e4c 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -17,7 +17,6 @@ const neode = getNeode() let authenticatedUser let user -// Wolle: let groupAvartar const categoryIds = ['cat9', 'cat4', 'cat15'] const descriptionAdditional100 = @@ -74,9 +73,6 @@ const seedBasicsAndClearAuthentication = async () => { icon: 'paw', }), ]) - // Wolle: groupAvartar = await Factory.build('image', { - // url: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', - // }) authenticatedUser = null } From 5f081cdb0ddde04200593a53b7127cd0c682b745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 5 Sep 2022 09:19:07 +0200 Subject: [PATCH 13/15] Refine test descriptions for groups - Cleanup. --- backend/src/db/graphql/groups.js | 2 -- backend/src/schema/resolvers/groups.spec.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index ddf179572..7d1b984b0 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -37,7 +37,6 @@ export const createGroupMutation = gql` groupType actionRadius categories { - # test this as result id slug name @@ -84,7 +83,6 @@ export const updateGroupMutation = gql` groupType actionRadius categories { - # test this as result id slug name diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 4eb713e4c..7ccac01c8 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -166,7 +166,7 @@ describe('in mode', () => { describe('description', () => { describe('length without HTML', () => { describe('less then 100 chars', () => { - it('throws error: "Too view categories!"', async () => { + it('throws error: "Description too short!"', async () => { const { errors } = await mutate({ mutation: createGroupMutation, variables: { @@ -2211,7 +2211,7 @@ describe('in mode', () => { describe('description', () => { describe('length without HTML', () => { describe('less then 100 chars', () => { - it('throws error: "Too view categories!"', async () => { + it('throws error: "Description too short!"', async () => { const { errors } = await mutate({ mutation: updateGroupMutation, variables: { From 685ef857525966f463b1a88f36789cb9aa3f6a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 5 Sep 2022 09:30:35 +0200 Subject: [PATCH 14/15] Test 'Group' with 'categories' as result - Cleanup. --- backend/src/db/graphql/groups.js | 3 +- backend/src/schema/resolvers/groups.spec.js | 36 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index 7d1b984b0..150bb5e9a 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -163,12 +163,13 @@ export const groupQuery = gql` actionRadius myRole categories { - # test this as result id slug name icon } + # avatar # test this as result + # locationName # test this as result } } ` diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 7ccac01c8..7a588dd8b 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -329,6 +329,42 @@ describe('in mode', () => { errors: undefined, }) }) + + describe('categories', () => { + beforeEach(() => { + CONFIG.CATEGORIES_ACTIVE = true + }) + + it('has set categories', async () => { + await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'my-group', + slug: 'the-best-group', + categories: expect.arrayContaining([ + expect.objectContaining({ id: 'cat4' }), + expect.objectContaining({ id: 'cat9' }), + expect.objectContaining({ id: 'cat15' }), + ]), + myRole: 'owner', + }), + expect.objectContaining({ + id: 'others-group', + slug: 'uninteresting-group', + categories: expect.arrayContaining([ + expect.objectContaining({ id: 'cat4' }), + expect.objectContaining({ id: 'cat9' }), + expect.objectContaining({ id: 'cat15' }), + ]), + myRole: null, + }), + ]), + }, + errors: undefined, + }) + }) + }) }) describe("id = 'my-group'", () => { From b2f0c0677d1942c2a69ccf217e375a72f904828d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 6 Sep 2022 10:39:56 +0200 Subject: [PATCH 15/15] Fix typo after Moriz suggestion Co-Authored-By: Mogge --- backend/src/schema/resolvers/groups.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 7a588dd8b..2ca40b3e7 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -2218,7 +2218,7 @@ describe('in mode', () => { mutation: updateGroupMutation, variables: { id: 'my-group', - name: 'The New Group For Our Coutry', + name: 'The New Group For Our Country', about: 'We will change the land!', description: 'Some country relevant description' + descriptionAdditional100, actionRadius: 'national', @@ -2230,8 +2230,8 @@ describe('in mode', () => { data: { UpdateGroup: { id: 'my-group', - name: 'The New Group For Our Coutry', - slug: 'the-new-group-for-our-coutry', // changing the slug is tested in the slugifyMiddleware + name: 'The New Group For Our Country', + slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware about: 'We will change the land!', description: 'Some country relevant description' + descriptionAdditional100, actionRadius: 'national', @@ -2331,7 +2331,7 @@ describe('in mode', () => { mutation: updateGroupMutation, variables: { id: 'my-group', - name: 'The New Group For Our Coutry', + name: 'The New Group For Our Country', about: 'We will change the land!', description: 'Some country relevant description' + descriptionAdditional100, actionRadius: 'national',