diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts new file mode 100644 index 000000000..59694914a --- /dev/null +++ b/backend/src/graphql/messages.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag' + +export const createMessageMutation = () => { + return gql` + mutation ( + $roomId: ID! + $content: String! + ) { + CreateMessage( + roomId: $roomId + content: $content + ) { + id + content + } + } + ` +} + +export const messageQuery = () => { + return gql` + query($roomId: ID!) { + Message(roomId: $roomId) { + id + content + author { + id + } + } + } + ` +} diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index fbca8846d..81ba93e3c 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -407,6 +407,7 @@ export default shield( availableRoles: isAdmin, getInviteCode: isAuthenticated, // and inviteRegistration Room: isAuthenticated, + Message: isAuthenticated, }, Mutation: { '*': deny, @@ -461,6 +462,7 @@ export default shield( markTeaserAsViewed: allow, saveCategorySettings: isAuthenticated, CreateRoom: isAuthenticated, + CreateMessage: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts new file mode 100644 index 000000000..e9cf26a22 --- /dev/null +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -0,0 +1,269 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory, { cleanDatabase } from '../../db/factories' +import { getNeode, getDriver } from '../../db/neo4j' +import { createRoomMutation } from '../../graphql/rooms' +import { createMessageMutation, messageQuery } from '../../graphql/messages' +import createServer from '../../server' + +const driver = getDriver() +const neode = getNeode() + +let query +let mutate +let authenticatedUser +let chattingUser, otherChattingUser, notChattingUser + +beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) + +afterAll(async () => { + await cleanDatabase() + driver.close() +}) + + +describe('Message', () => { + let roomId: string + + beforeAll(async () => { + [chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ + Factory.build( + 'user', + { + id: 'chatting-user', + name: 'Chatting User', + }, + ), + Factory.build( + 'user', + { + id: 'other-chatting-user', + name: 'Other Chatting User', + }, + ), + Factory.build( + 'user', + { + id: 'not-chatting-user', + name: 'Not Chatting User', + }, + ), + ]) + }) + + describe('create message', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: createMessageMutation(), variables: { + roomId: 'some-id', content: 'Some bla bla bla', } })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + }) + + describe('room does not exist', () => { + it('returns null', async () => { + await expect(mutate({ mutation: createMessageMutation(), variables: { + roomId: 'some-id', content: 'Some bla bla bla', } })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateMessage: null, + }, + }) + }) + }) + + describe('room exists', () => { + beforeAll(async () => { + const room = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'other-chatting-user', + }, + }) + roomId = room.data.CreateRoom.id + }) + + describe('user chats in room', () => { + it('returns the message', async () => { + await expect(mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to other chatting user', + } })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateMessage: { + id: expect.any(String), + content: 'Some nice message to other chatting user', + }, + }, + }) + }) + }) + + describe('user does not chat in room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns null', async () => { + await expect(mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'I have no access to this room!', + } })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateMessage: null, + }, + }) + }) + }) + }) + }) + }) + + describe('message query', () => { + describe('unauthenticated', () => { + beforeAll(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId: 'some-id' } + })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + authenticatedUser = await otherChattingUser.toJson() + }) + + describe('room does not exists', () => { + it('returns null', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId: 'some-id' + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [], + }, + }) + }) + }) + + describe('room exists with authenticated user chatting', () => { + it('returns the messages', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId, + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [{ + id: expect.any(String), + content: 'Some nice message to other chatting user', + author: { + id: 'chatting-user', + }, + }], + }, + }) + }) + + describe('more messages', () => { + beforeAll(async () => { + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Another nice message to other chatting user', + } + }) + }) + + it('returns the messages', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId, + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [ + { + id: expect.any(String), + content: 'Some nice message to other chatting user', + author: { + id: 'chatting-user', + }, + }, + { + id: expect.any(String), + content: 'Another nice message to other chatting user', + author: { + id: 'other-chatting-user', + }, + } + ], + }, + }) + }) + }) + }) + + describe('room exists, authenticated user not in room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns null', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId, + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [], + }, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts new file mode 100644 index 000000000..2cf72e9fe --- /dev/null +++ b/backend/src/schema/resolvers/messages.ts @@ -0,0 +1,61 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' +import Resolver from './helpers/Resolver' + +export default { + Query: { + Message: async (object, params, context, resolveInfo) => { + const { roomId } = params + delete params.roomId + if (!params.filter) params.filter = {} + params.filter.room = { + id: roomId, + users_some: { + id: context.user.id, + }, + } + return neo4jgraphql(object, params, context, resolveInfo) + }, + }, + Mutation: { + CreateMessage: async (_parent, params, context, _resolveInfo) => { + const { roomId, content } = params + const { user: { id: currentUserId } } = context + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const createMessageCypher = ` + MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) + MERGE (currentUser)-[:CREATED]->(message:Message)-[:INSIDE]->(room) + ON CREATE SET + message.createdAt = toString(datetime()), + message.id = apoc.create.uuid(), + message.content = $content + RETURN message { .* } + ` + const createMessageTxResponse = await transaction.run( + createMessageCypher, + { currentUserId, roomId, content } + ) + const [message] = await createMessageTxResponse.records.map((record) => + record.get('message'), + ) + return message + }) + try { + const message = await writeTxResultPromise + return message + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + }, + Message: { + ...Resolver('Message', { + hasOne: { + author: '<-[:CREATED]-(related:User)', + room: '-[:INSIDE]->(related:Room)', + } + }), + } +} diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 1c9333bd7..8c4d887cb 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -29,7 +29,7 @@ beforeAll(async () => { }) afterAll(async () => { - // await cleanDatabase() + await cleanDatabase() driver.close() }) diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/schema/types/type/Message.gql new file mode 100644 index 000000000..5b14104db --- /dev/null +++ b/backend/src/schema/types/type/Message.gql @@ -0,0 +1,25 @@ +# input _MessageFilter { +# room: _RoomFilter +# } + +type Message { + id: ID! + createdAt: String + updatedAt: String + + content: String! + + author: User! @relation(name: "CREATED", direction: "IN") + room: Room! @relation(name: "INSIDE", direction: "OUT") +} + +type Mutation { + CreateMessage( + roomId: ID! + content: String! + ): Message +} + +type Query { + Message(roomId: ID!): [Message] +} diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 8859ccc29..8792aa56a 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -1,6 +1,7 @@ # input _RoomFilter { # AND: [_RoomFilter!] # OR: [_RoomFilter!] +# id: ID # users_some: _UserFilter # }