Merge branch 'master' into C-1187-terms-and-conditions-confirmed-function

This commit is contained in:
Alexander Friedland 2019-09-04 06:42:40 +02:00 committed by GitHub
commit e618ff005d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2832 additions and 2387 deletions

View File

@ -19,9 +19,8 @@
"test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --", "test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --", "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --", "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
"db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", "db:reset": "babel-node src/seed/reset-db.js",
"db:reset": "cross-env babel-node src/seed/reset-db.js", "db:seed": "babel-node src/seed/seed-db.js"
"db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
}, },
"author": "Human Connection gGmbH", "author": "Human Connection gGmbH",
"license": "MIT", "license": "MIT",
@ -55,7 +54,7 @@
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~5.2.0", "cross-env": "~5.2.1",
"date-fns": "2.0.1", "date-fns": "2.0.1",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.1.0", "dotenv": "~8.1.0",
@ -110,7 +109,7 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.5.5", "@babel/preset-env": "~7.5.5",
"@babel/register": "~7.5.5", "@babel/register": "~7.5.5",
"apollo-server-testing": "~2.9.1", "apollo-server-testing": "~2.9.3",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3", "babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0", "babel-jest": "~24.9.0",

View File

@ -13,7 +13,7 @@ export default async (driver, authorizationHeader) => {
} }
const session = driver.session() const session = driver.session()
const query = ` const query = `
MATCH (user:User {id: {id} }) MATCH (user:User {id: $id, deleted: false, disabled: false })
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId} RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
LIMIT 1 LIMIT 1
` `
@ -23,7 +23,6 @@ export default async (driver, authorizationHeader) => {
return record.get('user') return record.get('user')
}) })
if (!currentUser) return null if (!currentUser) return null
if (currentUser.disabled) return null
return { return {
token, token,
...currentUser, ...currentUser,

View File

@ -0,0 +1,104 @@
import Factory from '../seed/factories/index'
import { getDriver } from '../bootstrap/neo4j'
import decode from './decode'
const factory = Factory()
const driver = getDriver()
// here is the decoded JWT token:
// {
// role: 'user',
// locationName: null,
// name: 'Jenny Rostock',
// about: null,
// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
// id: 'u3',
// email: 'user@example.org',
// slug: 'jenny-rostock',
// iat: 1550846680,
// exp: 1637246680,
// aud: 'http://localhost:3000',
// iss: 'http://localhost:4000',
// sub: 'u3'
// }
export const validAuthorizationHeader =
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc'
afterEach(async () => {
await factory.cleanDatabase()
})
describe('decode', () => {
let authorizationHeader
const returnsNull = async () => {
await expect(decode(driver, authorizationHeader)).resolves.toBeNull()
}
describe('given `null` as JWT Bearer token', () => {
beforeEach(() => {
authorizationHeader = null
})
it('returns null', returnsNull)
})
describe('given no JWT Bearer token', () => {
beforeEach(() => {
authorizationHeader = undefined
})
it('returns null', returnsNull)
})
describe('given malformed JWT Bearer token', () => {
beforeEach(() => {
authorizationHeader = 'blah'
})
it('returns null', returnsNull)
})
describe('given valid JWT Bearer token', () => {
beforeEach(() => {
authorizationHeader = validAuthorizationHeader
})
it('returns null', returnsNull)
describe('and corresponding user in the database', () => {
let user
beforeEach(async () => {
user = await factory.create('User', {
role: 'user',
name: 'Jenny Rostock',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
id: 'u3',
email: 'user@example.org',
slug: 'jenny-rostock',
})
})
it('returns user object except email', async () => {
await expect(decode(driver, authorizationHeader)).resolves.toMatchObject({
role: 'user',
name: 'Jenny Rostock',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
id: 'u3',
email: null,
slug: 'jenny-rostock',
})
})
describe('but user is deleted', () => {
beforeEach(async () => {
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
})
it('returns null', returnsNull)
})
describe('but user is disabled', () => {
beforeEach(async () => {
await user.update({ updatedAt: new Date().toISOString(), disabled: true })
})
it('returns null', returnsNull)
})
})
})
})

View File

@ -1,10 +1,16 @@
import { GraphQLClient } from 'graphql-request' import { gql } from '../../jest/helpers'
import { host, login } from '../../jest/helpers'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { neode } from '../../bootstrap/neo4j' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
const factory = Factory() const factory = Factory()
const instance = neode() const neode = getNeode()
const driver = getDriver()
let authenticatedUser
let user
let query
const currentUserParams = { const currentUserParams = {
id: 'u1', id: 'u1',
@ -26,24 +32,42 @@ const randomAuthorParams = {
const categoryIds = ['cat9'] const categoryIds = ['cat9']
beforeEach(async () => { beforeEach(async () => {
await Promise.all([ const [currentUser, followedAuthor, randomAuthor] = await Promise.all([
factory.create('User', currentUserParams), factory.create('User', currentUserParams),
factory.create('User', followedAuthorParams), factory.create('User', followedAuthorParams),
factory.create('User', randomAuthorParams), factory.create('User', randomAuthorParams),
]) ])
await instance.create('Category', { user = currentUser
await neode.create('Category', {
id: 'cat9', id: 'cat9',
name: 'Democracy & Politics', name: 'Democracy & Politics',
icon: 'university', icon: 'university',
}) })
const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([ await currentUser.relateTo(followedAuthor, 'following')
Factory().authenticateAs(currentUserParams), await factory.create('Post', {
Factory().authenticateAs(followedAuthorParams), author: followedAuthor,
Factory().authenticateAs(randomAuthorParams), title: 'This is the post of a followed user',
]) categoryIds,
await asYourself.follow({ id: 'u2', type: 'User' }) })
await asFollowedUser.create('Post', { title: 'This is the post of a followed user', categoryIds }) await factory.create('Post', {
await asSomeoneElse.create('Post', { title: 'This is some random post', categoryIds }) author: randomAuthor,
title: 'This is some random post',
categoryIds,
})
})
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
const client = createTestClient(server)
query = client.query
}) })
afterEach(async () => { afterEach(async () => {
@ -52,33 +76,44 @@ afterEach(async () => {
describe('Filter posts by author is followed by sb.', () => { describe('Filter posts by author is followed by sb.', () => {
describe('given an authenticated user', () => { describe('given an authenticated user', () => {
let authenticatedClient
beforeEach(async () => { beforeEach(async () => {
const headers = await login(currentUserParams) authenticatedUser = await user.toJson()
authenticatedClient = new GraphQLClient(host, { headers })
}) })
describe('no filter bubble', () => { describe('no filter bubble', () => {
it('returns all posts', async () => { it('returns all posts', async () => {
const query = '{ Post(filter: { }) { title } }' const postQuery = gql`
{
Post(filter: {}) {
title
}
}
`
const expected = { const expected = {
Post: [ data: {
{ title: 'This is some random post' }, Post: [
{ title: 'This is the post of a followed user' }, { title: 'This is some random post' },
], { title: 'This is the post of a followed user' },
],
},
} }
await expect(authenticatedClient.request(query)).resolves.toEqual(expected) await expect(query({ query: postQuery })).resolves.toMatchObject(expected)
}) })
}) })
describe('filtering for posts of followed users only', () => { describe('filtering for posts of followed users only', () => {
it('returns only posts authored by followed users', async () => { it('returns only posts authored by followed users', async () => {
const query = '{ Post( filter: { author: { followedBy_some: { id: "u1" } } }) { title } }' const postQuery = gql`
{
Post(filter: { author: { followedBy_some: { id: "u1" } } }) {
title
}
}
`
const expected = { const expected = {
Post: [{ title: 'This is the post of a followed user' }], data: { Post: [{ title: 'This is the post of a followed user' }] },
} }
await expect(authenticatedClient.request(query)).resolves.toEqual(expected) await expect(query({ query: postQuery })).resolves.toMatchObject(expected)
}) })
}) })
}) })

View File

@ -91,13 +91,11 @@ const isAuthor = rule({
resourceId, resourceId,
}, },
) )
session.close()
const [author] = result.records.map(record => { const [author] = result.records.map(record => {
return record.get('author') return record.get('author')
}) })
const { const authorId = author && author.properties && author.properties.id
properties: { id: authorId },
} = author
session.close()
return authorId === user.id return authorId === user.id
}) })
@ -131,7 +129,7 @@ const permissions = shield(
isLoggedIn: allow, isLoggedIn: allow,
Badge: allow, Badge: allow,
PostsEmotionsCountByEmotion: allow, PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: allow, PostsEmotionsByCurrentUser: isAuthenticated,
blockedUsers: isAuthenticated, blockedUsers: isAuthenticated,
notifications: isAuthenticated, notifications: isAuthenticated,
}, },

View File

@ -13,15 +13,16 @@ const setDefaultFilters = (resolve, root, args, context, info) => {
return resolve(root, args, context, info) return resolve(root, args, context, info)
} }
const obfuscateDisabled = async (resolve, root, args, context, info) => { const obfuscate = async (resolve, root, args, context, info) => {
if (!isModerator(context) && root.disabled) { if (root.deleted || (!isModerator(context) && root.disabled)) {
root.content = 'UNAVAILABLE' root.content = 'UNAVAILABLE'
root.contentExcerpt = 'UNAVAILABLE' root.contentExcerpt = 'UNAVAILABLE'
root.title = 'UNAVAILABLE' root.title = 'UNAVAILABLE'
root.image = 'UNAVAILABLE' root.slug = 'UNAVAILABLE'
root.avatar = 'UNAVAILABLE' root.avatar = 'UNAVAILABLE'
root.about = 'UNAVAILABLE' root.about = 'UNAVAILABLE'
root.name = 'UNAVAILABLE' root.name = 'UNAVAILABLE'
root.image = null // avoid unecessary 500 errors
} }
return resolve(root, args, context, info) return resolve(root, args, context, info)
} }
@ -40,7 +41,7 @@ export default {
} }
return resolve(root, args, context, info) return resolve(root, args, context, info)
}, },
Post: obfuscateDisabled, Post: obfuscate,
User: obfuscateDisabled, User: obfuscate,
Comment: obfuscateDisabled, Comment: obfuscate,
} }

View File

@ -1,49 +1,70 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { host, login } from '../jest/helpers' import { gql } from '../jest/helpers'
import { neode } from '../bootstrap/neo4j' import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
import createServer from '../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory() const factory = Factory()
const instance = neode() const neode = getNeode()
const driver = getDriver()
let client
let query let query
let action let mutate
let graphqlQuery
const categoryIds = ['cat9'] const categoryIds = ['cat9']
let authenticatedUser
let user
let moderator
let troll
const action = () => {
return query({ query: graphqlQuery })
}
beforeAll(async () => { beforeAll(async () => {
// For performance reasons we do this only once // For performance reasons we do this only once
await Promise.all([ const users = await Promise.all([
factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', password: '1234' }), factory.create('User', { id: 'u1', role: 'user' }),
factory.create('User', { factory.create('User', {
id: 'm1', id: 'm1',
role: 'moderator', role: 'moderator',
email: 'moderator@example.org',
password: '1234', password: '1234',
}), }),
factory.create('User', { factory.create('User', {
id: 'u2', id: 'u2',
role: 'user', role: 'user',
name: 'Offensive Name', name: 'Offensive Name',
slug: 'offensive-name',
avatar: '/some/offensive/avatar.jpg', avatar: '/some/offensive/avatar.jpg',
about: 'This self description is very offensive', about: 'This self description is very offensive',
email: 'troll@example.org',
password: '1234',
}),
instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
}), }),
]) ])
await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) user = users[0]
moderator = users[1]
troll = users[2]
await neode.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
await Promise.all([ await Promise.all([
factory.follow({ id: 'u2', type: 'User' }), user.relateTo(troll, 'following'),
factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true, categoryIds }),
factory.create('Post', { factory.create('Post', {
author: user,
id: 'p1',
title: 'Deleted post',
slug: 'deleted-post',
deleted: true,
categoryIds,
}),
factory.create('Post', {
author: user,
id: 'p3', id: 'p3',
title: 'Publicly visible post', title: 'Publicly visible post',
slug: 'publicly-visible-post',
deleted: false, deleted: false,
categoryIds, categoryIds,
}), }),
@ -51,32 +72,56 @@ beforeAll(async () => {
await Promise.all([ await Promise.all([
factory.create('Comment', { factory.create('Comment', {
author: user,
id: 'c2', id: 'c2',
postId: 'p3', postId: 'p3',
content: 'Enabled comment on public post', content: 'Enabled comment on public post',
}), }),
]) ])
await Promise.all([factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })]) await factory.create('Post', {
const asTroll = Factory()
await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
await asTroll.create('Post', {
id: 'p2', id: 'p2',
author: troll,
title: 'Disabled post', title: 'Disabled post',
content: 'This is an offensive post content', content: 'This is an offensive post content',
contentExcerpt: 'This is an offensive post content',
image: '/some/offensive/image.jpg', image: '/some/offensive/image.jpg',
deleted: false, deleted: false,
categoryIds, categoryIds,
}) })
await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' }) await factory.create('Comment', {
await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })]) id: 'c1',
author: troll,
postId: 'p3',
content: 'Disabled comment',
contentExcerpt: 'Disabled comment',
})
const asModerator = Factory() const { server } = createServer({
await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' }) context: () => {
await asModerator.mutate('mutation { disable( id: "p2") }') return {
await asModerator.mutate('mutation { disable( id: "c1") }') driver,
await asModerator.mutate('mutation { disable( id: "u2") }') neode,
user: authenticatedUser,
}
},
})
const client = createTestClient(server)
query = client.query
mutate = client.mutate
authenticatedUser = await moderator.toJson()
const disableMutation = gql`
mutation($id: ID!) {
disable(id: $id)
}
`
await Promise.all([
mutate({ mutation: disableMutation, variables: { id: 'c1' } }),
mutate({ mutation: disableMutation, variables: { id: 'u2' } }),
mutate({ mutation: disableMutation, variables: { id: 'p2' } }),
])
authenticatedUser = null
}) })
afterAll(async () => { afterAll(async () => {
@ -85,93 +130,124 @@ afterAll(async () => {
describe('softDeleteMiddleware', () => { describe('softDeleteMiddleware', () => {
describe('read disabled content', () => { describe('read disabled content', () => {
let user let subject
let post
let comment
const beforeComment = async () => { const beforeComment = async () => {
query = '{ User(id: "u1") { following { comments { content contentExcerpt } } } }' graphqlQuery = gql`
const response = await action() {
comment = response.User[0].following[0].comments[0] User(id: "u1") {
following {
comments {
content
contentExcerpt
}
}
}
}
`
const { data } = await action()
subject = data.User[0].following[0].comments[0]
} }
const beforeUser = async () => { const beforeUser = async () => {
query = '{ User(id: "u1") { following { name about avatar } } }' graphqlQuery = gql`
const response = await action() {
user = response.User[0].following[0] User(id: "u1") {
following {
name
slug
about
avatar
}
}
}
`
const { data } = await action()
subject = data.User[0].following[0]
} }
const beforePost = async () => { const beforePost = async () => {
query = graphqlQuery = gql`
'{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }' {
const response = await action() User(id: "u1") {
post = response.User[0].following[0].contributions[0] following {
} contributions {
title
action = () => { slug
return client.request(query) image
content
contentExcerpt
}
}
}
}
`
const { data } = await action()
subject = data.User[0].following[0].contributions[0]
} }
describe('as moderator', () => { describe('as moderator', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' }) authenticatedUser = await moderator.toJson()
client = new GraphQLClient(host, { headers })
}) })
describe('User', () => { describe('User', () => {
beforeEach(beforeUser) beforeEach(beforeUser)
it('displays name', () => expect(user.name).toEqual('Offensive Name')) it('displays name', () => expect(subject.name).toEqual('Offensive Name'))
it('displays slug', () => expect(subject.slug).toEqual('offensive-name'))
it('displays about', () => it('displays about', () =>
expect(user.about).toEqual('This self description is very offensive')) expect(subject.about).toEqual('This self description is very offensive'))
it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg')) it('displays avatar', () => expect(subject.avatar).toEqual('/some/offensive/avatar.jpg'))
}) })
describe('Post', () => { describe('Post', () => {
beforeEach(beforePost) beforeEach(beforePost)
it('displays title', () => expect(post.title).toEqual('Disabled post')) it('displays title', () => expect(subject.title).toEqual('Disabled post'))
it('displays slug', () => expect(subject.slug).toEqual('disabled-post'))
it('displays content', () => it('displays content', () =>
expect(post.content).toEqual('This is an offensive post content')) expect(subject.content).toEqual('This is an offensive post content'))
it('displays contentExcerpt', () => it('displays contentExcerpt', () =>
expect(post.contentExcerpt).toEqual('This is an offensive post content')) expect(subject.contentExcerpt).toEqual('This is an offensive post content'))
it('displays image', () => expect(post.image).toEqual('/some/offensive/image.jpg')) it('displays image', () => expect(subject.image).toEqual('/some/offensive/image.jpg'))
}) })
describe('Comment', () => { describe('Comment', () => {
beforeEach(beforeComment) beforeEach(beforeComment)
it('displays content', () => expect(comment.content).toEqual('Disabled comment')) it('displays content', () => expect(subject.content).toEqual('Disabled comment'))
it('displays contentExcerpt', () => it('displays contentExcerpt', () =>
expect(comment.contentExcerpt).toEqual('Disabled comment')) expect(subject.contentExcerpt).toEqual('Disabled comment'))
}) })
}) })
describe('as user', () => { describe('as user', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedUser = await user.toJson()
client = new GraphQLClient(host, { headers })
}) })
describe('User', () => { describe('User', () => {
beforeEach(beforeUser) beforeEach(beforeUser)
it('displays name', () => expect(user.name).toEqual('UNAVAILABLE')) it('obfuscates name', () => expect(subject.name).toEqual('UNAVAILABLE'))
it('obfuscates about', () => expect(user.about).toEqual('UNAVAILABLE')) it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
it('obfuscates avatar', () => expect(user.avatar).toEqual('UNAVAILABLE')) it('obfuscates about', () => expect(subject.about).toEqual('UNAVAILABLE'))
it('obfuscates avatar', () => expect(subject.avatar).toEqual('UNAVAILABLE'))
}) })
describe('Post', () => { describe('Post', () => {
beforeEach(beforePost) beforeEach(beforePost)
it('obfuscates title', () => expect(post.title).toEqual('UNAVAILABLE')) it('obfuscates title', () => expect(subject.title).toEqual('UNAVAILABLE'))
it('obfuscates content', () => expect(post.content).toEqual('UNAVAILABLE')) it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
it('obfuscates contentExcerpt', () => expect(post.contentExcerpt).toEqual('UNAVAILABLE')) it('obfuscates content', () => expect(subject.content).toEqual('UNAVAILABLE'))
it('obfuscates image', () => expect(post.image).toEqual('UNAVAILABLE')) it('obfuscates contentExcerpt', () => expect(subject.contentExcerpt).toEqual('UNAVAILABLE'))
it('obfuscates image', () => expect(subject.image).toEqual(null))
}) })
describe('Comment', () => { describe('Comment', () => {
beforeEach(beforeComment) beforeEach(beforeComment)
it('obfuscates content', () => expect(comment.content).toEqual('UNAVAILABLE')) it('obfuscates content', () => expect(subject.content).toEqual('UNAVAILABLE'))
it('obfuscates contentExcerpt', () => expect(comment.contentExcerpt).toEqual('UNAVAILABLE')) it('obfuscates contentExcerpt', () => expect(subject.contentExcerpt).toEqual('UNAVAILABLE'))
}) })
}) })
}) })
@ -179,43 +255,57 @@ describe('softDeleteMiddleware', () => {
describe('Query', () => { describe('Query', () => {
describe('Post', () => { describe('Post', () => {
beforeEach(async () => { beforeEach(async () => {
query = '{ Post { title } }' graphqlQuery = gql`
{
Post {
title
}
}
`
}) })
describe('as user', () => { describe('as user', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedUser = await user.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('hides deleted or disabled posts', async () => { it('hides deleted or disabled posts', async () => {
const expected = { Post: [{ title: 'Publicly visible post' }] } const expected = { data: { Post: [{ title: 'Publicly visible post' }] } }
await expect(action()).resolves.toEqual(expected) await expect(action()).resolves.toMatchObject(expected)
}) })
}) })
describe('as moderator', () => { describe('as moderator', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' }) authenticatedUser = await moderator.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('shows disabled but hides deleted posts', async () => { it('shows disabled but hides deleted posts', async () => {
const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }] const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }]
const { Post } = await action() const {
data: { Post },
} = await action()
await expect(Post).toEqual(expect.arrayContaining(expected)) await expect(Post).toEqual(expect.arrayContaining(expected))
}) })
}) })
describe('.comments', () => { describe('.comments', () => {
beforeEach(async () => { beforeEach(async () => {
query = '{ Post(id: "p3") { title comments { content } } }' graphqlQuery = gql`
{
Post(id: "p3") {
title
comments {
content
}
}
}
`
}) })
describe('as user', () => { describe('as user', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedUser = await user.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('conceals disabled comments', async () => { it('conceals disabled comments', async () => {
@ -224,7 +314,9 @@ describe('softDeleteMiddleware', () => {
{ content: 'UNAVAILABLE' }, { content: 'UNAVAILABLE' },
] ]
const { const {
Post: [{ comments }], data: {
Post: [{ comments }],
},
} = await action() } = await action()
await expect(comments).toEqual(expect.arrayContaining(expected)) await expect(comments).toEqual(expect.arrayContaining(expected))
}) })
@ -232,8 +324,7 @@ describe('softDeleteMiddleware', () => {
describe('as moderator', () => { describe('as moderator', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' }) authenticatedUser = await moderator.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('shows disabled comments', async () => { it('shows disabled comments', async () => {
@ -242,7 +333,9 @@ describe('softDeleteMiddleware', () => {
{ content: 'Disabled comment' }, { content: 'Disabled comment' },
] ]
const { const {
Post: [{ comments }], data: {
Post: [{ comments }],
},
} = await action() } = await action()
await expect(comments).toEqual(expect.arrayContaining(expected)) await expect(comments).toEqual(expect.arrayContaining(expected))
}) })
@ -251,58 +344,70 @@ describe('softDeleteMiddleware', () => {
describe('filter (deleted: true)', () => { describe('filter (deleted: true)', () => {
beforeEach(() => { beforeEach(() => {
query = '{ Post(deleted: true) { title } }' graphqlQuery = gql`
{
Post(deleted: true) {
title
}
}
`
}) })
describe('as user', () => { describe('as user', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedUser = await user.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('throws authorisation error', async () => { it('throws authorisation error', async () => {
await expect(action()).rejects.toThrow('Not Authorised!') const { data, errors } = await action()
expect(data).toEqual({ Post: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
}) })
describe('as moderator', () => { describe('as moderator', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' }) authenticatedUser = await moderator.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('shows deleted posts', async () => { it('does not show deleted posts', async () => {
const expected = { Post: [{ title: 'Deleted post' }] } const expected = { data: { Post: [{ title: 'UNAVAILABLE' }] } }
await expect(action()).resolves.toEqual(expected) await expect(action()).resolves.toMatchObject(expected)
}) })
}) })
}) })
describe('filter (disabled: true)', () => { describe('filter (disabled: true)', () => {
beforeEach(() => { beforeEach(() => {
query = '{ Post(disabled: true) { title } }' graphqlQuery = gql`
{
Post(disabled: true) {
title
}
}
`
}) })
describe('as user', () => { describe('as user', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedUser = await user.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('throws authorisation error', async () => { it('throws authorisation error', async () => {
await expect(action()).rejects.toThrow('Not Authorised!') const { data, errors } = await action()
expect(data).toEqual({ Post: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
}) })
describe('as moderator', () => { describe('as moderator', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' }) authenticatedUser = await moderator.toJson()
client = new GraphQLClient(host, { headers })
}) })
it('shows disabled posts', async () => { it('shows disabled posts', async () => {
const expected = { Post: [{ title: 'Disabled post' }] } const expected = { data: { Post: [{ title: 'Disabled post' }] } }
await expect(action()).resolves.toEqual(expected) await expect(action()).resolves.toMatchObject(expected)
}) })
}) })
}) })

View File

@ -52,29 +52,9 @@ const validatePost = async (resolve, root, args, context, info) => {
} }
const validateUpdatePost = async (resolve, root, args, context, info) => { const validateUpdatePost = async (resolve, root, args, context, info) => {
const { id, categoryIds } = args const { categoryIds } = args
const session = context.driver.session() if (typeof categoryIds === 'undefined') return resolve(root, args, context, info)
const categoryQueryRes = await session.run( return validatePost(resolve, root, args, context, info)
`
MATCH (post:Post {id: $id})-[:CATEGORIZED]->(category:Category)
RETURN category`,
{ id },
)
session.close()
const [category] = categoryQueryRes.records.map(record => {
return record.get('category')
})
if (category) {
if (categoryIds && categoryIds.length > 3) {
throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE)
}
} else {
if (!Array.isArray(categoryIds) || !categoryIds.length || categoryIds.length > 3) {
throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE)
}
}
return resolve(root, args, context, info)
} }
export default { export default {

View File

@ -0,0 +1,48 @@
import uuid from 'uuid/v4'
module.exports = {
id: { type: 'string', primary: true, default: uuid },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: {
type: 'string',
isoDate: true,
required: true,
default: () => new Date().toISOString(),
},
content: { type: 'string', disallow: [null], min: 3 },
contentExcerpt: { type: 'string', allow: [null] },
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
post: {
type: 'relationship',
relationship: 'COMMENTS',
target: 'Post',
direction: 'out',
},
author: {
type: 'relationship',
relationship: 'WROTE',
target: 'User',
direction: 'in',
},
disabledBy: {
type: 'relationship',
relationship: 'DISABLED',
target: 'User',
direction: 'in',
},
notified: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'User',
direction: 'out',
properties: {
read: { type: 'boolean', default: false },
reason: {
type: 'string',
valid: ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post'],
},
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
}

View File

@ -0,0 +1,21 @@
module.exports = {
id: { type: 'string', primary: true },
lat: { type: 'number' },
lng: { type: 'number' },
type: { type: 'string' },
name: { type: 'string' },
nameES: { type: 'string' },
nameFR: { type: 'string' },
nameIT: { type: 'string' },
nameEN: { type: 'string' },
namePT: { type: 'string' },
nameDE: { type: 'string' },
nameNL: { type: 'string' },
namePL: { type: 'string' },
isIn: {
type: 'relationship',
relationship: 'IS_IN',
target: 'Location',
direction: 'out',
},
}

View File

@ -23,6 +23,20 @@ module.exports = {
target: 'User', target: 'User',
direction: 'in', direction: 'in',
}, },
notified: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'User',
direction: 'out',
properties: {
read: { type: 'boolean', default: false },
reason: {
type: 'string',
valid: ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post'],
},
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { updatedAt: {
type: 'string', type: 'string',

View File

@ -8,7 +8,7 @@ module.exports = {
type: 'relationship', type: 'relationship',
relationship: 'OWNED_BY', relationship: 'OWNED_BY',
target: 'User', target: 'User',
direction: 'in', direction: 'out',
eager: true, eager: true,
cascade: 'detach', cascade: 'detach',
}, },

17
backend/src/models/Tag.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
id: { type: 'string', primary: true },
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
updatedAt: {
type: 'string',
isoDate: true,
required: true,
default: () => new Date().toISOString(),
},
post: {
type: 'relationship',
relationship: 'TAGGED',
target: 'Post',
direction: 'in',
},
}

View File

@ -93,4 +93,16 @@ module.exports = {
allow: [null], allow: [null],
// required: true, TODO // required: true, TODO
},*/ },*/
shouted: {
type: 'relationship',
relationship: 'SHOUTED',
target: 'Post',
direction: 'out',
},
isIn: {
type: 'relationship',
relationship: 'IS_IN',
target: 'Location',
direction: 'out',
},
} }

View File

@ -7,5 +7,8 @@ export default {
EmailAddress: require('./EmailAddress.js'), EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'), SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'), Post: require('./Post.js'),
Comment: require('./Comment.js'),
Category: require('./Category.js'), Category: require('./Category.js'),
Tag: require('./Tag.js'),
Location: require('./Location.js'),
} }

View File

@ -47,9 +47,19 @@ export default {
session.close() session.close()
return commentReturnedWithAuthor return commentReturnedWithAuthor
}, },
DeleteComment: async (object, params, context, resolveInfo) => { DeleteComment: async (object, args, context, resolveInfo) => {
const comment = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session()
const transactionRes = await session.run(
`
MATCH (comment:Comment {id: $commentId})
SET comment.deleted = TRUE
SET comment.content = 'UNAVAILABLE'
SET comment.contentExcerpt = 'UNAVAILABLE'
RETURN comment
`,
{ commentId: args.id },
)
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
return comment return comment
}, },
}, },

View File

@ -1,48 +1,34 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers' import { gql } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
let client const driver = getDriver()
let createCommentVariables const neode = getNeode()
let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost
let userParams
let headers
const factory = Factory() const factory = Factory()
const instance = neode()
const categoryIds = ['cat9']
const createPostMutation = gql` let variables
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) { let mutate
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { let authenticatedUser
id let commentAuthor
}
} beforeAll(() => {
` const { server } = createServer({
const createCommentMutation = gql` context: () => {
mutation($id: ID, $postId: ID!, $content: String!) { return {
CreateComment(id: $id, postId: $postId, content: $content) { driver,
id user: authenticatedUser,
content }
} },
} })
` const client = createTestClient(server)
const createPostVariables = { mutate = client.mutate
id: 'p1', })
title: 'post to comment on',
content: 'please comment on me',
categoryIds,
}
beforeEach(async () => { beforeEach(async () => {
userParams = { variables = {}
name: 'TestUser', await neode.create('Category', {
email: 'test@example.org',
password: '1234',
}
await factory.create('User', userParams)
await instance.create('Category', {
id: 'cat9', id: 'cat9',
name: 'Democracy & Politics', name: 'Democracy & Politics',
icon: 'university', icon: 'university',
@ -53,335 +39,243 @@ afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
content
author {
name
}
}
}
`
const setupPostAndComment = async () => {
commentAuthor = await factory.create('User')
await factory.create('Post', {
id: 'p1',
content: 'Post to be commented',
categoryIds: ['cat9'],
})
await factory.create('Comment', {
id: 'c456',
postId: 'p1',
author: commentAuthor,
content: 'Comment to be deleted',
})
variables = {
...variables,
id: 'c456',
content: 'The comment is updated',
}
}
describe('CreateComment', () => { describe('CreateComment', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
createCommentVariables = { variables = {
...variables,
postId: 'p1', postId: 'p1',
content: "I'm not authorised to comment", content: "I'm not authorised to comment",
} }
client = new GraphQLClient(host) const { errors } = await mutate({ mutation: createCommentMutation, variables })
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow( expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
'Not Authorised',
)
}) })
}) })
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login(userParams) const user = await neode.create('User', { name: 'Author' })
client = new GraphQLClient(host, { authenticatedUser = await user.toJson()
headers,
})
createCommentVariables = {
postId: 'p1',
content: "I'm authorised to comment",
}
await client.request(createPostMutation, createPostVariables)
}) })
it('creates a comment', async () => { describe('given a post', () => {
const expected = { beforeEach(async () => {
CreateComment: { await factory.create('Post', { categoryIds: ['cat9'], id: 'p1' })
variables = {
...variables,
postId: 'p1',
content: "I'm authorised to comment", content: "I'm authorised to comment",
},
}
await expect(
client.request(createCommentMutation, createCommentVariables),
).resolves.toMatchObject(expected)
})
it('assigns the authenticated user as author', async () => {
await client.request(createCommentMutation, createCommentVariables)
const { User } = await client.request(gql`
{
User(name: "TestUser") {
comments {
content
}
}
} }
`) })
expect(User).toEqual([ it('creates a comment', async () => {
{ await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
comments: [ {
{ data: { CreateComment: { content: "I'm authorised to comment" } },
content: "I'm authorised to comment", },
}, )
], })
},
])
})
it('throw an error if an empty string is sent from the editor as content', async () => { it('assigns the authenticated user as author', async () => {
createCommentVariables = { await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
postId: 'p1', {
content: '<p></p>', data: { CreateComment: { author: { name: 'Author' } } },
} },
)
})
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow( describe('comment content is empty', () => {
'Comment must be at least 1 character long!', beforeEach(() => {
) variables = { ...variables, content: '<p></p>' }
}) })
it('throws an error if a comment sent from the editor does not contain a single character', async () => { it('throw UserInput error', async () => {
createCommentVariables = { const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
postId: 'p1', expect(data).toEqual({ CreateComment: null })
content: '<p> </p>', expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
} })
})
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow( describe('comment content contains only whitespaces', () => {
'Comment must be at least 1 character long!', beforeEach(() => {
) variables = { ...variables, content: ' <p> </p> ' }
}) })
it('throws an error if postId is sent as an empty string', async () => { it('throw UserInput error', async () => {
createCommentVariables = { const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
postId: 'p1', expect(data).toEqual({ CreateComment: null })
content: '', expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
} })
})
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow( describe('invalid post id', () => {
'Comment must be at least 1 character long!', beforeEach(() => {
) variables = { ...variables, postId: 'does-not-exist' }
}) })
it('throws an error if content is sent as an string of empty characters', async () => { it('throw UserInput error', async () => {
createCommentVariables = { const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
postId: 'p1', expect(data).toEqual({ CreateComment: null })
content: ' ', expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
} })
})
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
'Comment must be at least 1 character long!',
)
})
it('throws an error if postId is sent as an empty string', async () => {
createCommentVariablesSansPostId = {
postId: '',
content: 'this comment should not be created',
}
await expect(
client.request(createCommentMutation, createCommentVariablesSansPostId),
).rejects.toThrow('Comment cannot be created without a post!')
})
it('throws an error if postId is sent as an string of empty characters', async () => {
createCommentVariablesSansPostId = {
postId: ' ',
content: 'this comment should not be created',
}
await expect(
client.request(createCommentMutation, createCommentVariablesSansPostId),
).rejects.toThrow('Comment cannot be created without a post!')
})
it('throws an error if the post does not exist in the database', async () => {
createCommentVariablesWithNonExistentPost = {
postId: 'p2',
content: "comment should not be created cause the post doesn't exist",
}
await expect(
client.request(createCommentMutation, createCommentVariablesWithNonExistentPost),
).rejects.toThrow('Comment cannot be created without a post!')
}) })
}) })
}) })
describe('ManageComments', () => { describe('UpdateComment', () => {
let authorParams const updateCommentMutation = gql`
beforeEach(async () => { mutation($content: String!, $id: ID!) {
authorParams = { UpdateComment(content: $content, id: $id) {
email: 'author@example.org', id
password: '1234', content
}
const asAuthor = Factory()
await asAuthor.create('User', authorParams)
await asAuthor.authenticateAs(authorParams)
await asAuthor.create('Post', {
id: 'p1',
content: 'Post to be commented',
categoryIds,
})
await asAuthor.create('Comment', {
id: 'c456',
postId: 'p1',
content: 'Comment to be deleted',
})
})
describe('UpdateComment', () => {
const updateCommentMutation = gql`
mutation($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) {
id
content
}
} }
`
let updateCommentVariables = {
id: 'c456',
content: 'The comment is updated',
} }
`
describe('given a post and a comment', () => {
beforeEach(setupPostAndComment)
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) const { errors } = await mutate({ mutation: updateCommentMutation, variables })
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
'Not Authorised',
)
}) })
}) })
describe('authenticated but not the author', () => { describe('authenticated but not the author', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ const randomGuy = await factory.create('User')
email: 'test@example.org', authenticatedUser = await randomGuy.toJson()
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
}) })
it('throws authorization error', async () => { it('throws authorization error', async () => {
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( const { errors } = await mutate({ mutation: updateCommentMutation, variables })
'Not Authorised', expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
)
}) })
}) })
describe('authenticated as author', () => { describe('authenticated as author', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login(authorParams) authenticatedUser = await commentAuthor.toJson()
client = new GraphQLClient(host, {
headers,
})
}) })
it('updates the comment', async () => { it('updates the comment', async () => {
const expected = { const expected = {
UpdateComment: { data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } },
id: 'c456',
content: 'The comment is updated',
},
} }
await expect( await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
client.request(updateCommentMutation, updateCommentVariables), expected,
).resolves.toEqual(expected)
})
it('throw an error if an empty string is sent from the editor as content', async () => {
updateCommentVariables = {
id: 'c456',
content: '<p></p>',
}
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
'Comment must be at least 1 character long!',
) )
}) })
it('throws an error if a comment sent from the editor does not contain a single letter character', async () => { describe('if `content` empty', () => {
updateCommentVariables = { beforeEach(() => {
id: 'c456', variables = { ...variables, content: ' <p> </p>' }
content: '<p> </p>', })
}
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( it('throws InputError', async () => {
'Comment must be at least 1 character long!', const { errors } = await mutate({ mutation: updateCommentMutation, variables })
) expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
})
}) })
it('throws an error if commentId is sent as an empty string', async () => { describe('if comment does not exist for given id', () => {
updateCommentVariables = { beforeEach(() => {
id: '', variables = { ...variables, id: 'does-not-exist' }
content: '<p>Hello</p>', })
}
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( it('returns null', async () => {
'Not Authorised!', const { data, errors } = await mutate({ mutation: updateCommentMutation, variables })
) expect(data).toMatchObject({ UpdateComment: null })
}) expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
it('throws an error if the comment does not exist in the database', async () => {
updateCommentVariables = {
id: 'c1000',
content: '<p>Hello</p>',
}
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
'Not Authorised!',
)
}) })
}) })
}) })
})
describe('DeleteComment', () => { describe('DeleteComment', () => {
const deleteCommentMutation = gql` const deleteCommentMutation = gql`
mutation($id: ID!) { mutation($id: ID!) {
DeleteComment(id: $id) { DeleteComment(id: $id) {
id id
} content
contentExcerpt
deleted
} }
`
const deleteCommentVariables = {
id: 'c456',
} }
`
describe('given a post and a comment', () => {
beforeEach(setupPostAndComment)
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) const result = await mutate({ mutation: deleteCommentMutation, variables })
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
'Not Authorised',
)
}) })
}) })
describe('authenticated but not the author', () => { describe('authenticated but not the author', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ const randomGuy = await factory.create('User')
email: 'test@example.org', authenticatedUser = await randomGuy.toJson()
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
}) })
it('throws authorization error', async () => { it('throws authorization error', async () => {
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( const { errors } = await mutate({ mutation: deleteCommentMutation, variables })
'Not Authorised', expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
)
}) })
}) })
describe('authenticated as author', () => { describe('authenticated as author', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login(authorParams) authenticatedUser = await commentAuthor.toJson()
client = new GraphQLClient(host, {
headers,
})
}) })
it('deletes the comment', async () => { it('marks the comment as deleted and blacks out content', async () => {
const { data } = await mutate({ mutation: deleteCommentMutation, variables })
const expected = { const expected = {
DeleteComment: { DeleteComment: {
id: 'c456', id: 'c456',
deleted: true,
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
}, },
} }
await expect( expect(data).toMatchObject(expected)
client.request(deleteCommentMutation, deleteCommentVariables),
).resolves.toEqual(expected)
}) })
}) })
}) })

View File

@ -44,7 +44,7 @@ export default {
try { try {
const cypher = ` const cypher = `
MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id}) MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
${whereClause} ${whereClause}
RETURN resource, notification, user RETURN resource, notification, user
${orderByClause} ${orderByClause}

View File

@ -1,20 +1,14 @@
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers' import { gql } from '../../jest/helpers'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'
const factory = Factory() const factory = Factory()
const neode = getNeode()
const driver = getDriver() const driver = getDriver()
const userParams = {
id: 'you',
email: 'test@example.org',
password: '1234',
}
let authenticatedUser let authenticatedUser
let user let user
let author
let variables let variables
let query let query
let mutate let mutate
@ -43,51 +37,81 @@ afterEach(async () => {
describe('given some notifications', () => { describe('given some notifications', () => {
beforeEach(async () => { beforeEach(async () => {
user = await factory.create('User', userParams) const categoryIds = ['cat1']
await factory.create('User', { id: 'neighbor' }) author = await factory.create('User', { id: 'author' })
await Promise.all(setupNotifications.map(s => neode.cypher(s))) user = await factory.create('User', { id: 'you' })
const [neighbor] = await Promise.all([
factory.create('User', { id: 'neighbor' }),
factory.create('Category', { id: 'cat1' }),
])
const [post1, post2, post3] = await Promise.all([
factory.create('Post', { author, id: 'p1', categoryIds, content: 'Not for you' }),
factory.create('Post', {
author,
id: 'p2',
categoryIds,
content: 'Already seen post mention',
}),
factory.create('Post', {
author,
id: 'p3',
categoryIds,
content: 'You have been mentioned in a post',
}),
])
const [comment1, comment2, comment3] = await Promise.all([
factory.create('Comment', {
author,
postId: 'p3',
id: 'c1',
content: 'You have seen this comment mentioning already',
}),
factory.create('Comment', {
author,
postId: 'p3',
id: 'c2',
content: 'You have been mentioned in a comment',
}),
factory.create('Comment', {
author,
postId: 'p3',
id: 'c3',
content: 'Somebody else was mentioned in a comment',
}),
])
await Promise.all([
post1.relateTo(neighbor, 'notified', {
createdAt: '2019-08-29T17:33:48.651Z',
read: false,
reason: 'mentioned_in_post',
}),
post2.relateTo(user, 'notified', {
createdAt: '2019-08-30T17:33:48.651Z',
read: true,
reason: 'mentioned_in_post',
}),
post3.relateTo(user, 'notified', {
createdAt: '2019-08-31T17:33:48.651Z',
read: false,
reason: 'mentioned_in_post',
}),
comment1.relateTo(user, 'notified', {
createdAt: '2019-08-30T15:33:48.651Z',
read: true,
reason: 'mentioned_in_comment',
}),
comment2.relateTo(user, 'notified', {
createdAt: '2019-08-30T19:33:48.651Z',
read: false,
reason: 'mentioned_in_comment',
}),
comment3.relateTo(neighbor, 'notified', {
createdAt: '2019-09-01T17:33:48.651Z',
read: false,
reason: 'mentioned_in_comment',
}),
])
}) })
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('notifications', () => { describe('notifications', () => {
const notificationQuery = gql` const notificationQuery = gql`
@ -109,8 +133,8 @@ describe('given some notifications', () => {
` `
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
const result = await query({ query: notificationQuery }) const { errors } = await query({ query: notificationQuery })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
}) })
@ -121,7 +145,7 @@ describe('given some notifications', () => {
describe('no filters', () => { describe('no filters', () => {
it('returns all notifications of current user', async () => { it('returns all notifications of current user', async () => {
const expected = expect.objectContaining({ const expected = {
data: { data: {
notifications: [ notifications: [
{ {
@ -135,7 +159,7 @@ describe('given some notifications', () => {
{ {
from: { from: {
__typename: 'Post', __typename: 'Post',
content: 'Already seen post mentioning', content: 'Already seen post mention',
}, },
read: true, read: true,
createdAt: '2019-08-30T17:33:48.651Z', createdAt: '2019-08-30T17:33:48.651Z',
@ -158,8 +182,10 @@ describe('given some notifications', () => {
}, },
], ],
}, },
}) }
await expect(query({ query: notificationQuery, variables })).resolves.toEqual(expected) await expect(query({ query: notificationQuery, variables })).resolves.toMatchObject(
expected,
)
}) })
}) })
@ -191,6 +217,36 @@ describe('given some notifications', () => {
query({ query: notificationQuery, variables: { ...variables, read: false } }), query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
describe('if a resource gets deleted', () => {
const deletePostAction = async () => {
authenticatedUser = await author.toJson()
const deletePostMutation = gql`
mutation($id: ID!) {
DeletePost(id: $id) {
id
deleted
}
}
`
await expect(
mutate({ mutation: deletePostMutation, variables: { id: 'p3' } }),
).resolves.toMatchObject({ data: { DeletePost: { id: 'p3', deleted: true } } })
authenticatedUser = await user.toJson()
}
it('reduces notifications list', async () => {
await expect(
query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toMatchObject({
data: { notifications: [expect.any(Object), expect.any(Object)] },
})
await deletePostAction()
await expect(
query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toMatchObject({ data: { notifications: [] } })
})
})
}) })
}) })
}) })

View File

@ -73,13 +73,42 @@ export default {
}, },
}, },
Mutation: { Mutation: {
CreatePost: async (object, params, context, resolveInfo) => {
const { categoryIds } = params
delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
params.id = params.id || uuid()
const createPostCypher = `CREATE (post:Post {params})
WITH post
MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author)
WITH post
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category)
RETURN post`
const createPostVariables = { userId: context.user.id, categoryIds, params }
const session = context.driver.session()
const transactionRes = await session.run(createPostCypher, createPostVariables)
const [post] = transactionRes.records.map(record => {
return record.get('post')
})
session.close()
return post.properties
},
UpdatePost: async (object, params, context, resolveInfo) => { UpdatePost: async (object, params, context, resolveInfo) => {
const { categoryIds } = params const { categoryIds } = params
delete params.categoryIds delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
const session = context.driver.session() const session = context.driver.session()
let updatePostCypher = `MATCH (post:Post {id: $params.id}) let updatePostCypher = `MATCH (post:Post {id: $params.id})
SET post = $params SET post = $params
` `
@ -112,34 +141,25 @@ export default {
return post.properties return post.properties
}, },
CreatePost: async (object, params, context, resolveInfo) => { DeletePost: async (object, args, context, resolveInfo) => {
const { categoryIds } = params
delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
params.id = params.id || uuid()
const createPostCypher = `CREATE (post:Post {params})
WITH post
MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author)
WITH post
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category)
RETURN post`
const createPostVariables = { userId: context.user.id, categoryIds, params }
const session = context.driver.session() const session = context.driver.session()
const transactionRes = await session.run(createPostCypher, createPostVariables) // we cannot set slug to 'UNAVAILABE' because of unique constraints
const transactionRes = await session.run(
const [post] = transactionRes.records.map(record => { `
return record.get('post') MATCH (post:Post {id: $postId})
}) OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
SET post.deleted = TRUE
session.close() SET post.content = 'UNAVAILABLE'
SET post.contentExcerpt = 'UNAVAILABLE'
return post.properties SET post.title = 'UNAVAILABLE'
SET comment.deleted = TRUE
REMOVE post.image
RETURN post
`,
{ postId: args.id },
)
const [post] = transactionRes.records.map(record => record.get('post').properties)
return post
}, },
AddPostEmotions: async (object, params, context, resolveInfo) => { AddPostEmotions: async (object, params, context, resolveInfo) => {
const session = context.driver.session() const session = context.driver.session()
@ -184,6 +204,7 @@ export default {
}, },
Post: { Post: {
...Resolver('Post', { ...Resolver('Post', {
undefinedToNull: ['activityId', 'objectId', 'image', 'language'],
hasMany: { hasMany: {
tags: '-[:TAGGED]->(related:Tag)', tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)', categories: '-[:CATEGORIZED]->(related:Category)',
@ -196,13 +217,15 @@ export default {
disabledBy: '<-[:DISABLED]-(related:User)', disabledBy: '<-[:DISABLED]-(related:User)',
}, },
count: { count: {
commentsCount:
'<-[:COMMENTS]-(related:Comment) WHERE NOT related.deleted = true AND NOT related.disabled = true',
shoutedCount: shoutedCount:
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true', '<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
emotionsCount: '<-[related:EMOTED]-(:User)', emotionsCount: '<-[related:EMOTED]-(:User)',
}, },
boolean: { boolean: {
shoutedByCurrentUser: shoutedByCurrentUser:
'<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', 'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
}, },
}), }),
relatedContributions: async (parent, params, context, resolveInfo) => { relatedContributions: async (parent, params, context, resolveInfo) => {
@ -210,6 +233,7 @@ export default {
const { id } = parent const { id } = parent
const statement = ` const statement = `
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
WHERE NOT post.deleted AND NOT post.disabled
RETURN DISTINCT post RETURN DISTINCT post
LIMIT 10 LIMIT 10
` `

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ describe('report', () => {
let returnedObject let returnedObject
let variables let variables
let createPostVariables let createPostVariables
let user
const categoryIds = ['cat9'] const categoryIds = ['cat9']
beforeEach(async () => { beforeEach(async () => {
@ -20,10 +21,10 @@ describe('report', () => {
id: 'whatever', id: 'whatever',
} }
headers = {} headers = {}
await factory.create('User', { user = await factory.create('User', {
id: 'u1',
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
id: 'u1',
}) })
await factory.create('User', { await factory.create('User', {
id: 'u2', id: 'u2',
@ -127,11 +128,8 @@ describe('report', () => {
describe('reported resource is a post', () => { describe('reported resource is a post', () => {
beforeEach(async () => { beforeEach(async () => {
await factory.authenticateAs({
email: 'test@example.org',
password: '1234',
})
await factory.create('Post', { await factory.create('Post', {
author: user,
id: 'p23', id: 'p23',
title: 'Matt and Robert having a pair-programming', title: 'Matt and Robert having a pair-programming',
categoryIds, categoryIds,
@ -182,12 +180,9 @@ describe('report', () => {
content: 'please comment on me', content: 'please comment on me',
categoryIds, categoryIds,
} }
const asAuthenticatedUser = await factory.authenticateAs({ await factory.create('Post', { ...createPostVariables, author: user })
email: 'test@example.org', await factory.create('Comment', {
password: '1234', author: user,
})
await asAuthenticatedUser.create('Post', createPostVariables)
await asAuthenticatedUser.create('Comment', {
postId: 'p1', postId: 'p1',
id: 'c34', id: 'c34',
content: 'Robert getting tired.', content: 'Robert getting tired.',

View File

@ -32,7 +32,7 @@ export default {
SocialMedia: Resolver('SocialMedia', { SocialMedia: Resolver('SocialMedia', {
idAttribute: 'url', idAttribute: 'url',
hasOne: { hasOne: {
ownedBy: '<-[:OWNED_BY]-(related:User)', ownedBy: '-[:OWNED_BY]->(related:User)',
}, },
}), }),
} }

View File

@ -23,11 +23,11 @@ export default {
// } // }
const session = driver.session() const session = driver.session()
const result = await session.run( const result = await session.run(
'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' + `
'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1', MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
{ RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
userEmail: email, `,
}, { userEmail: email },
) )
session.close() session.close()
const [currentUser] = await result.records.map(record => { const [currentUser] = await result.records.map(record => {

View File

@ -1,50 +1,54 @@
import { GraphQLClient, request } from 'graphql-request'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import CONFIG from './../../config' import CONFIG from './../../config'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers' import { gql } from '../../jest/helpers'
import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server'
const factory = Factory() const factory = Factory()
let query
let mutate
let variables
let req
let user
// here is the decoded JWT token: // This is a bearer token of a user with id `u3`:
// { const userBearerToken =
// role: 'user', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsIm5hbWUiOiJKZW5ueSBSb3N0b2NrIiwiZGlzYWJsZWQiOmZhbHNlLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL2tleXVyaTg1LzEyOC5qcGciLCJpZCI6InUzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUub3JnIiwic2x1ZyI6Implbm55LXJvc3RvY2siLCJpYXQiOjE1Njc0NjgyMDIsImV4cCI6MTU2NzU1NDYwMiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwIiwic3ViIjoidTMifQ.RkmrdJDL1kIqGnMWUBl_sJJ4grzfpTEGdT6doMsbLW8'
// locationName: null,
// name: 'Jenny Rostock', // This is a bearer token of a user with id `u2`:
// about: null, const moderatorBearerToken =
// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoibW9kZXJhdG9yIiwibmFtZSI6IkJvYiBkZXIgQmF1bWVpc3RlciIsImRpc2FibGVkIjpmYWxzZSwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9hbmRyZXdvZmZpY2VyLzEyOC5qcGciLCJpZCI6InUyIiwiZW1haWwiOiJtb2RlcmF0b3JAZXhhbXBsZS5vcmciLCJzbHVnIjoiYm9iLWRlci1iYXVtZWlzdGVyIiwiaWF0IjoxNTY3NDY4MDUwLCJleHAiOjE1Njc1NTQ0NTAsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUyIn0.LdVFPKqIcoY0a7_kFZSTgnc8NzmZD7CrR3vkWLSqedM'
// id: 'u3',
// email: 'user@example.org',
// slug: 'jenny-rostock',
// iat: 1550846680,
// exp: 1637246680,
// aud: 'http://localhost:3000',
// iss: 'http://localhost:4000',
// sub: 'u3'
// }
const jennyRostocksHeaders = {
authorization:
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc',
}
const disable = async id => { const disable = async id => {
const moderatorParams = { email: 'moderator@example.org', role: 'moderator', password: '1234' } await factory.create('User', { id: 'u2', role: 'moderator' })
const asModerator = Factory() req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } }
await asModerator.create('User', moderatorParams) await mutate({
await asModerator.authenticateAs(moderatorParams) mutation: gql`
await asModerator.mutate('mutation($id: ID!) { disable(id: $id) }', { id }) mutation($id: ID!) {
disable(id: $id)
}
`,
variables: { id },
})
req = { headers: {} }
} }
beforeEach(async () => { beforeEach(() => {
await factory.create('User', { user = null
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', req = { headers: {} }
id: 'acb2d923-f3af-479e-9f00-61b12e864666', })
name: 'Matilde Hermiston',
slug: 'matilde-hermiston', beforeAll(() => {
role: 'user', const { server } = createServer({
email: 'test@example.org', context: () => {
password: '1234', // One of the rare occasions where we test
// the actual `context` implementation here
return context({ req })
},
}) })
query = createTestClient(server).query
mutate = createTestClient(server).mutate
}) })
afterEach(async () => { afterEach(async () => {
@ -52,261 +56,266 @@ afterEach(async () => {
}) })
describe('isLoggedIn', () => { describe('isLoggedIn', () => {
const query = '{ isLoggedIn }' const isLoggedInQuery = gql`
{
isLoggedIn
}
`
const respondsWith = async expected => {
await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected)
}
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns false', async () => { it('returns false', async () => {
await expect(request(host, query)).resolves.toEqual({ await respondsWith({ data: { isLoggedIn: false } })
isLoggedIn: false,
})
}) })
}) })
describe('with malformed JWT Bearer token', () => { describe('authenticated', () => {
const headers = { authorization: 'blah' } beforeEach(async () => {
const client = new GraphQLClient(host, { headers }) user = await factory.create('User', { id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
it('returns false', async () => {
await expect(client.request(query)).resolves.toEqual({
isLoggedIn: false,
})
}) })
})
describe('with valid JWT Bearer token', () => { it('returns true', async () => {
const client = new GraphQLClient(host, { headers: jennyRostocksHeaders }) await respondsWith({ data: { isLoggedIn: true } })
})
it('returns false', async () => { describe('but user is disabled', () => {
await expect(client.request(query)).resolves.toEqual({ beforeEach(async () => {
isLoggedIn: false, await disable('u3')
})
it('returns false', async () => {
await respondsWith({ data: { isLoggedIn: false } })
}) })
}) })
describe('and a corresponding user in the database', () => { describe('but user is deleted', () => {
describe('user is enabled', () => { beforeEach(async () => {
it('returns true', async () => { await user.update({ updatedAt: new Date().toISOString(), deleted: true })
// see the decoded token above
await factory.create('User', { id: 'u3' })
await expect(client.request(query)).resolves.toEqual({
isLoggedIn: true,
})
})
}) })
describe('user is disabled', () => { it('returns false', async () => {
beforeEach(async () => { await respondsWith({ data: { isLoggedIn: false } })
await factory.create('User', { id: 'u3' })
await disable('u3')
})
it('returns false', async () => {
await expect(client.request(query)).resolves.toEqual({
isLoggedIn: false,
})
})
}) })
}) })
}) })
}) })
describe('currentUser', () => { describe('currentUser', () => {
const query = `{ const currentUserQuery = gql`
currentUser { {
id currentUser {
slug id
name slug
avatar name
email avatar
role email
role
}
} }
}` `
const respondsWith = async expected => {
await expect(query({ query: currentUserQuery, variables })).resolves.toMatchObject(expected)
}
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns null', async () => { it('returns null', async () => {
const expected = { currentUser: null } await respondsWith({ data: { currentUser: null } })
await expect(request(host, query)).resolves.toEqual(expected)
}) })
}) })
describe('with valid JWT Bearer Token', () => { describe('authenticated', () => {
let client
let headers
describe('but no corresponding user in the database', () => {
beforeEach(async () => {
client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
})
it('returns null', async () => {
const expected = { currentUser: null }
await expect(client.request(query)).resolves.toEqual(expected)
})
})
describe('and corresponding user in the database', () => { describe('and corresponding user in the database', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) await factory.create('User', {
client = new GraphQLClient(host, { headers }) id: 'u3',
// the `id` is the only thing that has to match the decoded JWT bearer token
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
email: 'test@example.org',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user',
})
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
}) })
it('returns the whole user object', async () => { it('returns the whole user object', async () => {
const expected = { const expected = {
currentUser: { data: {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', currentUser: {
email: 'test@example.org', id: 'u3',
id: 'acb2d923-f3af-479e-9f00-61b12e864666', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
name: 'Matilde Hermiston', email: 'test@example.org',
slug: 'matilde-hermiston', name: 'Matilde Hermiston',
role: 'user', slug: 'matilde-hermiston',
role: 'user',
},
}, },
} }
await expect(client.request(query)).resolves.toEqual(expected) await respondsWith(expected)
}) })
}) })
}) })
}) })
describe('login', () => { describe('login', () => {
const mutation = params => { const loginMutation = gql`
const { email, password } = params mutation($email: String!, $password: String!) {
return ` login(email: $email, password: $password)
mutation { }
login(email:"${email}", password:"${password}") `
}`
const respondsWith = async expected => {
await expect(mutate({ mutation: loginMutation, variables })).resolves.toMatchObject(expected)
} }
beforeEach(async () => {
variables = { email: 'test@example.org', password: '1234' }
user = await factory.create('User', {
...variables,
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
})
})
describe('ask for a `token`', () => { describe('ask for a `token`', () => {
describe('with valid email/password combination', () => { describe('with a valid email/password combination', () => {
it('responds with a JWT token', async () => { it('responds with a JWT bearer token', async done => {
const data = await request( const {
host, data: { login: token },
mutation({ } = await mutate({ mutation: loginMutation, variables })
email: 'test@example.org',
password: '1234',
}),
)
const token = data.login
jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => { jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
expect(data.email).toEqual('test@example.org') expect(data.email).toEqual('test@example.org')
expect(err).toBeNull() expect(err).toBeNull()
done()
})
})
describe('but user account is deleted', () => {
beforeEach(async () => {
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
})
it('responds with "Incorrect email address or password."', async () => {
await respondsWith({
data: null,
errors: [{ message: 'Incorrect email address or password.' }],
})
})
})
describe('but user account is disabled', () => {
beforeEach(async () => {
await disable('acb2d923-f3af-479e-9f00-61b12e864666')
})
it('responds with "Your account has been disabled."', async () => {
await respondsWith({
data: null,
errors: [{ message: 'Your account has been disabled.' }],
})
}) })
}) })
}) })
describe('valid email/password but user is disabled', () => {
it('responds with "Your account has been disabled."', async () => {
await disable('acb2d923-f3af-479e-9f00-61b12e864666')
await expect(
request(
host,
mutation({
email: 'test@example.org',
password: '1234',
}),
),
).rejects.toThrow('Your account has been disabled.')
})
})
describe('with a valid email but incorrect password', () => { describe('with a valid email but incorrect password', () => {
beforeEach(() => {
variables = { ...variables, email: 'test@example.org', password: 'wrong' }
})
it('responds with "Incorrect email address or password."', async () => { it('responds with "Incorrect email address or password."', async () => {
await expect( await respondsWith({
request( errors: [{ message: 'Incorrect email address or password.' }],
host, })
mutation({
email: 'test@example.org',
password: 'wrong',
}),
),
).rejects.toThrow('Incorrect email address or password.')
}) })
}) })
describe('with a non-existing email', () => { describe('with a non-existing email', () => {
beforeEach(() => {
variables = {
...variables,
email: 'non-existent@example.org',
password: '1234',
}
})
it('responds with "Incorrect email address or password."', async () => { it('responds with "Incorrect email address or password."', async () => {
await expect( await respondsWith({
request( errors: [{ message: 'Incorrect email address or password.' }],
host, })
mutation({
email: 'non-existent@example.org',
password: 'wrong',
}),
),
).rejects.toThrow('Incorrect email address or password.')
}) })
}) })
}) })
}) })
describe('change password', () => { describe('change password', () => {
let headers const changePasswordMutation = gql`
let client mutation($oldPassword: String!, $newPassword: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
}
`
beforeEach(async () => { const respondsWith = async expected => {
headers = await login({ email: 'test@example.org', password: '1234' }) await expect(mutate({ mutation: changePasswordMutation, variables })).resolves.toMatchObject(
client = new GraphQLClient(host, { headers }) expected,
}) )
const mutation = params => {
const { oldPassword, newPassword } = params
return `
mutation {
changePassword(oldPassword:"${oldPassword}", newPassword:"${newPassword}")
}`
} }
describe('should be authenticated before changing password', () => { beforeEach(async () => {
variables = { ...variables, oldPassword: 'what', newPassword: 'ever' }
})
describe('unauthenticated', () => {
it('throws "Not Authorised!"', async () => { it('throws "Not Authorised!"', async () => {
await expect( await respondsWith({ errors: [{ message: 'Not Authorised!' }] })
request(
host,
mutation({
oldPassword: '1234',
newPassword: '1234',
}),
),
).rejects.toThrow('Not Authorised!')
}) })
}) })
describe('old and new password should not match', () => { describe('authenticated', () => {
it('responds with "Old password and new password should be different"', async () => { beforeEach(async () => {
await expect( await factory.create('User', { id: 'u3' })
client.request( req = { headers: { authorization: `Bearer ${userBearerToken}` } }
mutation({
oldPassword: '1234',
newPassword: '1234',
}),
),
).rejects.toThrow('Old password and new password should be different')
}) })
}) describe('old password === new password', () => {
beforeEach(() => {
variables = { ...variables, oldPassword: '1234', newPassword: '1234' }
})
describe('incorrect old password', () => { it('responds with "Old password and new password should be different"', async () => {
it('responds with "Old password isn\'t valid"', async () => { await respondsWith({
await expect( errors: [{ message: 'Old password and new password should be different' }],
client.request( })
mutation({ })
oldPassword: 'notOldPassword',
newPassword: '12345',
}),
),
).rejects.toThrow('Old password is not correct')
}) })
})
describe('correct password', () => { describe('incorrect old password', () => {
it('changes the password if given correct credentials "', async () => { beforeEach(() => {
const response = await client.request( variables = {
mutation({ ...variables,
oldPassword: 'notOldPassword',
newPassword: '12345',
}
})
it('responds with "Old password isn\'t valid"', async () => {
await respondsWith({ errors: [{ message: 'Old password is not correct' }] })
})
})
describe('correct password', () => {
beforeEach(() => {
variables = {
...variables,
oldPassword: '1234', oldPassword: '1234',
newPassword: '12345', newPassword: '12345',
}), }
) })
await expect(response).toEqual(
expect.objectContaining({ it('changes the password if given correct credentials "', async () => {
changePassword: expect.any(String), await respondsWith({ data: { changePassword: expect.any(String) } })
}), })
)
}) })
}) })
}) })

View File

@ -173,23 +173,49 @@ export default {
const { resource } = params const { resource } = params
const session = context.driver.session() const session = context.driver.session()
if (resource && resource.length) { let user
await Promise.all( try {
resource.map(async node => { if (resource && resource.length) {
await session.run( await Promise.all(
` resource.map(async node => {
await session.run(
`
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId}) MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
SET resource.deleted = true SET resource.deleted = true
SET resource.content = 'UNAVAILABLE'
SET resource.contentExcerpt = 'UNAVAILABLE'
SET comment.deleted = true
RETURN author`, RETURN author`,
{ {
userId: context.user.id, userId: context.user.id,
}, },
) )
}), }),
)
}
// we cannot set slug to 'UNAVAILABE' because of unique constraints
const transactionResult = await session.run(
`
MATCH (user:User {id: $userId})
SET user.deleted = true
SET user.name = 'UNAVAILABLE'
SET user.about = 'UNAVAILABLE'
WITH user
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
DETACH DELETE email
WITH user
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
DETACH DELETE socialMedia
RETURN user`,
{ userId: context.user.id },
) )
user = transactionResult.records.map(r => r.get('user').properties)[0]
} finally {
session.close() session.close()
} }
return neo4jgraphql(object, params, context, resolveInfo, false) return user
}, },
}, },
User: { User: {
@ -224,12 +250,13 @@ export default {
hasOne: { hasOne: {
invitedBy: '<-[:INVITED]-(related:User)', invitedBy: '<-[:INVITED]-(related:User)',
disabledBy: '<-[:DISABLED]-(related:User)', disabledBy: '<-[:DISABLED]-(related:User)',
location: '-[:IS_IN]->(related:Location)',
}, },
hasMany: { hasMany: {
followedBy: '<-[:FOLLOWS]-(related:User)', followedBy: '<-[:FOLLOWS]-(related:User)',
following: '-[:FOLLOWS]->(related:User)', following: '-[:FOLLOWS]->(related:User)',
friends: '-[:FRIENDS]-(related:User)', friends: '-[:FRIENDS]-(related:User)',
socialMedia: '-[:OWNED_BY]->(related:SocialMedia', socialMedia: '<-[:OWNED_BY]-(related:SocialMedia)',
contributions: '-[:WROTE]->(related:Post)', contributions: '-[:WROTE]->(related:Post)',
comments: '-[:WROTE]->(related:Comment)', comments: '-[:WROTE]->(related:Comment)',
shouted: '-[:SHOUTED]->(related:Post)', shouted: '-[:SHOUTED]->(related:Post)',

View File

@ -1,55 +1,78 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers' import { gql } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j' import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
let client
const factory = Factory() const factory = Factory()
const instance = neode()
const categoryIds = ['cat9'] const categoryIds = ['cat9']
let user
let query
let mutate
let authenticatedUser
let variables
const driver = getDriver()
const neode = getNeode()
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
})
afterEach(async () => { afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('users', () => { describe('User', () => {
describe('User', () => { describe('query by email address', () => {
describe('query by email address', () => { beforeEach(async () => {
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
})
const userQuery = gql`
query($email: String) {
User(email: $email) {
name
}
}
`
const variables = { email: 'any-email-address@example.org' }
it('is forbidden', async () => {
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
})
})
describe('as admin', () => {
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' }) const admin = await factory.create('User', {
}) role: 'admin',
email: 'admin@example.org',
const query = `query($email: String) { User(email: $email) { name } }` password: '1234',
const variables = { email: 'any-email-address@example.org' }
beforeEach(() => {
client = new GraphQLClient(host)
})
it('is forbidden', async () => {
await expect(client.request(query, variables)).rejects.toThrow('Not Authorised')
})
describe('as admin', () => {
beforeEach(async () => {
const userParams = {
role: 'admin',
email: 'admin@example.org',
password: '1234',
}
const factory = Factory()
await factory.create('User', userParams)
const headers = await login(userParams)
client = new GraphQLClient(host, { headers })
}) })
authenticatedUser = await admin.toJson()
})
it('is permitted', async () => { it('is permitted', async () => {
await expect(client.request(query, variables)).resolves.toEqual({ await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
User: [{ name: 'Johnny' }], data: { User: [{ name: 'Johnny' }] },
})
}) })
}) })
}) })
}) })
})
describe('UpdateUser', () => { describe('UpdateUser', () => {
let userParams let userParams
@ -77,37 +100,39 @@ describe('users', () => {
name name
termsAndConditionsAgreedVersion termsAndConditionsAgreedVersion
} }
}
`
}
}
`
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', userParams) await factory.create('User', userParams)
}) })
describe('as another user', () => {
beforeEach(async () => {
const someoneElseParams = {
email: 'someone-else@example.org',
password: '1234',
name: 'James Doe',
}
await factory.create('User', someoneElseParams) describe('as another user', () => {
const headers = await login(someoneElseParams) beforeEach(async () => {
client = new GraphQLClient(host, { headers }) const someoneElseParams = {
}) email: 'someone-else@example.org',
password: '1234',
name: 'James Doe',
}
const someoneElse = await factory.create('User', someoneElseParams)
authenticatedUser = await someoneElse.toJson()
})
it('is not allowed to change other user accounts', async () => { it('is not allowed to change other user accounts', async () => {
await expect(client.request(UpdateUserMutation, variables)).rejects.toThrow('Not Authorised') await expect(client.request(UpdateUserMutation, variables)).rejects.toThrow('Not Authorised')
}) })
}) })
})
describe('as the same user', () => { describe('as the same user', () => {
beforeEach(async () => { beforeEach(async () => {
const headers = await login(userParams) authenticatedUser = await user.toJson()
client = new GraphQLClient(host, { headers }) })
})
it('name within specifications', async () => { it('name within specifications', async () => {
const expected = { const expected = {
@ -156,63 +181,77 @@ describe('users', () => {
'Invalid version format!', 'Invalid version format!',
) )
}) })
}) })
}) })
})
describe('DeleteUser', () => { describe('DeleteUser', () => {
let deleteUserVariables const deleteUserMutation = gql`
let asAuthor mutation($id: ID!, $resource: [Deletable]) {
const deleteUserMutation = gql` DeleteUser(id: $id, resource: $resource) {
mutation($id: ID!, $resource: [Deletable]) { id
DeleteUser(id: $id, resource: $resource) { name
about
deleted
contributions {
id id
contributions { content
id contentExcerpt
deleted deleted
}
comments { comments {
id id
content
contentExcerpt
deleted deleted
} }
} }
comments {
id
content
contentExcerpt
deleted
}
} }
` }
`
beforeEach(async () => {
variables = { id: ' u343', resource: [] }
user = await factory.create('User', {
name: 'My name should be deleted',
about: 'along with my about',
id: 'u343',
})
await factory.create('User', {
email: 'friends-account@example.org',
password: '1234',
id: 'not-my-account',
})
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { authenticatedUser = await user.toJson()
email: 'test@example.org',
password: '1234',
id: 'u343',
})
await factory.create('User', {
email: 'friends-account@example.org',
password: '1234',
id: 'u565',
})
deleteUserVariables = { id: 'u343', resource: [] }
}) })
describe('unauthenticated', () => { describe("attempting to delete another user's account", () => {
it('throws authorization error', async () => { beforeEach(() => {
client = new GraphQLClient(host) variables = { ...variables, id: 'not-my-account' }
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow( })
'Not Authorised',
) it('throws an authorization error', async () => {
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
}) })
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, { headers })
})
describe("attempting to delete another user's account", () => { describe("attempting to delete another user's account", () => {
it('throws an authorization error', async () => { it('throws an authorization error', async () => {
@ -221,88 +260,234 @@ describe('users', () => {
'Not Authorised', 'Not Authorised',
) )
}) })
}) })
describe('attempting to delete my own account', () => { describe('given posts and comments', () => {
let expectedResponse
beforeEach(async () => { beforeEach(async () => {
asAuthor = Factory() await factory.create('Category', {
await asAuthor.authenticateAs({
email: 'test@example.org',
password: '1234',
})
await instance.create('Category', {
id: 'cat9', id: 'cat9',
name: 'Democracy & Politics', name: 'Democracy & Politics',
icon: 'university', icon: 'university',
}) })
await asAuthor.create('Post', { await factory.create('Post', {
author: user,
id: 'p139', id: 'p139',
content: 'Post by user u343', content: 'Post by user u343',
categoryIds, categoryIds,
}) })
await asAuthor.create('Comment', { await factory.create('Comment', {
author: user,
id: 'c155', id: 'c155',
postId: 'p139',
content: 'Comment by user u343', content: 'Comment by user u343',
}) })
expectedResponse = { await factory.create('Comment', {
DeleteUser: { postId: 'p139',
id: 'u343', id: 'c156',
contributions: [{ id: 'p139', deleted: false }], content: "A comment by someone else on user u343's post",
comments: [{ id: 'c155', deleted: false }], })
})
it("deletes my account, but doesn't delete posts or comments by default", async () => {
const expectedResponse = {
data: {
DeleteUser: {
id: 'u343',
name: 'UNAVAILABLE',
about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'Post by user u343',
contentExcerpt: 'Post by user u343',
deleted: false,
comments: [
{
id: 'c156',
content: "A comment by someone else on user u343's post",
contentExcerpt: "A comment by someone else on user u343's post",
deleted: false,
},
],
},
],
comments: [
{
id: 'c155',
content: 'Comment by user u343',
contentExcerpt: 'Comment by user u343',
deleted: false,
},
],
},
}, },
} }
}) await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
it("deletes my account, but doesn't delete posts or comments by default", async () => {
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
expectedResponse, expectedResponse,
) )
}) })
describe("deletes a user's", () => { describe('deletion of all post requested', () => {
it('posts on request', async () => { beforeEach(() => {
deleteUserVariables = { id: 'u343', resource: ['Post'] } variables = { ...variables, resource: ['Post'] }
expectedResponse = {
DeleteUser: {
id: 'u343',
contributions: [{ id: 'p139', deleted: true }],
comments: [{ id: 'c155', deleted: false }],
},
}
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
expectedResponse,
)
}) })
it('comments on request', async () => { describe("marks user's posts as deleted", () => {
deleteUserVariables = { id: 'u343', resource: ['Comment'] } it('posts on request', async () => {
expectedResponse = { const expectedResponse = {
DeleteUser: { data: {
id: 'u343', DeleteUser: {
contributions: [{ id: 'p139', deleted: false }], id: 'u343',
comments: [{ id: 'c155', deleted: true }], name: 'UNAVAILABLE',
}, about: 'UNAVAILABLE',
} deleted: true,
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual( contributions: [
expectedResponse, {
) id: 'p139',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
comments: [
{
id: 'c156',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
],
comments: [
{
id: 'c155',
content: 'Comment by user u343',
contentExcerpt: 'Comment by user u343',
deleted: false,
},
],
},
},
}
await expect(
mutate({ mutation: deleteUserMutation, variables }),
).resolves.toMatchObject(expectedResponse)
})
})
})
describe('deletion of all comments requested', () => {
beforeEach(() => {
variables = { ...variables, resource: ['Comment'] }
}) })
it('posts and comments on request', async () => { it('marks comments as deleted', async () => {
deleteUserVariables = { id: 'u343', resource: ['Post', 'Comment'] } const expectedResponse = {
expectedResponse = { data: {
DeleteUser: { DeleteUser: {
id: 'u343', id: 'u343',
contributions: [{ id: 'p139', deleted: true }], name: 'UNAVAILABLE',
comments: [{ id: 'c155', deleted: true }], about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'Post by user u343',
contentExcerpt: 'Post by user u343',
deleted: false,
comments: [
{
id: 'c156',
content: "A comment by someone else on user u343's post",
contentExcerpt: "A comment by someone else on user u343's post",
deleted: false,
},
],
},
],
comments: [
{
id: 'c155',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
}, },
} }
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual( await expect(
expectedResponse, mutate({ mutation: deleteUserMutation, variables }),
) ).resolves.toMatchObject(expectedResponse)
}) })
}) })
describe('deletion of all post and comments requested', () => {
beforeEach(() => {
variables = { ...variables, resource: ['Post', 'Comment'] }
})
it('marks posts and comments as deleted', async () => {
const expectedResponse = {
data: {
DeleteUser: {
id: 'u343',
name: 'UNAVAILABLE',
about: 'UNAVAILABLE',
deleted: true,
contributions: [
{
id: 'p139',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
comments: [
{
id: 'c156',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
],
comments: [
{
id: 'c155',
content: 'UNAVAILABLE',
contentExcerpt: 'UNAVAILABLE',
deleted: true,
},
],
},
},
}
await expect(
mutate({ mutation: deleteUserMutation, variables }),
).resolves.toMatchObject(expectedResponse)
})
})
})
describe('connected `EmailAddress` nodes', () => {
it('will be removed completely', async () => {
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
})
})
describe('connected `SocialMedia` nodes', () => {
beforeEach(async () => {
const socialMedia = await factory.create('SocialMedia')
await socialMedia.relateTo(user, 'ownedBy')
})
it('will be removed completely', async () => {
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
})
}) })
}) })
}) })

View File

@ -0,0 +1,17 @@
type Location {
id: ID!
name: String!
nameEN: String
nameDE: String
nameFR: String
nameNL: String
nameIT: String
nameES: String
namePT: String
namePL: String
type: String!
lat: Float
lng: Float
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
}

View File

@ -51,23 +51,6 @@ type Statistics {
countShouts: Int! countShouts: Int!
} }
type Location {
id: ID!
name: String!
nameEN: String
nameDE: String
nameFR: String
nameNL: String
nameIT: String
nameES: String
namePT: String
namePL: String
type: String!
lat: Float
lng: Float
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
}
type Report { type Report {
id: ID! id: ID!
submitter: User @relation(name: "REPORTED", direction: "IN") submitter: User @relation(name: "REPORTED", direction: "IN")

View File

@ -20,6 +20,7 @@ type Post {
@cypher( @cypher(
statement: """ statement: """
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
WHERE NOT post.deleted AND NOT post.disabled
RETURN DISTINCT post RETURN DISTINCT post
LIMIT 10 LIMIT 10
""" """
@ -29,6 +30,11 @@ type Post {
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN") comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
commentsCount: Int!
@cypher(
statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
)
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! shoutedCount: Int!
@cypher( @cypher(
@ -38,10 +44,7 @@ type Post {
# Has the currently logged in user shouted that post? # Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean! shoutedByCurrentUser: Boolean!
@cypher( @cypher(
statement: """ statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
) )
emotions: [EMOTED] emotions: [EMOTED]
@ -52,26 +55,18 @@ type Post {
type Mutation { type Mutation {
CreatePost( CreatePost(
id: ID id: ID
activityId: String
objectId: String
title: String! title: String!
slug: String slug: String
content: String! content: String!
image: String image: String
imageUpload: Upload imageUpload: Upload
visibility: Visibility visibility: Visibility
deleted: Boolean
disabled: Boolean
createdAt: String
updatedAt: String
language: String language: String
categoryIds: [ID] categoryIds: [ID]
contentExcerpt: String contentExcerpt: String
): Post ): Post
UpdatePost( UpdatePost(
id: ID! id: ID!
activityId: String
objectId: String
title: String! title: String!
slug: String slug: String
content: String! content: String!
@ -79,10 +74,6 @@ type Mutation {
image: String image: String
imageUpload: Upload imageUpload: Upload
visibility: Visibility visibility: Visibility
deleted: Boolean
disabled: Boolean
createdAt: String
updatedAt: String
language: String language: String
categoryIds: [ID] categoryIds: [ID]
): Post ): Post

View File

@ -17,7 +17,7 @@ type User {
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String locationName: String
about: String about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "OUT") socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
#createdAt: DateTime #createdAt: DateTime
#updatedAt: DateTime #updatedAt: DateTime

View File

@ -1,17 +1,18 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
export default function(params) { export default function create() {
const { id = uuid(), name, slug, icon } = params
return { return {
mutation: ` factory: async ({ args, neodeInstance }) => {
mutation($id: ID, $name: String!, $slug: String, $icon: String!) { const defaults = {
CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) { id: uuid(),
id icon: 'img/badges/fundraisingbox_de_airship.svg',
name name: 'Some category name',
} }
} args = {
`, ...defaults,
variables: { id, name, slug, icon }, ...args,
}
return neodeInstance.create('Category', args)
},
} }
} }

View File

@ -1,21 +1,38 @@
import faker from 'faker' import faker from 'faker'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
export default function(params) { export default function create() {
const {
id = uuid(),
postId = 'p6',
content = [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
} = params
return { return {
mutation: ` factory: async ({ args, neodeInstance, factoryInstance }) => {
mutation($id: ID!, $postId: ID!, $content: String!) { const defaults = {
CreateComment(id: $id, postId: $postId, content: $content) { id: uuid(),
id content: [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
}
} }
`, args = {
variables: { id, postId, content }, ...defaults,
...args,
}
args.contentExcerpt = args.contentExcerpt || args.content
let { post, postId } = args
delete args.post
delete args.postId
if (post && postId) throw new Error('You provided both post and postId')
if (postId) post = await neodeInstance.find('Post', postId)
post = post || (await factoryInstance.create('Post'))
let { author, authorId } = args
delete args.author
delete args.authorId
if (author && authorId) throw new Error('You provided both author and authorId')
if (authorId) author = await neodeInstance.find('User', authorId)
author = author || (await factoryInstance.create('User'))
delete args.author
const comment = await neodeInstance.create('Comment', args)
await comment.relateTo(post, 'post')
await comment.relateTo(author, 'author')
return comment
},
} }
} }

View File

@ -2,12 +2,12 @@ import { GraphQLClient, request } from 'graphql-request'
import { getDriver, neode } from '../../bootstrap/neo4j' import { getDriver, neode } from '../../bootstrap/neo4j'
import createBadge from './badges.js' import createBadge from './badges.js'
import createUser from './users.js' import createUser from './users.js'
import createOrganization from './organizations.js'
import createPost from './posts.js' import createPost from './posts.js'
import createComment from './comments.js' import createComment from './comments.js'
import createCategory from './categories.js' import createCategory from './categories.js'
import createTag from './tags.js' import createTag from './tags.js'
import createReport from './reports.js' import createSocialMedia from './socialMedia.js'
import createLocation from './locations.js'
export const seedServerHost = 'http://127.0.0.1:4001' export const seedServerHost = 'http://127.0.0.1:4001'
@ -24,12 +24,12 @@ const authenticatedHeaders = async ({ email, password }, host) => {
const factories = { const factories = {
Badge: createBadge, Badge: createBadge,
User: createUser, User: createUser,
Organization: createOrganization,
Post: createPost, Post: createPost,
Comment: createComment, Comment: createComment,
Category: createCategory, Category: createCategory,
Tag: createTag, Tag: createTag,
Report: createReport, SocialMedia: createSocialMedia,
Location: createLocation,
} }
export const cleanDatabase = async (options = {}) => { export const cleanDatabase = async (options = {}) => {
@ -79,6 +79,7 @@ export default function Factory(options = {}) {
this.lastResponse = await factory({ this.lastResponse = await factory({
args, args,
neodeInstance, neodeInstance,
factoryInstance: this,
}) })
return this.lastResponse return this.lastResponse
} else { } else {

View File

@ -0,0 +1,24 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
name: 'Germany',
namePT: 'Alemanha',
nameDE: 'Deutschland',
nameES: 'Alemania',
nameNL: 'Duitsland',
namePL: 'Niemcy',
nameFR: 'Allemagne',
nameIT: 'Germania',
nameEN: 'Germany',
id: 'country.10743216036480410',
type: 'country',
}
args = {
...defaults,
...args,
}
return neodeInstance.create('Location', args)
},
}
}

View File

@ -1,21 +0,0 @@
import faker from 'faker'
import uuid from 'uuid/v4'
export default function create(params) {
const {
id = uuid(),
name = faker.company.companyName(),
description = faker.company.catchPhrase(),
} = params
return {
mutation: `
mutation($id: ID!, $name: String!, $description: String!) {
CreateOrganization(id: $id, name: $name, description: $description) {
name
}
}
`,
variables: { id, name, description },
}
}

View File

@ -1,51 +1,60 @@
import faker from 'faker' import faker from 'faker'
import slugify from 'slug'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
export default function(params) { export default function create() {
const {
id = uuid(),
slug = '',
title = faker.lorem.sentence(),
content = [
faker.lorem.sentence(),
faker.lorem.sentence(),
faker.lorem.sentence(),
faker.lorem.sentence(),
faker.lorem.sentence(),
].join('. '),
image = faker.image.unsplash.imageUrl(),
visibility = 'public',
deleted = false,
categoryIds,
} = params
return { return {
mutation: ` factory: async ({ args, neodeInstance, factoryInstance }) => {
mutation( const defaults = {
$id: ID! id: uuid(),
$slug: String title: faker.lorem.sentence(),
$title: String! content: [
$content: String! faker.lorem.sentence(),
$image: String faker.lorem.sentence(),
$visibility: Visibility faker.lorem.sentence(),
$deleted: Boolean faker.lorem.sentence(),
$categoryIds: [ID] faker.lorem.sentence(),
) { ].join('. '),
CreatePost( image: faker.image.unsplash.imageUrl(),
id: $id visibility: 'public',
slug: $slug deleted: false,
title: $title categoryIds: [],
content: $content
image: $image
visibility: $visibility
deleted: $deleted
categoryIds: $categoryIds
) {
title
content
}
} }
`, args = {
variables: { id, slug, title, content, image, visibility, deleted, categoryIds }, ...defaults,
...args,
}
args.slug = args.slug || slugify(args.title, { lower: true })
args.contentExcerpt = args.contentExcerpt || args.content
let { categories, categoryIds } = args
delete args.categories
delete args.categoryIds
if (categories && categoryIds) throw new Error('You provided both category and categoryIds')
if (categoryIds)
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
categories = categories || (await Promise.all([factoryInstance.create('Category')]))
const { tagIds = [] } = args
delete args.tags
const tags = await Promise.all(
tagIds.map(t => {
return neodeInstance.find('Tag', t)
}),
)
let { author, authorId } = args
delete args.author
delete args.authorId
if (author && authorId) throw new Error('You provided both author and authorId')
if (authorId) author = await neodeInstance.find('User', authorId)
author = author || (await factoryInstance.create('User'))
const post = await neodeInstance.create('Post', args)
await post.relateTo(author, 'author')
await Promise.all(categories.map(c => c.relateTo(post, 'post')))
await Promise.all(tags.map(t => t.relateTo(post, 'post')))
return post
},
} }
} }

View File

@ -1,19 +0,0 @@
import faker from 'faker'
export default function create(params) {
const { description = faker.lorem.sentence(), id } = params
return {
mutation: `
mutation($id: ID!, $description: String!) {
report(description: $description, id: $id) {
id
}
}
`,
variables: {
id,
description,
},
}
}

View File

@ -0,0 +1,14 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
url: 'https://mastodon.social/@Gargron',
}
args = {
...defaults,
...args,
}
return neodeInstance.create('SocialMedia', args)
},
}
}

View File

@ -1,16 +1,12 @@
import uuid from 'uuid/v4' export default function create() {
export default function(params) {
const { id = uuid(), name = '#human-connection' } = params
return { return {
mutation: ` factory: async ({ args, neodeInstance }) => {
mutation($id: ID!) { const defaults = { name: '#human-connection' }
CreateTag(id: $id) { args = {
id ...defaults,
} ...args,
} }
`, return neodeInstance.create('Tag', args)
variables: { id, name }, },
} }
} }

View File

@ -1,10 +1,110 @@
import faker from 'faker' import faker from 'faker'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import Factory from './factories' import Factory from './factories'
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
import { gql } from '../jest/helpers'
/* eslint-disable no-multi-spaces */ /* eslint-disable no-multi-spaces */
;(async function() { ;(async function() {
let authenticatedUser = null
const driver = getDriver()
const factory = Factory()
const neode = getNeode()
try { try {
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
const { mutate } = createTestClient(server)
const f = Factory() const f = Factory()
const [Hamburg, Berlin, Germany, Paris, France] = await Promise.all([
f.create('Location', {
id: 'region.5127278006398860',
name: 'Hamburg',
type: 'region',
lat: 10.0,
lng: 53.55,
nameES: 'Hamburgo',
nameFR: 'Hambourg',
nameIT: 'Amburgo',
nameEN: 'Hamburg',
namePT: 'Hamburgo',
nameDE: 'Hamburg',
nameNL: 'Hamburg',
namePL: 'Hamburg',
}),
f.create('Location', {
id: 'region.14880313158564380',
type: 'region',
name: 'Berlin',
lat: 13.38333,
lng: 52.51667,
nameES: 'Berlín',
nameFR: 'Berlin',
nameIT: 'Berlino',
nameEN: 'Berlin',
namePT: 'Berlim',
nameDE: 'Berlin',
nameNL: 'Berlijn',
namePL: 'Berlin',
}),
f.create('Location', {
id: 'country.10743216036480410',
name: 'Germany',
type: 'country',
namePT: 'Alemanha',
nameDE: 'Deutschland',
nameES: 'Alemania',
nameNL: 'Duitsland',
namePL: 'Niemcy',
nameFR: 'Allemagne',
nameIT: 'Germania',
nameEN: 'Germany',
}),
f.create('Location', {
id: 'region.9397217726497330',
name: 'Paris',
type: 'region',
lat: 2.35183,
lng: 48.85658,
nameES: 'París',
nameFR: 'Paris',
nameIT: 'Parigi',
nameEN: 'Paris',
namePT: 'Paris',
nameDE: 'Paris',
nameNL: 'Parijs',
namePL: 'Paryż',
}),
f.create('Location', {
id: 'country.9759535382641660',
name: 'France',
type: 'country',
namePT: 'França',
nameDE: 'Frankreich',
nameES: 'Francia',
nameNL: 'Frankrijk',
namePL: 'Francja',
nameFR: 'France',
nameIT: 'Francia',
nameEN: 'France',
}),
])
await Promise.all([
Berlin.relateTo(Germany, 'isIn'),
Hamburg.relateTo(Germany, 'isIn'),
Paris.relateTo(France, 'isIn'),
])
const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([ const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
f.create('Badge', { f.create('Badge', {
id: 'indiegogo_en_racoon', id: 'indiegogo_en_racoon',
@ -36,9 +136,9 @@ import Factory from './factories'
peterLustig, peterLustig,
bobDerBaumeister, bobDerBaumeister,
jennyRostock, jennyRostock,
tick, // eslint-disable-line no-unused-vars huey,
trick, // eslint-disable-line no-unused-vars dewey,
track, // eslint-disable-line no-unused-vars louie,
dagobert, dagobert,
] = await Promise.all([ ] = await Promise.all([
f.create('User', { f.create('User', {
@ -64,22 +164,22 @@ import Factory from './factories'
}), }),
f.create('User', { f.create('User', {
id: 'u4', id: 'u4',
name: 'Huey (Tick)', name: 'Huey',
slug: 'huey-tick', slug: 'huey',
role: 'user', role: 'user',
email: 'huey@example.org', email: 'huey@example.org',
}), }),
f.create('User', { f.create('User', {
id: 'u5', id: 'u5',
name: 'Dewey (Trick)', name: 'Dewey',
slug: 'dewey-trick', slug: 'dewey',
role: 'user', role: 'user',
email: 'dewey@example.org', email: 'dewey@example.org',
}), }),
f.create('User', { f.create('User', {
id: 'u6', id: 'u6',
name: 'Louie (Track)', name: 'Louie',
slug: 'louie-track', slug: 'louie',
role: 'user', role: 'user',
email: 'louie@example.org', email: 'louie@example.org',
}), }),
@ -92,31 +192,11 @@ import Factory from './factories'
}), }),
]) ])
const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([ await Promise.all([
Factory().authenticateAs({ peterLustig.relateTo(Berlin, 'isIn'),
email: 'admin@example.org', bobDerBaumeister.relateTo(Hamburg, 'isIn'),
password: '1234', jennyRostock.relateTo(Paris, 'isIn'),
}), huey.relateTo(Paris, 'isIn'),
Factory().authenticateAs({
email: 'moderator@example.org',
password: '1234',
}),
Factory().authenticateAs({
email: 'user@example.org',
password: '1234',
}),
Factory().authenticateAs({
email: 'huey@example.org',
password: '1234',
}),
Factory().authenticateAs({
email: 'dewey@example.org',
password: '1234',
}),
Factory().authenticateAs({
email: 'louie@example.org',
password: '1234',
}),
]) ])
await Promise.all([ await Promise.all([
@ -133,16 +213,16 @@ import Factory from './factories'
bobDerBaumeister.relateTo(jennyRostock, 'friends'), bobDerBaumeister.relateTo(jennyRostock, 'friends'),
peterLustig.relateTo(jennyRostock, 'following'), peterLustig.relateTo(jennyRostock, 'following'),
peterLustig.relateTo(tick, 'following'), peterLustig.relateTo(huey, 'following'),
bobDerBaumeister.relateTo(tick, 'following'), bobDerBaumeister.relateTo(huey, 'following'),
jennyRostock.relateTo(tick, 'following'), jennyRostock.relateTo(huey, 'following'),
tick.relateTo(track, 'following'), huey.relateTo(dewey, 'following'),
trick.relateTo(tick, 'following'), dewey.relateTo(huey, 'following'),
track.relateTo(jennyRostock, 'following'), louie.relateTo(jennyRostock, 'following'),
dagobert.relateTo(tick, 'blocked'), dagobert.relateTo(huey, 'blocked'),
dagobert.relateTo(trick, 'blocked'), dagobert.relateTo(dewey, 'blocked'),
dagobert.relateTo(track, 'blocked'), dagobert.relateTo(louie, 'blocked'),
]) ])
await Promise.all([ await Promise.all([
@ -244,25 +324,90 @@ import Factory from './factories'
}), }),
]) ])
await Promise.all([ const [environment, nature, democracy, freedom] = await Promise.all([
f.create('Tag', { f.create('Tag', {
id: 'Umwelt', id: 'Environment',
name: 'Umwelt',
}), }),
f.create('Tag', { f.create('Tag', {
id: 'Naturschutz', id: 'Nature',
name: 'Naturschutz',
}), }),
f.create('Tag', { f.create('Tag', {
id: 'Demokratie', id: 'Democracy',
name: 'Demokratie',
}), }),
f.create('Tag', { f.create('Tag', {
id: 'Freiheit', id: 'Freedom',
name: 'Freiheit',
}), }),
]) ])
const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([
factory.create('Post', {
author: peterLustig,
id: 'p0',
image: faker.image.unsplash.food(),
categoryIds: ['cat16'],
}),
factory.create('Post', {
author: bobDerBaumeister,
id: 'p1',
image: faker.image.unsplash.technology(),
categoryIds: ['cat1'],
}),
factory.create('Post', {
author: huey,
id: 'p3',
categoryIds: ['cat3'],
}),
factory.create('Post', {
author: dewey,
id: 'p4',
categoryIds: ['cat4'],
}),
factory.create('Post', {
author: louie,
id: 'p5',
categoryIds: ['cat5'],
}),
factory.create('Post', {
authorId: 'u1',
id: 'p6',
image: faker.image.unsplash.buildings(),
categoryIds: ['cat6'],
}),
factory.create('Post', {
author: huey,
id: 'p9',
categoryIds: ['cat9'],
}),
factory.create('Post', {
author: dewey,
id: 'p10',
categoryIds: ['cat10'],
}),
factory.create('Post', {
author: louie,
id: 'p11',
image: faker.image.unsplash.people(),
categoryIds: ['cat11'],
}),
factory.create('Post', {
author: bobDerBaumeister,
id: 'p13',
categoryIds: ['cat13'],
}),
factory.create('Post', {
author: jennyRostock,
id: 'p14',
image: faker.image.unsplash.objects(),
categoryIds: ['cat14'],
}),
factory.create('Post', {
author: huey,
id: 'p15',
categoryIds: ['cat15'],
}),
])
authenticatedUser = await louie.toJson()
const mention1 = const mention1 =
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?' 'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
const mention2 = const mention2 =
@ -271,468 +416,271 @@ import Factory from './factories'
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!' 'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
const hashtagAndMention1 = const hashtagAndMention1 =
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)' 'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
const createPostMutation = gql`
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
}
}
`
await Promise.all([ await Promise.all([
asAdmin.create('Post', { mutate({
id: 'p0', mutation: createPostMutation,
image: faker.image.unsplash.food(), variables: {
categoryIds: ['cat16'], id: 'p2',
title: `Nature Philosophy Yoga`,
content: hashtag1,
categoryIds: ['cat2'],
},
}), }),
asModerator.create('Post', { mutate({
id: 'p1', mutation: createPostMutation,
image: faker.image.unsplash.technology(), variables: {
categoryIds: ['cat1'], id: 'p7',
title: 'This is post #7',
content: `${mention1} ${faker.lorem.paragraph()}`,
categoryIds: ['cat7'],
},
}), }),
asUser.create('Post', { mutate({
id: 'p2', mutation: createPostMutation,
title: `Nature Philosophy Yoga`, variables: {
content: `${hashtag1}`, id: 'p8',
categoryIds: ['cat2'], image: faker.image.unsplash.nature(),
title: `Quantum Flow Theory explains Quantum Gravity`,
content: hashtagAndMention1,
categoryIds: ['cat8'],
},
}), }),
asTick.create('Post', { mutate({
id: 'p3', mutation: createPostMutation,
categoryIds: ['cat3'], variables: {
}), id: 'p12',
asTrick.create('Post', { title: 'This is post #12',
id: 'p4', content: `${mention2} ${faker.lorem.paragraph()}`,
categoryIds: ['cat4'], categoryIds: ['cat12'],
}), },
asTrack.create('Post', {
id: 'p5',
categoryIds: ['cat5'],
}),
asAdmin.create('Post', {
id: 'p6',
image: faker.image.unsplash.buildings(),
categoryIds: ['cat6'],
}),
asModerator.create('Post', {
id: 'p7',
content: `${mention1} ${faker.lorem.paragraph()}`,
categoryIds: ['cat7'],
}),
asUser.create('Post', {
id: 'p8',
image: faker.image.unsplash.nature(),
title: `Quantum Flow Theory explains Quantum Gravity`,
content: `${hashtagAndMention1}`,
categoryIds: ['cat8'],
}),
asTick.create('Post', {
id: 'p9',
categoryIds: ['cat9'],
}),
asTrick.create('Post', {
id: 'p10',
categoryIds: ['cat10'],
}),
asTrack.create('Post', {
id: 'p11',
image: faker.image.unsplash.people(),
categoryIds: ['cat11'],
}),
asAdmin.create('Post', {
id: 'p12',
content: `${mention2} ${faker.lorem.paragraph()}`,
categoryIds: ['cat12'],
}),
asModerator.create('Post', {
id: 'p13',
categoryIds: ['cat13'],
}),
asUser.create('Post', {
id: 'p14',
image: faker.image.unsplash.objects(),
categoryIds: ['cat14'],
}),
asTick.create('Post', {
id: 'p15',
categoryIds: ['cat15'],
}),
])
await Promise.all([
f.relate('Post', 'Tags', {
from: 'p0',
to: 'Freiheit',
}),
f.relate('Post', 'Tags', {
from: 'p1',
to: 'Umwelt',
}),
f.relate('Post', 'Tags', {
from: 'p2',
to: 'Naturschutz',
}),
f.relate('Post', 'Tags', {
from: 'p3',
to: 'Demokratie',
}),
f.relate('Post', 'Tags', {
from: 'p4',
to: 'Freiheit',
}),
f.relate('Post', 'Tags', {
from: 'p5',
to: 'Umwelt',
}),
f.relate('Post', 'Tags', {
from: 'p6',
to: 'Naturschutz',
}),
f.relate('Post', 'Tags', {
from: 'p7',
to: 'Demokratie',
}),
f.relate('Post', 'Tags', {
from: 'p8',
to: 'Freiheit',
}),
f.relate('Post', 'Tags', {
from: 'p9',
to: 'Umwelt',
}),
f.relate('Post', 'Tags', {
from: 'p10',
to: 'Naturschutz',
}),
f.relate('Post', 'Tags', {
from: 'p11',
to: 'Demokratie',
}),
f.relate('Post', 'Tags', {
from: 'p12',
to: 'Freiheit',
}),
f.relate('Post', 'Tags', {
from: 'p13',
to: 'Umwelt',
}),
f.relate('Post', 'Tags', {
from: 'p14',
to: 'Naturschutz',
}),
f.relate('Post', 'Tags', {
from: 'p15',
to: 'Demokratie',
}),
f.emote({
from: 'u1',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u2',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u3',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u4',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u5',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u6',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u7',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u2',
to: 'p14',
data: 'cry',
}),
f.emote({
from: 'u3',
to: 'p13',
data: 'angry',
}),
f.emote({
from: 'u4',
to: 'p12',
data: 'funny',
}),
f.emote({
from: 'u5',
to: 'p11',
data: 'surprised',
}),
f.emote({
from: 'u6',
to: 'p10',
data: 'cry',
}),
f.emote({
from: 'u5',
to: 'p9',
data: 'happy',
}),
f.emote({
from: 'u4',
to: 'p8',
data: 'angry',
}),
f.emote({
from: 'u3',
to: 'p7',
data: 'funny',
}),
f.emote({
from: 'u2',
to: 'p6',
data: 'surprised',
}),
f.emote({
from: 'u1',
to: 'p5',
data: 'cry',
}),
f.emote({
from: 'u2',
to: 'p4',
data: 'happy',
}),
f.emote({
from: 'u3',
to: 'p3',
data: 'angry',
}),
f.emote({
from: 'u4',
to: 'p2',
data: 'funny',
}),
f.emote({
from: 'u5',
to: 'p1',
data: 'surprised',
}),
f.emote({
from: 'u6',
to: 'p0',
data: 'cry',
}),
])
await Promise.all([
asAdmin.shout({
id: 'p2',
type: 'Post',
}),
asAdmin.shout({
id: 'p6',
type: 'Post',
}),
asModerator.shout({
id: 'p0',
type: 'Post',
}),
asModerator.shout({
id: 'p6',
type: 'Post',
}),
asUser.shout({
id: 'p6',
type: 'Post',
}),
asUser.shout({
id: 'p7',
type: 'Post',
}),
asTick.shout({
id: 'p8',
type: 'Post',
}),
asTick.shout({
id: 'p9',
type: 'Post',
}),
asTrack.shout({
id: 'p10',
type: 'Post',
}),
])
await Promise.all([
asAdmin.shout({
id: 'p2',
type: 'Post',
}),
asAdmin.shout({
id: 'p6',
type: 'Post',
}),
asModerator.shout({
id: 'p0',
type: 'Post',
}),
asModerator.shout({
id: 'p6',
type: 'Post',
}),
asUser.shout({
id: 'p6',
type: 'Post',
}),
asUser.shout({
id: 'p7',
type: 'Post',
}),
asTick.shout({
id: 'p8',
type: 'Post',
}),
asTick.shout({
id: 'p9',
type: 'Post',
}),
asTrack.shout({
id: 'p10',
type: 'Post',
}), }),
]) ])
const [p2, p7, p8, p12] = await Promise.all(
['p2', 'p7', 'p8', 'p12'].map(id => neode.find('Post', id)),
)
authenticatedUser = null
authenticatedUser = await dewey.toJson()
const mentionInComment1 = const mentionInComment1 =
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.' 'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.'
const mentionInComment2 = const mentionInComment2 =
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> told you?' 'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> told you?'
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`
await Promise.all([
mutate({
mutation: createCommentMutation,
variables: {
id: 'c4',
postId: 'p2',
content: mentionInComment1,
},
}),
mutate({
mutation: createCommentMutation,
variables: {
id: 'c4-1',
postId: 'p2',
content: mentionInComment2,
},
}),
mutate({
mutation: createCommentMutation,
variables: {
postId: 'p14',
content: faker.lorem.paragraph(),
},
}), // should send a notification
])
authenticatedUser = null
await Promise.all([ await Promise.all([
asUser.create('Comment', { factory.create('Comment', {
author: jennyRostock,
id: 'c1', id: 'c1',
postId: 'p1', postId: 'p1',
}), }),
asTick.create('Comment', { factory.create('Comment', {
author: huey,
id: 'c2', id: 'c2',
postId: 'p1', postId: 'p1',
}), }),
asTrack.create('Comment', { factory.create('Comment', {
author: louie,
id: 'c3', id: 'c3',
postId: 'p3', postId: 'p3',
}), }),
asTrick.create('Comment', { factory.create('Comment', {
id: 'c4', author: bobDerBaumeister,
postId: 'p2',
content: `${mentionInComment1}`,
}),
asUser.create('Comment', {
id: 'c4-1',
postId: 'p2',
content: `${mentionInComment2}`,
}),
asModerator.create('Comment', {
id: 'c5', id: 'c5',
postId: 'p3', postId: 'p3',
}), }),
asAdmin.create('Comment', { factory.create('Comment', {
author: peterLustig,
id: 'c6', id: 'c6',
postId: 'p4', postId: 'p4',
}), }),
asUser.create('Comment', { factory.create('Comment', {
author: jennyRostock,
id: 'c7', id: 'c7',
postId: 'p2', postId: 'p2',
}), }),
asTick.create('Comment', { factory.create('Comment', {
author: huey,
id: 'c8', id: 'c8',
postId: 'p15', postId: 'p15',
}), }),
asTrick.create('Comment', { factory.create('Comment', {
author: dewey,
id: 'c9', id: 'c9',
postId: 'p15', postId: 'p15',
}), }),
asTrack.create('Comment', { factory.create('Comment', {
author: louie,
id: 'c10', id: 'c10',
postId: 'p15', postId: 'p15',
}), }),
asUser.create('Comment', { factory.create('Comment', {
author: jennyRostock,
id: 'c11', id: 'c11',
postId: 'p15', postId: 'p15',
}), }),
asUser.create('Comment', { factory.create('Comment', {
author: jennyRostock,
id: 'c12', id: 'c12',
postId: 'p15', postId: 'p15',
}), }),
]) ])
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
await Promise.all([ await Promise.all([
asModerator.mutate(disableMutation, { democracy.relateTo(p3, 'post'),
id: 'p11', democracy.relateTo(p11, 'post'),
}), democracy.relateTo(p15, 'post'),
asModerator.mutate(disableMutation, { democracy.relateTo(p7, 'post'),
id: 'c5', environment.relateTo(p1, 'post'),
}), environment.relateTo(p5, 'post'),
environment.relateTo(p9, 'post'),
environment.relateTo(p13, 'post'),
freedom.relateTo(p0, 'post'),
freedom.relateTo(p4, 'post'),
freedom.relateTo(p8, 'post'),
freedom.relateTo(p12, 'post'),
nature.relateTo(p2, 'post'),
nature.relateTo(p6, 'post'),
nature.relateTo(p10, 'post'),
nature.relateTo(p14, 'post'),
peterLustig.relateTo(p15, 'emoted', { emotion: 'surprised' }),
bobDerBaumeister.relateTo(p15, 'emoted', { emotion: 'surprised' }),
jennyRostock.relateTo(p15, 'emoted', { emotion: 'surprised' }),
huey.relateTo(p15, 'emoted', { emotion: 'surprised' }),
dewey.relateTo(p15, 'emoted', { emotion: 'surprised' }),
louie.relateTo(p15, 'emoted', { emotion: 'surprised' }),
dagobert.relateTo(p15, 'emoted', { emotion: 'surprised' }),
bobDerBaumeister.relateTo(p14, 'emoted', { emotion: 'cry' }),
jennyRostock.relateTo(p13, 'emoted', { emotion: 'angry' }),
huey.relateTo(p12, 'emoted', { emotion: 'funny' }),
dewey.relateTo(p11, 'emoted', { emotion: 'surprised' }),
louie.relateTo(p10, 'emoted', { emotion: 'cry' }),
dewey.relateTo(p9, 'emoted', { emotion: 'happy' }),
huey.relateTo(p8, 'emoted', { emotion: 'angry' }),
jennyRostock.relateTo(p7, 'emoted', { emotion: 'funny' }),
bobDerBaumeister.relateTo(p6, 'emoted', { emotion: 'surprised' }),
peterLustig.relateTo(p5, 'emoted', { emotion: 'cry' }),
bobDerBaumeister.relateTo(p4, 'emoted', { emotion: 'happy' }),
jennyRostock.relateTo(p3, 'emoted', { emotion: 'angry' }),
huey.relateTo(p2, 'emoted', { emotion: 'funny' }),
dewey.relateTo(p1, 'emoted', { emotion: 'surprised' }),
louie.relateTo(p0, 'emoted', { emotion: 'cry' }),
]) ])
await Promise.all([ await Promise.all([
asTick.create('Report', { peterLustig.relateTo(p1, 'shouted'),
description: "I don't like this comment", peterLustig.relateTo(p6, 'shouted'),
id: 'c1', bobDerBaumeister.relateTo(p0, 'shouted'),
}), bobDerBaumeister.relateTo(p6, 'shouted'),
asTrick.create('Report', { jennyRostock.relateTo(p6, 'shouted'),
description: "I don't like this post", jennyRostock.relateTo(p7, 'shouted'),
id: 'p1', huey.relateTo(p8, 'shouted'),
}), huey.relateTo(p9, 'shouted'),
asTrack.create('Report', { dewey.relateTo(p10, 'shouted'),
description: "I don't like this user", peterLustig.relateTo(p2, 'shouted'),
id: 'u1', peterLustig.relateTo(p6, 'shouted'),
}), bobDerBaumeister.relateTo(p0, 'shouted'),
bobDerBaumeister.relateTo(p6, 'shouted'),
jennyRostock.relateTo(p6, 'shouted'),
jennyRostock.relateTo(p7, 'shouted'),
huey.relateTo(p8, 'shouted'),
huey.relateTo(p9, 'shouted'),
louie.relateTo(p10, 'shouted'),
]) ])
const disableMutation = gql`
mutation($id: ID!) {
disable(id: $id)
}
`
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([ await Promise.all([
f.create('Organization', { mutate({
id: 'o1', mutation: disableMutation,
name: 'Democracy Deutschland', variables: {
description: 'Description for democracy-deutschland.', id: 'p11',
},
}), }),
f.create('Organization', { mutate({
id: 'o2', mutation: disableMutation,
name: 'Human-Connection', variables: {
description: 'Description for human-connection.', id: 'c5',
}), },
f.create('Organization', {
id: 'o3',
name: 'Pro Veg',
description: 'Description for pro-veg.',
}),
f.create('Organization', {
id: 'o4',
name: 'Greenpeace',
description: 'Description for greenpeace.',
}), }),
]) ])
authenticatedUser = null
const reportMutation = gql`
mutation($id: ID!, $description: String!) {
report(description: $description, id: $id) {
id
}
}
`
authenticatedUser = await huey.toJson()
await Promise.all([ await Promise.all([
f.relate('Organization', 'CreatedBy', { mutate({
from: 'u1', mutation: reportMutation,
to: 'o1', variables: {
description: "I don't like this comment",
id: 'c1',
},
}), }),
f.relate('Organization', 'CreatedBy', { mutate({
from: 'u1', mutation: reportMutation,
to: 'o2', variables: {
description: "I don't like this post",
id: 'p1',
},
}), }),
f.relate('Organization', 'OwnedBy', { mutate({
from: 'u2', mutation: reportMutation,
to: 'o2', variables: {
}), description: "I don't like this user",
f.relate('Organization', 'OwnedBy', { id: 'u1',
from: 'u2', },
to: 'o3',
}), }),
]) ])
authenticatedUser = null
await Promise.all( await Promise.all(
[...Array(30).keys()].map(i => { [...Array(30).keys()].map(i => {

View File

@ -18,20 +18,22 @@ Object.entries(requiredConfigs).map(entry => {
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
export const context = async ({ req }) => {
const user = await decode(driver, req.headers.authorization)
return {
driver,
neode,
user,
req,
cypherParams: {
currentUserId: user ? user.id : null,
},
}
}
const createServer = options => { const createServer = options => {
const defaults = { const defaults = {
context: async ({ req }) => { context,
const user = await decode(driver, req.headers.authorization)
return {
driver,
neode,
user,
req,
cypherParams: {
currentUserId: user ? user.id : null,
},
}
},
schema: middleware(schema), schema: middleware(schema),
debug: !!CONFIG.DEBUG, debug: !!CONFIG.DEBUG,
tracing: !!CONFIG.DEBUG, tracing: !!CONFIG.DEBUG,

View File

@ -1538,14 +1538,6 @@ anymatch@^2.0.0:
micromatch "^3.1.4" micromatch "^3.1.4"
normalize-path "^2.1.1" normalize-path "^2.1.1"
apollo-cache-control@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.2.tgz#0687e323053f907fd9bb601c1921de10799e24a0"
integrity sha512-rvx4DdoAAbWhm3L0IoWrxN+Zq2Xk4uAYbaiZk0Nhuc/y4AQUww3JV/z4EfCp3O5cy5/lNMW/tPOozcqi941awA==
dependencies:
apollo-server-env "2.4.2"
graphql-extensions "0.10.1"
apollo-cache-control@^0.8.4: apollo-cache-control@^0.8.4:
version "0.8.4" version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7"
@ -1587,14 +1579,6 @@ apollo-client@~2.6.4:
tslib "^1.9.3" tslib "^1.9.3"
zen-observable "^0.8.0" zen-observable "^0.8.0"
apollo-datasource@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.2.tgz#3eeb8f9660304a223c3f7aecfe0274376c876307"
integrity sha512-GlqTfLjKFxNYxGGACDjDXUpm/vPfvXhUI/Qc/YdkY4ess/wn7EFdrmbZGIY56RJtXD5M7qjsQIH15t132KoPmQ==
dependencies:
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-datasource@^0.6.3: apollo-datasource@^0.6.3:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13" resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13"
@ -1603,26 +1587,13 @@ apollo-datasource@^0.6.3:
apollo-server-caching "^0.5.0" apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3" apollo-server-env "^2.4.3"
apollo-engine-reporting-protobuf@0.4.0, apollo-engine-reporting-protobuf@^0.4.0: apollo-engine-reporting-protobuf@^0.4.0:
version "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" resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec"
integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA== integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==
dependencies: dependencies:
protobufjs "^6.8.6" protobufjs "^6.8.6"
apollo-engine-reporting@1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.4.tgz#ab232dcaa81fe9718fb23e9782457c66dc86e817"
integrity sha512-FOk/HooLMesoKHo/TGOPYZuc2t4q9YwoeM+z0AGRUY70hL2o5Ie3x0XiMb+I5IVibR+jBIRRKP2ngmSFJ+LqSg==
dependencies:
apollo-engine-reporting-protobuf "0.4.0"
apollo-graphql "^0.3.3"
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-server-types "0.2.2"
async-retry "^1.2.1"
graphql-extensions "0.10.1"
apollo-engine-reporting@^1.4.6: apollo-engine-reporting@^1.4.6:
version "1.4.6" version "1.4.6"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f" resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f"
@ -1697,40 +1668,13 @@ apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
tslib "^1.9.3" tslib "^1.9.3"
zen-observable-ts "^0.8.19" 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" version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46"
integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw== integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==
dependencies: dependencies:
lru-cache "^5.0.0" lru-cache "^5.0.0"
apollo-server-core@2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.1.tgz#ed876cd2f954dc3f4f1e735b997d4dbf29a629a5"
integrity sha512-ZWPGNdZv/SiPjfEU7Wwut9N9oAucGlbVT+XCnpUl93agvkg3fbeTCLYBbjAdSA0Q6opq0tvWVGzwXPLpx6jZcQ==
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.2"
apollo-datasource "0.6.2"
apollo-engine-reporting "1.4.4"
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-server-errors "2.3.2"
apollo-server-plugin-base "0.6.2"
apollo-server-types "0.2.2"
apollo-tracing "0.8.2"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.10.1"
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-core@^2.9.3: apollo-server-core@^2.9.3:
version "2.9.3" version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f"
@ -1758,14 +1702,6 @@ apollo-server-core@^2.9.3:
subscriptions-transport-ws "^0.9.11" subscriptions-transport-ws "^0.9.11"
ws "^6.0.0" 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"
integrity sha512-Qyi8fP8CWsBRAKs0fawMFauJj03I6N3ncWcGaVTuDppYluo4zjV6LqHfZ+YPWOx6apBihFNZap19RAhSnSwJLg==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-env@^2.4.3: apollo-server-env@^2.4.3:
version "2.4.3" version "2.4.3"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
@ -1774,11 +1710,6 @@ apollo-server-env@^2.4.3:
node-fetch "^2.1.2" node-fetch "^2.1.2"
util.promisify "^1.0.0" 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-errors@^2.3.3: apollo-server-errors@^2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff"
@ -1806,13 +1737,6 @@ apollo-server-express@^2.9.0, apollo-server-express@^2.9.3:
subscriptions-transport-ws "^0.9.16" subscriptions-transport-ws "^0.9.16"
type-is "^1.6.16" type-is "^1.6.16"
apollo-server-plugin-base@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.2.tgz#807e734e130c6750db680a58cd0e572cc0794184"
integrity sha512-f7grbfoI5fPxGJDmrvG0ulWq8vFHwvJSUrcEChhiUCSMFZlpBil/1TSaxJRESiQqnoZ9s5WrVhzuwejxODGMYw==
dependencies:
apollo-server-types "0.2.2"
apollo-server-plugin-base@^0.6.4: apollo-server-plugin-base@^0.6.4:
version "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" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.4.tgz#63ea4fd0bbb6c4510bc8d0d2ad0a0684c8d0da8c"
@ -1820,21 +1744,12 @@ apollo-server-plugin-base@^0.6.4:
dependencies: dependencies:
apollo-server-types "^0.2.4" apollo-server-types "^0.2.4"
apollo-server-testing@~2.9.1: apollo-server-testing@~2.9.3:
version "2.9.1" version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49" resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.3.tgz#38a86b5fa0bce57f8ec4fb581e5419437178b3e2"
integrity sha512-TzlHIYNZgF1OkGji/ew3zPxboifvA9aGXDwWJFu54o1400svH0Uh5L7TMhsTZ8F992syQUsUuI+KKMOFNg73+w== integrity sha512-n2bIcVXQNFzr84FZK1S0o4PFqwb1pPuIg/fymjPYjtFP2OHmLLvGRm+KaXhUjxEAUh+/9zAQLhmgx+p6GMUAhA==
dependencies: dependencies:
apollo-server-core "2.9.1" apollo-server-core "^2.9.3"
apollo-server-types@0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.2.tgz#c26ff57ca0b45d67dfd72312094097e2b1c28980"
integrity sha512-/G4yXUF4Kc6PVCIF12r+oB8AXkE4UVnJoyZHeHiPeDpXklrjwIAtov2WM2mTcSZuZe1EuEkeDci4+tj5zFD39Q==
dependencies:
apollo-engine-reporting-protobuf "0.4.0"
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-server-types@^0.2.4: apollo-server-types@^0.2.4:
version "0.2.4" version "0.2.4"
@ -1856,14 +1771,6 @@ apollo-server@~2.9.3:
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0" graphql-tools "^4.0.0"
apollo-tracing@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.2.tgz#2d1ebef434c4e2803f9a3adfc7d2409690b3c378"
integrity sha512-4SVxHZkKZX/7E6/4hAvEJXdHm+1BjQqtgEkv3ywyiVXoaKn0YNJL8BVIOI4GAt0qoc3KzT9MDJ1nf+SurUFjLQ==
dependencies:
apollo-server-env "2.4.2"
graphql-extensions "0.10.1"
apollo-tracing@^0.8.4: apollo-tracing@^0.8.4:
version "0.8.4" version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de" resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de"
@ -2781,13 +2688,12 @@ create-error-class@^3.0.0:
dependencies: dependencies:
capture-stack-trace "^1.0.0" capture-stack-trace "^1.0.0"
cross-env@~5.2.0: cross-env@~5.2.1:
version "5.2.0" version "5.2.1"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d"
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg== integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==
dependencies: dependencies:
cross-spawn "^6.0.5" cross-spawn "^6.0.5"
is-windows "^1.0.0"
cross-fetch@2.2.2: cross-fetch@2.2.2:
version "2.2.2" version "2.2.2"
@ -4171,15 +4077,6 @@ graphql-custom-directives@~0.2.14:
moment "^2.22.2" moment "^2.22.2"
numeral "^2.0.6" numeral "^2.0.6"
graphql-extensions@0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.1.tgz#9e1abd502f3f802a7ab60c3a28d2fe705e53d4cb"
integrity sha512-RIlC/jgBKZ/qyrb+cAu7oJVYLC0dJh6al35tNy8dnqE9JImNucy/gFWVOPW7q3fAaXqCHzbBEtdb+ws1L43LgQ==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
apollo-server-env "2.4.2"
apollo-server-types "0.2.2"
graphql-extensions@^0.10.3: graphql-extensions@^0.10.3:
version "0.10.3" version "0.10.3"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea"
@ -4978,7 +4875,7 @@ is-valid-path@0.1.1:
dependencies: dependencies:
is-invalid-path "^0.1.0" is-invalid-path "^0.1.0"
is-windows@^1.0.0, is-windows@^1.0.2: is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==

View File

@ -122,7 +122,11 @@ Given('somebody reported the following posts:', table => {
cy.factory() cy.factory()
.create('User', submitter) .create('User', submitter)
.authenticateAs(submitter) .authenticateAs(submitter)
.create('Report', { .mutate(`mutation($id: ID!, $description: String!) {
report(description: $description, id: $id) {
id
}
}`, {
id, id,
description: 'Offensive content' description: 'Offensive content'
}) })

View File

@ -15,6 +15,7 @@ let loginCredentials = {
const termsAndConditionsAgreedVersion = { termsAndConditionsAgreedVersion: "0.0.2" }; const termsAndConditionsAgreedVersion = { termsAndConditionsAgreedVersion: "0.0.2" };
const narratorParams = { const narratorParams = {
id: 'id-of-peter-pan',
name: "Peter Pan", name: "Peter Pan",
slug: "peter-pan", slug: "peter-pan",
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
@ -34,10 +35,10 @@ Given("we have a selection of categories", () => {
Given("we have a selection of tags and categories as well as posts", () => { Given("we have a selection of tags and categories as well as posts", () => {
cy.createCategories("cat12") cy.createCategories("cat12")
.factory() .factory()
.authenticateAs(loginCredentials)
.create("Tag", { id: "Ecology" }) .create("Tag", { id: "Ecology" })
.create("Tag", { id: "Nature" }) .create("Tag", { id: "Nature" })
.create("Tag", { id: "Democracy" }); .create("Tag", { id: "Democracy" });
const someAuthor = { const someAuthor = {
id: "authorId", id: "authorId",
email: "author@example.org", email: "author@example.org",
@ -50,26 +51,17 @@ Given("we have a selection of tags and categories as well as posts", () => {
password: "1234", password: "1234",
...termsAndConditionsAgreedVersion ...termsAndConditionsAgreedVersion
}; };
cy.factory() cy.factory()
.create("User", someAuthor) .create("User", { id: 'a1' })
.authenticateAs(someAuthor) .create("Post", {authorId: 'a1', tagIds: [ "Ecology", "Nature", "Democracy" ], categoryIds: ["cat12"] })
.create("Post", { id: "p0", categoryIds: ["cat12"] }) .create("Post", {authorId: 'a1', tagIds: [ "Nature", "Democracy" ], categoryIds: ["cat121"] });
.create("Post", { id: "p1", categoryIds: ["cat121"] });
cy.factory() cy.factory()
.create("User", yetAnotherAuthor) .create("User", { id: 'a2'})
.authenticateAs(yetAnotherAuthor) .create("Post", { authorId: 'a2', tagIds: ['Nature', 'Democracy'], categoryIds: ["cat12"] });
.create("Post", { id: "p2", categoryIds: ["cat12"] });
cy.factory() cy.factory()
.authenticateAs(loginCredentials) .create("Post", { authorId: narratorParams.id, tagIds: ['Democracy'], categoryIds: ["cat122"] })
.create("Post", { id: "p3", categoryIds: ["cat122"] })
.relate("Post", "Tags", { from: "p0", to: "Ecology" })
.relate("Post", "Tags", { from: "p0", to: "Nature" })
.relate("Post", "Tags", { from: "p0", to: "Democracy" })
.relate("Post", "Tags", { from: "p1", to: "Nature" })
.relate("Post", "Tags", { from: "p1", to: "Democracy" })
.relate("Post", "Tags", { from: "p2", to: "Nature" })
.relate("Post", "Tags", { from: "p2", to: "Democracy" })
.relate("Post", "Tags", { from: "p3", to: "Democracy" });
}); });
Given("we have the following user accounts:", table => { Given("we have the following user accounts:", table => {
@ -167,7 +159,13 @@ When("I press {string}", label => {
cy.contains(label).click(); cy.contains(label).click();
}); });
Given("we have this user in our database:", table => {
const [firstRow] = table.hashes()
cy.factory().create('User', firstRow)
})
Given("we have the following posts in our database:", table => { Given("we have the following posts in our database:", table => {
table.hashes().forEach(({ Author, ...postAttributes }, i) => { table.hashes().forEach(({ Author, ...postAttributes }, i) => {
Author = Author || `author-${i}`; Author = Author || `author-${i}`;
const userAttributes = { const userAttributes = {
@ -201,8 +199,10 @@ Given("we have the following posts in our database:", table => {
.create("User", moderatorParams) .create("User", moderatorParams)
.authenticateAs(moderatorParams) .authenticateAs(moderatorParams)
.mutate("mutation($id: ID!) { disable(id: $id) }", postAttributes); .mutate("mutation($id: ID!) { disable(id: $id) }", postAttributes);
} }
}); cy.factory().create("Post", postAttributes);
})
}); });
Then("I see a success message:", message => { Then("I see a success message:", message => {
@ -221,11 +221,11 @@ When(
); );
Given("I previously created a post", () => { Given("I previously created a post", () => {
lastPost.authorId = narratorParams.id
lastPost.title = "previously created post"; lastPost.title = "previously created post";
lastPost.content = "with some content"; lastPost.content = "with some content";
lastPost.categoryIds = "cat0"; lastPost.categoryIds = ["cat0"];
cy.factory() cy.factory()
.authenticateAs(loginCredentials)
.create("Post", lastPost); .create("Post", lastPost);
}); });
@ -437,11 +437,7 @@ Given("I follow the user {string}", name => {
Given('"Spammy Spammer" wrote a post {string}', title => { Given('"Spammy Spammer" wrote a post {string}', title => {
cy.createCategories("cat21") cy.createCategories("cat21")
.factory() .factory()
.authenticateAs({ .create("Post", { authorId: 'annoying-user', title, categoryIds: ["cat21"] });
email: "spammy-spammer@example.org",
password: "1234"
})
.create("Post", { title, categoryIds: ["cat21"] });
}); });
Then("the list of posts of this user is empty", () => { Then("the list of posts of this user is empty", () => {
@ -460,8 +456,7 @@ Then("nobody is following the user profile anymore", () => {
Given("I wrote a post {string}", title => { Given("I wrote a post {string}", title => {
cy.createCategories(`cat213`, title) cy.createCategories(`cat213`, title)
.factory() .factory()
.authenticateAs(loginCredentials) .create("Post", { authorId: narratorParams.id, title, categoryIds: ["cat213"] });
.create("Post", { title, categoryIds: ["cat213"] });
}); });
When("I block the user {string}", name => { When("I block the user {string}", name => {

View File

@ -8,10 +8,12 @@ Feature: Report and Moderate
So I can look into it and decide what to do So I can look into it and decide what to do
Background: Background:
Given we have this user in our database:
| id | name |
| u67 | David Irving|
Given we have the following posts in our database: Given we have the following posts in our database:
| Author | id | title | content | | authorId | id | title | content |
| David Irving | p1 | The Truth about the Holocaust | It never existed! | | u67 | p1 | The Truth about the Holocaust | It never existed! |
Scenario Outline: Report a post from various pages Scenario Outline: Report a post from various pages
Given I am logged in with a "user" role Given I am logged in with a "user" role

View File

@ -7,7 +7,6 @@ Feature: Block a User
Given I have a user account Given I have a user account
And there is an annoying user called "Spammy Spammer" And there is an annoying user called "Spammy Spammer"
And I am logged in And I am logged in
And we have a selection of categories
Scenario: Block a user Scenario: Block a user
Given I am on the profile page of the annoying user Given I am on the profile page of the annoying user
@ -27,8 +26,8 @@ Feature: Block a User
Scenario: Posts of blocked users are filtered from search results Scenario: Posts of blocked users are filtered from search results
Given we have the following posts in our database: Given we have the following posts in our database:
| Author | id | title | content | | id | title | content |
| Some unblocked user | im-not-blocked | Post that should be seen | cause I'm not blocked | | im-not-blocked | Post that should be seen | cause I'm not blocked |
Given "Spammy Spammer" wrote a post "Spam Spam Spam" Given "Spammy Spammer" wrote a post "Spam Spam Spam"
When I search for "Spam" When I search for "Spam"
Then I should see the following posts in the select dropdown: Then I should see the following posts in the select dropdown:

View File

@ -54,6 +54,7 @@ services:
- SMTP_HOST=mailserver - SMTP_HOST=mailserver
- SMTP_PORT=25 - SMTP_PORT=25
- SMTP_IGNORE_TLS=true - SMTP_IGNORE_TLS=true
- "DEBUG=${DEBUG}"
neo4j: neo4j:
environment: environment:
- NEO4J_AUTH=none - NEO4J_AUTH=none

View File

@ -21,7 +21,7 @@
"devDependencies": { "devDependencies": {
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"codecov": "^3.5.0", "codecov": "^3.5.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.1",
"cypress": "^3.4.1", "cypress": "^3.4.1",
"cypress-cucumber-preprocessor": "^1.16.0", "cypress-cucumber-preprocessor": "^1.16.0",
"cypress-file-upload": "^3.3.3", "cypress-file-upload": "^3.3.3",

View File

@ -83,6 +83,8 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
padding: $space-x-small $space-small; padding: $space-x-small $space-small;
box-shadow: $box-shadow-large; box-shadow: $box-shadow-large;
overflow: auto;
max-height: 73.5vh; // magic! fully visible on mobile, no scrolling on wide screen
} }
@include arrow(5px, "tooltip", $background-color-inverse-soft); @include arrow(5px, "tooltip", $background-color-inverse-soft);

View File

@ -50,6 +50,11 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
background: #fff; background: #fff;
} }
body.dropdown-open {
height: 100vh;
overflow: hidden;
}
blockquote { blockquote {
display: block; display: block;
padding: 15px 20px 15px 45px; padding: 15px 20px 15px 45px;
@ -164,3 +169,9 @@ hr {
.v-popover.open .trigger a { .v-popover.open .trigger a {
color: $text-color-link-active; color: $text-color-link-active;
} }
.hyphenate-text {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
}

View File

@ -25,11 +25,16 @@ describe('Comment.vue', () => {
success: jest.fn(), success: jest.fn(),
error: jest.fn(), error: jest.fn(),
}, },
$i18n: {
locale: () => 'en',
},
$filters: { $filters: {
truncate: a => a, truncate: a => a,
}, },
$apollo: { $apollo: {
mutate: jest.fn().mockResolvedValue(), mutate: jest.fn().mockResolvedValue({
data: { DeleteComment: { id: 'it-is-the-deleted-comment' } },
}),
}, },
} }
getters = { getters = {
@ -113,24 +118,22 @@ describe('Comment.vue', () => {
}) })
describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => { describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
beforeEach(() => { beforeEach(async () => {
wrapper.vm.deleteCommentCallback() await wrapper.vm.deleteCommentCallback()
}) })
describe('after timeout', () => { it('emits "deleteComment"', () => {
beforeEach(jest.runAllTimers) expect(wrapper.emitted('deleteComment')).toEqual([
[{ id: 'it-is-the-deleted-comment' }],
])
})
it('emits "deleteComment"', () => { it('does call mutation', () => {
expect(wrapper.emitted().deleteComment.length).toBe(1) expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
}) })
it('does call mutation', () => { it('mutation is successful', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('mutation is successful', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
}) })
}) })
}) })

View File

@ -68,7 +68,6 @@ import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer' import ContentViewer from '~/components/Editor/ContentViewer'
import HcEditCommentForm from '~/components/EditCommentForm/EditCommentForm' import HcEditCommentForm from '~/components/EditCommentForm/EditCommentForm'
import CommentMutations from '~/graphql/CommentMutations' import CommentMutations from '~/graphql/CommentMutations'
import PostQuery from '~/graphql/PostQuery'
export default { export default {
data: function() { data: function() {
@ -143,26 +142,14 @@ export default {
}, },
async deleteCommentCallback() { async deleteCommentCallback() {
try { try {
await this.$apollo.mutate({ const {
data: { DeleteComment },
} = await this.$apollo.mutate({
mutation: CommentMutations(this.$i18n).DeleteComment, mutation: CommentMutations(this.$i18n).DeleteComment,
variables: { id: this.comment.id }, variables: { id: this.comment.id },
update: async store => {
const data = await store.readQuery({
query: PostQuery(this.$i18n),
variables: { id: this.post.id },
})
const index = data.Post[0].comments.findIndex(
deletedComment => deletedComment.id === this.comment.id,
)
if (index !== -1) {
data.Post[0].comments.splice(index, 1)
}
await store.writeQuery({ query: PostQuery(this.$i18n), data })
},
}) })
this.$toast.success(this.$t(`delete.comment.success`)) this.$toast.success(this.$t(`delete.comment.success`))
this.$emit('deleteComment') this.$emit('deleteComment', DeleteComment)
} catch (err) { } catch (err) {
this.$toast.error(err.message) this.$toast.error(err.message)
} }

View File

@ -20,6 +20,9 @@ describe('CommentForm.vue', () => {
beforeEach(() => { beforeEach(() => {
mocks = { mocks = {
$t: jest.fn(), $t: jest.fn(),
$i18n: {
locale: () => 'en',
},
$apollo: { $apollo: {
mutate: jest mutate: jest
.fn() .fn()

View File

@ -18,11 +18,11 @@
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
<div v-if="post.comments && post.comments.length" id="comments" class="comments"> <div v-if="post.comments && post.comments.length" id="comments" class="comments">
<comment <comment
v-for="(comment, index) in post.comments" v-for="comment in post.comments"
:key="comment.id" :key="comment.id"
:comment="comment" :comment="comment"
:post="post" :post="post"
@deleteComment="post.comments.splice(index, 1)" @deleteComment="deleteComment"
/> />
</div> </div>
<hc-empty v-else name="empty" icon="messages" /> <hc-empty v-else name="empty" icon="messages" />
@ -40,5 +40,12 @@ export default {
props: { props: {
post: { type: Object, default: () => {} }, post: { type: Object, default: () => {} },
}, },
methods: {
deleteComment(deleted) {
this.post.comments = this.post.comments.map(comment => {
return comment.id === deleted.id ? deleted : comment
})
},
},
} }
</script> </script>

View File

@ -68,7 +68,7 @@ export default {
this.disabled = true this.disabled = true
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: CommentMutations().UpdateComment, mutation: CommentMutations(this.$i18n).UpdateComment,
variables: { variables: {
content: this.form.content, content: this.form.content,
id: this.comment.id, id: this.comment.id,

View File

@ -38,7 +38,7 @@ const post = {
], ],
__typename: 'User', __typename: 'User',
}, },
commentedCount: 12, commentsCount: 12,
categories: [], categories: [],
shoutedCount: 421, shoutedCount: 421,
__typename: 'Post', __typename: 'Post',

View File

@ -50,7 +50,9 @@ describe('PostCard', () => {
error: jest.fn(), error: jest.fn(),
}, },
$apollo: { $apollo: {
mutate: jest.fn().mockResolvedValue(), mutate: jest.fn().mockResolvedValue({
data: { DeletePost: { id: 'deleted-post-id' } },
}),
}, },
} }
getters = { getters = {
@ -94,7 +96,7 @@ describe('PostCard', () => {
}) })
it('emits "removePostFromList"', () => { it('emits "removePostFromList"', () => {
expect(wrapper.emitted().removePostFromList).toHaveLength(1) expect(wrapper.emitted('removePostFromList')).toEqual([[{ id: 'deleted-post-id' }]])
}) })
}) })
}) })

View File

@ -20,12 +20,12 @@
</div> </div>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<!-- Post Title --> <!-- Post Title -->
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading> <ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<!-- Post Content Excerpt --> <!-- Post Content Excerpt -->
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view --> <!-- TODO: replace editor content with tiptap render view -->
<div class="hc-editor-content" v-html="excerpt" /> <div class="hc-editor-content hyphenate-text" v-html="excerpt" />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<!-- Footer o the Post --> <!-- Footer o the Post -->
<template slot="footer"> <template slot="footer">
@ -51,9 +51,9 @@
</span> </span>
&nbsp; &nbsp;
<!-- Comments Count --> <!-- Comments Count -->
<span :style="{ opacity: post.commentedCount ? 1 : 0.5 }"> <span :style="{ opacity: post.commentsCount ? 1 : 0.5 }">
<ds-icon name="comments" /> <ds-icon name="comments" />
<small>{{ post.commentedCount }}</small> <small>{{ post.commentsCount }}</small>
</span> </span>
<!-- Menu --> <!-- Menu -->
<content-menu <content-menu
@ -118,9 +118,11 @@ export default {
methods: { methods: {
async deletePostCallback() { async deletePostCallback() {
try { try {
await this.$apollo.mutate(deletePostMutation(this.post.id)) const {
data: { DeletePost },
} = await this.$apollo.mutate(deletePostMutation(this.post.id))
this.$toast.success(this.$t('delete.contribution.success')) this.$toast.success(this.$t('delete.contribution.success'))
this.$emit('removePostFromList') this.$emit('removePostFromList', DeletePost)
} catch (err) { } catch (err) {
this.$toast.error(err.message) this.$toast.error(err.message)
} }

View File

@ -122,7 +122,7 @@ describe('SearchInput.vue', () => {
name: 'Trick', name: 'Trick',
slug: 'trick', slug: 'trick',
}, },
commentedCount: 0, commentsCount: 0,
createdAt: '2019-03-13T11:00:20.835Z', createdAt: '2019-03-13T11:00:20.835Z',
id: 'p10', id: 'p10',
label: 'Eos aut illo omnis quis eaque et iure aut.', label: 'Eos aut illo omnis quis eaque et iure aut.',

View File

@ -46,7 +46,7 @@
<ds-flex-item> <ds-flex-item>
<ds-text size="small" color="softer" class="search-meta"> <ds-text size="small" color="softer" class="search-meta">
<span style="text-align: right;"> <span style="text-align: right;">
<b>{{ option.commentedCount }}</b> <b>{{ option.commentsCount }}</b>
<ds-icon name="comments" /> <ds-icon name="comments" />
</span> </span>
<span style="width: 36px; display: inline-block; text-align: right;"> <span style="width: 36px; display: inline-block; text-align: right;">

View File

@ -42,7 +42,7 @@ export default {
const { const {
data: { markAsRead }, data: { markAsRead },
} = await this.$apollo.mutate({ } = await this.$apollo.mutate({
mutation: markAsReadMutation(), mutation: markAsReadMutation(this.$i18n),
variables, variables,
}) })
if (!(markAsRead && markAsRead.read === true)) return if (!(markAsRead && markAsRead.read === true)) return
@ -56,12 +56,14 @@ export default {
}, },
computed: { computed: {
totalNotifications() { totalNotifications() {
return this.notifications.length return (this.notifications || []).length
}, },
}, },
apollo: { apollo: {
notifications: { notifications: {
query: notificationQuery(), query() {
return notificationQuery(this.$i18n)
},
}, },
}, },
} }

View File

@ -1,6 +1,7 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export default i18n => { export default i18n => {
const lang = i18n.locale().toUpperCase()
return { return {
CreateComment: gql` CreateComment: gql`
mutation($postId: ID!, $content: String!) { mutation($postId: ID!, $content: String!) {
@ -55,6 +56,31 @@ export default i18n => {
mutation($id: ID!) { mutation($id: ID!) {
DeleteComment(id: $id) { DeleteComment(id: $id) {
id id
contentExcerpt
content
createdAt
disabled
deleted
author {
id
slug
name
avatar
disabled
deleted
shoutedCount
contributionsCount
commentedCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
icon
}
}
} }
} }
`, `,

View File

@ -0,0 +1,74 @@
import gql from 'graphql-tag'
export const userFragment = lang => gql`
fragment user on User {
id
slug
name
avatar
disabled
deleted
shoutedCount
contributionsCount
commentedCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
icon
}
}
`
export const postCountsFragment = gql`
fragment postCounts on Post {
commentsCount
shoutedCount
shoutedByCurrentUser
emotionsCount
}
`
export const postFragment = lang => gql`
${userFragment(lang)}
fragment post on Post {
id
title
content
contentExcerpt
createdAt
disabled
deleted
slug
image
author {
...user
}
tags {
id
}
categories {
id
name
icon
}
}
`
export const commentFragment = lang => gql`
${userFragment(lang)}
fragment comment on Comment {
id
createdAt
disabled
deleted
content
contentExcerpt
author {
...user
}
}
`

View File

@ -1,77 +1,20 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { postFragment, commentFragment, postCountsFragment } from './Fragments'
export default i18n => { export default i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${postFragment(lang)}
${postCountsFragment}
${commentFragment(lang)}
query Post($id: ID!) { query Post($id: ID!) {
Post(id: $id) { Post(id: $id) {
id ...post
title ...postCounts
content
createdAt
disabled
deleted
slug
image
author {
id
slug
name
avatar
disabled
deleted
shoutedCount
contributionsCount
commentedCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
icon
}
}
tags {
id
}
comments(orderBy: createdAt_asc) { comments(orderBy: createdAt_asc) {
id ...comment
contentExcerpt
content
createdAt
disabled
deleted
author {
id
slug
name
avatar
disabled
deleted
shoutedCount
contributionsCount
commentedCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
icon
}
}
} }
categories {
id
name
icon
}
shoutedCount
shoutedByCurrentUser
emotionsCount
} }
} }
` `
@ -80,45 +23,16 @@ export default i18n => {
export const filterPosts = i18n => { export const filterPosts = i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { ${postFragment(lang)}
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { ${postCountsFragment}
id
title query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
contentExcerpt Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
createdAt ...post
disabled ...postCounts
deleted
slug
image
author {
id
avatar
slug
name
disabled
deleted
contributionsCount
shoutedCount
commentedCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
icon
}
} }
categories {
id
name
icon
}
shoutedCount
} }
} `
`
} }
export const PostsEmotionsByCurrentUser = () => { export const PostsEmotionsByCurrentUser = () => {
@ -128,3 +42,22 @@ export const PostsEmotionsByCurrentUser = () => {
} }
` `
} }
export const relatedContributions = i18n => {
const lang = i18n.locale().toUpperCase()
return gql`
${postFragment(lang)}
${postCountsFragment}
query Post($slug: String!) {
Post(slug: $slug) {
...post
...postCounts
relatedContributions(first: 2) {
...post
...postCounts
}
}
}
`
}

View File

@ -1,57 +1,5 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { postFragment, commentFragment } from './Fragments'
const fragments = gql`
fragment post on Post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
fragment comment on Comment {
id
createdAt
disabled
deleted
contentExcerpt
author {
id
slug
name
disabled
deleted
avatar
}
post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
}
`
export default i18n => { export default i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
@ -129,9 +77,12 @@ export default i18n => {
` `
} }
export const notificationQuery = () => { export const notificationQuery = i18n => {
const lang = i18n.locale().toUpperCase()
return gql` return gql`
${fragments} ${commentFragment(lang)}
${postFragment(lang)}
query { query {
notifications(read: false, orderBy: createdAt_desc) { notifications(read: false, orderBy: createdAt_desc) {
read read
@ -139,17 +90,27 @@ export const notificationQuery = () => {
createdAt createdAt
from { from {
__typename __typename
...post ... on Post {
...comment ...post
}
... on Comment {
...comment
post {
...post
}
}
} }
} }
} }
` `
} }
export const markAsReadMutation = () => { export const markAsReadMutation = i18n => {
const lang = i18n.locale().toUpperCase()
return gql` return gql`
${fragments} ${commentFragment(lang)}
${postFragment(lang)}
mutation($id: ID!) { mutation($id: ID!) {
markAsRead(id: $id) { markAsRead(id: $id) {
read read
@ -157,8 +118,15 @@ export const markAsReadMutation = () => {
createdAt createdAt
from { from {
__typename __typename
...post ... on Post {
...comment ...post
}
... on Comment {
...comment
post {
...post
}
}
} }
} }
} }

View File

@ -121,7 +121,7 @@
</div> </div>
</ds-container> </ds-container>
</div> </div>
<ds-container style="word-break: break-all"> <ds-container>
<div class="main-container"> <div class="main-container">
<nuxt /> <nuxt />
</div> </div>

View File

@ -60,7 +60,7 @@
"apollo-cache-inmemory": "~1.6.3", "apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4", "apollo-client": "~2.6.4",
"cookie-universal-nuxt": "~2.0.17", "cookie-universal-nuxt": "~2.0.17",
"cross-env": "~5.2.0", "cross-env": "~5.2.1",
"date-fns": "2.0.1", "date-fns": "2.0.1",
"express": "~4.17.1", "express": "~4.17.1",
"graphql": "~14.5.4", "graphql": "~14.5.4",

View File

@ -20,7 +20,7 @@
<hc-post-card <hc-post-card
:post="post" :post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }" :width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@removePostFromList="deletePost(index, post.id)" @removePostFromList="deletePost"
/> />
</masonry-grid-item> </masonry-grid-item>
</template> </template>
@ -115,7 +115,7 @@ export default {
label: this.$t('sorting.commented'), label: this.$t('sorting.commented'),
value: 'Commented', value: 'Commented',
icons: 'comment', icons: 'comment',
order: 'commentedCount_desc', order: 'commentsCount_desc',
}, },
], ],
} }
@ -164,9 +164,9 @@ export default {
showMoreContributions() { showMoreContributions() {
this.offset += this.pageSize this.offset += this.pageSize
}, },
deletePost(_index, postId) { deletePost(deletedPost) {
this.posts = this.posts.filter(post => { this.posts = this.posts.filter(post => {
return post.id !== postId return post.id !== deletedPost.id
}) })
}, },
}, },
@ -185,7 +185,8 @@ export default {
return result return result
}, },
update({ Post }) { update({ Post }) {
this.hasMore = Post.length >= this.pageSize this.hasMore = Post && Post.length >= this.pageSize
if (!Post) return
const posts = uniqBy([...this.posts, ...Post], 'id') const posts = uniqBy([...this.posts, ...Post], 'id')
this.posts = posts this.posts = posts
}, },

View File

@ -18,9 +18,9 @@
/> />
</client-only> </client-only>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading> <ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<content-viewer class="content" :content="post.content" /> <content-viewer class="content hyphenate-text" :content="post.content" />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<ds-space margin="xx-large" /> <ds-space margin="xx-large" />
<!-- Categories --> <!-- Categories -->

View File

@ -36,11 +36,11 @@
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;"> <ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small"> <ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
<hc-post-card <hc-post-card
v-for="(relatedPost, index) in post.relatedContributions" v-for="relatedPost in post.relatedContributions"
:key="relatedPost.id" :key="relatedPost.id"
:post="relatedPost" :post="relatedPost"
:width="{ base: '100%', lg: 1 }" :width="{ base: '100%', lg: 1 }"
@removePostFromList="post.relatedContributions.splice(index, 1)" @removePostFromList="removePostFromList"
/> />
</ds-flex> </ds-flex>
<hc-empty v-else margin="large" icon="file" message="No related Posts" /> <hc-empty v-else margin="large" icon="file" message="No related Posts" />
@ -50,9 +50,9 @@
</template> </template>
<script> <script>
import gql from 'graphql-tag'
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard'
import HcEmpty from '~/components/Empty.vue' import HcEmpty from '~/components/Empty.vue'
import { relatedContributions } from '~/graphql/PostQuery'
export default { export default {
transition: { transition: {
@ -68,57 +68,17 @@ export default {
return this.Post ? this.Post[0] || {} : {} return this.Post ? this.Post[0] || {} : {}
}, },
}, },
methods: {
removePostFromList(deletedPost) {
this.post.relatedContributions = this.post.relatedContributions.filter(contribution => {
return contribution.id !== deletedPost.id
})
},
},
apollo: { apollo: {
Post: { Post: {
query() { query() {
return gql` return relatedContributions(this.$i18n)
query Post($slug: String!) {
Post(slug: $slug) {
id
title
tags {
id
name
}
categories {
id
name
icon
}
relatedContributions(first: 2) {
id
title
slug
contentExcerpt
shoutedCount
commentedCount
categories {
id
name
icon
}
author {
id
name
slug
avatar
contributionsCount
followedByCount
followedByCurrentUser
commentedCount
location {
name: name${this.$i18n.locale().toUpperCase()}
}
badges {
id
icon
}
}
}
shoutedCount
}
}
`
}, },
variables() { variables() {
return { return {

View File

@ -72,7 +72,7 @@
<template v-if="user.about"> <template v-if="user.about">
<hr /> <hr />
<ds-space margin-top="small" margin-bottom="small"> <ds-space margin-top="small" margin-bottom="small">
<ds-text color="soft" size="small">{{ user.about }}</ds-text> <ds-text color="soft" size="small" class="hyphenate-text">{{ user.about }}</ds-text>
</ds-space> </ds-space>
</template> </template>
</ds-card> </ds-card>
@ -220,11 +220,11 @@
</ds-grid-item> </ds-grid-item>
<template v-if="posts.length"> <template v-if="posts.length">
<masonry-grid-item v-for="(post, index) in posts" :key="post.id"> <masonry-grid-item v-for="post in posts" :key="post.id">
<hc-post-card <hc-post-card
:post="post" :post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }" :width="{ base: '100%', md: '100%', xl: '50%' }"
@removePostFromList="removePostFromList(index)" @removePostFromList="removePostFromList"
/> />
</masonry-grid-item> </masonry-grid-item>
</template> </template>
@ -345,8 +345,10 @@ export default {
}, },
}, },
methods: { methods: {
removePostFromList(index) { removePostFromList(deletedPost) {
this.posts.splice(index, 1) this.posts = this.posts.filter(post => {
return post.id !== deletedPost.id
})
}, },
handleTab(tab) { handleTab(tab) {
this.tabActive = tab this.tabActive = tab
@ -396,11 +398,11 @@ export default {
}, },
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
update({ Post }) { update({ Post }) {
this.hasMore = Post && Post.length >= this.pageSize
if (!Post) return if (!Post) return
// TODO: find out why `update` gets called twice initially. // TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same // We have to filter for uniq posts only because we get the same
// result set twice. // result set twice.
this.hasMore = Post.length >= this.pageSize
this.posts = this.uniq([...this.posts, ...Post]) this.posts = this.uniq([...this.posts, ...Post])
}, },
}, },

View File

@ -5371,13 +5371,12 @@ create-react-context@^0.2.1:
fbjs "^0.8.0" fbjs "^0.8.0"
gud "^1.0.0" gud "^1.0.0"
cross-env@~5.2.0: cross-env@~5.2.1:
version "5.2.0" version "5.2.1"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d"
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg== integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==
dependencies: dependencies:
cross-spawn "^6.0.5" cross-spawn "^6.0.5"
is-windows "^1.0.0"
cross-fetch@^3.0.4: cross-fetch@^3.0.4:
version "3.0.4" version "3.0.4"
@ -8739,7 +8738,7 @@ is-window@^1.0.2:
resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0=
is-windows@^1.0.0, is-windows@^1.0.2: is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==

View File

@ -1772,13 +1772,12 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
cross-env@^5.2.0: cross-env@^5.2.1:
version "5.2.0" version "5.2.1"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d"
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg== integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==
dependencies: dependencies:
cross-spawn "^6.0.5" cross-spawn "^6.0.5"
is-windows "^1.0.0"
cross-fetch@2.2.2: cross-fetch@2.2.2:
version "2.2.2" version "2.2.2"
@ -2974,7 +2973,7 @@ is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-windows@^1.0.0, is-windows@^1.0.2: is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==