diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/rooms.ts new file mode 100644 index 000000000..38d10a1d8 --- /dev/null +++ b/backend/src/graphql/rooms.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag' + +export const createRoomMutation = () => { + return gql` + mutation ( + $userId: ID! + ) { + CreateRoom( + userId: $userId + ) { + id + } + } + ` +} + +export const roomQuery = () => { + return gql` + query { + Room { + id + users { + id + } + } + } + ` +} diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 6cd8f39d6..fbca8846d 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -406,6 +406,7 @@ export default shield( queryLocations: isAuthenticated, availableRoles: isAdmin, getInviteCode: isAuthenticated, // and inviteRegistration + Room: isAuthenticated, }, Mutation: { '*': deny, @@ -459,6 +460,7 @@ export default shield( switchUserRole: isAdmin, markTeaserAsViewed: allow, saveCategorySettings: isAuthenticated, + CreateRoom: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts new file mode 100644 index 000000000..1c9333bd7 --- /dev/null +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -0,0 +1,220 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory, { cleanDatabase } from '../../db/factories' +import { getNeode, getDriver } from '../../db/neo4j' +import { createRoomMutation, roomQuery } from '../../graphql/rooms' +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('Room', () => { + 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 room', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: createRoomMutation(), variables: { + userId: 'some-id' } })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + let roomId: string + + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + }) + + describe('user id does not exist', () => { + it('returns null', async () => { + await expect(mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'not-existing-user', + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateRoom: null, + }, + }) + }) + }) + + describe('user id exists', () => { + it('returns the id of the room', async () => { + const result = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'other-chatting-user', + }, + }) + roomId = result.data.CreateRoom.id + await expect(result).toMatchObject({ + errors: undefined, + data: { + CreateRoom: { + id: expect.any(String), + }, + }, + }) + }) + }) + + describe('create room with same user id', () => { + it('returns the id of the room', async () => { + await expect(mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'other-chatting-user', + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateRoom: { + id: roomId, + }, + }, + }) + }) + }) + }) + }) + + describe('query room', () => { + describe('unauthenticated', () => { + beforeAll(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + describe('as creator of room', () => { + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + }) + + it('returns the room', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [ + { + id: expect.any(String), + users: expect.arrayContaining([ + { + id: 'chatting-user', + }, + { + id: 'other-chatting-user', + }, + ]), + }, + ], + }, + }) + }) + }) + + describe('as chatter of room', () => { + beforeAll(async () => { + authenticatedUser = await otherChattingUser.toJson() + }) + + it('returns the room', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [ + { + id: expect.any(String), + users: expect.arrayContaining([ + { + id: 'chatting-user', + }, + { + id: 'other-chatting-user', + }, + ]), + }, + ], + }, + }) + }) + }) + + describe('as not chatter of room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns no rooms', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [], + }, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts new file mode 100644 index 000000000..f3ea05cc9 --- /dev/null +++ b/backend/src/schema/resolvers/rooms.ts @@ -0,0 +1,55 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' +import Resolver from './helpers/Resolver' + +export default { + Query: { + Room: async (object, params, context, resolveInfo) => { + if (!params.filter) params.filter = {} + params.filter.users_some = { + id: context.user.id, + } + return neo4jgraphql(object, params, context, resolveInfo) + }, + }, + Mutation: { + CreateRoom: async (_parent, params, context, _resolveInfo) => { + const { userId } = params + const { user: { id: currentUserId } } = context + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const createRoomCypher = ` + MATCH (currentUser:User { id: $currentUserId }) + MATCH (user:User { id: $userId }) + MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) + ON CREATE SET + room.createdAt = toString(datetime()), + room.id = apoc.create.uuid() + RETURN room { .* } + ` + const createRommTxResponse = await transaction.run( + createRoomCypher, + { userId, currentUserId } + ) + const [room] = await createRommTxResponse.records.map((record) => + record.get('room'), + ) + return room + }) + try { + const room = await writeTxResultPromise + return room + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + }, + Room: { + ...Resolver('Room', { + hasMany: { + users: '<-[:CHATS_IN]-(related:User)', + } + }), + } +} diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql new file mode 100644 index 000000000..8859ccc29 --- /dev/null +++ b/backend/src/schema/types/type/Room.gql @@ -0,0 +1,23 @@ +# input _RoomFilter { +# AND: [_RoomFilter!] +# OR: [_RoomFilter!] +# users_some: _UserFilter +# } + +type Room { + id: ID! + createdAt: String + updatedAt: String + + users: [User]! @relation(name: "CHATS_IN", direction: "IN") +} + +type Mutation { + CreateRoom( + userId: ID! + ): Room +} + +type Query { + Room: [Room] +}