From bedbb21def48d0fb7c5177f36bab40438d822b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 12 Aug 2019 11:47:18 +0200 Subject: [PATCH] Refactor notification spec Wow this took me the entire day: If you run `createServer` multiple times, more and more middlewares get added to the schema. That's why the test would create 2^n notifications for n times you called `createServer`. This is related to the following bug: https://github.com/prisma/graphql-middleware/issues/63 --- .../handleContentData.spec.js | 224 ++++++++---------- .../notifications/extractMentionedUsers.js | 6 +- .../extractMentionedUsers.spec.js | 6 + backend/src/models/Notification.js | 19 ++ backend/src/models/User.js | 6 + backend/src/models/index.js | 1 + backend/src/schema/types/schema.gql | 8 - .../src/schema/types/type/Notification.gql | 7 + 8 files changed, 147 insertions(+), 130 deletions(-) create mode 100644 backend/src/models/Notification.js create mode 100644 backend/src/schema/types/type/Notification.gql diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js index 6fe5d3891..1effeb5d6 100644 --- a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js +++ b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js @@ -1,12 +1,36 @@ -import { GraphQLClient } from 'graphql-request' -import { host, login, gql } from '../../jest/helpers' +import { gql } from '../../jest/helpers' import Factory from '../../seed/factories' +import { createTestClient } from 'apollo-server-testing' +import { neode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' const factory = Factory() -let client +const driver = getDriver() +const instance = neode() +let server +let query +let mutate +let user +let authenticatedUser + +beforeAll(() => { + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode: instance, + driver, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) beforeEach(async () => { - await factory.create('User', { + user = await instance.create('User', { id: 'you', name: 'Al Capone', slug: 'al-capone', @@ -19,8 +43,8 @@ afterEach(async () => { await factory.cleanDatabase() }) -describe('currentUser { notifications }', () => { - const query = gql` +describe('notifications', () => { + const notificationQuery = gql` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -34,77 +58,54 @@ describe('currentUser { notifications }', () => { ` describe('authenticated', () => { - let headers beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) + authenticatedUser = user }) describe('given another user', () => { - let authorClient - let authorParams - let authorHeaders - + let author beforeEach(async () => { - authorParams = { + author = await instance.create('User', { email: 'author@example.org', password: '1234', id: 'author', - } - await factory.create('User', authorParams) - authorHeaders = await login(authorParams) + }) }) describe('who mentions me in a post', () => { - let post const title = 'Mentioning Al Capone' const content = 'Hey @al-capone how do you do?' beforeEach(async () => { const createPostMutation = gql` - mutation($title: String!, $content: String!) { - CreatePost(title: $title, content: $content) { + mutation($id: ID, $title: String!, $content: String!) { + CreatePost(id: $id, title: $title, content: $content) { id title content } } ` - authorClient = new GraphQLClient(host, { - headers: authorHeaders, + authenticatedUser = await author.toJson() + await mutate({ + mutation: createPostMutation, + variables: { id: 'p47', title, content }, }) - const { CreatePost } = await authorClient.request(createPostMutation, { - title, - content, - }) - post = CreatePost + authenticatedUser = await user.toJson() }) it('sends you a notification', async () => { const expectedContent = 'Hey @al-capone how do you do?' - const expected = { - currentUser: { - notifications: [ - { - read: false, - post: { - content: expectedContent, - }, - }, - ], + const expected = expect.objectContaining({ + data: { + currentUser: { notifications: [{ read: false, post: { content: expectedContent } }] }, }, - } + }) + const { query } = createTestClient(server) await expect( - client.request(query, { - read: false, - }), + query({ query: notificationQuery, variables: { read: false } }), ).resolves.toEqual(expected) }) @@ -132,41 +133,33 @@ describe('currentUser { notifications }', () => { } } ` - authorClient = new GraphQLClient(host, { - headers: authorHeaders, - }) - await authorClient.request(updatePostMutation, { - id: post.id, - title: post.title, - content: updatedContent, + authenticatedUser = await author.toJson() + await mutate({ + mutation: updatePostMutation, + variables: { + id: 'p47', + title, + content: updatedContent, + }, }) + authenticatedUser = await user.toJson() }) it('creates exactly one more notification', async () => { const expectedContent = '
One more mention to

@al-capone

and again:

@al-capone

and again

@al-capone

' - const expected = { - currentUser: { - notifications: [ - { - read: false, - post: { - content: expectedContent, - }, - }, - { - read: false, - post: { - content: expectedContent, - }, - }, - ], + const expected = expect.objectContaining({ + data: { + currentUser: { + notifications: [ + { read: false, post: { content: expectedContent } }, + { read: false, post: { content: expectedContent } }, + ], + }, }, - } + }) await expect( - client.request(query, { - read: false, - }), + query({ query: notificationQuery, variables: { read: false } }), ).resolves.toEqual(expected) }) }) @@ -204,46 +197,40 @@ describe('Hashtags', () => { ` describe('authenticated', () => { - let headers beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) + authenticatedUser = await user.toJson() }) describe('create a Post with Hashtags', () => { beforeEach(async () => { - await client.request(createPostMutation, { - postId, - postTitle, - postContent, + await mutate({ + mutation: createPostMutation, + variables: { + postId, + postTitle, + postContent, + }, }) }) - it('both Hashtags are created with the "id" set to thier "name"', async () => { + it('both Hashtags are created with the "id" set to their "name"', async () => { const expected = [ - { - id: 'Democracy', - name: 'Democracy', - }, - { - id: 'Liberty', - name: 'Liberty', - }, + { id: 'Democracy', name: 'Democracy' }, + { id: 'Liberty', name: 'Liberty' }, ] await expect( - client.request(postWithHastagsQuery, postWithHastagsVariables), - ).resolves.toEqual({ - Post: [ - { - tags: expect.arrayContaining(expected), + query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], }, - ], - }) + }), + ) }) describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { @@ -261,31 +248,28 @@ describe('Hashtags', () => { ` it('only one previous Hashtag and the new Hashtag exists', async () => { - await client.request(updatePostMutation, { - postId, - postTitle, - updatedPostContent, + await mutate({ + mutation: updatePostMutation, + variables: { + postId, + postTitle, + updatedPostContent, + }, }) const expected = [ - { - id: 'Elections', - name: 'Elections', - }, - { - id: 'Liberty', - name: 'Liberty', - }, + { id: 'Elections', name: 'Elections' }, + { id: 'Liberty', name: 'Liberty' }, ] await expect( - client.request(postWithHastagsQuery, postWithHastagsVariables), - ).resolves.toEqual({ - Post: [ - { - tags: expect.arrayContaining(expected), + query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + Post: [{ tags: expect.arrayContaining(expected) }], }, - ], - }) + }), + ) }) }) }) diff --git a/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js index d08309f0b..e245e84b5 100644 --- a/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js +++ b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js @@ -3,11 +3,13 @@ import cheerio from 'cheerio' export default function(content) { if (!content) return [] const $ = cheerio.load(content) - let userIds = $('a.mention[data-mention-id]') + const userIds = $('a.mention[data-mention-id]') .map((_, el) => { return $(el).attr('data-mention-id') }) .get() - userIds = userIds.map(id => id.trim()).filter(id => !!id) return userIds + .map(id => id.trim()) + .filter(id => !!id) + .filter((id, index, allIds) => allIds.indexOf(id) === index) } diff --git a/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js index a631b64a3..9865eab0d 100644 --- a/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js +++ b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js @@ -6,6 +6,8 @@ const contentEmptyMentions = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' const contentWithPlainLinks = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' +const contentWithDuplicateIds = + 'One more mention to @al-capone and again: @al-capone and again @al-capone ' describe('extractMentionedUsers', () => { describe('content undefined', () => { @@ -18,6 +20,10 @@ describe('extractMentionedUsers', () => { expect(extractMentionedUsers(contentWithPlainLinks)).toEqual([]) }) + it('removes duplicates', () => { + expect(extractMentionedUsers(contentWithDuplicateIds)).toEqual(['you']) + }) + describe('given a link with .mention class and `data-mention-id` attribute ', () => { it('extracts ids', () => { expect(extractMentionedUsers(contentWithMentions)).toEqual(['u3']) diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js new file mode 100644 index 000000000..b8690b8c1 --- /dev/null +++ b/backend/src/models/Notification.js @@ -0,0 +1,19 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'uuid', primary: true, default: uuid }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + read: { type: 'boolean', default: false }, + user: { + type: 'relationship', + relationship: 'NOTIFIED', + target: 'User', + direction: 'out', + }, + post: { + type: 'relationship', + relationship: 'NOTIFIED', + target: 'Post', + direction: 'in', + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js index 3dfcef06a..fa578f8ad 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -77,4 +77,10 @@ module.exports = { target: 'User', direction: 'out', }, + notifications: { + type: 'relationship', + relationship: 'NOTIFIED', + target: 'Notification', + direction: 'in', + }, } diff --git a/backend/src/models/index.js b/backend/src/models/index.js index b8dc451d7..6f6b300f8 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -7,4 +7,5 @@ export default { EmailAddress: require('./EmailAddress.js'), SocialMedia: require('./SocialMedia.js'), Post: require('./Post.js'), + Notification: require('./Notification.js'), } diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 41fe8f4e6..b8e5f5290 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -50,14 +50,6 @@ type Statistics { countShouts: Int! } -type Notification { - id: ID! - read: Boolean - user: User @relation(name: "NOTIFIED", direction: "OUT") - post: Post @relation(name: "NOTIFIED", direction: "IN") - createdAt: String -} - type Location { id: ID! name: String! diff --git a/backend/src/schema/types/type/Notification.gql b/backend/src/schema/types/type/Notification.gql new file mode 100644 index 000000000..e4bc16fec --- /dev/null +++ b/backend/src/schema/types/type/Notification.gql @@ -0,0 +1,7 @@ +type Notification { + id: ID! + read: Boolean + user: User @relation(name: "NOTIFIED", direction: "OUT") + post: Post @relation(name: "NOTIFIED", direction: "IN") + createdAt: String +}