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 c6f110ed1..150bb5e9a 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -12,6 +12,7 @@ export const createGroupMutation = gql` $groupType: GroupType! $actionRadius: GroupActionRadius! $categoryIds: [ID] + $locationName: String ) { CreateGroup( id: $id @@ -22,6 +23,7 @@ export const createGroupMutation = gql` groupType: $groupType actionRadius: $actionRadius categoryIds: $categoryIds + locationName: $locationName ) { id name @@ -34,6 +36,60 @@ export const createGroupMutation = gql` description groupType actionRadius + categories { + id + slug + name + icon + } + # locationName # test this as result + myRole + } + } +` + +export const updateGroupMutation = gql` + mutation ( + $id: ID! + $name: String + $slug: String + $about: String + $description: String + $actionRadius: GroupActionRadius + $categoryIds: [ID] + $avatar: ImageInput + $locationName: String + ) { + UpdateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + actionRadius: $actionRadius + categoryIds: $categoryIds + avatar: $avatar + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + groupType + actionRadius + categories { + id + slug + name + icon + } + # avatar # test this as result + # locationName # test this as result myRole } } @@ -112,6 +168,8 @@ export const groupQuery = gql` name icon } + # avatar # test this as result + # locationName # test this as result } } ` 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 cf57aae85..f6f675008 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})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + RETURN group {.*}, owner {.*, myRoleInGroup: membership.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 isAllowedSeeingMembersOfGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -252,6 +282,7 @@ export default shield( SignupVerification: allow, UpdateUser: onlyYourself, CreateGroup: isAuthenticated, + UpdateGroup: isAllowedToChangeGroupSettings, JoinGroup: isAllowedToJoinGroup, ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, CreatePost: isAuthenticated, 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) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 3fea526ee..edb6b64eb 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -2,12 +2,13 @@ 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 { createGroupMutation, updateGroupMutation } from '../db/graphql/groups' import { createPostMutation } from '../db/graphql/posts' 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,17 @@ 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: [ { @@ -189,9 +189,163 @@ describe('slugifyMiddleware', () => { }) }) - describe('CreatePost', () => { - const categoryIds = ['cat9'] + describe('UpdateGroup', () => { + let createGroupResult + beforeEach(async () => { + createGroupResult = await mutate({ + mutation: createGroupMutation, + variables: { + name: 'The Best Group', + slug: 'the-best-group', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + categoryIds, + }, + }) + }) + + describe('if group exists', () => { + describe('if new slug not(!) exists', () => { + 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', + }, + }, + }) + }) + }) + + 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', () => { + beforeEach(async () => { + 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, + }, + }) + }) + + describe('setting slug by group name', () => { + it('has unique slug "*-1"', async () => { + await expect( + mutate({ + mutation: updateGroupMutation, + variables: { + id: createGroupResult.data.CreateGroup.id, + name: 'Pre-Existing Group', + }, + }), + ).resolves.toMatchObject({ + data: { + UpdateGroup: { + name: 'Pre-Existing Group', + slug: 'pre-existing-group-1', + about: 'Some about', + description: 'Some description' + descriptionAdditional100, + groupType: 'closed', + actionRadius: 'national', + myRole: 'owner', + }, + }, + }) + }) + }) + + 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} + + 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('CreatePost', () => { beforeEach(() => { variables = { ...variables, @@ -252,16 +406,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 +427,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: [ { @@ -313,6 +468,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..2111aa54a 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -5,38 +5,40 @@ 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: { 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} ` } } - 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 +56,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 +133,76 @@ export default { session.close() } }, + UpdateGroup: async (_parent, params, context, _resolveInfo) => { + const { categoryIds } = params + const { id: groupId, avatar: avatarInput } = params + delete params.categoryIds + 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 && + removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN + ) { + 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}) + SET group += $params + SET group.updatedAt = toString(datetime()) + WITH group + ` + if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) { + updateGroupCypher += ` + UNWIND $categoryIds AS categoryId + MATCH (category:Category {id: categoryId}) + MERGE (group)-[:CATEGORIZED]->(category) + WITH group + ` + } + updateGroupCypher += ` + OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) + 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')) + if (avatarInput) { + await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction }) + } + 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 +220,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 +248,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/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 1d272de2b..2ca40b3e7 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, @@ -106,6 +107,7 @@ describe('in mode', () => { groupType: 'public', actionRadius: 'regional', categoryIds, + // locationName, // test this as result } }) @@ -129,6 +131,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, @@ -161,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: { @@ -182,13 +187,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!') }) }) @@ -287,6 +329,61 @@ 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'", () => { + 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', () => { @@ -2043,5 +2140,211 @@ describe('in mode', () => { }) }) }) + + describe('UpdateGroup', () => { + beforeAll(async () => { + await seedBasicsAndClearAuthentication() + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('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('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 Country', + 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: { + UpdateGroup: { + id: 'my-group', + 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', + // avatar, // test this as result + // locationName, // test this as result + myRole: 'owner', + }, + }, + errors: undefined, + }) + }) + + describe('description', () => { + describe('length without HTML', () => { + describe('less then 100 chars', () => { + it('throws error: "Description too short!"', 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, + }) + }) + }) + + 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('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 Country', + 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 + }, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + }) + }) + }) }) }) 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/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 }, }) diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index e254e5086..c1b097857 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,24 +96,27 @@ type Mutation { id: ID name: String! slug: String - avatar: ImageInput about: String description: String! groupType: GroupType! actionRadius: GroupActionRadius! categoryIds: [ID] - locationName: String + # avatar: ImageInput # a group can not be created with an avatar + locationName: String # test this as result ): Group - # UpdateGroup( - # id: ID! - # name: String - # slug: String - # avatar: ImageInput - # locationName: String - # about: String - # description: String - # ): Group + UpdateGroup( + id: ID! + name: String + 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 # test this as result + locationName: String # test this as result + ): Group # DeleteGroup(id: ID!): Group