Spec for notifications passing

This commit is contained in:
roschaefer 2019-08-29 21:56:56 +02:00
parent cf8ead10f3
commit 2846a6a8d3
4 changed files with 205 additions and 254 deletions

View File

@ -41,32 +41,6 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id return socialMedia.ownedBy.node.id === user.id
}) })
const belongsToMe = rule({
cache: 'no_cache',
})(async (_, args, context) => {
const {
driver,
user: { id: userId },
} = context
const { id: notificationId } = args
const session = driver.session()
const result = await session.run(
`
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
RETURN n
`,
{
userId,
notificationId,
},
)
const [notification] = result.records.map(record => {
return record.get('n')
})
session.close()
return Boolean(notification)
})
/* TODO: decide if we want to remove this check: the check /* TODO: decide if we want to remove this check: the check
* `onlyEnabledContent` throws authorization errors only if you have * `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter * arguments for `disabled` or `deleted` assuming these are filter
@ -197,7 +171,7 @@ const permissions = shield(
RemovePostEmotions: isAuthenticated, RemovePostEmotions: isAuthenticated,
block: isAuthenticated, block: isAuthenticated,
unblock: isAuthenticated, unblock: isAuthenticated,
markAsRead: belongsToMe, markAsRead: isAuthenticated,
}, },
User: { User: {
email: isMyOwn, email: isMyOwn,

View File

@ -56,8 +56,36 @@ export default {
}, },
}, },
Mutation: { Mutation: {
markAsRead: async (parent, params, context, resolveInfo) => { markAsRead: async (parent, args, context, resolveInfo) => {
return null const { user } = context
const session = context.driver.session()
let notification
try {
const cypher = `
MATCH (resource {id: $resourceId})-[notification:NOTIFIED]->(user:User {id:$id})
SET notification.read = TRUE
RETURN resource, notification, user
`
const result = await session.run(cypher, { resourceId: args.id, id: user.id })
const resourceTypes = ['Post', 'Comment']
const notifications = await result.records.map(record => {
return {
...record.get('notification').properties,
from: {
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
...record.get('resource').properties,
},
to: {
__typename: 'User',
...record.get('user').properties,
},
}
})
notification = notifications[0]
} finally {
session.close()
}
return notification
}, },
}, },
} }

View File

@ -1,11 +1,9 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers' import { gql } from '../../jest/helpers'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'
let client
const factory = Factory() const factory = Factory()
const neode = getNeode() const neode = getNeode()
const driver = getDriver() const driver = getDriver()
@ -16,6 +14,7 @@ const userParams = {
} }
let authenticatedUser let authenticatedUser
let user
let variables let variables
let query let query
let mutate let mutate
@ -42,90 +41,82 @@ afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('notifications', () => { describe('given some notifications', () => {
const notificationQuery = gql` beforeEach(async () => {
query($read: Boolean, $orderBy: NOTIFIEDOrdering) { user = await factory.create('User', userParams)
notifications(read: $read, orderBy: $orderBy) { await factory.create('User', { id: 'neighbor' })
from { await Promise.all(setupNotifications.map(s => neode.cypher(s)))
__typename })
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
const setupNotifications = [ const setupNotifications = [
` `MATCH(user:User {id: 'neighbor'})
MATCH(user:User {id: 'neighbor'}) MERGE (:Post {id: 'p1', content: 'Not for you'})
MERGE (:Post {id: 'p1', content: 'Not for you'}) -[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
-[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}] ->(user);
->(user); `,
`, `MATCH(user:User {id: 'you'})
` MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'})
MATCH(user:User {id: 'you'}) -[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}]
MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'}) ->(user);
-[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}] `,
->(user); `MATCH(user:User {id: 'you'})
`, MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'})
` -[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
MATCH(user:User {id: 'you'}) ->(user);
MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'}) `,
-[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}] `MATCH(user:User {id: 'you'})
->(user); MATCH(post:Post {id: 'p3'})
`, CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'})
` MERGE (comment)-[:COMMENTS]->(post)
MATCH(user:User {id: 'you'}) MERGE (comment)
MATCH(post:Post {id: 'p3'}) -[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}]
CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'}) ->(user);
MERGE (comment)-[:COMMENTS]->(post) `,
MERGE (comment) `MATCH(user:User {id: 'you'})
-[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}] MATCH(post:Post {id: 'p3'})
->(user); CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'})
`, MERGE (comment)-[:COMMENTS]->(post)
` MERGE (comment)
MATCH(user:User {id: 'you'}) -[:NOTIFIED {createdAt: "2019-08-30T19:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
MATCH(post:Post {id: 'p3'}) ->(user);
CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'}) `,
MERGE (comment)-[:COMMENTS]->(post) `MATCH(user:User {id: 'neighbor'})
MERGE (comment) MATCH(post:Post {id: 'p3'})
-[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}] CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'})
->(user); MERGE (comment)-[:COMMENTS]->(post)
`, MERGE (comment)
` -[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
MATCH(user:User {id: 'neighbor'}) ->(user);
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`, `,
] ]
describe('unauthenticated', () => { describe('notifications', () => {
it('throws authorization error', async () => { const notificationQuery = gql`
const result = await query({ query: notificationQuery }) query($read: Boolean, $orderBy: NotificationOrdering) {
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') notifications(read: $read, orderBy: $orderBy) {
}) from {
}) __typename
... on Post {
describe('authenticated', () => { content
beforeEach(async () => { }
const user = await factory.create('User', userParams) ... on Comment {
authenticatedUser = await user.toJson() content
}
}
read
createdAt
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const result = await query({ query: notificationQuery })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
}) })
describe('given some notifications', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { id: 'neighbor' }) authenticatedUser = await user.toJson()
await Promise.all(setupNotifications.map(s => neode.cypher(s)))
}) })
describe('no filters', () => { describe('no filters', () => {
@ -155,7 +146,7 @@ MERGE (comment)
content: 'You have been mentioned in a comment', content: 'You have been mentioned in a comment',
}, },
read: false, read: false,
createdAt: '2019-08-31T17:33:48.651Z', createdAt: '2019-08-30T19:33:48.651Z',
}, },
{ {
from: { from: {
@ -173,179 +164,130 @@ MERGE (comment)
}) })
describe('filter for read: false', () => { describe('filter for read: false', () => {
const queryCurrentUserNotificationsFilterRead = gql`
query($read: Boolean) {
notifications(read: $read, orderBy: createdAt_desc) {
id
post {
id
}
comment {
id
}
}
}
`
it('returns only unread notifications of current user', async () => { it('returns only unread notifications of current user', async () => {
const expected = { const expected = expect.objectContaining({
currentUser: { data: {
notifications: expect.arrayContaining([ notifications: [
{ {
id: 'post-mention-unseen', from: {
post: { __typename: 'Comment',
id: 'p1', content: 'You have been mentioned in a comment',
}, },
comment: null, read: false,
createdAt: '2019-08-30T19:33:48.651Z',
}, },
{ {
id: 'comment-mention-unseen', from: {
post: null, __typename: 'Post',
comment: { content: 'You have been mentioned in a post',
id: 'c1',
}, },
read: false,
createdAt: '2019-08-31T17:33:48.651Z',
}, },
]), ],
}, },
} })
await expect( await expect(
client.request(queryCurrentUserNotificationsFilterRead, variables), query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
}) })
}) })
}) })
})
describe('UpdateNotification', () => { describe('markAsRead', () => {
const mutationUpdateNotification = gql` const markAsReadMutation = gql`
mutation($id: ID!, $read: Boolean) { mutation($id: ID!) {
UpdateNotification(id: $id, read: $read) { markAsRead(id: $id) {
id from {
read __typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
} }
} `
`
const variablesPostUpdateNotification = {
id: 'post-mention-to-be-updated',
read: true,
}
const variablesCommentUpdateNotification = {
id: 'comment-mention-to-be-updated',
read: true,
}
describe('given some notifications', () => {
let headers
beforeEach(async () => {
const mentionedParams = {
id: 'mentioned-1',
email: 'mentioned@example.org',
password: '1234',
slug: 'mentioned',
}
await Promise.all([
factory.create('User', mentionedParams),
factory.create('Notification', {
id: 'post-mention-to-be-updated',
reason: 'mentioned_in_post',
}),
factory.create('Notification', {
id: 'comment-mention-to-be-updated',
reason: 'mentioned_in_comment',
}),
])
await factory.authenticateAs(userParams)
await factory.create('Post', { id: 'p1', categoryIds })
await Promise.all([
factory.relate('Notification', 'User', {
from: 'post-mention-to-be-updated',
to: 'mentioned-1',
}),
factory.relate('Notification', 'Post', {
from: 'p1',
to: 'post-mention-to-be-updated',
}),
])
// Comment and its notifications
await Promise.all([
factory.create('Comment', {
id: 'c1',
postId: 'p1',
}),
])
await Promise.all([
factory.relate('Notification', 'User', {
from: 'comment-mention-to-be-updated',
to: 'mentioned-1',
}),
factory.relate('Notification', 'Comment', {
from: 'p1',
to: 'comment-mention-to-be-updated',
}),
])
})
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) const result = await mutate({
await expect( mutation: markAsReadMutation,
client.request(mutationUpdateNotification, variablesPostUpdateNotification), variables: { ...variables, id: 'p1' },
).rejects.toThrow('Not Authorised') })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
}) })
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ authenticatedUser = await user.toJson()
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
}) })
it('throws authorization error', async () => { describe('not being notified at all', () => {
await expect(
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
).rejects.toThrow('Not Authorised')
})
describe('and owner', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ variables = {
email: 'mentioned@example.org', ...variables,
password: '1234', id: 'p1',
}
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
describe('being notified', () => {
describe('on a post', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p3',
}
}) })
client = new GraphQLClient(host, {
headers, it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
expect(data).toEqual({
markAsRead: {
from: {
__typename: 'Post',
content: 'You have been mentioned in a post',
},
read: true,
createdAt: '2019-08-31T17:33:48.651Z',
},
})
}) })
}) })
it('updates post notification', async () => { describe('on a comment', () => {
const expected = { beforeEach(async () => {
UpdateNotification: { variables = {
id: 'post-mention-to-be-updated', ...variables,
read: true, id: 'c2',
}, }
} })
await expect(
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
).resolves.toEqual(expected)
})
it('updates comment notification', async () => { it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const expected = { const { data } = await mutate({ mutation: markAsReadMutation, variables })
UpdateNotification: { expect(data).toEqual({
id: 'comment-mention-to-be-updated', markAsRead: {
read: true, from: {
}, __typename: 'Comment',
} content: 'You have been mentioned in a comment',
await expect( },
client.request(mutationUpdateNotification, variablesCommentUpdateNotification), read: true,
).resolves.toEqual(expected) createdAt: '2019-08-30T19:33:48.651Z',
},
})
})
}) })
}) })
}) })

View File

@ -3,17 +3,24 @@ type NOTIFIED {
to: User to: User
createdAt: String createdAt: String
read: Boolean read: Boolean
reason: NotificationReason
} }
union NotificationSource = Post | Comment union NotificationSource = Post | Comment
enum NOTIFIEDOrdering { enum NotificationOrdering {
createdAt_asc createdAt_asc
createdAt_desc createdAt_desc
} }
enum NotificationReason {
mentioned_in_post
mentioned_in_comment
comment_on_post
}
type Query { type Query {
notifications(read: Boolean, orderBy: NOTIFIEDOrdering): [NOTIFIED] notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED]
} }
type Mutation { type Mutation {