Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): messages #6450

Merged
merged 9 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions backend/src/graphql/messages.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
`
}
2 changes: 2 additions & 0 deletions backend/src/middleware/permissionsMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export default shield(
availableRoles: isAdmin,
getInviteCode: isAuthenticated, // and inviteRegistration
Room: isAuthenticated,
Message: isAuthenticated,
},
Mutation: {
'*': deny,
Expand Down Expand Up @@ -461,6 +462,7 @@ export default shield(
markTeaserAsViewed: allow,
saveCategorySettings: isAuthenticated,
CreateRoom: isAuthenticated,
CreateMessage: isAuthenticated,
},
User: {
email: or(isMyOwn, isAdmin),
Expand Down
269 changes: 269 additions & 0 deletions backend/src/schema/resolvers/messages.spec.ts
Original file line number Diff line number Diff line change
@@ -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: [],
},
})
})
})
})
})
})
61 changes: 61 additions & 0 deletions backend/src/schema/resolvers/messages.ts
Original file line number Diff line number Diff line change
@@ -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
Mogge marked this conversation as resolved.
Show resolved Hide resolved
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)',
}
}),
}
}
2 changes: 1 addition & 1 deletion backend/src/schema/resolvers/rooms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ beforeAll(async () => {
})

afterAll(async () => {
// await cleanDatabase()
await cleanDatabase()
driver.close()
})

Expand Down
Loading