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
})
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
* `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter
@ -197,7 +171,7 @@ const permissions = shield(
RemovePostEmotions: isAuthenticated,
block: isAuthenticated,
unblock: isAuthenticated,
markAsRead: belongsToMe,
markAsRead: isAuthenticated,
},
User: {
email: isMyOwn,

View File

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

View File

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