diff --git a/backend/package.json b/backend/package.json index d1a2f19d4..00f7be4bb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -49,7 +49,7 @@ "apollo-client": "~2.6.4", "apollo-link-context": "~1.0.18", "apollo-link-http": "~1.5.15", - "apollo-server": "~2.9.1", + "apollo-server": "~2.9.3", "apollo-server-express": "^2.9.0", "babel-plugin-transform-runtime": "^6.23.0", "bcryptjs": "~2.4.3", @@ -61,7 +61,7 @@ "dotenv": "~8.1.0", "express": "^4.17.1", "faker": "Marak/faker.js#master", - "graphql": "^14.5.3", + "graphql": "^14.5.4", "graphql-custom-directives": "~0.2.14", "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.5", @@ -90,7 +90,7 @@ "metascraper-video": "^5.6.5", "metascraper-youtube": "^5.6.3", "minimatch": "^3.0.4", - "neo4j-driver": "~1.7.5", + "neo4j-driver": "~1.7.6", "neo4j-graphql-js": "^2.7.2", "neode": "^0.3.2", "node-fetch": "~2.6.0", @@ -116,12 +116,12 @@ "babel-jest": "~24.9.0", "chai": "~4.2.0", "cucumber": "~5.1.0", - "eslint": "~6.2.2", + "eslint": "~6.3.0", "eslint-config-prettier": "~6.1.0", "eslint-config-standard": "~14.1.0", "eslint-plugin-import": "~2.18.2", - "eslint-plugin-jest": "~22.15.2", - "eslint-plugin-node": "~9.1.0", + "eslint-plugin-jest": "~22.16.0", + "eslint-plugin-node": "~9.2.0", "eslint-plugin-prettier": "~3.1.0", "eslint-plugin-promise": "~4.2.1", "eslint-plugin-standard": "~4.0.1", diff --git a/backend/src/middleware/dateTimeMiddleware.js b/backend/src/middleware/dateTimeMiddleware.js index c8af53a7a..ff1fcc996 100644 --- a/backend/src/middleware/dateTimeMiddleware.js +++ b/backend/src/middleware/dateTimeMiddleware.js @@ -12,11 +12,9 @@ export default { CreatePost: setCreatedAt, CreateComment: setCreatedAt, CreateOrganization: setCreatedAt, - CreateNotification: setCreatedAt, UpdateUser: setUpdatedAt, UpdatePost: setUpdatedAt, UpdateComment: setUpdatedAt, UpdateOrganization: setUpdatedAt, - UpdateNotification: setUpdatedAt, }, } diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index c9dfe406c..64386800d 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -4,13 +4,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return // Checked here, because it does not go through GraphQL checks at all in this file. - const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'] + const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post'] if (!reasonsAllowed.includes(reason)) { throw new Error('Notification reason is not allowed!') } if ( (label === 'Post' && reason !== 'mentioned_in_post') || - (label === 'Comment' && !['mentioned_in_comment', 'comment_on_post'].includes(reason)) + (label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason)) ) { throw new Error('Notification does not fit the reason!') } @@ -25,8 +25,9 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { MATCH (user: User) WHERE user.id in $idsOfUsers AND NOT (user)<-[:BLOCKED]-(author) - CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) - MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) + MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) + SET notification.read = FALSE + SET notification.createdAt = $createdAt ` break } @@ -37,20 +38,22 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { WHERE user.id in $idsOfUsers AND NOT (user)<-[:BLOCKED]-(author) AND NOT (user)<-[:BLOCKED]-(postAuthor) - CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) - MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) + MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) + SET notification.read = FALSE + SET notification.createdAt = $createdAt ` break } - case 'comment_on_post': { + case 'commented_on_post': { cypher = ` MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) MATCH (user: User) WHERE user.id in $idsOfUsers AND NOT (user)<-[:BLOCKED]-(author) AND NOT (author)<-[:BLOCKED]-(user) - CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) - MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) + MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) + SET notification.read = FALSE + SET notification.createdAt = $createdAt ` break } @@ -105,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => return record.get('user') }) if (context.user.id !== postAuthor.id) { - await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_post', context) + await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context) } } diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index 624cedddc..b737768f2 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -77,14 +77,18 @@ afterEach(async () => { describe('notifications', () => { const notificationQuery = gql` query($read: Boolean) { - currentUser { - notifications(read: $read, orderBy: createdAt_desc) { - read - reason - post { + notifications(read: $read, orderBy: createdAt_desc) { + read + reason + createdAt + from { + __typename + ... on Post { + id content } - comment { + ... on Comment { + id content } } @@ -154,18 +158,18 @@ describe('notifications', () => { await createCommentOnPostAction() const expected = expect.objectContaining({ data: { - currentUser: { - notifications: [ - { - read: false, - reason: 'comment_on_post', - post: null, - comment: { - content: commentContent, - }, + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'commented_on_post', + from: { + __typename: 'Comment', + id: 'c47', + content: commentContent, }, - ], - }, + }, + ], }, }) const { query } = createTestClient(server) @@ -183,11 +187,7 @@ describe('notifications', () => { await notifiedUser.relateTo(commentAuthor, 'blocked') await createCommentOnPostAction() const expected = expect.objectContaining({ - data: { - currentUser: { - notifications: [], - }, - }, + data: { notifications: [] }, }) const { query } = createTestClient(server) await expect( @@ -211,11 +211,7 @@ describe('notifications', () => { await notifiedUser.relateTo(commentAuthor, 'blocked') await createCommentOnPostAction() const expected = expect.objectContaining({ - data: { - currentUser: { - notifications: [], - }, - }, + data: { notifications: [] }, }) const { query } = createTestClient(server) await expect( @@ -253,18 +249,18 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' const expected = expect.objectContaining({ data: { - currentUser: { - notifications: [ - { - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, - }, - comment: null, + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + id: 'p47', + content: expectedContent, }, - ], - }, + }, + ], }, }) const { query } = createTestClient(server) @@ -278,7 +274,7 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) - describe('many times', () => { + describe('updates the post and mentions me again', () => { const updatePostAction = async () => { const updatedContent = ` One more mention to @@ -307,33 +303,25 @@ describe('notifications', () => { authenticatedUser = await notifiedUser.toJson() } - it('creates exactly one more notification', async () => { + it('creates no duplicate notification for the same resource', async () => { + const expectedUpdatedContent = + '
One more mention to

@al-capone

and again:

@al-capone

and again

@al-capone

' await createPostAction() await updatePostAction() - const expectedContent = - '
One more mention to

@al-capone

and again:

@al-capone

and again

@al-capone

' const expected = expect.objectContaining({ data: { - currentUser: { - notifications: [ - { - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, - }, - comment: null, + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + id: 'p47', + content: expectedUpdatedContent, }, - { - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, - }, - comment: null, - }, - ], - }, + }, + ], }, }) await expect( @@ -345,6 +333,68 @@ describe('notifications', () => { }), ).resolves.toEqual(expected) }) + + describe('if the notification was marked as read earlier', () => { + const markAsReadAction = async () => { + const mutation = gql` + mutation($id: ID!) { + markAsRead(id: $id) { + read + } + } + ` + await mutate({ mutation, variables: { id: 'p47' } }) + } + + describe('but the next mention happens after the notification was marked as read', () => { + it('sets the `read` attribute to false again', async () => { + await createPostAction() + await markAsReadAction() + const { + data: { + notifications: [{ read: readBefore }], + }, + } = await query({ + query: notificationQuery, + }) + await updatePostAction() + const { + data: { + notifications: [{ read: readAfter }], + }, + } = await query({ + query: notificationQuery, + }) + expect(readBefore).toEqual(true) + expect(readAfter).toEqual(false) + }) + + it('updates the `createdAt` attribute', async () => { + await createPostAction() + await markAsReadAction() + const { + data: { + notifications: [{ createdAt: createdAtBefore }], + }, + } = await query({ + query: notificationQuery, + }) + await updatePostAction() + const { + data: { + notifications: [{ createdAt: createdAtAfter }], + }, + } = await query({ + query: notificationQuery, + }) + expect(createdAtBefore).toBeTruthy() + expect(Date.parse(createdAtBefore)).toEqual(expect.any(Number)) + expect(createdAtAfter).toBeTruthy() + expect(Date.parse(createdAtAfter)).toEqual(expect.any(Number)) + expect(createdAtBefore).not.toEqual(createdAtAfter) + }) + }) + }) }) describe('but the author of the post blocked me', () => { @@ -355,11 +405,7 @@ describe('notifications', () => { it('sends no notification', async () => { await createPostAction() const expected = expect.objectContaining({ - data: { - currentUser: { - notifications: [], - }, - }, + data: { notifications: [] }, }) const { query } = createTestClient(server) await expect( @@ -397,18 +443,18 @@ describe('notifications', () => { await createCommentOnPostAction() const expected = expect.objectContaining({ data: { - currentUser: { - notifications: [ - { - read: false, - reason: 'mentioned_in_comment', - post: null, - comment: { - content: commentContent, - }, + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'mentioned_in_comment', + from: { + __typename: 'Comment', + id: 'c47', + content: commentContent, }, - ], - }, + }, + ], }, }) const { query } = createTestClient(server) @@ -440,11 +486,7 @@ describe('notifications', () => { it('sends no notification', async () => { await createCommentOnPostAction() const expected = expect.objectContaining({ - data: { - currentUser: { - notifications: [], - }, - }, + data: { notifications: [] }, }) const { query } = createTestClient(server) await expect( diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 83c29d19d..2a52e54af 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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 @@ -149,7 +123,6 @@ const permissions = shield( Category: allow, Tag: allow, Report: isModerator, - Notification: isAdmin, statistics: allow, currentUser: allow, Post: or(onlyEnabledContent, isModerator), @@ -160,6 +133,7 @@ const permissions = shield( PostsEmotionsCountByEmotion: allow, PostsEmotionsByCurrentUser: allow, blockedUsers: isAuthenticated, + notifications: isAuthenticated, }, Mutation: { '*': deny, @@ -168,7 +142,6 @@ const permissions = shield( Signup: isAdmin, SignupVerification: allow, CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)), - UpdateNotification: belongsToMe, UpdateUser: onlyYourself, CreatePost: isAuthenticated, UpdatePost: isAuthor, @@ -198,6 +171,7 @@ const permissions = shield( RemovePostEmotions: isAuthenticated, block: isAuthenticated, unblock: isAuthenticated, + markAsRead: isAuthenticated, }, User: { email: isMyOwn, diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js deleted file mode 100644 index b54a99574..000000000 --- a/backend/src/models/Notification.js +++ /dev/null @@ -1,36 +0,0 @@ -import uuid from 'uuid/v4' - -module.exports = { - id: { - type: 'uuid', - primary: true, - default: uuid, - }, - read: { - type: 'boolean', - default: false, - }, - reason: { - type: 'string', - valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'], - invalid: [null], - default: 'mentioned_in_post', - }, - createdAt: { - type: 'string', - isoDate: true, - default: () => new Date().toISOString(), - }, - user: { - type: 'relationship', - relationship: 'NOTIFIED', - target: 'User', - direction: 'out', - }, - post: { - type: 'relationship', - relationship: 'NOTIFIED', - target: 'Post', - direction: 'in', - }, -} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 295082de4..5a4510ac6 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -7,6 +7,5 @@ export default { EmailAddress: require('./EmailAddress.js'), SocialMedia: require('./SocialMedia.js'), Post: require('./Post.js'), - Notification: require('./Notification.js'), Category: require('./Category.js'), } diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index b8f120057..e8fa63d97 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -20,6 +20,7 @@ export default applyScalars( 'Statistics', 'LoggedInUser', 'SocialMedia', + 'NOTIFIED', ], // add 'User' here as soon as possible }, @@ -32,6 +33,7 @@ export default applyScalars( 'Statistics', 'LoggedInUser', 'SocialMedia', + 'NOTIFIED', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 89a2040f4..c8fb8db3f 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -1,4 +1,5 @@ import { neo4jgraphql } from 'neo4j-graphql-js' +import Resolver from './helpers/Resolver' export default { Mutation: { @@ -52,4 +53,13 @@ export default { return comment }, }, + Comment: { + ...Resolver('Comment', { + hasOne: { + author: '<-[:WROTE]-(related:User)', + post: '-[:COMMENTS]->(related:Post)', + disabledBy: '<-[:DISABLED]-(related:User)', + }, + }), + }, } diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index fd41205a3..9a6f77513 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -61,7 +61,6 @@ export default function Resolver(type, options = {}) { const id = parent[idAttribute] const statement = ` MATCH(u:${type} {${idAttribute}: {id}})${connection} - WHERE NOT related.deleted = true AND NOT related.disabled = true RETURN COUNT(DISTINCT(related)) as count ` const result = await instance.cypher(statement, { id }) diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index ddc1985cf..65c92b4be 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -1,14 +1,80 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' +const resourceTypes = ['Post', 'Comment'] + +const transformReturnType = record => { + return { + ...record.get('notification').properties, + from: { + __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), + ...record.get('resource').properties, + }, + to: { + ...record.get('user').properties, + }, + } +} export default { Query: { - Notification: (object, params, context, resolveInfo) => { - return neo4jgraphql(object, params, context, resolveInfo, false) + notifications: async (parent, args, context, resolveInfo) => { + const { user: currentUser } = context + const session = context.driver.session() + let notifications + let whereClause + let orderByClause + switch (args.read) { + case true: + whereClause = 'WHERE notification.read = TRUE' + break + case false: + whereClause = 'WHERE notification.read = FALSE' + break + default: + whereClause = '' + } + switch (args.orderBy) { + case 'createdAt_asc': + orderByClause = 'ORDER BY notification.createdAt ASC' + break + case 'createdAt_desc': + orderByClause = 'ORDER BY notification.createdAt DESC' + break + default: + orderByClause = '' + } + + try { + const cypher = ` + MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id}) + ${whereClause} + RETURN resource, notification, user + ${orderByClause} + ` + const result = await session.run(cypher, { id: currentUser.id }) + notifications = await result.records.map(transformReturnType) + } finally { + session.close() + } + return notifications }, }, Mutation: { - UpdateNotification: (object, params, context, resolveInfo) => { - return neo4jgraphql(object, params, context, resolveInfo, false) + markAsRead: async (parent, args, context, resolveInfo) => { + const { user: currentUser } = context + const session = context.driver.session() + let notification + try { + const cypher = ` + MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id}) + SET notification.read = TRUE + RETURN resource, notification, user + ` + const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id }) + const notifications = await result.records.map(transformReturnType) + notification = notifications[0] + } finally { + session.close() + } + return notification }, }, } diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 3ca7727e4..b321d449f 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,397 +1,309 @@ -import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' -import { neode } from '../../bootstrap/neo4j' +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 instance = neode() +const neode = getNeode() +const driver = getDriver() const userParams = { id: 'you', email: 'test@example.org', password: '1234', } -const categoryIds = ['cat9'] + +let authenticatedUser +let user +let variables +let query +let mutate + +beforeAll(() => { + const { server } = createServer({ + context: () => { + return { + driver, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) beforeEach(async () => { - await factory.create('User', userParams) - await instance.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) + authenticatedUser = null + variables = { orderBy: 'createdAt_asc' } }) afterEach(async () => { await factory.cleanDatabase() }) -describe('Notification', () => { - const notificationQuery = gql` - query { - Notification { - id - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised') - }) +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-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('currentUser notifications', () => { - const variables = {} - - describe('authenticated', () => { - let headers - beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) - }) - - describe('given some notifications', () => { - beforeEach(async () => { - const neighborParams = { - email: 'neighbor@example.org', - password: '1234', - id: 'neighbor', + 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 } - await Promise.all([ - factory.create('User', neighborParams), - factory.create('Notification', { - id: 'post-mention-not-for-you', - reason: 'mentioned_in_post', - }), - factory.create('Notification', { - id: 'post-mention-already-seen', - read: true, - reason: 'mentioned_in_post', - }), - factory.create('Notification', { - id: 'post-mention-unseen', - reason: 'mentioned_in_post', - }), - factory.create('Notification', { - id: 'comment-mention-not-for-you', - reason: 'mentioned_in_comment', - }), - factory.create('Notification', { - id: 'comment-mention-already-seen', - read: true, - reason: 'mentioned_in_comment', - }), - factory.create('Notification', { - id: 'comment-mention-unseen', - reason: 'mentioned_in_comment', - }), - ]) - await factory.authenticateAs(neighborParams) - await factory.create('Post', { id: 'p1', categoryIds }) - await Promise.all([ - factory.relate('Notification', 'User', { - from: 'post-mention-not-for-you', - to: 'neighbor', - }), - factory.relate('Notification', 'Post', { - from: 'p1', - to: 'post-mention-not-for-you', - }), - factory.relate('Notification', 'User', { - from: 'post-mention-unseen', - to: 'you', - }), - factory.relate('Notification', 'Post', { - from: 'p1', - to: 'post-mention-unseen', - }), - factory.relate('Notification', 'User', { - from: 'post-mention-already-seen', - to: 'you', - }), - factory.relate('Notification', 'Post', { - from: 'p1', - to: 'post-mention-already-seen', - }), - ]) - // Comment and its notifications - await Promise.all([ - factory.create('Comment', { - id: 'c1', - postId: 'p1', - }), - ]) - await Promise.all([ - factory.relate('Notification', 'User', { - from: 'comment-mention-not-for-you', - to: 'neighbor', - }), - factory.relate('Notification', 'Comment', { - from: 'c1', - to: 'comment-mention-not-for-you', - }), - factory.relate('Notification', 'User', { - from: 'comment-mention-unseen', - to: 'you', - }), - factory.relate('Notification', 'Comment', { - from: 'c1', - to: 'comment-mention-unseen', - }), - factory.relate('Notification', 'User', { - from: 'comment-mention-already-seen', - to: 'you', - }), - factory.relate('Notification', 'Comment', { - from: 'c1', - to: 'comment-mention-already-seen', - }), - ]) - }) - - describe('filter for read: false', () => { - const queryCurrentUserNotificationsFilterRead = gql` - query($read: Boolean) { - currentUser { - notifications(read: $read, orderBy: createdAt_desc) { - id - post { - id - } - comment { - id - } - } - } - } - ` - const variables = { read: false } - it('returns only unread notifications of current user', async () => { - const expected = { - currentUser: { - notifications: expect.arrayContaining([ - { - id: 'post-mention-unseen', - post: { - id: 'p1', - }, - comment: null, - }, - { - id: 'comment-mention-unseen', - post: null, - comment: { - id: 'c1', - }, - }, - ]), - }, - } - await expect( - client.request(queryCurrentUserNotificationsFilterRead, variables), - ).resolves.toEqual(expected) - }) - }) - - describe('no filters', () => { - const queryCurrentUserNotifications = gql` - query { - currentUser { - notifications(orderBy: createdAt_desc) { - id - post { - id - } - comment { - id - } - } - } - } - ` - it('returns all notifications of current user', async () => { - const expected = { - currentUser: { - notifications: expect.arrayContaining([ - { - id: 'post-mention-unseen', - post: { - id: 'p1', - }, - comment: null, - }, - { - id: 'post-mention-already-seen', - post: { - id: 'p1', - }, - comment: null, - }, - { - id: 'comment-mention-unseen', - comment: { - id: 'c1', - }, - post: null, - }, - { - id: 'comment-mention-already-seen', - comment: { - id: 'c1', - }, - post: null, - }, - ]), - }, - } - await expect(client.request(queryCurrentUserNotifications, variables)).resolves.toEqual( - expected, - ) - }) - }) - }) - }) -}) - -describe('UpdateNotification', () => { - const mutationUpdateNotification = gql` - mutation($id: ID!, $read: Boolean) { - UpdateNotification(id: $id, read: $read) { - id - read } - } - ` - 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 query({ query: notificationQuery }) + 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() + }) + + describe('no filters', () => { + it('returns all notifications of current user', async () => { + const expected = expect.objectContaining({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + content: 'You have seen this comment mentioning already', + }, + read: true, + createdAt: '2019-08-30T15:33:48.651Z', + }, + { + from: { + __typename: 'Post', + content: 'Already seen post mentioning', + }, + read: true, + createdAt: '2019-08-30T17:33:48.651Z', + }, + { + from: { + __typename: 'Comment', + content: 'You have been mentioned in a comment', + }, + read: false, + createdAt: '2019-08-30T19:33:48.651Z', + }, + { + from: { + __typename: 'Post', + content: 'You have been mentioned in a post', + }, + read: false, + createdAt: '2019-08-31T17:33:48.651Z', + }, + ], + }, + }) + await expect(query({ query: notificationQuery, variables })).resolves.toEqual(expected) }) }) - it('throws authorization error', async () => { - await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).rejects.toThrow('Not Authorised') - }) - - describe('and owner', () => { - beforeEach(async () => { - headers = await login({ - email: 'mentioned@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) - }) - - it('updates post notification', async () => { - const expected = { - UpdateNotification: { - id: 'post-mention-to-be-updated', - read: true, + describe('filter for read: false', () => { + it('returns only unread notifications of current user', async () => { + const expected = expect.objectContaining({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + content: 'You have been mentioned in a comment', + }, + read: false, + createdAt: '2019-08-30T19:33:48.651Z', + }, + { + from: { + __typename: 'Post', + content: 'You have been mentioned in a post', + }, + read: false, + createdAt: '2019-08-31T17:33:48.651Z', + }, + ], }, - } + }) await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).resolves.toEqual(expected) - }) - - it('updates comment notification', async () => { - const expected = { - UpdateNotification: { - id: 'comment-mention-to-be-updated', - read: true, - }, - } - await expect( - client.request(mutationUpdateNotification, variablesCommentUpdateNotification), + query({ query: notificationQuery, variables: { ...variables, read: false } }), ).resolves.toEqual(expected) }) }) }) }) + + describe('markAsRead', () => { + const markAsReadMutation = gql` + mutation($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const result = await mutate({ + mutation: markAsReadMutation, + variables: { ...variables, id: 'p1' }, + }) + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + describe('not being notified at all', () => { + beforeEach(async () => { + 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', + } + }) + + 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', + }, + }) + }) + + describe('but notification was already marked as read', () => { + beforeEach(async () => { + variables = { + ...variables, + id: 'p2', + } + }) + it('returns null', async () => { + const response = await mutate({ mutation: markAsReadMutation, variables }) + expect(response.data.markAsRead).toEqual(null) + expect(response.errors).toBeUndefined() + }) + }) + }) + + describe('on a comment', () => { + beforeEach(async () => { + variables = { + ...variables, + id: 'c2', + } + }) + + 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', + }, + }) + }) + }) + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 46d7c414f..f9e4f0718 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' import { getBlockedUsers, getBlockedByUsers } from './users.js' import { mergeWith, isArray } from 'lodash' +import Resolver from './helpers/Resolver' const filterForBlockedUsers = async (params, context) => { if (!context.user) return params @@ -11,6 +12,8 @@ const filterForBlockedUsers = async (params, context) => { getBlockedByUsers(context), ]) const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)] + if (!badIds.length) return params + params.filter = mergeWith( params.filter, { @@ -179,4 +182,46 @@ export default { return emoted }, }, + Post: { + ...Resolver('Post', { + hasMany: { + tags: '-[:TAGGED]->(related:Tag)', + categories: '-[:CATEGORIZED]->(related:Category)', + comments: '<-[:COMMENTS]-(related:Comment)', + shoutedBy: '<-[:SHOUTED]-(related:User)', + emotions: '<-[related:EMOTED]', + }, + hasOne: { + author: '<-[:WROTE]-(related:User)', + disabledBy: '<-[:DISABLED]-(related:User)', + }, + count: { + shoutedCount: + '<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true', + emotionsCount: '<-[related:EMOTED]-(:User)', + }, + boolean: { + shoutedByCurrentUser: + '<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', + }, + }), + relatedContributions: async (parent, params, context, resolveInfo) => { + if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions + const { id } = parent + const statement = ` + MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) + RETURN DISTINCT post + LIMIT 10 + ` + let relatedContributions + const session = context.driver.session() + try { + const result = await session.run(statement, { id }) + relatedContributions = result.records.map(r => r.get('post').properties) + } finally { + session.close() + } + return relatedContributions + }, + }, } diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 8d9b194de..e736846df 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -210,12 +210,15 @@ export default { 'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', }, count: { - contributionsCount: '-[:WROTE]->(related:Post)', + contributionsCount: + '-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', friendsCount: '<-[:FRIENDS]->(related:User)', followingCount: '-[:FOLLOWS]->(related:User)', followedByCount: '<-[:FOLLOWS]-(related:User)', - commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)', - shoutedCount: '-[:SHOUTED]->(related:Post)', + commentedCount: + '-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', + shoutedCount: + '-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', badgesCount: '<-[:REWARDED]-(related:Badge)', }, hasOne: { diff --git a/backend/src/schema/types/enum/ReasonNotification.gql b/backend/src/schema/types/enum/ReasonNotification.gql index a66c446be..e870e01dc 100644 --- a/backend/src/schema/types/enum/ReasonNotification.gql +++ b/backend/src/schema/types/enum/ReasonNotification.gql @@ -1,5 +1,5 @@ enum ReasonNotification { mentioned_in_post mentioned_in_comment - comment_on_post -} \ No newline at end of file + commented_on_post +} diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index e0a2c328b..62f84d677 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -4,7 +4,7 @@ type Query { currentUser: User # Get the latest Network Statistics statistics: Statistics! - findPosts(query: String!, limit: Int = 10): [Post]! + findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]! @cypher( statement: """ CALL db.index.fulltext.queryNodes('full_text_search', $query) diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql new file mode 100644 index 000000000..b90e30598 --- /dev/null +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -0,0 +1,28 @@ +type NOTIFIED { + from: NotificationSource + to: User + createdAt: String + read: Boolean + reason: NotificationReason +} + +union NotificationSource = Post | Comment + +enum NotificationOrdering { + createdAt_asc + createdAt_desc +} + +enum NotificationReason { + mentioned_in_post + mentioned_in_comment + commented_on_post +} + +type Query { + notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED] +} + +type Mutation { + markAsRead(id: ID!): NOTIFIED +} diff --git a/backend/src/schema/types/type/Notification.gql b/backend/src/schema/types/type/Notification.gql deleted file mode 100644 index a3543445f..000000000 --- a/backend/src/schema/types/type/Notification.gql +++ /dev/null @@ -1,9 +0,0 @@ -type Notification { - id: ID! - read: Boolean - reason: ReasonNotification - createdAt: String - user: User @relation(name: "NOTIFIED", direction: "OUT") - post: Post @relation(name: "NOTIFIED", direction: "IN") - comment: Comment @relation(name: "NOTIFIED", direction: "IN") -} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 7bdfa4aee..f4e120300 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -24,10 +24,12 @@ type User { createdAt: String updatedAt: String + termsAndConditionsAgreedVersion: String notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") + friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") @@ -66,7 +68,7 @@ type User { ) comments: [Comment]! @relation(name: "WROTE", direction: "OUT") - commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment)-[:COMMENTS]->(p:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true AND NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))") + commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))") shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 56518bd06..4cc143e68 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -8,7 +8,6 @@ import createComment from './comments.js' import createCategory from './categories.js' import createTag from './tags.js' import createReport from './reports.js' -import createNotification from './notifications.js' export const seedServerHost = 'http://127.0.0.1:4001' @@ -31,7 +30,6 @@ const factories = { Category: createCategory, Tag: createTag, Report: createReport, - Notification: createNotification, } export const cleanDatabase = async (options = {}) => { diff --git a/backend/src/seed/factories/notifications.js b/backend/src/seed/factories/notifications.js deleted file mode 100644 index d14d4294a..000000000 --- a/backend/src/seed/factories/notifications.js +++ /dev/null @@ -1,17 +0,0 @@ -import uuid from 'uuid/v4' - -export default function(params) { - const { id = uuid(), read = false } = params - - return { - mutation: ` - mutation($id: ID, $read: Boolean) { - CreateNotification(id: $id, read: $read) { - id - read - } - } - `, - variables: { id, read }, - } -} diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index bbacd2149..5a5c7716b 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -270,7 +270,7 @@ import Factory from './factories' const hashtag1 = 'See #NaturphilosophieYoga can really help you!' const hashtagAndMention1 = - 'The new physics of #QuantenFlussTheorie can explain #QuantumGravity! @peter-lustig got that already. ;-)' + 'The new physics of #QuantenFlussTheorie can explain #QuantumGravity! @peter-lustig got that already. ;-)' await Promise.all([ asAdmin.create('Post', { diff --git a/backend/yarn.lock b/backend/yarn.lock index cbf7b7ff5..f3d6db70d 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -689,7 +689,7 @@ core-js "^2.6.5" regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== @@ -1546,6 +1546,14 @@ apollo-cache-control@0.8.2: apollo-server-env "2.4.2" graphql-extensions "0.10.1" +apollo-cache-control@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7" + integrity sha512-IZ1d3AXZtkZhLYo0kWqTbZ6nqLFaeUvLdMESs+9orMadBZ7mvzcAfBwrhKyCWPGeAAZ/jKv8FtYHybpchHgFAg== + dependencies: + apollo-server-env "^2.4.3" + graphql-extensions "^0.10.3" + apollo-cache-inmemory@~1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" @@ -1587,7 +1595,15 @@ apollo-datasource@0.6.2: apollo-server-caching "0.5.0" apollo-server-env "2.4.2" -apollo-engine-reporting-protobuf@0.4.0: +apollo-datasource@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13" + integrity sha512-gRYyFVpJgHE2hhS+VxMeOerxXQ/QYxWG7T6QddfugJWYAG9DRCl65e2b7txcGq2NP3r+O1iCm4GNwhRBDJbd8A== + dependencies: + apollo-server-caching "^0.5.0" + apollo-server-env "^2.4.3" + +apollo-engine-reporting-protobuf@0.4.0, apollo-engine-reporting-protobuf@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec" integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA== @@ -1607,6 +1623,19 @@ apollo-engine-reporting@1.4.4: async-retry "^1.2.1" graphql-extensions "0.10.1" +apollo-engine-reporting@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f" + integrity sha512-acfb7oFnru/8YQdY4x6+7WJbZfzdVETI8Cl+9ImgUrvUnE8P+f2SsGTKXTC1RuUvve4c56PAvaPgE+z8X1a1Mw== + dependencies: + apollo-engine-reporting-protobuf "^0.4.0" + apollo-graphql "^0.3.3" + apollo-server-caching "^0.5.0" + apollo-server-env "^2.4.3" + apollo-server-types "^0.2.4" + async-retry "^1.2.1" + graphql-extensions "^0.10.3" + apollo-env@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3" @@ -1668,7 +1697,7 @@ apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3: tslib "^1.9.3" zen-observable-ts "^0.8.19" -apollo-server-caching@0.5.0: +apollo-server-caching@0.5.0, apollo-server-caching@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46" integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw== @@ -1702,6 +1731,33 @@ apollo-server-core@2.9.1: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" +apollo-server-core@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f" + integrity sha512-KQpOM3nAXdMqKVE0HHcOkH/EVhyDqFEKLNFlsyGHGOn9ujpI6RsltX+YpXRyAdbfQHpTk11v/IAo6XksWN+g1Q== + dependencies: + "@apollographql/apollo-tools" "^0.4.0" + "@apollographql/graphql-playground-html" "1.6.24" + "@types/graphql-upload" "^8.0.0" + "@types/ws" "^6.0.0" + apollo-cache-control "^0.8.4" + apollo-datasource "^0.6.3" + apollo-engine-reporting "^1.4.6" + apollo-server-caching "^0.5.0" + apollo-server-env "^2.4.3" + apollo-server-errors "^2.3.3" + apollo-server-plugin-base "^0.6.4" + apollo-server-types "^0.2.4" + apollo-tracing "^0.8.4" + fast-json-stable-stringify "^2.0.0" + graphql-extensions "^0.10.3" + graphql-tag "^2.9.2" + graphql-tools "^4.0.0" + graphql-upload "^8.0.2" + sha.js "^2.4.11" + subscriptions-transport-ws "^0.9.11" + ws "^6.0.0" + apollo-server-env@2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.2.tgz#8549caa7c8f57af88aadad5c2a0bb7adbcc5f76e" @@ -1710,15 +1766,28 @@ apollo-server-env@2.4.2: node-fetch "^2.1.2" util.promisify "^1.0.0" +apollo-server-env@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2" + integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA== + dependencies: + node-fetch "^2.1.2" + util.promisify "^1.0.0" + apollo-server-errors@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.2.tgz#86bbd1ff8f0b5f16bfdcbb1760398928f9fce539" integrity sha512-twVCP8tNHFzxOzU3jf84ppBFSvjvisZVWlgF82vwG+qEEUaAE5h5DVpeJbcI1vRW4VQPuFV+B+FIsnlweFKqtQ== -apollo-server-express@2.9.1, apollo-server-express@^2.9.0: - version "2.9.1" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.1.tgz#9a8cb7fba579e68ddfa1953dfd066b751bca32f0" - integrity sha512-3mmuojt9s9Gyqdf8fbdKtbw23UFYrtVQtTNASgVW8zCabZqs2WjYnijMRf1aL4u9VSl+BFMOZUPMYaeBX+u38w== +apollo-server-errors@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff" + integrity sha512-MO4oJ129vuCcbqwr5ZwgxqGGiLz3hCyowz0bstUF7MR+vNGe4oe3DWajC9lv4CxrhcqUHQOeOPViOdIo1IxE3g== + +apollo-server-express@^2.9.0, apollo-server-express@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.3.tgz#67573404030c2676be49a7bf97d423b8462e295c" + integrity sha512-Hkfs+ce6GqaoSzDOJs8Pj7W3YUjH0BzGglo5HMsOXOnjPZ0pJE9v8fmK76rlkITLw7GjvIq5GKlafymC31FMBw== dependencies: "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" @@ -1726,10 +1795,11 @@ apollo-server-express@2.9.1, apollo-server-express@^2.9.0: "@types/cors" "^2.8.4" "@types/express" "4.17.1" accepts "^1.3.5" - apollo-server-core "2.9.1" - apollo-server-types "0.2.2" + apollo-server-core "^2.9.3" + apollo-server-types "^0.2.4" body-parser "^1.18.3" cors "^2.8.4" + express "^4.17.1" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" parseurl "^1.3.2" @@ -1743,6 +1813,13 @@ apollo-server-plugin-base@0.6.2: dependencies: apollo-server-types "0.2.2" +apollo-server-plugin-base@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.4.tgz#63ea4fd0bbb6c4510bc8d0d2ad0a0684c8d0da8c" + integrity sha512-4rY+cBAIpQomGWYBtk8hHkLQWHrh5hgIBPQqmhXh00YFdcY+Ob1/cU2/2iqTcIzhtcaezsc8OZ63au6ahSBQqg== + dependencies: + apollo-server-types "^0.2.4" + apollo-server-testing@~2.9.1: version "2.9.1" resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49" @@ -1759,13 +1836,22 @@ apollo-server-types@0.2.2: apollo-server-caching "0.5.0" apollo-server-env "2.4.2" -apollo-server@~2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.1.tgz#16ff443d43ea38f72fe20adea0803c46037b2b3b" - integrity sha512-iCGoRBOvwTUkDz6Nq/rKguMyhDiQdL3VneF0GTjBGrelTIp3YTIxk/qBFkIr2Chtm9ZZYkS6o+ZldUnxYFKg7A== +apollo-server-types@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.4.tgz#28864900ffc7f9711a859297c143a833fdb6aa43" + integrity sha512-G4FvBVgGQcTW6ZBS2+hvcDQkSfdOIKV+cHADduXA275v+5zl42g+bCaGd/hCCKTDRjmQvObLiMxH/BJ6pDMQgA== dependencies: - apollo-server-core "2.9.1" - apollo-server-express "2.9.1" + apollo-engine-reporting-protobuf "^0.4.0" + apollo-server-caching "^0.5.0" + apollo-server-env "^2.4.3" + +apollo-server@~2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.3.tgz#2a79fcee25da0b0673eb70d73839c40c3c4b8cca" + integrity sha512-JQoeseSo3yOBu3WJzju0NTreoqYckNILybgXNUOhdurE55VFpZ8dsBEO6nMfdO2y1A70W14mnnVWCBEm+1rE8w== + dependencies: + apollo-server-core "^2.9.3" + apollo-server-express "^2.9.3" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" @@ -1778,6 +1864,14 @@ apollo-tracing@0.8.2: apollo-server-env "2.4.2" graphql-extensions "0.10.1" +apollo-tracing@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de" + integrity sha512-DjbFW0IvHicSlTVG+vK+1WINfBMRCdPPHJSW/j65JMir9Oe56WGeqL8qz8hptdUUmLYEb+azvcyyGsJsiR3zpQ== + dependencies: + apollo-server-env "^2.4.3" + graphql-extensions "^0.10.3" + apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" @@ -3311,12 +3405,12 @@ eslint-module-utils@^2.4.0: debug "^2.6.8" pkg-dir "^2.0.0" -eslint-plugin-es@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6" - integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw== +eslint-plugin-es@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998" + integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA== dependencies: - eslint-utils "^1.3.0" + eslint-utils "^1.4.2" regexpp "^2.0.1" eslint-plugin-import@~2.18.2: @@ -3336,20 +3430,20 @@ eslint-plugin-import@~2.18.2: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~22.15.2: - version "22.15.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398" - integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg== +eslint-plugin-jest@~22.16.0: + version "22.16.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05" + integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg== dependencies: "@typescript-eslint/experimental-utils" "^1.13.0" -eslint-plugin-node@~9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a" - integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw== +eslint-plugin-node@~9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a" + integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA== dependencies: - eslint-plugin-es "^1.4.0" - eslint-utils "^1.3.1" + eslint-plugin-es "^1.4.1" + eslint-utils "^1.4.2" ignore "^5.1.1" minimatch "^3.0.4" resolve "^1.10.1" @@ -3388,7 +3482,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.3.0, eslint-utils@^1.3.1, eslint-utils@^1.4.2: +eslint-utils@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== @@ -3400,10 +3494,10 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== -eslint@~6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f" - integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw== +eslint@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a" + integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -4086,6 +4180,15 @@ graphql-extensions@0.10.1: apollo-server-env "2.4.2" apollo-server-types "0.2.2" +graphql-extensions@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea" + integrity sha512-kwU0gUe+Qdfr8iZYT91qrPSwQNgPhB/ClF1m1LEPdxlptk5FhFmjpxAcbMZ8q7j0kjfnbp2IeV1OhRDCEPqz2w== + dependencies: + "@apollographql/apollo-tools" "^0.4.0" + apollo-server-env "^2.4.3" + apollo-server-types "^0.2.4" + graphql-import@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223" @@ -4180,10 +4283,10 @@ graphql-upload@^8.0.2: http-errors "^1.7.2" object-path "^0.11.4" -graphql@^14.2.1, graphql@^14.5.3: - version "14.5.3" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0" - integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg== +graphql@^14.2.1, graphql@^14.5.4: + version "14.5.4" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13" + integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw== dependencies: iterall "^1.2.2" @@ -6175,12 +6278,12 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== -neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4" - integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw== +neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6: + version "1.7.6" + resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49" + integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA== dependencies: - "@babel/runtime" "^7.4.4" + "@babel/runtime" "^7.5.5" text-encoding-utf-8 "^1.0.2" uri-js "^4.2.2" diff --git a/cypress/README.md b/cypress/README.md index 2dd662a66..2adcff925 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -28,6 +28,17 @@ To start the services that are required for cypress testing, run this: $ yarn cypress:setup ``` +## Install cypress + +Even if the required services for testing run via docker, depending on your +setup, the cypress tests themselves run on your host machine. So with our +without docker, you would have to install cypress and its dependencies first: + +``` +# in the root folder / +yarn install +``` + ## Run cypress After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command: diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 511f27e62..5084a538a 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -1,5 +1,5 @@ import { Given, When, Then } from "cypress-cucumber-preprocessor/steps"; -import { getLangByName } from "../../support/helpers"; +import helpers from "../../support/helpers"; import slugify from "slug"; // import { VERSION } from '../../../webapp/pages/terms-and-conditions.vue'; @@ -140,14 +140,17 @@ Then("I am still logged in", () => { When("I select {string} in the language menu", name => { cy.switchLanguage(name, true); }); + Given("I previously switched the language to {string}", name => { cy.switchLanguage(name, true); }); + Then("the whole user interface appears in {string}", name => { - const lang = getLangByName(name); - cy.get(`html[lang=${lang.code}]`); - cy.getCookie("locale").should("have.property", "value", lang.code); + const { code } = helpers.getLangByName(name); + cy.get(`html[lang=${code}]`); + cy.getCookie("locale").should("have.property", "value", code); }); + Then("I see a button with the label {string}", label => { cy.contains("button", label); }); @@ -175,13 +178,13 @@ Given("we have the following posts in our database:", table => { }; postAttributes.deleted = Boolean(postAttributes.deleted); const disabled = Boolean(postAttributes.disabled); - postAttributes.categoryIds = [`cat${i}`]; + postAttributes.categoryIds = [`cat${i}${new Date()}`]; postAttributes; cy.factory() .create("User", userAttributes) .authenticateAs(userAttributes) .create("Category", { - id: `cat${i}`, + id: `cat${i}${new Date()}`, name: "Just For Fun", slug: `just-for-fun-${i}`, icon: "smile" @@ -364,7 +367,7 @@ When("mention {string} in the text", mention => { }); Then("the notification gets marked as read", () => { - cy.get(".post.createdAt") + cy.get(".notifications-menu-popover .notification") .first() .should("have.class", "read"); }); diff --git a/cypress/integration/user_profile/blocked-users/Blocking.feature b/cypress/integration/user_profile/blocked-users/Blocking.feature index 3ce4fd6c4..ed784b803 100644 --- a/cypress/integration/user_profile/blocked-users/Blocking.feature +++ b/cypress/integration/user_profile/blocked-users/Blocking.feature @@ -26,6 +26,9 @@ Feature: Block a User And nobody is following the user profile anymore Scenario: Posts of blocked users are filtered from search results + Given we have the following posts in our database: + | Author | id | title | content | + | Some unblocked user | im-not-blocked | Post that should be seen | cause I'm not blocked | Given "Spammy Spammer" wrote a post "Spam Spam Spam" When I search for "Spam" Then I should see the following posts in the select dropdown: @@ -35,3 +38,7 @@ Feature: Block a User And I refresh the page And I search for "Spam" Then the search has no results + But I search for "not blocked" + Then I should see the following posts in the select dropdown: + | title | + | Post that should be seen | diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e69d296dc..630b52935 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -14,7 +14,7 @@ /* globals Cypress cy */ import "cypress-file-upload"; -import { getLangByName } from "./helpers"; +import helpers from "./helpers"; import users from "../fixtures/users.json"; const switchLang = name => { @@ -22,8 +22,9 @@ const switchLang = name => { cy.contains(".locale-menu-popover a", name).click(); }; + Cypress.Commands.add("switchLanguage", (name, force) => { - const code = getLangByName(name).code; + const { code } = helpers.getLangByName(name); if (force) { switchLang(name); } else { diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js index 4a8376ec0..7d66af5d6 100644 --- a/cypress/support/helpers.js +++ b/cypress/support/helpers.js @@ -1,10 +1,8 @@ import find from 'lodash/find' +import locales from '../../webapp/locales' -const helpers = { - locales: require('../../webapp/locales'), - getLangByName: name => { - return find(helpers.locales, { name }) +export default { + getLangByName(name) { + return find(locales, { name }) } } - -export default helpers diff --git a/deployment/human-connection/deployment-neo4j.yaml b/deployment/human-connection/deployment-neo4j.yaml index 297f4b551..593f87d2b 100644 --- a/deployment/human-connection/deployment-neo4j.yaml +++ b/deployment/human-connection/deployment-neo4j.yaml @@ -30,17 +30,6 @@ memory: "1G" limits: memory: "2G" - env: - - name: NEO4J_apoc_import_file_enabled - value: "true" - - name: NEO4J_dbms_memory_pagecache_size - value: "490M" - - name: NEO4J_dbms_memory_heap_max__size - value: "500M" - - name: NEO4J_dbms_memory_heap_initial__size - value: "500M" - - name: NEO4J_dbms_security_procedures_unrestricted - value: "algo.*,apoc.*" envFrom: - configMapRef: name: configmap diff --git a/deployment/human-connection/templates/configmap.template.yaml b/deployment/human-connection/templates/configmap.template.yaml index 1bd227af0..07c0bb53f 100644 --- a/deployment/human-connection/templates/configmap.template.yaml +++ b/deployment/human-connection/templates/configmap.template.yaml @@ -9,6 +9,11 @@ NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687" NEO4J_AUTH: "none" CLIENT_URI: "https://nitro-staging.human-connection.org" + NEO4J_apoc_import_file_enabled: "true" + NEO4J_dbms_memory_pagecache_size: "490M" + NEO4J_dbms_memory_heap_max__size: "500M" + NEO4J_dbms_memory_heap_initial__size: "500M" + NEO4J_dbms_security_procedures_unrestricted: "algo.*,apoc.*" SENTRY_DSN_WEBAPP: "" SENTRY_DSN_BACKEND: "" COMMIT: "" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql index af81528f2..6fad4218d 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql @@ -137,8 +137,8 @@ p.contentExcerpt = post.contentExcerpt, p.visibility = toLower(post.visibility), p.createdAt = post.createdAt.`$date`, p.updatedAt = post.updatedAt.`$date`, -p.deleted = COALESCE(post.deleted,false), -p.disabled = NOT post.isEnabled +p.deleted = COALESCE(post.deleted, false), +p.disabled = COALESCE(NOT post.isEnabled, false) WITH p, post MATCH (u:User {id: post.userId}) MERGE (u)-[:WROTE]->(p) diff --git a/package.json b/package.json index 31670214e..a0785adf5 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,13 @@ "codecov": "^3.5.0", "cross-env": "^5.2.0", "cypress": "^3.4.1", - "cypress-cucumber-preprocessor": "^1.15.1", + "cypress-cucumber-preprocessor": "^1.16.0", "cypress-file-upload": "^3.3.3", "cypress-plugin-retries": "^1.2.2", "dotenv": "^8.1.0", "faker": "Marak/faker.js#master", "graphql-request": "^1.8.2", - "neo4j-driver": "^1.7.5", + "neo4j-driver": "^1.7.6", "neode": "^0.3.2", "npm-run-all": "^4.1.5", "slug": "^1.1.0" diff --git a/webapp/components/Editor/Editor.spec.js b/webapp/components/Editor/Editor.spec.js index 81c6cce9b..bc5f6f3e8 100644 --- a/webapp/components/Editor/Editor.spec.js +++ b/webapp/components/Editor/Editor.spec.js @@ -3,6 +3,7 @@ import Editor from './Editor' import Vuex from 'vuex' import Styleguide from '@human-connection/styleguide' import MutationObserver from 'mutation-observer' +import Vue from 'vue' global.MutationObserver = MutationObserver @@ -55,9 +56,11 @@ describe('Editor.vue', () => { propsData.value = 'I am a piece of text' }) - it.skip('renders', () => { + it('renders', async () => { wrapper = Wrapper() - expect(wrapper.find('.ProseMirror').text()).toContain('I am a piece of text') + await Vue.nextTick().then(() => { + expect(wrapper.find('.editor-content').text()).toContain(propsData.value) + }) }) }) @@ -88,6 +91,29 @@ describe('Editor.vue', () => { ) }) + describe('limists suggestion list to 15 users', () => { + beforeEach(() => { + let manyUsersList = [] + for (let i = 0; i < 25; i++) { + manyUsersList.push({ id: `user${i}` }) + } + propsData.users = manyUsersList + wrapper = Wrapper() + }) + + it('when query is empty', () => { + expect( + wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users), + ).toHaveLength(15) + }) + + it('when query is present', () => { + expect( + wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users, 'user'), + ).toHaveLength(15) + }) + }) + it('sets the Hashtag items to the hashtags', () => { propsData.hashtags = [ { @@ -105,6 +131,29 @@ describe('Editor.vue', () => { }), ) }) + + describe('limists suggestion list to 15 hashtags', () => { + beforeEach(() => { + let manyHashtagsList = [] + for (let i = 0; i < 25; i++) { + manyHashtagsList.push({ id: `hashtag${i}` }) + } + propsData.hashtags = manyHashtagsList + wrapper = Wrapper() + }) + + it('when query is empty', () => { + expect( + wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags), + ).toHaveLength(15) + }) + + it('when query is present', () => { + expect( + wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags, 'hashtag'), + ).toHaveLength(15) + }) + }) }) }) }) diff --git a/webapp/components/Editor/Editor.vue b/webapp/components/Editor/Editor.vue index 33fe6b5d4..27a94e6b6 100644 --- a/webapp/components/Editor/Editor.vue +++ b/webapp/components/Editor/Editor.vue @@ -203,12 +203,14 @@ export default { filterSuggestionList(items, query) { query = this.sanitizeQuery(query) if (!query) { - return items + return items.slice(0, 15) } - return items.filter(item => { + + const filteredList = items.filter(item => { const itemString = item.slug || item.id return itemString.toLowerCase().includes(query.toLowerCase()) }) + return filteredList.slice(0, 15) }, sanitizeQuery(query) { if (this.suggestionType === HASHTAG) { diff --git a/webapp/components/notifications/Notification/Notification.spec.js b/webapp/components/notifications/Notification/Notification.spec.js index 279500f7f..9e5586fc2 100644 --- a/webapp/components/notifications/Notification/Notification.spec.js +++ b/webapp/components/notifications/Notification/Notification.spec.js @@ -37,9 +37,9 @@ describe('Notification', () => { describe('given a notification about a comment on a post', () => { beforeEach(() => { propsData.notification = { - reason: 'comment_on_post', - post: null, - comment: { + reason: 'commented_on_post', + from: { + __typename: 'Comment', id: 'comment-1', contentExcerpt: '@dagobert-duck is the best on this comment.', @@ -56,7 +56,7 @@ describe('Notification', () => { it('renders reason', () => { wrapper = Wrapper() expect(wrapper.find('.reason-text-for-test').text()).toEqual( - 'notifications.menu.comment_on_post', + 'notifications.menu.commented_on_post', ) }) it('renders title', () => { @@ -92,14 +92,14 @@ describe('Notification', () => { beforeEach(() => { propsData.notification = { reason: 'mentioned_in_post', - post: { + from: { + __typename: 'Post', title: "It's a post title", id: 'post-1', slug: 'its-a-title', contentExcerpt: '@jenny-rostock is the best on this post.', }, - comment: null, } }) @@ -138,8 +138,8 @@ describe('Notification', () => { beforeEach(() => { propsData.notification = { reason: 'mentioned_in_comment', - post: null, - comment: { + from: { + __typename: 'Comment', id: 'comment-1', contentExcerpt: '@dagobert-duck is the best on this comment.', diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index 193b5f67b..d55cdf1b2 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -1,14 +1,8 @@ @@ -24,8 +24,8 @@ export default { }, }, methods: { - markAsRead(notificationId) { - this.$emit('markAsRead', notificationId) + markAsRead(notificationSourceId) { + this.$emit('markAsRead', notificationSourceId) }, }, } diff --git a/webapp/components/notifications/NotificationMenu/NotificationMenu.vue b/webapp/components/notifications/NotificationMenu/NotificationMenu.vue index c534f2986..9b0842301 100644 --- a/webapp/components/notifications/NotificationMenu/NotificationMenu.vue +++ b/webapp/components/notifications/NotificationMenu/NotificationMenu.vue @@ -18,7 +18,7 @@