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
This commit is contained in:
Robert Schäfer 2019-08-12 11:47:18 +02:00 committed by roschaefer
parent 8de0195cf8
commit bedbb21def
8 changed files with 147 additions and 130 deletions

View File

@ -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 <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> 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 <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> 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 =
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
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) }],
},
],
})
}),
)
})
})
})

View File

@ -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)
}

View File

@ -6,6 +6,8 @@ const contentEmptyMentions =
'<p>Something inspirational about <a href="/profile/u2" data-mention-id="" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" data-mention-id target="_blank">@jenny-rostock</a>.</p>'
const contentWithPlainLinks =
'<p>Something inspirational about <a class="mention" href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
const contentWithDuplicateIds =
'One more mention to <a data-mention-id="you" class="mention" href="/profile/you"> @al-capone </a> and again: <a data-mention-id="you" class="mention" href="/profile/you"> @al-capone </a> and again <a data-mention-id="you" class="mention" href="/profile/you"> @al-capone </a>'
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'])

View File

@ -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',
},
}

View File

@ -77,4 +77,10 @@ module.exports = {
target: 'User',
direction: 'out',
},
notifications: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'Notification',
direction: 'in',
},
}

View File

@ -7,4 +7,5 @@ export default {
EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Notification: require('./Notification.js'),
}

View File

@ -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!

View File

@ -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
}