mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #1440 from Human-Connection/1414-soft_delete
1414 Implement soft delete
This commit is contained in:
commit
290bdf5ff4
@ -19,9 +19,8 @@
|
||||
"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: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": "cross-env babel-node src/seed/reset-db.js",
|
||||
"db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
|
||||
"db:reset": "babel-node src/seed/reset-db.js",
|
||||
"db:seed": "babel-node src/seed/seed-db.js"
|
||||
},
|
||||
"author": "Human Connection gGmbH",
|
||||
"license": "MIT",
|
||||
|
||||
@ -13,7 +13,7 @@ export default async (driver, authorizationHeader) => {
|
||||
}
|
||||
const session = driver.session()
|
||||
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}
|
||||
LIMIT 1
|
||||
`
|
||||
@ -23,7 +23,6 @@ export default async (driver, authorizationHeader) => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (!currentUser) return null
|
||||
if (currentUser.disabled) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
|
||||
104
backend/src/jwt/decode.spec.js
Normal file
104
backend/src/jwt/decode.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,16 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
import { gql } from '../../jest/helpers'
|
||||
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 instance = neode()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
let authenticatedUser
|
||||
let user
|
||||
let query
|
||||
|
||||
const currentUserParams = {
|
||||
id: 'u1',
|
||||
@ -26,24 +32,42 @@ const randomAuthorParams = {
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
beforeEach(async () => {
|
||||
await Promise.all([
|
||||
const [currentUser, followedAuthor, randomAuthor] = await Promise.all([
|
||||
factory.create('User', currentUserParams),
|
||||
factory.create('User', followedAuthorParams),
|
||||
factory.create('User', randomAuthorParams),
|
||||
])
|
||||
await instance.create('Category', {
|
||||
user = currentUser
|
||||
await neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([
|
||||
Factory().authenticateAs(currentUserParams),
|
||||
Factory().authenticateAs(followedAuthorParams),
|
||||
Factory().authenticateAs(randomAuthorParams),
|
||||
])
|
||||
await asYourself.follow({ id: 'u2', type: 'User' })
|
||||
await asFollowedUser.create('Post', { title: 'This is the post of a followed user', categoryIds })
|
||||
await asSomeoneElse.create('Post', { title: 'This is some random post', categoryIds })
|
||||
await currentUser.relateTo(followedAuthor, 'following')
|
||||
await factory.create('Post', {
|
||||
author: followedAuthor,
|
||||
title: 'This is the post of a followed user',
|
||||
categoryIds,
|
||||
})
|
||||
await factory.create('Post', {
|
||||
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 () => {
|
||||
@ -52,33 +76,44 @@ afterEach(async () => {
|
||||
|
||||
describe('Filter posts by author is followed by sb.', () => {
|
||||
describe('given an authenticated user', () => {
|
||||
let authenticatedClient
|
||||
|
||||
beforeEach(async () => {
|
||||
const headers = await login(currentUserParams)
|
||||
authenticatedClient = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('no filter bubble', () => {
|
||||
it('returns all posts', async () => {
|
||||
const query = '{ Post(filter: { }) { title } }'
|
||||
const postQuery = gql`
|
||||
{
|
||||
Post(filter: {}) {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = {
|
||||
Post: [
|
||||
{ title: 'This is some random post' },
|
||||
{ title: 'This is the post of a followed user' },
|
||||
],
|
||||
data: {
|
||||
Post: [
|
||||
{ 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', () => {
|
||||
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 = {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -91,13 +91,11 @@ const isAuthor = rule({
|
||||
resourceId,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [author] = result.records.map(record => {
|
||||
return record.get('author')
|
||||
})
|
||||
const {
|
||||
properties: { id: authorId },
|
||||
} = author
|
||||
session.close()
|
||||
const authorId = author && author.properties && author.properties.id
|
||||
return authorId === user.id
|
||||
})
|
||||
|
||||
@ -131,7 +129,7 @@ const permissions = shield(
|
||||
isLoggedIn: allow,
|
||||
Badge: allow,
|
||||
PostsEmotionsCountByEmotion: allow,
|
||||
PostsEmotionsByCurrentUser: allow,
|
||||
PostsEmotionsByCurrentUser: isAuthenticated,
|
||||
blockedUsers: isAuthenticated,
|
||||
notifications: isAuthenticated,
|
||||
},
|
||||
|
||||
@ -13,15 +13,16 @@ const setDefaultFilters = (resolve, root, args, context, info) => {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
const obfuscateDisabled = async (resolve, root, args, context, info) => {
|
||||
if (!isModerator(context) && root.disabled) {
|
||||
const obfuscate = async (resolve, root, args, context, info) => {
|
||||
if (root.deleted || (!isModerator(context) && root.disabled)) {
|
||||
root.content = 'UNAVAILABLE'
|
||||
root.contentExcerpt = 'UNAVAILABLE'
|
||||
root.title = 'UNAVAILABLE'
|
||||
root.image = 'UNAVAILABLE'
|
||||
root.slug = 'UNAVAILABLE'
|
||||
root.avatar = 'UNAVAILABLE'
|
||||
root.about = 'UNAVAILABLE'
|
||||
root.name = 'UNAVAILABLE'
|
||||
root.image = null // avoid unecessary 500 errors
|
||||
}
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
@ -40,7 +41,7 @@ export default {
|
||||
}
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
Post: obfuscateDisabled,
|
||||
User: obfuscateDisabled,
|
||||
Comment: obfuscateDisabled,
|
||||
Post: obfuscate,
|
||||
User: obfuscate,
|
||||
Comment: obfuscate,
|
||||
}
|
||||
|
||||
@ -1,49 +1,70 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../seed/factories'
|
||||
import { host, login } from '../jest/helpers'
|
||||
import { neode } from '../bootstrap/neo4j'
|
||||
import { gql } from '../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||
import createServer from '../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
let client
|
||||
let query
|
||||
let action
|
||||
let mutate
|
||||
let graphqlQuery
|
||||
const categoryIds = ['cat9']
|
||||
let authenticatedUser
|
||||
let user
|
||||
let moderator
|
||||
let troll
|
||||
|
||||
const action = () => {
|
||||
return query({ query: graphqlQuery })
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// For performance reasons we do this only once
|
||||
await Promise.all([
|
||||
factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', password: '1234' }),
|
||||
const users = await Promise.all([
|
||||
factory.create('User', { id: 'u1', role: 'user' }),
|
||||
factory.create('User', {
|
||||
id: 'm1',
|
||||
role: 'moderator',
|
||||
email: 'moderator@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
factory.create('User', {
|
||||
id: 'u2',
|
||||
role: 'user',
|
||||
name: 'Offensive Name',
|
||||
slug: 'offensive-name',
|
||||
avatar: '/some/offensive/avatar.jpg',
|
||||
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([
|
||||
factory.follow({ id: 'u2', type: 'User' }),
|
||||
factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true, categoryIds }),
|
||||
user.relateTo(troll, 'following'),
|
||||
factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p1',
|
||||
title: 'Deleted post',
|
||||
slug: 'deleted-post',
|
||||
deleted: true,
|
||||
categoryIds,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p3',
|
||||
title: 'Publicly visible post',
|
||||
slug: 'publicly-visible-post',
|
||||
deleted: false,
|
||||
categoryIds,
|
||||
}),
|
||||
@ -51,32 +72,56 @@ beforeAll(async () => {
|
||||
|
||||
await Promise.all([
|
||||
factory.create('Comment', {
|
||||
author: user,
|
||||
id: 'c2',
|
||||
postId: 'p3',
|
||||
content: 'Enabled comment on public post',
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })])
|
||||
|
||||
const asTroll = Factory()
|
||||
await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
|
||||
await asTroll.create('Post', {
|
||||
await factory.create('Post', {
|
||||
id: 'p2',
|
||||
author: troll,
|
||||
title: 'Disabled post',
|
||||
content: 'This is an offensive post content',
|
||||
contentExcerpt: 'This is an offensive post content',
|
||||
image: '/some/offensive/image.jpg',
|
||||
deleted: false,
|
||||
categoryIds,
|
||||
})
|
||||
await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
|
||||
await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })])
|
||||
await factory.create('Comment', {
|
||||
id: 'c1',
|
||||
author: troll,
|
||||
postId: 'p3',
|
||||
content: 'Disabled comment',
|
||||
contentExcerpt: 'Disabled comment',
|
||||
})
|
||||
|
||||
const asModerator = Factory()
|
||||
await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' })
|
||||
await asModerator.mutate('mutation { disable( id: "p2") }')
|
||||
await asModerator.mutate('mutation { disable( id: "c1") }')
|
||||
await asModerator.mutate('mutation { disable( id: "u2") }')
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
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 () => {
|
||||
@ -85,93 +130,124 @@ afterAll(async () => {
|
||||
|
||||
describe('softDeleteMiddleware', () => {
|
||||
describe('read disabled content', () => {
|
||||
let user
|
||||
let post
|
||||
let comment
|
||||
let subject
|
||||
const beforeComment = async () => {
|
||||
query = '{ User(id: "u1") { following { comments { content contentExcerpt } } } }'
|
||||
const response = await action()
|
||||
comment = response.User[0].following[0].comments[0]
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
User(id: "u1") {
|
||||
following {
|
||||
comments {
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const { data } = await action()
|
||||
subject = data.User[0].following[0].comments[0]
|
||||
}
|
||||
const beforeUser = async () => {
|
||||
query = '{ User(id: "u1") { following { name about avatar } } }'
|
||||
const response = await action()
|
||||
user = response.User[0].following[0]
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
User(id: "u1") {
|
||||
following {
|
||||
name
|
||||
slug
|
||||
about
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const { data } = await action()
|
||||
subject = data.User[0].following[0]
|
||||
}
|
||||
const beforePost = async () => {
|
||||
query =
|
||||
'{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }'
|
||||
const response = await action()
|
||||
post = response.User[0].following[0].contributions[0]
|
||||
}
|
||||
|
||||
action = () => {
|
||||
return client.request(query)
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
User(id: "u1") {
|
||||
following {
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
image
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const { data } = await action()
|
||||
subject = data.User[0].following[0].contributions[0]
|
||||
}
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
describe('User', () => {
|
||||
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', () =>
|
||||
expect(user.about).toEqual('This self description is very offensive'))
|
||||
it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg'))
|
||||
expect(subject.about).toEqual('This self description is very offensive'))
|
||||
it('displays avatar', () => expect(subject.avatar).toEqual('/some/offensive/avatar.jpg'))
|
||||
})
|
||||
|
||||
describe('Post', () => {
|
||||
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', () =>
|
||||
expect(post.content).toEqual('This is an offensive post content'))
|
||||
expect(subject.content).toEqual('This is an offensive post content'))
|
||||
it('displays contentExcerpt', () =>
|
||||
expect(post.contentExcerpt).toEqual('This is an offensive post content'))
|
||||
it('displays image', () => expect(post.image).toEqual('/some/offensive/image.jpg'))
|
||||
expect(subject.contentExcerpt).toEqual('This is an offensive post content'))
|
||||
it('displays image', () => expect(subject.image).toEqual('/some/offensive/image.jpg'))
|
||||
})
|
||||
|
||||
describe('Comment', () => {
|
||||
beforeEach(beforeComment)
|
||||
|
||||
it('displays content', () => expect(comment.content).toEqual('Disabled comment'))
|
||||
it('displays content', () => expect(subject.content).toEqual('Disabled comment'))
|
||||
it('displays contentExcerpt', () =>
|
||||
expect(comment.contentExcerpt).toEqual('Disabled comment'))
|
||||
expect(subject.contentExcerpt).toEqual('Disabled comment'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('User', () => {
|
||||
beforeEach(beforeUser)
|
||||
|
||||
it('displays name', () => expect(user.name).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates about', () => expect(user.about).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates avatar', () => expect(user.avatar).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates name', () => expect(subject.name).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates about', () => expect(subject.about).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates avatar', () => expect(subject.avatar).toEqual('UNAVAILABLE'))
|
||||
})
|
||||
|
||||
describe('Post', () => {
|
||||
beforeEach(beforePost)
|
||||
|
||||
it('obfuscates title', () => expect(post.title).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates content', () => expect(post.content).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates contentExcerpt', () => expect(post.contentExcerpt).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates image', () => expect(post.image).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates title', () => expect(subject.title).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates content', () => expect(subject.content).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates contentExcerpt', () => expect(subject.contentExcerpt).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates image', () => expect(subject.image).toEqual(null))
|
||||
})
|
||||
|
||||
describe('Comment', () => {
|
||||
beforeEach(beforeComment)
|
||||
|
||||
it('obfuscates content', () => expect(comment.content).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates contentExcerpt', () => expect(comment.contentExcerpt).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates content', () => expect(subject.content).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates contentExcerpt', () => expect(subject.contentExcerpt).toEqual('UNAVAILABLE'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -179,43 +255,57 @@ describe('softDeleteMiddleware', () => {
|
||||
describe('Query', () => {
|
||||
describe('Post', () => {
|
||||
beforeEach(async () => {
|
||||
query = '{ Post { title } }'
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
Post {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('hides deleted or disabled posts', async () => {
|
||||
const expected = { Post: [{ title: 'Publicly visible post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
const expected = { data: { Post: [{ title: 'Publicly visible post' }] } }
|
||||
await expect(action()).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('shows disabled but hides deleted posts', async () => {
|
||||
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))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.comments', () => {
|
||||
beforeEach(async () => {
|
||||
query = '{ Post(id: "p3") { title comments { content } } }'
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
Post(id: "p3") {
|
||||
title
|
||||
comments {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('conceals disabled comments', async () => {
|
||||
@ -224,7 +314,9 @@ describe('softDeleteMiddleware', () => {
|
||||
{ content: 'UNAVAILABLE' },
|
||||
]
|
||||
const {
|
||||
Post: [{ comments }],
|
||||
data: {
|
||||
Post: [{ comments }],
|
||||
},
|
||||
} = await action()
|
||||
await expect(comments).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
@ -232,8 +324,7 @@ describe('softDeleteMiddleware', () => {
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('shows disabled comments', async () => {
|
||||
@ -242,7 +333,9 @@ describe('softDeleteMiddleware', () => {
|
||||
{ content: 'Disabled comment' },
|
||||
]
|
||||
const {
|
||||
Post: [{ comments }],
|
||||
data: {
|
||||
Post: [{ comments }],
|
||||
},
|
||||
} = await action()
|
||||
await expect(comments).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
@ -251,58 +344,70 @@ describe('softDeleteMiddleware', () => {
|
||||
|
||||
describe('filter (deleted: true)', () => {
|
||||
beforeEach(() => {
|
||||
query = '{ Post(deleted: true) { title } }'
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
Post(deleted: true) {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
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', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('shows deleted posts', async () => {
|
||||
const expected = { Post: [{ title: 'Deleted post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
it('does not show deleted posts', async () => {
|
||||
const expected = { data: { Post: [{ title: 'UNAVAILABLE' }] } }
|
||||
await expect(action()).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter (disabled: true)', () => {
|
||||
beforeEach(() => {
|
||||
query = '{ Post(disabled: true) { title } }'
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
Post(disabled: true) {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
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', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('shows disabled posts', async () => {
|
||||
const expected = { Post: [{ title: 'Disabled post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
const expected = { data: { Post: [{ title: 'Disabled post' }] } }
|
||||
await expect(action()).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -52,29 +52,9 @@ const validatePost = async (resolve, root, args, context, info) => {
|
||||
}
|
||||
|
||||
const validateUpdatePost = async (resolve, root, args, context, info) => {
|
||||
const { id, categoryIds } = args
|
||||
const session = context.driver.session()
|
||||
const categoryQueryRes = await session.run(
|
||||
`
|
||||
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)
|
||||
const { categoryIds } = args
|
||||
if (typeof categoryIds === 'undefined') return resolve(root, args, context, info)
|
||||
return validatePost(resolve, root, args, context, info)
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
48
backend/src/models/Comment.js
Normal file
48
backend/src/models/Comment.js
Normal 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() },
|
||||
},
|
||||
},
|
||||
}
|
||||
21
backend/src/models/Location.js
Normal file
21
backend/src/models/Location.js
Normal 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',
|
||||
},
|
||||
}
|
||||
@ -23,6 +23,20 @@ module.exports = {
|
||||
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() },
|
||||
},
|
||||
},
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
|
||||
@ -8,7 +8,7 @@ module.exports = {
|
||||
type: 'relationship',
|
||||
relationship: 'OWNED_BY',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
direction: 'out',
|
||||
eager: true,
|
||||
cascade: 'detach',
|
||||
},
|
||||
|
||||
17
backend/src/models/Tag.js
Normal file
17
backend/src/models/Tag.js
Normal 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',
|
||||
},
|
||||
}
|
||||
@ -83,4 +83,16 @@ module.exports = {
|
||||
target: 'Notification',
|
||||
direction: 'in',
|
||||
},
|
||||
shouted: {
|
||||
type: 'relationship',
|
||||
relationship: 'SHOUTED',
|
||||
target: 'Post',
|
||||
direction: 'out',
|
||||
},
|
||||
isIn: {
|
||||
type: 'relationship',
|
||||
relationship: 'IS_IN',
|
||||
target: 'Location',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
|
||||
@ -7,5 +7,8 @@ export default {
|
||||
EmailAddress: require('./EmailAddress.js'),
|
||||
SocialMedia: require('./SocialMedia.js'),
|
||||
Post: require('./Post.js'),
|
||||
Comment: require('./Comment.js'),
|
||||
Category: require('./Category.js'),
|
||||
Tag: require('./Tag.js'),
|
||||
Location: require('./Location.js'),
|
||||
}
|
||||
|
||||
@ -47,9 +47,19 @@ export default {
|
||||
session.close()
|
||||
return commentReturnedWithAuthor
|
||||
},
|
||||
DeleteComment: async (object, params, context, resolveInfo) => {
|
||||
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
|
||||
DeleteComment: async (object, args, context, resolveInfo) => {
|
||||
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
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,48 +1,34 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login, gql } from '../../jest/helpers'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import { gql } from '../../jest/helpers'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../../server'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
|
||||
let client
|
||||
let createCommentVariables
|
||||
let createCommentVariablesSansPostId
|
||||
let createCommentVariablesWithNonExistentPost
|
||||
let userParams
|
||||
let headers
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
const createPostVariables = {
|
||||
id: 'p1',
|
||||
title: 'post to comment on',
|
||||
content: 'please comment on me',
|
||||
categoryIds,
|
||||
}
|
||||
let variables
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let commentAuthor
|
||||
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
const client = createTestClient(server)
|
||||
mutate = client.mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
name: 'TestUser',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
await factory.create('User', userParams)
|
||||
await instance.create('Category', {
|
||||
variables = {}
|
||||
await neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
@ -53,335 +39,243 @@ afterEach(async () => {
|
||||
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('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
createCommentVariables = {
|
||||
variables = {
|
||||
...variables,
|
||||
postId: 'p1',
|
||||
content: "I'm not authorised to comment",
|
||||
}
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
const { errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login(userParams)
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
createCommentVariables = {
|
||||
postId: 'p1',
|
||||
content: "I'm authorised to comment",
|
||||
}
|
||||
await client.request(createPostMutation, createPostVariables)
|
||||
const user = await neode.create('User', { name: 'Author' })
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('creates a comment', async () => {
|
||||
const expected = {
|
||||
CreateComment: {
|
||||
describe('given a post', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Post', { categoryIds: ['cat9'], id: 'p1' })
|
||||
variables = {
|
||||
...variables,
|
||||
postId: 'p1',
|
||||
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([
|
||||
{
|
||||
comments: [
|
||||
{
|
||||
content: "I'm authorised to comment",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
it('creates a comment', async () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { content: "I'm authorised to comment" } },
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('throw an error if an empty string is sent from the editor as content', async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'p1',
|
||||
content: '<p></p>',
|
||||
}
|
||||
it('assigns the authenticated user as author', async () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { author: { name: 'Author' } } },
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
||||
'Comment must be at least 1 character long!',
|
||||
)
|
||||
})
|
||||
describe('comment content is empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: '<p></p>' }
|
||||
})
|
||||
|
||||
it('throws an error if a comment sent from the editor does not contain a single character', async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'p1',
|
||||
content: '<p> </p>',
|
||||
}
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
||||
'Comment must be at least 1 character long!',
|
||||
)
|
||||
})
|
||||
describe('comment content contains only whitespaces', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: ' <p> </p> ' }
|
||||
})
|
||||
|
||||
it('throws an error if postId is sent as an empty string', async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'p1',
|
||||
content: '',
|
||||
}
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
||||
'Comment must be at least 1 character long!',
|
||||
)
|
||||
})
|
||||
describe('invalid post id', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, postId: 'does-not-exist' }
|
||||
})
|
||||
|
||||
it('throws an error if content is sent as an string of empty characters', async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'p1',
|
||||
content: ' ',
|
||||
}
|
||||
|
||||
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!')
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ManageComments', () => {
|
||||
let authorParams
|
||||
beforeEach(async () => {
|
||||
authorParams = {
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
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
|
||||
}
|
||||
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', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
const randomGuy = await factory.create('User')
|
||||
authenticatedUser = await randomGuy.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as author', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login(authorParams)
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
authenticatedUser = await commentAuthor.toJson()
|
||||
})
|
||||
|
||||
it('updates the comment', async () => {
|
||||
const expected = {
|
||||
UpdateComment: {
|
||||
id: 'c456',
|
||||
content: 'The comment is updated',
|
||||
},
|
||||
data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } },
|
||||
}
|
||||
await expect(
|
||||
client.request(updateCommentMutation, updateCommentVariables),
|
||||
).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!',
|
||||
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error if a comment sent from the editor does not contain a single letter character', async () => {
|
||||
updateCommentVariables = {
|
||||
id: 'c456',
|
||||
content: '<p> </p>',
|
||||
}
|
||||
describe('if `content` empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: ' <p> </p>' }
|
||||
})
|
||||
|
||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||
'Comment must be at least 1 character long!',
|
||||
)
|
||||
it('throws InputError', async () => {
|
||||
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 () => {
|
||||
updateCommentVariables = {
|
||||
id: '',
|
||||
content: '<p>Hello</p>',
|
||||
}
|
||||
describe('if comment does not exist for given id', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'does-not-exist' }
|
||||
})
|
||||
|
||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||
'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!',
|
||||
)
|
||||
it('returns null', async () => {
|
||||
const { data, errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(data).toMatchObject({ UpdateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteComment', () => {
|
||||
const deleteCommentMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
DeleteComment(id: $id) {
|
||||
id
|
||||
}
|
||||
describe('DeleteComment', () => {
|
||||
const deleteCommentMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
DeleteComment(id: $id) {
|
||||
id
|
||||
content
|
||||
contentExcerpt
|
||||
deleted
|
||||
}
|
||||
`
|
||||
|
||||
const deleteCommentVariables = {
|
||||
id: 'c456',
|
||||
}
|
||||
`
|
||||
|
||||
describe('given a post and a comment', () => {
|
||||
beforeEach(setupPostAndComment)
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
const result = await mutate({ mutation: deleteCommentMutation, variables })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
const randomGuy = await factory.create('User')
|
||||
authenticatedUser = await randomGuy.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
const { errors } = await mutate({ mutation: deleteCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as author', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login(authorParams)
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
authenticatedUser = await commentAuthor.toJson()
|
||||
})
|
||||
|
||||
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 = {
|
||||
DeleteComment: {
|
||||
id: 'c456',
|
||||
deleted: true,
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(deleteCommentMutation, deleteCommentVariables),
|
||||
).resolves.toEqual(expected)
|
||||
expect(data).toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -44,7 +44,7 @@ export default {
|
||||
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id})
|
||||
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
|
||||
${whereClause}
|
||||
RETURN resource, notification, user
|
||||
${orderByClause}
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
import Factory from '../../seed/factories'
|
||||
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 createServer from '../.././server'
|
||||
|
||||
const factory = Factory()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
const userParams = {
|
||||
id: 'you',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
|
||||
let authenticatedUser
|
||||
let user
|
||||
let author
|
||||
let variables
|
||||
let query
|
||||
let mutate
|
||||
@ -43,51 +37,81 @@ afterEach(async () => {
|
||||
|
||||
describe('given some notifications', () => {
|
||||
beforeEach(async () => {
|
||||
user = await factory.create('User', userParams)
|
||||
await factory.create('User', { id: 'neighbor' })
|
||||
await Promise.all(setupNotifications.map(s => neode.cypher(s)))
|
||||
const categoryIds = ['cat1']
|
||||
author = await factory.create('User', { id: 'author' })
|
||||
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', () => {
|
||||
const notificationQuery = gql`
|
||||
@ -109,8 +133,8 @@ describe('given some notifications', () => {
|
||||
`
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const result = await query({ query: notificationQuery })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
const { errors } = await query({ query: notificationQuery })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -121,7 +145,7 @@ describe('given some notifications', () => {
|
||||
|
||||
describe('no filters', () => {
|
||||
it('returns all notifications of current user', async () => {
|
||||
const expected = expect.objectContaining({
|
||||
const expected = {
|
||||
data: {
|
||||
notifications: [
|
||||
{
|
||||
@ -135,7 +159,7 @@ describe('given some notifications', () => {
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'Already seen post mentioning',
|
||||
content: 'Already seen post mention',
|
||||
},
|
||||
read: true,
|
||||
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 } }),
|
||||
).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: [] } })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -73,13 +73,42 @@ export default {
|
||||
},
|
||||
},
|
||||
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) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
const session = context.driver.session()
|
||||
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
SET post = $params
|
||||
`
|
||||
|
||||
@ -112,34 +141,25 @@ export default {
|
||||
return post.properties
|
||||
},
|
||||
|
||||
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 }
|
||||
|
||||
DeletePost: async (object, args, context, resolveInfo) => {
|
||||
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
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
SET post.content = 'UNAVAILABLE'
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
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) => {
|
||||
const session = context.driver.session()
|
||||
@ -184,6 +204,7 @@ export default {
|
||||
},
|
||||
Post: {
|
||||
...Resolver('Post', {
|
||||
undefinedToNull: ['activityId', 'objectId', 'image', 'language'],
|
||||
hasMany: {
|
||||
tags: '-[:TAGGED]->(related:Tag)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
@ -196,13 +217,15 @@ export default {
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
},
|
||||
count: {
|
||||
commentsCount:
|
||||
'<-[:COMMENTS]-(related:Comment) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
||||
shoutedCount:
|
||||
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
||||
emotionsCount: '<-[related:EMOTED]-(:User)',
|
||||
},
|
||||
boolean: {
|
||||
shoutedByCurrentUser:
|
||||
'<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
||||
},
|
||||
}),
|
||||
relatedContributions: async (parent, params, context, resolveInfo) => {
|
||||
@ -210,6 +233,7 @@ export default {
|
||||
const { id } = parent
|
||||
const statement = `
|
||||
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
WHERE NOT post.deleted AND NOT post.disabled
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ describe('report', () => {
|
||||
let returnedObject
|
||||
let variables
|
||||
let createPostVariables
|
||||
let user
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -20,10 +21,10 @@ describe('report', () => {
|
||||
id: 'whatever',
|
||||
}
|
||||
headers = {}
|
||||
await factory.create('User', {
|
||||
id: 'u1',
|
||||
user = await factory.create('User', {
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
id: 'u1',
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
@ -127,11 +128,8 @@ describe('report', () => {
|
||||
|
||||
describe('reported resource is a post', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.authenticateAs({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p23',
|
||||
title: 'Matt and Robert having a pair-programming',
|
||||
categoryIds,
|
||||
@ -182,12 +180,9 @@ describe('report', () => {
|
||||
content: 'please comment on me',
|
||||
categoryIds,
|
||||
}
|
||||
const asAuthenticatedUser = await factory.authenticateAs({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await asAuthenticatedUser.create('Post', createPostVariables)
|
||||
await asAuthenticatedUser.create('Comment', {
|
||||
await factory.create('Post', { ...createPostVariables, author: user })
|
||||
await factory.create('Comment', {
|
||||
author: user,
|
||||
postId: 'p1',
|
||||
id: 'c34',
|
||||
content: 'Robert getting tired.',
|
||||
|
||||
@ -32,7 +32,7 @@ export default {
|
||||
SocialMedia: Resolver('SocialMedia', {
|
||||
idAttribute: 'url',
|
||||
hasOne: {
|
||||
ownedBy: '<-[:OWNED_BY]-(related:User)',
|
||||
ownedBy: '-[:OWNED_BY]->(related:User)',
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
@ -24,11 +24,11 @@ export default {
|
||||
// }
|
||||
const session = driver.session()
|
||||
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',
|
||||
{
|
||||
userEmail: email,
|
||||
},
|
||||
`
|
||||
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 },
|
||||
)
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
|
||||
@ -1,50 +1,54 @@
|
||||
import { GraphQLClient, request } from 'graphql-request'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import CONFIG from './../../config'
|
||||
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()
|
||||
let query
|
||||
let mutate
|
||||
let variables
|
||||
let req
|
||||
let user
|
||||
|
||||
// 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'
|
||||
// }
|
||||
const jennyRostocksHeaders = {
|
||||
authorization:
|
||||
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc',
|
||||
}
|
||||
// This is a bearer token of a user with id `u3`:
|
||||
const userBearerToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsIm5hbWUiOiJKZW5ueSBSb3N0b2NrIiwiZGlzYWJsZWQiOmZhbHNlLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL2tleXVyaTg1LzEyOC5qcGciLCJpZCI6InUzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUub3JnIiwic2x1ZyI6Implbm55LXJvc3RvY2siLCJpYXQiOjE1Njc0NjgyMDIsImV4cCI6MTU2NzU1NDYwMiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwIiwic3ViIjoidTMifQ.RkmrdJDL1kIqGnMWUBl_sJJ4grzfpTEGdT6doMsbLW8'
|
||||
|
||||
// This is a bearer token of a user with id `u2`:
|
||||
const moderatorBearerToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoibW9kZXJhdG9yIiwibmFtZSI6IkJvYiBkZXIgQmF1bWVpc3RlciIsImRpc2FibGVkIjpmYWxzZSwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9hbmRyZXdvZmZpY2VyLzEyOC5qcGciLCJpZCI6InUyIiwiZW1haWwiOiJtb2RlcmF0b3JAZXhhbXBsZS5vcmciLCJzbHVnIjoiYm9iLWRlci1iYXVtZWlzdGVyIiwiaWF0IjoxNTY3NDY4MDUwLCJleHAiOjE1Njc1NTQ0NTAsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUyIn0.LdVFPKqIcoY0a7_kFZSTgnc8NzmZD7CrR3vkWLSqedM'
|
||||
|
||||
const disable = async id => {
|
||||
const moderatorParams = { email: 'moderator@example.org', role: 'moderator', password: '1234' }
|
||||
const asModerator = Factory()
|
||||
await asModerator.create('User', moderatorParams)
|
||||
await asModerator.authenticateAs(moderatorParams)
|
||||
await asModerator.mutate('mutation($id: ID!) { disable(id: $id) }', { id })
|
||||
await factory.create('User', { id: 'u2', role: 'moderator' })
|
||||
req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } }
|
||||
await mutate({
|
||||
mutation: gql`
|
||||
mutation($id: ID!) {
|
||||
disable(id: $id)
|
||||
}
|
||||
`,
|
||||
variables: { id },
|
||||
})
|
||||
req = { headers: {} }
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
role: 'user',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
beforeEach(() => {
|
||||
user = null
|
||||
req = { headers: {} }
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
// 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 () => {
|
||||
@ -52,261 +56,266 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('isLoggedIn', () => {
|
||||
const query = '{ isLoggedIn }'
|
||||
const isLoggedInQuery = gql`
|
||||
{
|
||||
isLoggedIn
|
||||
}
|
||||
`
|
||||
const respondsWith = async expected => {
|
||||
await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected)
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns false', async () => {
|
||||
await expect(request(host, query)).resolves.toEqual({
|
||||
isLoggedIn: false,
|
||||
})
|
||||
await respondsWith({ data: { isLoggedIn: false } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with malformed JWT Bearer token', () => {
|
||||
const headers = { authorization: 'blah' }
|
||||
const client = new GraphQLClient(host, { headers })
|
||||
|
||||
it('returns false', async () => {
|
||||
await expect(client.request(query)).resolves.toEqual({
|
||||
isLoggedIn: false,
|
||||
})
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
user = await factory.create('User', { id: 'u3' })
|
||||
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
|
||||
})
|
||||
})
|
||||
|
||||
describe('with valid JWT Bearer token', () => {
|
||||
const client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
|
||||
it('returns true', async () => {
|
||||
await respondsWith({ data: { isLoggedIn: true } })
|
||||
})
|
||||
|
||||
it('returns false', async () => {
|
||||
await expect(client.request(query)).resolves.toEqual({
|
||||
isLoggedIn: false,
|
||||
describe('but user is disabled', () => {
|
||||
beforeEach(async () => {
|
||||
await disable('u3')
|
||||
})
|
||||
|
||||
it('returns false', async () => {
|
||||
await respondsWith({ data: { isLoggedIn: false } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('and a corresponding user in the database', () => {
|
||||
describe('user is enabled', () => {
|
||||
it('returns true', async () => {
|
||||
// see the decoded token above
|
||||
await factory.create('User', { id: 'u3' })
|
||||
await expect(client.request(query)).resolves.toEqual({
|
||||
isLoggedIn: true,
|
||||
})
|
||||
})
|
||||
describe('but user is deleted', () => {
|
||||
beforeEach(async () => {
|
||||
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
||||
})
|
||||
|
||||
describe('user is disabled', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { id: 'u3' })
|
||||
await disable('u3')
|
||||
})
|
||||
|
||||
it('returns false', async () => {
|
||||
await expect(client.request(query)).resolves.toEqual({
|
||||
isLoggedIn: false,
|
||||
})
|
||||
})
|
||||
it('returns false', async () => {
|
||||
await respondsWith({ data: { isLoggedIn: false } })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('currentUser', () => {
|
||||
const query = `{
|
||||
currentUser {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
email
|
||||
role
|
||||
const currentUserQuery = gql`
|
||||
{
|
||||
currentUser {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
email
|
||||
role
|
||||
}
|
||||
}
|
||||
}`
|
||||
`
|
||||
|
||||
const respondsWith = async expected => {
|
||||
await expect(query({ query: currentUserQuery, variables })).resolves.toMatchObject(expected)
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns null', async () => {
|
||||
const expected = { currentUser: null }
|
||||
await expect(request(host, query)).resolves.toEqual(expected)
|
||||
await respondsWith({ data: { currentUser: null } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with valid JWT Bearer Token', () => {
|
||||
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('authenticated', () => {
|
||||
describe('and corresponding user in the database', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
await factory.create('User', {
|
||||
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 () => {
|
||||
const expected = {
|
||||
currentUser: {
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
email: 'test@example.org',
|
||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
role: 'user',
|
||||
data: {
|
||||
currentUser: {
|
||||
id: 'u3',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
email: 'test@example.org',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
role: 'user',
|
||||
},
|
||||
},
|
||||
}
|
||||
await expect(client.request(query)).resolves.toEqual(expected)
|
||||
await respondsWith(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
const mutation = params => {
|
||||
const { email, password } = params
|
||||
return `
|
||||
mutation {
|
||||
login(email:"${email}", password:"${password}")
|
||||
}`
|
||||
const loginMutation = gql`
|
||||
mutation($email: String!, $password: String!) {
|
||||
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('with valid email/password combination', () => {
|
||||
it('responds with a JWT token', async () => {
|
||||
const data = await request(
|
||||
host,
|
||||
mutation({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
)
|
||||
const token = data.login
|
||||
describe('with a valid email/password combination', () => {
|
||||
it('responds with a JWT bearer token', async done => {
|
||||
const {
|
||||
data: { login: token },
|
||||
} = await mutate({ mutation: loginMutation, variables })
|
||||
jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
|
||||
expect(data.email).toEqual('test@example.org')
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, email: 'test@example.org', password: 'wrong' }
|
||||
})
|
||||
|
||||
it('responds with "Incorrect email address or password."', async () => {
|
||||
await expect(
|
||||
request(
|
||||
host,
|
||||
mutation({
|
||||
email: 'test@example.org',
|
||||
password: 'wrong',
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow('Incorrect email address or password.')
|
||||
await respondsWith({
|
||||
errors: [{ message: 'Incorrect email address or password.' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {
|
||||
await expect(
|
||||
request(
|
||||
host,
|
||||
mutation({
|
||||
email: 'non-existent@example.org',
|
||||
password: 'wrong',
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow('Incorrect email address or password.')
|
||||
await respondsWith({
|
||||
errors: [{ message: 'Incorrect email address or password.' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('change password', () => {
|
||||
let headers
|
||||
let client
|
||||
const changePasswordMutation = gql`
|
||||
mutation($oldPassword: String!, $newPassword: String!) {
|
||||
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
const mutation = params => {
|
||||
const { oldPassword, newPassword } = params
|
||||
return `
|
||||
mutation {
|
||||
changePassword(oldPassword:"${oldPassword}", newPassword:"${newPassword}")
|
||||
}`
|
||||
const respondsWith = async expected => {
|
||||
await expect(mutate({ mutation: changePasswordMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
}
|
||||
|
||||
describe('should be authenticated before changing password', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, oldPassword: 'what', newPassword: 'ever' }
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws "Not Authorised!"', async () => {
|
||||
await expect(
|
||||
request(
|
||||
host,
|
||||
mutation({
|
||||
oldPassword: '1234',
|
||||
newPassword: '1234',
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow('Not Authorised!')
|
||||
await respondsWith({ errors: [{ message: 'Not Authorised!' }] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('old and new password should not match', () => {
|
||||
it('responds with "Old password and new password should be different"', async () => {
|
||||
await expect(
|
||||
client.request(
|
||||
mutation({
|
||||
oldPassword: '1234',
|
||||
newPassword: '1234',
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow('Old password and new password should be different')
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { id: 'u3' })
|
||||
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
|
||||
})
|
||||
})
|
||||
describe('old password === new password', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, oldPassword: '1234', newPassword: '1234' }
|
||||
})
|
||||
|
||||
describe('incorrect old password', () => {
|
||||
it('responds with "Old password isn\'t valid"', async () => {
|
||||
await expect(
|
||||
client.request(
|
||||
mutation({
|
||||
oldPassword: 'notOldPassword',
|
||||
newPassword: '12345',
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow('Old password is not correct')
|
||||
it('responds with "Old password and new password should be different"', async () => {
|
||||
await respondsWith({
|
||||
errors: [{ message: 'Old password and new password should be different' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('correct password', () => {
|
||||
it('changes the password if given correct credentials "', async () => {
|
||||
const response = await client.request(
|
||||
mutation({
|
||||
describe('incorrect old password', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...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',
|
||||
newPassword: '12345',
|
||||
}),
|
||||
)
|
||||
await expect(response).toEqual(
|
||||
expect.objectContaining({
|
||||
changePassword: expect.any(String),
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('changes the password if given correct credentials "', async () => {
|
||||
await respondsWith({ data: { changePassword: expect.any(String) } })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -102,23 +102,49 @@ export default {
|
||||
const { resource } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
if (resource && resource.length) {
|
||||
await Promise.all(
|
||||
resource.map(async node => {
|
||||
await session.run(
|
||||
`
|
||||
let user
|
||||
try {
|
||||
if (resource && resource.length) {
|
||||
await Promise.all(
|
||||
resource.map(async node => {
|
||||
await session.run(
|
||||
`
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
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()
|
||||
}
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
return user
|
||||
},
|
||||
},
|
||||
User: {
|
||||
@ -161,12 +187,13 @@ export default {
|
||||
hasOne: {
|
||||
invitedBy: '<-[:INVITED]-(related:User)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
location: '-[:IS_IN]->(related:Location)',
|
||||
},
|
||||
hasMany: {
|
||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||
following: '-[:FOLLOWS]->(related:User)',
|
||||
friends: '-[:FRIENDS]-(related:User)',
|
||||
socialMedia: '-[:OWNED_BY]->(related:SocialMedia',
|
||||
socialMedia: '<-[:OWNED_BY]-(related:SocialMedia)',
|
||||
contributions: '-[:WROTE]->(related:Post)',
|
||||
comments: '-[:WROTE]->(related:Comment)',
|
||||
shouted: '-[:SHOUTED]->(related:Post)',
|
||||
|
||||
@ -1,274 +1,463 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login, gql } from '../../jest/helpers'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import { gql } from '../../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
let client
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
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 () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('users', () => {
|
||||
describe('User', () => {
|
||||
describe('query by email address', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
|
||||
})
|
||||
|
||||
const query = `query($email: String) { User(email: $email) { name } }`
|
||||
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 })
|
||||
})
|
||||
|
||||
it('is permitted', async () => {
|
||||
await expect(client.request(query, variables)).resolves.toEqual({
|
||||
User: [{ name: 'Johnny' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('User', () => {
|
||||
describe('query by email address', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
const userParams = {
|
||||
email: 'user@example.org',
|
||||
password: '1234',
|
||||
id: 'u47',
|
||||
name: 'John Doe',
|
||||
}
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: 'John Doughnut',
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
id
|
||||
const userQuery = gql`
|
||||
query($email: String) {
|
||||
User(email: $email) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = { email: 'any-email-address@example.org' }
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', userParams)
|
||||
it('is forbidden', async () => {
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('as another user', () => {
|
||||
describe('as admin', () => {
|
||||
beforeEach(async () => {
|
||||
const someoneElseParams = {
|
||||
email: 'someone-else@example.org',
|
||||
password: '1234',
|
||||
name: 'James Doe',
|
||||
}
|
||||
|
||||
await factory.create('User', someoneElseParams)
|
||||
const headers = await login(someoneElseParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('is not allowed to change other user accounts', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as the same user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('name within specifications', async () => {
|
||||
const expected = {
|
||||
UpdateUser: {
|
||||
id: 'u47',
|
||||
name: 'John Doughnut',
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('with `null` as name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: null,
|
||||
}
|
||||
const expected = '"name" must be a string'
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
||||
})
|
||||
|
||||
it('with too short name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: ' ',
|
||||
}
|
||||
const expected = '"name" length must be at least 3 characters long'
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteUser', () => {
|
||||
let deleteUserVariables
|
||||
let asAuthor
|
||||
const deleteUserMutation = gql`
|
||||
mutation($id: ID!, $resource: [Deletable]) {
|
||||
DeleteUser(id: $id, resource: $resource) {
|
||||
id
|
||||
contributions {
|
||||
id
|
||||
deleted
|
||||
}
|
||||
comments {
|
||||
id
|
||||
deleted
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
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', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
const admin = await factory.create('User', {
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, { headers })
|
||||
authenticatedUser = await admin.toJson()
|
||||
})
|
||||
|
||||
describe("attempting to delete another user's account", () => {
|
||||
it('throws an authorization error', async () => {
|
||||
deleteUserVariables = { id: 'u565' }
|
||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('attempting to delete my own account', () => {
|
||||
let expectedResponse
|
||||
beforeEach(async () => {
|
||||
asAuthor = Factory()
|
||||
await asAuthor.authenticateAs({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
await asAuthor.create('Post', {
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
categoryIds,
|
||||
})
|
||||
await asAuthor.create('Comment', {
|
||||
id: 'c155',
|
||||
postId: 'p139',
|
||||
content: 'Comment by user u343',
|
||||
})
|
||||
expectedResponse = {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
contributions: [{ id: 'p139', deleted: false }],
|
||||
comments: [{ id: 'c155', deleted: false }],
|
||||
},
|
||||
}
|
||||
})
|
||||
it("deletes my account, but doesn't delete posts or comments by default", async () => {
|
||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||
expectedResponse,
|
||||
)
|
||||
})
|
||||
|
||||
describe("deletes a user's", () => {
|
||||
it('posts on request', async () => {
|
||||
deleteUserVariables = { id: 'u343', 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 () => {
|
||||
deleteUserVariables = { id: 'u343', resource: ['Comment'] }
|
||||
expectedResponse = {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
contributions: [{ id: 'p139', deleted: false }],
|
||||
comments: [{ id: 'c155', deleted: true }],
|
||||
},
|
||||
}
|
||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||
expectedResponse,
|
||||
)
|
||||
})
|
||||
|
||||
it('posts and comments on request', async () => {
|
||||
deleteUserVariables = { id: 'u343', resource: ['Post', 'Comment'] }
|
||||
expectedResponse = {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
contributions: [{ id: 'p139', deleted: true }],
|
||||
comments: [{ id: 'c155', deleted: true }],
|
||||
},
|
||||
}
|
||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||
expectedResponse,
|
||||
)
|
||||
})
|
||||
it('is permitted', async () => {
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
data: { User: [{ name: 'Johnny' }] },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
const userParams = {
|
||||
email: 'user@example.org',
|
||||
password: '1234',
|
||||
id: 'u47',
|
||||
name: 'John Doe',
|
||||
}
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: 'John Doughnut',
|
||||
}
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await factory.create('User', userParams)
|
||||
})
|
||||
|
||||
describe('as another user', () => {
|
||||
beforeEach(async () => {
|
||||
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 () => {
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as the same user', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('name within specifications', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
UpdateUser: {
|
||||
id: 'u47',
|
||||
name: 'John Doughnut',
|
||||
},
|
||||
},
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('with `null` as name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: null,
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty(
|
||||
'message',
|
||||
'child "name" fails because ["name" contains an invalid value, "name" must be a string]',
|
||||
)
|
||||
})
|
||||
|
||||
it('with too short name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: ' ',
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty(
|
||||
'message',
|
||||
'child "name" fails because ["name" length must be at least 3 characters long]',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteUser', () => {
|
||||
const deleteUserMutation = gql`
|
||||
mutation($id: ID!, $resource: [Deletable]) {
|
||||
DeleteUser(id: $id, resource: $resource) {
|
||||
id
|
||||
name
|
||||
about
|
||||
deleted
|
||||
contributions {
|
||||
id
|
||||
content
|
||||
contentExcerpt
|
||||
deleted
|
||||
comments {
|
||||
id
|
||||
content
|
||||
contentExcerpt
|
||||
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 () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe("attempting to delete another user's account", () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'not-my-account' }
|
||||
})
|
||||
|
||||
it('throws an authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('attempting to delete my own account', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'u343' }
|
||||
})
|
||||
|
||||
describe('given posts and comments', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
await factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
categoryIds,
|
||||
})
|
||||
await factory.create('Comment', {
|
||||
author: user,
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
})
|
||||
await factory.create('Comment', {
|
||||
postId: 'p139',
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
})
|
||||
})
|
||||
|
||||
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(
|
||||
expectedResponse,
|
||||
)
|
||||
})
|
||||
|
||||
describe('deletion of all post requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post'] }
|
||||
})
|
||||
|
||||
describe("marks user's posts as deleted", () => {
|
||||
it('posts on request', 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: '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('marks comments as deleted', 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: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
17
backend/src/schema/types/Location.gql
Normal file
17
backend/src/schema/types/Location.gql
Normal 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")
|
||||
}
|
||||
|
||||
@ -51,23 +51,6 @@ type Statistics {
|
||||
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 {
|
||||
id: ID!
|
||||
submitter: User @relation(name: "REPORTED", direction: "IN")
|
||||
|
||||
@ -20,6 +20,7 @@ type Post {
|
||||
@cypher(
|
||||
statement: """
|
||||
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
WHERE NOT post.deleted AND NOT post.disabled
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
"""
|
||||
@ -29,6 +30,11 @@ type Post {
|
||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
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")
|
||||
shoutedCount: Int!
|
||||
@cypher(
|
||||
@ -38,10 +44,7 @@ type Post {
|
||||
# Has the currently logged in user shouted that post?
|
||||
shoutedByCurrentUser: Boolean!
|
||||
@cypher(
|
||||
statement: """
|
||||
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
|
||||
RETURN COUNT(u) >= 1
|
||||
"""
|
||||
statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
|
||||
)
|
||||
|
||||
emotions: [EMOTED]
|
||||
@ -52,26 +55,18 @@ type Post {
|
||||
type Mutation {
|
||||
CreatePost(
|
||||
id: ID
|
||||
activityId: String
|
||||
objectId: String
|
||||
title: String!
|
||||
slug: String
|
||||
content: String!
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: Visibility
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
contentExcerpt: String
|
||||
): Post
|
||||
UpdatePost(
|
||||
id: ID!
|
||||
activityId: String
|
||||
objectId: String
|
||||
title: String!
|
||||
slug: String
|
||||
content: String!
|
||||
@ -79,10 +74,6 @@ type Mutation {
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: Visibility
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
): Post
|
||||
|
||||
@ -17,7 +17,7 @@ type User {
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
locationName: String
|
||||
about: String
|
||||
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "OUT")
|
||||
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
|
||||
|
||||
#createdAt: DateTime
|
||||
#updatedAt: DateTime
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function(params) {
|
||||
const { id = uuid(), name, slug, icon } = params
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
mutation: `
|
||||
mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
|
||||
CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
|
||||
id
|
||||
name
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
const defaults = {
|
||||
id: uuid(),
|
||||
icon: 'img/badges/fundraisingbox_de_airship.svg',
|
||||
name: 'Some category name',
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id, name, slug, icon },
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
return neodeInstance.create('Category', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,38 @@
|
||||
import faker from 'faker'
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function(params) {
|
||||
const {
|
||||
id = uuid(),
|
||||
postId = 'p6',
|
||||
content = [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
|
||||
} = params
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
mutation: `
|
||||
mutation($id: ID!, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
factory: async ({ args, neodeInstance, factoryInstance }) => {
|
||||
const defaults = {
|
||||
id: uuid(),
|
||||
content: [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
|
||||
}
|
||||
`,
|
||||
variables: { id, postId, content },
|
||||
args = {
|
||||
...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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,12 @@ import { GraphQLClient, request } from 'graphql-request'
|
||||
import { getDriver, neode } from '../../bootstrap/neo4j'
|
||||
import createBadge from './badges.js'
|
||||
import createUser from './users.js'
|
||||
import createOrganization from './organizations.js'
|
||||
import createPost from './posts.js'
|
||||
import createComment from './comments.js'
|
||||
import createCategory from './categories.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'
|
||||
|
||||
@ -24,12 +24,12 @@ const authenticatedHeaders = async ({ email, password }, host) => {
|
||||
const factories = {
|
||||
Badge: createBadge,
|
||||
User: createUser,
|
||||
Organization: createOrganization,
|
||||
Post: createPost,
|
||||
Comment: createComment,
|
||||
Category: createCategory,
|
||||
Tag: createTag,
|
||||
Report: createReport,
|
||||
SocialMedia: createSocialMedia,
|
||||
Location: createLocation,
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
@ -79,6 +79,7 @@ export default function Factory(options = {}) {
|
||||
this.lastResponse = await factory({
|
||||
args,
|
||||
neodeInstance,
|
||||
factoryInstance: this,
|
||||
})
|
||||
return this.lastResponse
|
||||
} else {
|
||||
|
||||
24
backend/src/seed/factories/locations.js
Normal file
24
backend/src/seed/factories/locations.js
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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 },
|
||||
}
|
||||
}
|
||||
@ -1,51 +1,60 @@
|
||||
import faker from 'faker'
|
||||
import slugify from 'slug'
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function(params) {
|
||||
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
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
mutation: `
|
||||
mutation(
|
||||
$id: ID!
|
||||
$slug: String
|
||||
$title: String!
|
||||
$content: String!
|
||||
$image: String
|
||||
$visibility: Visibility
|
||||
$deleted: Boolean
|
||||
$categoryIds: [ID]
|
||||
) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
slug: $slug
|
||||
title: $title
|
||||
content: $content
|
||||
image: $image
|
||||
visibility: $visibility
|
||||
deleted: $deleted
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
title
|
||||
content
|
||||
}
|
||||
factory: async ({ args, neodeInstance, factoryInstance }) => {
|
||||
const defaults = {
|
||||
id: uuid(),
|
||||
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: [],
|
||||
}
|
||||
`,
|
||||
variables: { id, slug, title, content, image, visibility, deleted, categoryIds },
|
||||
args = {
|
||||
...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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
14
backend/src/seed/factories/socialMedia.js
Normal file
14
backend/src/seed/factories/socialMedia.js
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,12 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function(params) {
|
||||
const { id = uuid(), name = '#human-connection' } = params
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
mutation: `
|
||||
mutation($id: ID!) {
|
||||
CreateTag(id: $id) {
|
||||
id
|
||||
}
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
const defaults = { name: '#human-connection' }
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
`,
|
||||
variables: { id, name },
|
||||
return neodeInstance.create('Tag', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,110 @@
|
||||
import faker from 'faker'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../server'
|
||||
import Factory from './factories'
|
||||
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||
import { gql } from '../jest/helpers'
|
||||
|
||||
/* eslint-disable no-multi-spaces */
|
||||
;(async function() {
|
||||
let authenticatedUser = null
|
||||
const driver = getDriver()
|
||||
const factory = Factory()
|
||||
const neode = getNeode()
|
||||
|
||||
try {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
const { mutate } = createTestClient(server)
|
||||
|
||||
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([
|
||||
f.create('Badge', {
|
||||
id: 'indiegogo_en_racoon',
|
||||
@ -36,9 +136,9 @@ import Factory from './factories'
|
||||
peterLustig,
|
||||
bobDerBaumeister,
|
||||
jennyRostock,
|
||||
tick, // eslint-disable-line no-unused-vars
|
||||
trick, // eslint-disable-line no-unused-vars
|
||||
track, // eslint-disable-line no-unused-vars
|
||||
huey,
|
||||
dewey,
|
||||
louie,
|
||||
dagobert,
|
||||
] = await Promise.all([
|
||||
f.create('User', {
|
||||
@ -64,22 +164,22 @@ import Factory from './factories'
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u4',
|
||||
name: 'Huey (Tick)',
|
||||
slug: 'huey-tick',
|
||||
name: 'Huey',
|
||||
slug: 'huey',
|
||||
role: 'user',
|
||||
email: 'huey@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u5',
|
||||
name: 'Dewey (Trick)',
|
||||
slug: 'dewey-trick',
|
||||
name: 'Dewey',
|
||||
slug: 'dewey',
|
||||
role: 'user',
|
||||
email: 'dewey@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u6',
|
||||
name: 'Louie (Track)',
|
||||
slug: 'louie-track',
|
||||
name: 'Louie',
|
||||
slug: 'louie',
|
||||
role: 'user',
|
||||
email: 'louie@example.org',
|
||||
}),
|
||||
@ -92,31 +192,11 @@ import Factory from './factories'
|
||||
}),
|
||||
])
|
||||
|
||||
const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([
|
||||
Factory().authenticateAs({
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
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([
|
||||
peterLustig.relateTo(Berlin, 'isIn'),
|
||||
bobDerBaumeister.relateTo(Hamburg, 'isIn'),
|
||||
jennyRostock.relateTo(Paris, 'isIn'),
|
||||
huey.relateTo(Paris, 'isIn'),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
@ -133,16 +213,16 @@ import Factory from './factories'
|
||||
bobDerBaumeister.relateTo(jennyRostock, 'friends'),
|
||||
|
||||
peterLustig.relateTo(jennyRostock, 'following'),
|
||||
peterLustig.relateTo(tick, 'following'),
|
||||
bobDerBaumeister.relateTo(tick, 'following'),
|
||||
jennyRostock.relateTo(tick, 'following'),
|
||||
tick.relateTo(track, 'following'),
|
||||
trick.relateTo(tick, 'following'),
|
||||
track.relateTo(jennyRostock, 'following'),
|
||||
peterLustig.relateTo(huey, 'following'),
|
||||
bobDerBaumeister.relateTo(huey, 'following'),
|
||||
jennyRostock.relateTo(huey, 'following'),
|
||||
huey.relateTo(dewey, 'following'),
|
||||
dewey.relateTo(huey, 'following'),
|
||||
louie.relateTo(jennyRostock, 'following'),
|
||||
|
||||
dagobert.relateTo(tick, 'blocked'),
|
||||
dagobert.relateTo(trick, 'blocked'),
|
||||
dagobert.relateTo(track, 'blocked'),
|
||||
dagobert.relateTo(huey, 'blocked'),
|
||||
dagobert.relateTo(dewey, 'blocked'),
|
||||
dagobert.relateTo(louie, 'blocked'),
|
||||
])
|
||||
|
||||
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', {
|
||||
id: 'Umwelt',
|
||||
name: 'Umwelt',
|
||||
id: 'Environment',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
id: 'Naturschutz',
|
||||
name: 'Naturschutz',
|
||||
id: 'Nature',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
id: 'Demokratie',
|
||||
name: 'Demokratie',
|
||||
id: 'Democracy',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
id: 'Freiheit',
|
||||
name: 'Freiheit',
|
||||
id: 'Freedom',
|
||||
}),
|
||||
])
|
||||
|
||||
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 =
|
||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||
const mention2 =
|
||||
@ -271,468 +416,271 @@ import Factory from './factories'
|
||||
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||
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. ;-)'
|
||||
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([
|
||||
asAdmin.create('Post', {
|
||||
id: 'p0',
|
||||
image: faker.image.unsplash.food(),
|
||||
categoryIds: ['cat16'],
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'p2',
|
||||
title: `Nature Philosophy Yoga`,
|
||||
content: hashtag1,
|
||||
categoryIds: ['cat2'],
|
||||
},
|
||||
}),
|
||||
asModerator.create('Post', {
|
||||
id: 'p1',
|
||||
image: faker.image.unsplash.technology(),
|
||||
categoryIds: ['cat1'],
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'p7',
|
||||
title: 'This is post #7',
|
||||
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat7'],
|
||||
},
|
||||
}),
|
||||
asUser.create('Post', {
|
||||
id: 'p2',
|
||||
title: `Nature Philosophy Yoga`,
|
||||
content: `${hashtag1}`,
|
||||
categoryIds: ['cat2'],
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'p8',
|
||||
image: faker.image.unsplash.nature(),
|
||||
title: `Quantum Flow Theory explains Quantum Gravity`,
|
||||
content: hashtagAndMention1,
|
||||
categoryIds: ['cat8'],
|
||||
},
|
||||
}),
|
||||
asTick.create('Post', {
|
||||
id: 'p3',
|
||||
categoryIds: ['cat3'],
|
||||
}),
|
||||
asTrick.create('Post', {
|
||||
id: 'p4',
|
||||
categoryIds: ['cat4'],
|
||||
}),
|
||||
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',
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'p12',
|
||||
title: 'This is post #12',
|
||||
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat12'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
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 =
|
||||
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.'
|
||||
const mentionInComment2 =
|
||||
'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([
|
||||
asUser.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
asTick.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: huey,
|
||||
id: 'c2',
|
||||
postId: 'p1',
|
||||
}),
|
||||
asTrack.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: louie,
|
||||
id: 'c3',
|
||||
postId: 'p3',
|
||||
}),
|
||||
asTrick.create('Comment', {
|
||||
id: 'c4',
|
||||
postId: 'p2',
|
||||
content: `${mentionInComment1}`,
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
id: 'c4-1',
|
||||
postId: 'p2',
|
||||
content: `${mentionInComment2}`,
|
||||
}),
|
||||
asModerator.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: bobDerBaumeister,
|
||||
id: 'c5',
|
||||
postId: 'p3',
|
||||
}),
|
||||
asAdmin.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: peterLustig,
|
||||
id: 'c6',
|
||||
postId: 'p4',
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
id: 'c7',
|
||||
postId: 'p2',
|
||||
}),
|
||||
asTick.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: huey,
|
||||
id: 'c8',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asTrick.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: dewey,
|
||||
id: 'c9',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asTrack.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: louie,
|
||||
id: 'c10',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
id: 'c11',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
id: 'c12',
|
||||
postId: 'p15',
|
||||
}),
|
||||
])
|
||||
|
||||
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
|
||||
await Promise.all([
|
||||
asModerator.mutate(disableMutation, {
|
||||
id: 'p11',
|
||||
}),
|
||||
asModerator.mutate(disableMutation, {
|
||||
id: 'c5',
|
||||
}),
|
||||
democracy.relateTo(p3, 'post'),
|
||||
democracy.relateTo(p11, 'post'),
|
||||
democracy.relateTo(p15, 'post'),
|
||||
democracy.relateTo(p7, 'post'),
|
||||
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([
|
||||
asTick.create('Report', {
|
||||
description: "I don't like this comment",
|
||||
id: 'c1',
|
||||
}),
|
||||
asTrick.create('Report', {
|
||||
description: "I don't like this post",
|
||||
id: 'p1',
|
||||
}),
|
||||
asTrack.create('Report', {
|
||||
description: "I don't like this user",
|
||||
id: 'u1',
|
||||
}),
|
||||
peterLustig.relateTo(p1, 'shouted'),
|
||||
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'),
|
||||
dewey.relateTo(p10, 'shouted'),
|
||||
peterLustig.relateTo(p2, 'shouted'),
|
||||
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([
|
||||
f.create('Organization', {
|
||||
id: 'o1',
|
||||
name: 'Democracy Deutschland',
|
||||
description: 'Description for democracy-deutschland.',
|
||||
mutate({
|
||||
mutation: disableMutation,
|
||||
variables: {
|
||||
id: 'p11',
|
||||
},
|
||||
}),
|
||||
f.create('Organization', {
|
||||
id: 'o2',
|
||||
name: 'Human-Connection',
|
||||
description: 'Description for human-connection.',
|
||||
}),
|
||||
f.create('Organization', {
|
||||
id: 'o3',
|
||||
name: 'Pro Veg',
|
||||
description: 'Description for pro-veg.',
|
||||
}),
|
||||
f.create('Organization', {
|
||||
id: 'o4',
|
||||
name: 'Greenpeace',
|
||||
description: 'Description for greenpeace.',
|
||||
mutate({
|
||||
mutation: disableMutation,
|
||||
variables: {
|
||||
id: 'c5',
|
||||
},
|
||||
}),
|
||||
])
|
||||
authenticatedUser = null
|
||||
|
||||
const reportMutation = gql`
|
||||
mutation($id: ID!, $description: String!) {
|
||||
report(description: $description, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
authenticatedUser = await huey.toJson()
|
||||
await Promise.all([
|
||||
f.relate('Organization', 'CreatedBy', {
|
||||
from: 'u1',
|
||||
to: 'o1',
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: "I don't like this comment",
|
||||
id: 'c1',
|
||||
},
|
||||
}),
|
||||
f.relate('Organization', 'CreatedBy', {
|
||||
from: 'u1',
|
||||
to: 'o2',
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: "I don't like this post",
|
||||
id: 'p1',
|
||||
},
|
||||
}),
|
||||
f.relate('Organization', 'OwnedBy', {
|
||||
from: 'u2',
|
||||
to: 'o2',
|
||||
}),
|
||||
f.relate('Organization', 'OwnedBy', {
|
||||
from: 'u2',
|
||||
to: 'o3',
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: "I don't like this user",
|
||||
id: 'u1',
|
||||
},
|
||||
}),
|
||||
])
|
||||
authenticatedUser = null
|
||||
|
||||
await Promise.all(
|
||||
[...Array(30).keys()].map(i => {
|
||||
|
||||
@ -18,20 +18,22 @@ Object.entries(requiredConfigs).map(entry => {
|
||||
const driver = getDriver()
|
||||
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 defaults = {
|
||||
context: async ({ req }) => {
|
||||
const user = await decode(driver, req.headers.authorization)
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user,
|
||||
req,
|
||||
cypherParams: {
|
||||
currentUserId: user ? user.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
context,
|
||||
schema: middleware(schema),
|
||||
debug: !!CONFIG.DEBUG,
|
||||
tracing: !!CONFIG.DEBUG,
|
||||
|
||||
@ -121,7 +121,11 @@ Given('somebody reported the following posts:', table => {
|
||||
cy.factory()
|
||||
.create('User', submitter)
|
||||
.authenticateAs(submitter)
|
||||
.create('Report', {
|
||||
.mutate(`mutation($id: ID!, $description: String!) {
|
||||
report(description: $description, id: $id) {
|
||||
id
|
||||
}
|
||||
}`, {
|
||||
id,
|
||||
description: 'Offensive content'
|
||||
})
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||
import helpers from "../../support/helpers";
|
||||
import slugify from "slug";
|
||||
|
||||
/* global cy */
|
||||
|
||||
@ -11,6 +10,7 @@ let loginCredentials = {
|
||||
password: "1234"
|
||||
};
|
||||
const narratorParams = {
|
||||
id: 'id-of-peter-pan',
|
||||
name: "Peter Pan",
|
||||
slug: "peter-pan",
|
||||
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
|
||||
@ -28,40 +28,20 @@ Given("we have a selection of categories", () => {
|
||||
Given("we have a selection of tags and categories as well as posts", () => {
|
||||
cy.createCategories("cat12")
|
||||
.factory()
|
||||
.authenticateAs(loginCredentials)
|
||||
.create("Tag", { id: "Ecology" })
|
||||
.create("Tag", { id: "Nature" })
|
||||
.create("Tag", { id: "Democracy" });
|
||||
const someAuthor = {
|
||||
id: "authorId",
|
||||
email: "author@example.org",
|
||||
password: "1234"
|
||||
};
|
||||
const yetAnotherAuthor = {
|
||||
id: "yetAnotherAuthor",
|
||||
email: "yet-another-author@example.org",
|
||||
password: "1234"
|
||||
};
|
||||
|
||||
cy.factory()
|
||||
.create("User", someAuthor)
|
||||
.authenticateAs(someAuthor)
|
||||
.create("Post", { id: "p0", categoryIds: ["cat12"] })
|
||||
.create("Post", { id: "p1", categoryIds: ["cat121"] });
|
||||
.create("User", { id: 'a1' })
|
||||
.create("Post", {authorId: 'a1', tagIds: [ "Ecology", "Nature", "Democracy" ], categoryIds: ["cat12"] })
|
||||
.create("Post", {authorId: 'a1', tagIds: [ "Nature", "Democracy" ], categoryIds: ["cat121"] });
|
||||
|
||||
cy.factory()
|
||||
.create("User", yetAnotherAuthor)
|
||||
.authenticateAs(yetAnotherAuthor)
|
||||
.create("Post", { id: "p2", categoryIds: ["cat12"] });
|
||||
.create("User", { id: 'a2'})
|
||||
.create("Post", { authorId: 'a2', tagIds: ['Nature', 'Democracy'], categoryIds: ["cat12"] });
|
||||
cy.factory()
|
||||
.authenticateAs(loginCredentials)
|
||||
.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" });
|
||||
.create("Post", { authorId: narratorParams.id, tagIds: ['Democracy'], categoryIds: ["cat122"] })
|
||||
});
|
||||
|
||||
Given("we have the following user accounts:", table => {
|
||||
@ -158,40 +138,28 @@ When("I press {string}", label => {
|
||||
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 => {
|
||||
table.hashes().forEach(({ Author, ...postAttributes }, i) => {
|
||||
Author = Author || `author-${i}`;
|
||||
const userAttributes = {
|
||||
name: Author,
|
||||
email: `${slugify(Author, { lower: true })}@example.org`,
|
||||
password: "1234"
|
||||
};
|
||||
postAttributes.deleted = Boolean(postAttributes.deleted);
|
||||
const disabled = Boolean(postAttributes.disabled);
|
||||
postAttributes.categoryIds = [`cat${i}${new Date()}`];
|
||||
postAttributes;
|
||||
cy.factory()
|
||||
.create("User", userAttributes)
|
||||
.authenticateAs(userAttributes)
|
||||
.create("Category", {
|
||||
id: `cat${i}${new Date()}`,
|
||||
name: "Just For Fun",
|
||||
slug: `just-for-fun-${i}`,
|
||||
icon: "smile"
|
||||
})
|
||||
.create("Post", postAttributes);
|
||||
if (disabled) {
|
||||
const moderatorParams = {
|
||||
email: "moderator@example.org",
|
||||
role: "moderator",
|
||||
password: "1234"
|
||||
};
|
||||
cy.factory()
|
||||
.create("User", moderatorParams)
|
||||
.authenticateAs(moderatorParams)
|
||||
.mutate("mutation($id: ID!) { disable(id: $id) }", postAttributes);
|
||||
cy.factory().create('Category', {
|
||||
id: `cat-456`,
|
||||
name: "Just For Fun",
|
||||
slug: `just-for-fun`,
|
||||
icon: "smile"
|
||||
})
|
||||
|
||||
table.hashes().forEach(({ ...postAttributes }, i) => {
|
||||
postAttributes = {
|
||||
...postAttributes,
|
||||
deleted: Boolean(postAttributes.deleted),
|
||||
disabled: Boolean(postAttributes.disabled),
|
||||
categoryIds: ['cat-456']
|
||||
}
|
||||
});
|
||||
cy.factory().create("Post", postAttributes);
|
||||
})
|
||||
});
|
||||
|
||||
Then("I see a success message:", message => {
|
||||
@ -210,11 +178,11 @@ When(
|
||||
);
|
||||
|
||||
Given("I previously created a post", () => {
|
||||
lastPost.authorId = narratorParams.id
|
||||
lastPost.title = "previously created post";
|
||||
lastPost.content = "with some content";
|
||||
lastPost.categoryIds = "cat0";
|
||||
lastPost.categoryIds = ["cat0"];
|
||||
cy.factory()
|
||||
.authenticateAs(loginCredentials)
|
||||
.create("Post", lastPost);
|
||||
});
|
||||
|
||||
@ -422,11 +390,7 @@ Given("I follow the user {string}", name => {
|
||||
Given('"Spammy Spammer" wrote a post {string}', title => {
|
||||
cy.createCategories("cat21")
|
||||
.factory()
|
||||
.authenticateAs({
|
||||
email: "spammy-spammer@example.org",
|
||||
password: "1234"
|
||||
})
|
||||
.create("Post", { title, categoryIds: ["cat21"] });
|
||||
.create("Post", { authorId: 'annoying-user', title, categoryIds: ["cat21"] });
|
||||
});
|
||||
|
||||
Then("the list of posts of this user is empty", () => {
|
||||
@ -445,8 +409,7 @@ Then("nobody is following the user profile anymore", () => {
|
||||
Given("I wrote a post {string}", title => {
|
||||
cy.createCategories(`cat213`, title)
|
||||
.factory()
|
||||
.authenticateAs(loginCredentials)
|
||||
.create("Post", { title, categoryIds: ["cat213"] });
|
||||
.create("Post", { authorId: narratorParams.id, title, categoryIds: ["cat213"] });
|
||||
});
|
||||
|
||||
When("I block the user {string}", name => {
|
||||
|
||||
@ -8,10 +8,12 @@ Feature: Report and Moderate
|
||||
So I can look into it and decide what to do
|
||||
|
||||
Background:
|
||||
Given we have this user in our database:
|
||||
| id | name |
|
||||
| u67 | David Irving|
|
||||
Given we have the following posts in our database:
|
||||
| Author | id | title | content |
|
||||
| David Irving | p1 | The Truth about the Holocaust | It never existed! |
|
||||
|
||||
| authorId | id | title | content |
|
||||
| u67 | p1 | The Truth about the Holocaust | It never existed! |
|
||||
|
||||
Scenario Outline: Report a post from various pages
|
||||
Given I am logged in with a "user" role
|
||||
|
||||
@ -7,7 +7,6 @@ Feature: Block a User
|
||||
Given I have a user account
|
||||
And there is an annoying user called "Spammy Spammer"
|
||||
And I am logged in
|
||||
And we have a selection of categories
|
||||
|
||||
Scenario: Block a 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
|
||||
Given we have the following posts in our database:
|
||||
| Author | id | title | content |
|
||||
| Some unblocked user | im-not-blocked | Post that should be seen | cause I'm not blocked |
|
||||
| id | title | content |
|
||||
| im-not-blocked | Post that should be seen | cause I'm not blocked |
|
||||
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
|
||||
When I search for "Spam"
|
||||
Then I should see the following posts in the select dropdown:
|
||||
|
||||
@ -54,6 +54,7 @@ services:
|
||||
- SMTP_HOST=mailserver
|
||||
- SMTP_PORT=25
|
||||
- SMTP_IGNORE_TLS=true
|
||||
- "DEBUG=${DEBUG}"
|
||||
neo4j:
|
||||
environment:
|
||||
- NEO4J_AUTH=none
|
||||
|
||||
@ -25,11 +25,16 @@ describe('Comment.vue', () => {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
},
|
||||
$filters: {
|
||||
truncate: a => a,
|
||||
},
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue(),
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
data: { DeleteComment: { id: 'it-is-the-deleted-comment' } },
|
||||
}),
|
||||
},
|
||||
}
|
||||
getters = {
|
||||
@ -113,24 +118,22 @@ describe('Comment.vue', () => {
|
||||
})
|
||||
|
||||
describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.deleteCommentCallback()
|
||||
beforeEach(async () => {
|
||||
await wrapper.vm.deleteCommentCallback()
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
beforeEach(jest.runAllTimers)
|
||||
it('emits "deleteComment"', () => {
|
||||
expect(wrapper.emitted('deleteComment')).toEqual([
|
||||
[{ id: 'it-is-the-deleted-comment' }],
|
||||
])
|
||||
})
|
||||
|
||||
it('emits "deleteComment"', () => {
|
||||
expect(wrapper.emitted().deleteComment.length).toBe(1)
|
||||
})
|
||||
it('does call mutation', () => {
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does call mutation', () => {
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('mutation is successful', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('mutation is successful', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -68,7 +68,6 @@ import ContentMenu from '~/components/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import HcEditCommentForm from '~/components/EditCommentForm/EditCommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import PostQuery from '~/graphql/PostQuery'
|
||||
|
||||
export default {
|
||||
data: function() {
|
||||
@ -143,26 +142,14 @@ export default {
|
||||
},
|
||||
async deleteCommentCallback() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
const {
|
||||
data: { DeleteComment },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||
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.$emit('deleteComment')
|
||||
this.$emit('deleteComment', DeleteComment)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
|
||||
@ -20,6 +20,9 @@ describe('CommentForm.vue', () => {
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
},
|
||||
$apollo: {
|
||||
mutate: jest
|
||||
.fn()
|
||||
|
||||
@ -18,11 +18,11 @@
|
||||
<ds-space margin-bottom="large" />
|
||||
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
||||
<comment
|
||||
v-for="(comment, index) in post.comments"
|
||||
v-for="comment in post.comments"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
:post="post"
|
||||
@deleteComment="post.comments.splice(index, 1)"
|
||||
@deleteComment="deleteComment"
|
||||
/>
|
||||
</div>
|
||||
<hc-empty v-else name="empty" icon="messages" />
|
||||
@ -40,5 +40,12 @@ export default {
|
||||
props: {
|
||||
post: { type: Object, default: () => {} },
|
||||
},
|
||||
methods: {
|
||||
deleteComment(deleted) {
|
||||
this.post.comments = this.post.comments.map(comment => {
|
||||
return comment.id === deleted.id ? deleted : comment
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -68,7 +68,7 @@ export default {
|
||||
this.disabled = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: CommentMutations().UpdateComment,
|
||||
mutation: CommentMutations(this.$i18n).UpdateComment,
|
||||
variables: {
|
||||
content: this.form.content,
|
||||
id: this.comment.id,
|
||||
|
||||
@ -38,7 +38,7 @@ const post = {
|
||||
],
|
||||
__typename: 'User',
|
||||
},
|
||||
commentedCount: 12,
|
||||
commentsCount: 12,
|
||||
categories: [],
|
||||
shoutedCount: 421,
|
||||
__typename: 'Post',
|
||||
|
||||
@ -50,7 +50,9 @@ describe('PostCard', () => {
|
||||
error: jest.fn(),
|
||||
},
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue(),
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
data: { DeletePost: { id: 'deleted-post-id' } },
|
||||
}),
|
||||
},
|
||||
}
|
||||
getters = {
|
||||
@ -94,7 +96,7 @@ describe('PostCard', () => {
|
||||
})
|
||||
|
||||
it('emits "removePostFromList"', () => {
|
||||
expect(wrapper.emitted().removePostFromList).toHaveLength(1)
|
||||
expect(wrapper.emitted('removePostFromList')).toEqual([[{ id: 'deleted-post-id' }]])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -51,9 +51,9 @@
|
||||
</span>
|
||||
|
||||
<!-- Comments Count -->
|
||||
<span :style="{ opacity: post.commentedCount ? 1 : 0.5 }">
|
||||
<span :style="{ opacity: post.commentsCount ? 1 : 0.5 }">
|
||||
<ds-icon name="comments" />
|
||||
<small>{{ post.commentedCount }}</small>
|
||||
<small>{{ post.commentsCount }}</small>
|
||||
</span>
|
||||
<!-- Menu -->
|
||||
<content-menu
|
||||
@ -118,9 +118,11 @@ export default {
|
||||
methods: {
|
||||
async deletePostCallback() {
|
||||
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.$emit('removePostFromList')
|
||||
this.$emit('removePostFromList', DeletePost)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ describe('SearchInput.vue', () => {
|
||||
name: 'Trick',
|
||||
slug: 'trick',
|
||||
},
|
||||
commentedCount: 0,
|
||||
commentsCount: 0,
|
||||
createdAt: '2019-03-13T11:00:20.835Z',
|
||||
id: 'p10',
|
||||
label: 'Eos aut illo omnis quis eaque et iure aut.',
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<ds-flex-item>
|
||||
<ds-text size="small" color="softer" class="search-meta">
|
||||
<span style="text-align: right;">
|
||||
<b>{{ option.commentedCount }}</b>
|
||||
<b>{{ option.commentsCount }}</b>
|
||||
<ds-icon name="comments" />
|
||||
</span>
|
||||
<span style="width: 36px; display: inline-block; text-align: right;">
|
||||
|
||||
@ -42,7 +42,7 @@ export default {
|
||||
const {
|
||||
data: { markAsRead },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: markAsReadMutation(),
|
||||
mutation: markAsReadMutation(this.$i18n),
|
||||
variables,
|
||||
})
|
||||
if (!(markAsRead && markAsRead.read === true)) return
|
||||
@ -56,12 +56,14 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
totalNotifications() {
|
||||
return this.notifications.length
|
||||
return (this.notifications || []).length
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
notifications: {
|
||||
query: notificationQuery(),
|
||||
query() {
|
||||
return notificationQuery(this.$i18n)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return {
|
||||
CreateComment: gql`
|
||||
mutation($postId: ID!, $content: String!) {
|
||||
@ -55,6 +56,31 @@ export default i18n => {
|
||||
mutation($id: ID!) {
|
||||
DeleteComment(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
74
webapp/graphql/Fragments.js
Normal file
74
webapp/graphql/Fragments.js
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,77 +1,20 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { postFragment, commentFragment, postCountsFragment } from './Fragments'
|
||||
|
||||
export default i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${postFragment(lang)}
|
||||
${postCountsFragment}
|
||||
${commentFragment(lang)}
|
||||
|
||||
query Post($id: ID!) {
|
||||
Post(id: $id) {
|
||||
id
|
||||
title
|
||||
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
|
||||
}
|
||||
...post
|
||||
...postCounts
|
||||
comments(orderBy: createdAt_asc) {
|
||||
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
|
||||
}
|
||||
}
|
||||
...comment
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
icon
|
||||
}
|
||||
shoutedCount
|
||||
shoutedByCurrentUser
|
||||
emotionsCount
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -80,45 +23,16 @@ export default i18n => {
|
||||
export const filterPosts = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
id
|
||||
title
|
||||
contentExcerpt
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
slug
|
||||
image
|
||||
author {
|
||||
id
|
||||
avatar
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
contributionsCount
|
||||
shoutedCount
|
||||
commentedCount
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
location {
|
||||
name: name${lang}
|
||||
}
|
||||
badges {
|
||||
id
|
||||
icon
|
||||
}
|
||||
${postFragment(lang)}
|
||||
${postCountsFragment}
|
||||
|
||||
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
...post
|
||||
...postCounts
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
icon
|
||||
}
|
||||
shoutedCount
|
||||
}
|
||||
}
|
||||
`
|
||||
`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -1,57 +1,5 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
import { postFragment, commentFragment } from './Fragments'
|
||||
|
||||
export default i18n => {
|
||||
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`
|
||||
${fragments}
|
||||
${commentFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
|
||||
query {
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
read
|
||||
@ -139,17 +90,27 @@ export const notificationQuery = () => {
|
||||
createdAt
|
||||
from {
|
||||
__typename
|
||||
...post
|
||||
...comment
|
||||
... on Post {
|
||||
...post
|
||||
}
|
||||
... on Comment {
|
||||
...comment
|
||||
post {
|
||||
...post
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const markAsReadMutation = () => {
|
||||
export const markAsReadMutation = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${fragments}
|
||||
${commentFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
|
||||
mutation($id: ID!) {
|
||||
markAsRead(id: $id) {
|
||||
read
|
||||
@ -157,8 +118,15 @@ export const markAsReadMutation = () => {
|
||||
createdAt
|
||||
from {
|
||||
__typename
|
||||
...post
|
||||
...comment
|
||||
... on Post {
|
||||
...post
|
||||
}
|
||||
... on Comment {
|
||||
...comment
|
||||
post {
|
||||
...post
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<hc-post-card
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
@removePostFromList="deletePost(index, post.id)"
|
||||
@removePostFromList="deletePost"
|
||||
/>
|
||||
</masonry-grid-item>
|
||||
</template>
|
||||
@ -115,7 +115,7 @@ export default {
|
||||
label: this.$t('sorting.commented'),
|
||||
value: 'Commented',
|
||||
icons: 'comment',
|
||||
order: 'commentedCount_desc',
|
||||
order: 'commentsCount_desc',
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -164,9 +164,9 @@ export default {
|
||||
showMoreContributions() {
|
||||
this.offset += this.pageSize
|
||||
},
|
||||
deletePost(_index, postId) {
|
||||
deletePost(deletedPost) {
|
||||
this.posts = this.posts.filter(post => {
|
||||
return post.id !== postId
|
||||
return post.id !== deletedPost.id
|
||||
})
|
||||
},
|
||||
},
|
||||
@ -185,7 +185,8 @@ export default {
|
||||
return result
|
||||
},
|
||||
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')
|
||||
this.posts = posts
|
||||
},
|
||||
|
||||
@ -36,11 +36,11 @@
|
||||
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
|
||||
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
|
||||
<hc-post-card
|
||||
v-for="(relatedPost, index) in post.relatedContributions"
|
||||
v-for="relatedPost in post.relatedContributions"
|
||||
:key="relatedPost.id"
|
||||
:post="relatedPost"
|
||||
:width="{ base: '100%', lg: 1 }"
|
||||
@removePostFromList="post.relatedContributions.splice(index, 1)"
|
||||
@removePostFromList="removePostFromList"
|
||||
/>
|
||||
</ds-flex>
|
||||
<hc-empty v-else margin="large" icon="file" message="No related Posts" />
|
||||
@ -50,9 +50,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import HcPostCard from '~/components/PostCard'
|
||||
import HcEmpty from '~/components/Empty.vue'
|
||||
import { relatedContributions } from '~/graphql/PostQuery'
|
||||
|
||||
export default {
|
||||
transition: {
|
||||
@ -68,57 +68,17 @@ export default {
|
||||
return this.Post ? this.Post[0] || {} : {}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removePostFromList(deletedPost) {
|
||||
this.post.relatedContributions = this.post.relatedContributions.filter(contribution => {
|
||||
return contribution.id !== deletedPost.id
|
||||
})
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
Post: {
|
||||
query() {
|
||||
return gql`
|
||||
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
|
||||
}
|
||||
}
|
||||
`
|
||||
return relatedContributions(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
|
||||
@ -220,11 +220,11 @@
|
||||
</ds-grid-item>
|
||||
|
||||
<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
|
||||
:post="post"
|
||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||
@removePostFromList="removePostFromList(index)"
|
||||
@removePostFromList="removePostFromList"
|
||||
/>
|
||||
</masonry-grid-item>
|
||||
</template>
|
||||
@ -345,8 +345,10 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removePostFromList(index) {
|
||||
this.posts.splice(index, 1)
|
||||
removePostFromList(deletedPost) {
|
||||
this.posts = this.posts.filter(post => {
|
||||
return post.id !== deletedPost.id
|
||||
})
|
||||
},
|
||||
handleTab(tab) {
|
||||
this.tabActive = tab
|
||||
@ -396,11 +398,11 @@ export default {
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
update({ Post }) {
|
||||
this.hasMore = Post && Post.length >= this.pageSize
|
||||
if (!Post) return
|
||||
// TODO: find out why `update` gets called twice initially.
|
||||
// We have to filter for uniq posts only because we get the same
|
||||
// result set twice.
|
||||
this.hasMore = Post.length >= this.pageSize
|
||||
this.posts = this.uniq([...this.posts, ...Post])
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user