mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-01-20 20:01:22 +00:00
Merged master in and updated what had to be updated
This commit is contained in:
parent
d16a1f62ff
commit
5c91962808
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Human-Connection gGmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
docker-compose.cypress.yml
Normal file
18
docker-compose.cypress.yml
Normal file
@ -0,0 +1,18 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
neo4j:
|
||||
environment:
|
||||
- NEO4J_AUTH=none
|
||||
ports:
|
||||
- 7687:7687
|
||||
- 7474:7474
|
||||
backend:
|
||||
ports:
|
||||
- 4001:4001
|
||||
- 4123:4123
|
||||
image: humanconnection/nitro-backend:builder
|
||||
build:
|
||||
context: .
|
||||
target: builder
|
||||
command: yarn run test:cypress
|
||||
BIN
humanconnection.png
Normal file
BIN
humanconnection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
29
src/jwt/decode.js
Normal file
29
src/jwt/decode.js
Normal file
@ -0,0 +1,29 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export default async (driver, authorizationHeader) => {
|
||||
if (!authorizationHeader) return null
|
||||
const token = authorizationHeader.replace('Bearer ', '')
|
||||
let id = null
|
||||
try {
|
||||
const decoded = await jwt.verify(token, process.env.JWT_SECRET)
|
||||
id = decoded.sub
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const session = driver.session()
|
||||
const query = `
|
||||
MATCH (user:User {id: {id} })
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role} as user
|
||||
LIMIT 1
|
||||
`
|
||||
const result = await session.run(query, { id })
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map((record) => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser
|
||||
}
|
||||
}
|
||||
17
src/jwt/encode.js
Normal file
17
src/jwt/encode.js
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import ms from 'ms'
|
||||
|
||||
// Generate an Access Token for the given User ID
|
||||
export default function encode (user) {
|
||||
const token = jwt.sign(user, process.env.JWT_SECRET, {
|
||||
expiresIn: ms('1d'),
|
||||
issuer: process.env.GRAPHQL_URI,
|
||||
audience: process.env.CLIENT_URI,
|
||||
subject: user.id.toString()
|
||||
})
|
||||
// jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => {
|
||||
// console.log('token verification:', err, data)
|
||||
// })
|
||||
return token
|
||||
}
|
||||
130
src/middleware/softDeleteMiddleware.spec.js
Normal file
130
src/middleware/softDeleteMiddleware.spec.js
Normal file
@ -0,0 +1,130 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { host, login } from '../jest/helpers'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
let query
|
||||
let action
|
||||
|
||||
beforeEach(async () => {
|
||||
await Promise.all([
|
||||
factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }),
|
||||
factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' })
|
||||
])
|
||||
await factory.authenticateAs({ email: 'user@example.org', password: '1234' })
|
||||
await Promise.all([
|
||||
factory.create('Post', { title: 'Deleted post', deleted: true }),
|
||||
factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }),
|
||||
factory.create('Post', { title: 'Publicly visible post', deleted: false })
|
||||
])
|
||||
const moderatorFactory = Factory()
|
||||
await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' })
|
||||
const disableMutation = `
|
||||
mutation {
|
||||
disable(resource: {
|
||||
id: "p2"
|
||||
type: contribution
|
||||
})
|
||||
}
|
||||
`
|
||||
await moderatorFactory.mutate(disableMutation)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('softDeleteMiddleware', () => {
|
||||
describe('Post', () => {
|
||||
action = () => {
|
||||
return client.request(query)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
query = '{ Post { title } }'
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('hides deleted or disabled posts', async () => {
|
||||
const expected = { Post: [{ title: 'Publicly visible post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('hides deleted or disabled posts', async () => {
|
||||
const expected = { Post: [{ title: 'Publicly visible post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter (deleted: true)', () => {
|
||||
beforeEach(() => {
|
||||
query = '{ Post(deleted: true) { title } }'
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorisation error', async () => {
|
||||
await expect(action()).rejects.toThrow('Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('shows deleted posts', async () => {
|
||||
const expected = { Post: [{ title: 'Deleted post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter (disabled: true)', () => {
|
||||
beforeEach(() => {
|
||||
query = '{ Post(disabled: true) { title } }'
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorisation error', async () => {
|
||||
await expect(action()).rejects.toThrow('Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('shows disabled posts', async () => {
|
||||
const expected = { Post: [{ title: 'Disabled post' }] }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
223
src/resolvers/badges.spec.js
Normal file
223
src/resolvers/badges.spec.js
Normal file
@ -0,0 +1,223 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
|
||||
describe('badges', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
email: 'user@example.org',
|
||||
role: 'user',
|
||||
password: '1234'
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
role: 'moderator',
|
||||
email: 'moderator@example.org'
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u3',
|
||||
role: 'admin',
|
||||
email: 'admin@example.org'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('CreateBadge', () => {
|
||||
const variables = {
|
||||
id: 'b1',
|
||||
key: 'indiegogo_en_racoon',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg'
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation(
|
||||
$id: ID
|
||||
$key: String!
|
||||
$type: BadgeTypeEnum!
|
||||
$status: BadgeStatusEnum!
|
||||
$icon: String!
|
||||
) {
|
||||
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
|
||||
id,
|
||||
key,
|
||||
type,
|
||||
status,
|
||||
icon
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
let client
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(
|
||||
client.request(mutation, variables)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
it('creates a badge', async () => {
|
||||
const expected = {
|
||||
CreateBadge: {
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||
id: 'b1',
|
||||
key: 'indiegogo_en_racoon',
|
||||
status: 'permanent',
|
||||
type: 'crowdfunding'
|
||||
}
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
client.request(mutation, variables)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateBadge', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
|
||||
await factory.create('Badge', { id: 'b1' })
|
||||
})
|
||||
const variables = {
|
||||
id: 'b1',
|
||||
key: 'whatever'
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($id: ID!, $key: String!) {
|
||||
UpdateBadge(id: $id, key: $key) {
|
||||
id
|
||||
key
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
let client
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(
|
||||
client.request(mutation, variables)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
client.request(mutation, variables)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
it('updates a badge', async () => {
|
||||
const expected = {
|
||||
UpdateBadge: {
|
||||
id: 'b1',
|
||||
key: 'whatever'
|
||||
}
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteBadge', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
|
||||
await factory.create('Badge', { id: 'b1' })
|
||||
})
|
||||
const variables = {
|
||||
id: 'b1'
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($id: ID!) {
|
||||
DeleteBadge(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
let client
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(
|
||||
client.request(mutation, variables)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
client.request(mutation, variables)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
it('deletes a badge', async () => {
|
||||
const expected = {
|
||||
DeleteBadge: {
|
||||
id: 'b1'
|
||||
}
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
115
src/resolvers/follow.spec.js
Normal file
115
src/resolvers/follow.spec.js
Normal file
@ -0,0 +1,115 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let clientUser1
|
||||
|
||||
const mutationFollowUser = (id) => `
|
||||
mutation {
|
||||
follow(id: "${id}", type: User)
|
||||
}
|
||||
`
|
||||
const mutationUnfollowUser = (id) => `
|
||||
mutation {
|
||||
unfollow(id: "${id}", type: User)
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
id: 'u1',
|
||||
email: 'test@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
email: 'test2@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('follow ', () => {
|
||||
describe('(un)follow user', () => {
|
||||
let headersUser1
|
||||
beforeEach(async () => {
|
||||
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
|
||||
clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
|
||||
})
|
||||
|
||||
it('I can follow another user', async () => {
|
||||
const res = await clientUser1.request(
|
||||
mutationFollowUser('u2')
|
||||
)
|
||||
const expected = {
|
||||
follow: true
|
||||
}
|
||||
expect(res).toMatchObject(expected)
|
||||
|
||||
const { User } = await clientUser1.request(`{
|
||||
User(id: "u2") {
|
||||
followedBy { id }
|
||||
followedByCurrentUser
|
||||
}
|
||||
}`)
|
||||
const expected2 = {
|
||||
followedBy: [
|
||||
{ id: 'u1' }
|
||||
],
|
||||
followedByCurrentUser: true
|
||||
}
|
||||
expect(User[0]).toMatchObject(expected2)
|
||||
})
|
||||
|
||||
it('I can unfollow a user', async () => {
|
||||
// follow
|
||||
await clientUser1.request(
|
||||
mutationFollowUser('u2')
|
||||
)
|
||||
const expected = {
|
||||
unfollow: true
|
||||
}
|
||||
// unfollow
|
||||
const res = await clientUser1.request(mutationUnfollowUser('u2'))
|
||||
expect(res).toMatchObject(expected)
|
||||
|
||||
const { User } = await clientUser1.request(`{
|
||||
User(id: "u2") {
|
||||
followedBy { id }
|
||||
followedByCurrentUser
|
||||
}
|
||||
}`)
|
||||
const expected2 = {
|
||||
followedBy: [],
|
||||
followedByCurrentUser: false
|
||||
}
|
||||
expect(User[0]).toMatchObject(expected2)
|
||||
})
|
||||
|
||||
it('I can`t follow myself', async () => {
|
||||
const res = await clientUser1.request(
|
||||
mutationFollowUser('u1')
|
||||
)
|
||||
const expected = {
|
||||
follow: false
|
||||
}
|
||||
expect(res).toMatchObject(expected)
|
||||
|
||||
const { User } = await clientUser1.request(`{
|
||||
User(id: "u1") {
|
||||
followedBy { id }
|
||||
followedByCurrentUser
|
||||
}
|
||||
}`)
|
||||
const expected2 = {
|
||||
followedBy: [],
|
||||
followedByCurrentUser: false
|
||||
}
|
||||
expect(User[0]).toMatchObject(expected2)
|
||||
})
|
||||
})
|
||||
})
|
||||
30
src/resolvers/moderation.js
Normal file
30
src/resolvers/moderation.js
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
Mutation: {
|
||||
disable: async (object, params, { user, driver }) => {
|
||||
const { resource: { id } } = params
|
||||
const { id: userId } = user
|
||||
const cypher = `
|
||||
MATCH (u:User {id: $userId})
|
||||
MATCH (r {id: $id})
|
||||
SET r.disabled = true
|
||||
MERGE (r)<-[:DISABLED]-(u)
|
||||
`
|
||||
const session = driver.session()
|
||||
const res = await session.run(cypher, { id, userId })
|
||||
session.close()
|
||||
return Boolean(res)
|
||||
},
|
||||
enable: async (object, params, { user, driver }) => {
|
||||
const { resource: { id } } = params
|
||||
const cypher = `
|
||||
MATCH (r {id: $id})<-[d:DISABLED]-()
|
||||
SET r.disabled = false
|
||||
DELETE d
|
||||
`
|
||||
const session = driver.session()
|
||||
const res = await session.run(cypher, { id })
|
||||
session.close()
|
||||
return Boolean(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
370
src/resolvers/moderation.spec.js
Normal file
370
src/resolvers/moderation.spec.js
Normal file
@ -0,0 +1,370 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
|
||||
const setupAuthenticateClient = (params) => {
|
||||
const authenticateClient = async () => {
|
||||
await factory.create('User', params)
|
||||
const headers = await login(params)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
}
|
||||
return authenticateClient
|
||||
}
|
||||
|
||||
let setup
|
||||
const runSetup = async () => {
|
||||
await setup.createResource()
|
||||
await setup.authenticateClient()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setup = {
|
||||
createResource: () => {
|
||||
},
|
||||
authenticateClient: () => {
|
||||
client = new GraphQLClient(host)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('disable', () => {
|
||||
const mutation = `
|
||||
mutation($id: ID!, $type: ResourceEnum!) {
|
||||
disable(resource: { id: $id, type: $type })
|
||||
}
|
||||
`
|
||||
let variables
|
||||
|
||||
beforeEach(() => {
|
||||
// our defaul set of variables
|
||||
variables = {
|
||||
id: 'blabla',
|
||||
type: 'contribution'
|
||||
}
|
||||
})
|
||||
|
||||
const action = async () => {
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await runSetup()
|
||||
await expect(action()).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(() => {
|
||||
setup.authenticateClient = setupAuthenticateClient({
|
||||
email: 'user@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await runSetup()
|
||||
await expect(action()).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(() => {
|
||||
setup.authenticateClient = setupAuthenticateClient({
|
||||
id: 'u7',
|
||||
email: 'moderator@example.org',
|
||||
password: '1234',
|
||||
role: 'moderator'
|
||||
})
|
||||
})
|
||||
|
||||
describe('on a comment', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
id: 'c47',
|
||||
type: 'comment'
|
||||
}
|
||||
|
||||
setup.createResource = async () => {
|
||||
await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' })
|
||||
await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
|
||||
await Promise.all([
|
||||
factory.create('Post', { id: 'p3' }),
|
||||
factory.create('Comment', { id: 'c47' })
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }),
|
||||
factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' })
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
const expected = { disable: true }
|
||||
await runSetup()
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('changes .disabledBy', async () => {
|
||||
const before = { Comment: [{ id: 'c47', disabledBy: null }] }
|
||||
const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Comment { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action()
|
||||
await expect(client.request(
|
||||
'{ Comment(disabled: true) { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('updates .disabled on comment', async () => {
|
||||
const before = { Comment: [ { id: 'c47', disabled: false } ] }
|
||||
const expected = { Comment: [ { id: 'c47', disabled: true } ] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Comment { id disabled } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action()
|
||||
await expect(client.request(
|
||||
'{ Comment(disabled: true) { id disabled } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on a post', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
id: 'p9',
|
||||
type: 'contribution'
|
||||
}
|
||||
|
||||
setup.createResource = async () => {
|
||||
await factory.create('User', { email: 'author@example.org', password: '1234' })
|
||||
await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
|
||||
await factory.create('Post', {
|
||||
id: 'p9' // that's the ID we will look for
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
const expected = { disable: true }
|
||||
await runSetup()
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('changes .disabledBy', async () => {
|
||||
const before = { Post: [{ id: 'p9', disabledBy: null }] }
|
||||
const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Post { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action()
|
||||
await expect(client.request(
|
||||
'{ Post(disabled: true) { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('updates .disabled on post', async () => {
|
||||
const before = { Post: [ { id: 'p9', disabled: false } ] }
|
||||
const expected = { Post: [ { id: 'p9', disabled: true } ] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Post { id disabled } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action()
|
||||
await expect(client.request(
|
||||
'{ Post(disabled: true) { id disabled } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('enable', () => {
|
||||
const mutation = `
|
||||
mutation($id: ID!, $type: ResourceEnum!) {
|
||||
enable(resource: { id: $id, type: $type })
|
||||
}
|
||||
`
|
||||
let variables
|
||||
|
||||
const action = async () => {
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// our defaul set of variables
|
||||
variables = {
|
||||
id: 'blabla',
|
||||
type: 'contribution'
|
||||
}
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await runSetup()
|
||||
await expect(action()).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(() => {
|
||||
setup.authenticateClient = setupAuthenticateClient({
|
||||
email: 'user@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await runSetup()
|
||||
await expect(action()).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
setup.authenticateClient = setupAuthenticateClient({
|
||||
role: 'moderator',
|
||||
email: 'someUser@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
describe('on a comment', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
id: 'c456',
|
||||
type: 'comment'
|
||||
}
|
||||
|
||||
setup.createResource = async () => {
|
||||
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
|
||||
await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
|
||||
await Promise.all([
|
||||
factory.create('Post', { id: 'p9' }),
|
||||
factory.create('Comment', { id: 'c456' })
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }),
|
||||
factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' })
|
||||
])
|
||||
|
||||
const disableMutation = `
|
||||
mutation {
|
||||
disable(resource: {
|
||||
id: "c456"
|
||||
type: comment
|
||||
})
|
||||
}
|
||||
`
|
||||
await factory.mutate(disableMutation) // that's we want to delete
|
||||
}
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
const expected = { enable: true }
|
||||
await runSetup()
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('changes .disabledBy', async () => {
|
||||
const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] }
|
||||
const expected = { Comment: [{ id: 'c456', disabledBy: null }] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Comment(disabled: true) { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action()
|
||||
await expect(client.request(
|
||||
'{ Comment { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('updates .disabled on post', async () => {
|
||||
const before = { Comment: [ { id: 'c456', disabled: true } ] }
|
||||
const expected = { Comment: [ { id: 'c456', disabled: false } ] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Comment(disabled: true) { id disabled } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action() // this updates .disabled
|
||||
await expect(client.request(
|
||||
'{ Comment { id disabled } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on a post', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
id: 'p9',
|
||||
type: 'contribution'
|
||||
}
|
||||
|
||||
setup.createResource = async () => {
|
||||
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
|
||||
await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
|
||||
await factory.create('Post', {
|
||||
id: 'p9' // that's the ID we will look for
|
||||
})
|
||||
|
||||
const disableMutation = `
|
||||
mutation {
|
||||
disable(resource: {
|
||||
id: "p9"
|
||||
type: contribution
|
||||
})
|
||||
}
|
||||
`
|
||||
await factory.mutate(disableMutation) // that's we want to delete
|
||||
}
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
const expected = { enable: true }
|
||||
await runSetup()
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('changes .disabledBy', async () => {
|
||||
const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] }
|
||||
const expected = { Post: [{ id: 'p9', disabledBy: null }] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Post(disabled: true) { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action()
|
||||
await expect(client.request(
|
||||
'{ Post { id, disabledBy { id } } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('updates .disabled on post', async () => {
|
||||
const before = { Post: [ { id: 'p9', disabled: true } ] }
|
||||
const expected = { Post: [ { id: 'p9', disabled: false } ] }
|
||||
|
||||
await runSetup()
|
||||
await expect(client.request(
|
||||
'{ Post(disabled: true) { id disabled } }'
|
||||
)).resolves.toEqual(before)
|
||||
await action() // this updates .disabled
|
||||
await expect(client.request(
|
||||
'{ Post { id disabled } }'
|
||||
)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
63
src/resolvers/posts.js
Normal file
63
src/resolvers/posts.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import { activityPub } from '../activitypub/ActivityPub'
|
||||
import uuid from 'uuid/v4'
|
||||
import as from 'activitystrea.ms'
|
||||
/*
|
||||
import as from 'activitystrea.ms'
|
||||
import request from 'request'
|
||||
*/
|
||||
|
||||
const debug = require('debug')('backend:schema')
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: async (object, params, context, resolveInfo) => {
|
||||
params.activityId = uuid()
|
||||
const result = await neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
|
||||
const session = context.driver.session()
|
||||
const author = await session.run(
|
||||
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
|
||||
'MERGE (post)<-[:WROTE]-(author) ' +
|
||||
'RETURN author', {
|
||||
userId: context.user.id,
|
||||
postId: result.id
|
||||
})
|
||||
|
||||
debug(`actorId = ${author.records[0]._fields[0].properties.actorId}`)
|
||||
if (Array.isArray(author.records) && author.records.length > 0) {
|
||||
const actorId = author.records[0]._fields[0].properties.actorId
|
||||
const createActivity = await new Promise((resolve, reject) => {
|
||||
as.create()
|
||||
.id(`${actorId}/status/${params.activityId}`)
|
||||
.actor(`${actorId}`)
|
||||
.object(
|
||||
as.article()
|
||||
.id(`${actorId}/status/${result.id}`)
|
||||
.content(result.content)
|
||||
.to('https://www.w3.org/ns/activitystreams#Public')
|
||||
.publishedNow()
|
||||
.attributedTo(`${actorId}`)
|
||||
).prettyWrite((err, doc) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
debug(doc)
|
||||
const parsedDoc = JSON.parse(doc)
|
||||
parsedDoc.send = true
|
||||
resolve(JSON.stringify(parsedDoc))
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
await activityPub.sendActivity(createActivity)
|
||||
} catch (e) {
|
||||
debug(`error sending post activity = ${JSON.stringify(e, null, 2)}`)
|
||||
}
|
||||
}
|
||||
session.close()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
202
src/resolvers/posts.spec.js
Normal file
202
src/resolvers/posts.spec.js
Normal file
@ -0,0 +1,202 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
email: 'test@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('CreatePost', () => {
|
||||
const mutation = `
|
||||
mutation {
|
||||
CreatePost(title: "I am a title", content: "Some content") {
|
||||
title
|
||||
content
|
||||
slug
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('creates a post', async () => {
|
||||
const expected = {
|
||||
CreatePost: {
|
||||
title: 'I am a title',
|
||||
content: 'Some content'
|
||||
}
|
||||
}
|
||||
await expect(client.request(mutation)).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
it('assigns the authenticated user as author', async () => {
|
||||
await client.request(mutation)
|
||||
const { User } = await client.request(`{
|
||||
User(email:"test@example.org") {
|
||||
contributions {
|
||||
title
|
||||
}
|
||||
}
|
||||
}`, { headers })
|
||||
expect(User).toEqual([ { contributions: [ { title: 'I am a title' } ] } ])
|
||||
})
|
||||
|
||||
describe('disabled and deleted', () => {
|
||||
it('initially false', async () => {
|
||||
const expected = { CreatePost: { disabled: false, deleted: false } }
|
||||
await expect(client.request(mutation)).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdatePost', () => {
|
||||
const mutation = `
|
||||
mutation($id: ID!, $content: String) {
|
||||
UpdatePost(id: $id, content: $content) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let variables = {
|
||||
id: 'p1',
|
||||
content: 'New content'
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const asAuthor = Factory()
|
||||
await asAuthor.create('User', {
|
||||
email: 'author@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await asAuthor.authenticateAs({
|
||||
email: 'author@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await asAuthor.create('Post', {
|
||||
id: 'p1',
|
||||
content: 'Old content'
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'author@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('updates a post', async () => {
|
||||
const expected = { UpdatePost: { id: 'p1', content: 'New content' } }
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeletePost', () => {
|
||||
const mutation = `
|
||||
mutation($id: ID!) {
|
||||
DeletePost(id: $id) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let variables = {
|
||||
id: 'p1'
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const asAuthor = Factory()
|
||||
await asAuthor.create('User', {
|
||||
email: 'author@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await asAuthor.authenticateAs({
|
||||
email: 'author@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await asAuthor.create('Post', {
|
||||
id: 'p1',
|
||||
content: 'To be deleted'
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'author@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('deletes a post', async () => {
|
||||
const expected = { DeletePost: { id: 'p1', content: 'To be deleted' } }
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
51
src/resolvers/reports.js
Normal file
51
src/resolvers/reports.js
Normal file
@ -0,0 +1,51 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
report: async (parent, { resource, description }, { driver, req, user }, resolveInfo) => {
|
||||
const contextId = uuid()
|
||||
const session = driver.session()
|
||||
const data = {
|
||||
id: contextId,
|
||||
type: resource.type,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
description: resource.description
|
||||
}
|
||||
await session.run(
|
||||
'CREATE (r:Report $report) ' +
|
||||
'RETURN r.id, r.type, r.description', {
|
||||
report: data
|
||||
}
|
||||
)
|
||||
let contentType
|
||||
|
||||
switch (resource.type) {
|
||||
case 'post':
|
||||
case 'contribution':
|
||||
contentType = 'Post'
|
||||
break
|
||||
case 'comment':
|
||||
contentType = 'Comment'
|
||||
break
|
||||
case 'user':
|
||||
contentType = 'User'
|
||||
break
|
||||
}
|
||||
|
||||
await session.run(
|
||||
`MATCH (author:User {id: $userId}), (context:${contentType} {id: $resourceId}), (report:Report {id: $contextId}) ` +
|
||||
'MERGE (report)<-[:REPORTED]-(author) ' +
|
||||
'MERGE (context)<-[:REPORTED]-(report) ' +
|
||||
'RETURN context', {
|
||||
resourceId: resource.id,
|
||||
userId: user.id,
|
||||
contextId: contextId
|
||||
}
|
||||
)
|
||||
session.close()
|
||||
|
||||
// TODO: output Report compatible object
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/resolvers/reports.spec.js
Normal file
68
src/resolvers/reports.spec.js
Normal file
@ -0,0 +1,68 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
|
||||
describe('report', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
email: 'test@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
name: 'abusive-user',
|
||||
role: 'user',
|
||||
email: 'abusive-user@example.org'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
let client
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(
|
||||
client.request(`mutation {
|
||||
report(
|
||||
description: "I don't like this user",
|
||||
resource: {
|
||||
id: "u2",
|
||||
type: user
|
||||
}
|
||||
) { id, createdAt }
|
||||
}`)
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
let response
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
response = await client.request(`mutation {
|
||||
report(
|
||||
description: "I don't like this user",
|
||||
resource: {
|
||||
id: "u2",
|
||||
type: user
|
||||
}
|
||||
) { id, createdAt }
|
||||
}`,
|
||||
{ headers }
|
||||
)
|
||||
})
|
||||
it('creates a report', () => {
|
||||
let { id, createdAt } = response.report
|
||||
expect(response).toEqual({
|
||||
report: { id, createdAt }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
126
src/resolvers/shout.spec.js
Normal file
126
src/resolvers/shout.spec.js
Normal file
@ -0,0 +1,126 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let clientUser1, clientUser2
|
||||
|
||||
const mutationShoutPost = (id) => `
|
||||
mutation {
|
||||
shout(id: "${id}", type: Post)
|
||||
}
|
||||
`
|
||||
const mutationUnshoutPost = (id) => `
|
||||
mutation {
|
||||
unshout(id: "${id}", type: Post)
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
id: 'u1',
|
||||
email: 'test@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
email: 'test2@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('shout ', () => {
|
||||
describe('(un)shout foreign post', () => {
|
||||
let headersUser1, headersUser2
|
||||
beforeEach(async () => {
|
||||
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
|
||||
headersUser2 = await login({ email: 'test2@example.org', password: '1234' })
|
||||
clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
|
||||
clientUser2 = new GraphQLClient(host, { headers: headersUser2 })
|
||||
|
||||
await clientUser1.request(`
|
||||
mutation {
|
||||
CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`)
|
||||
await clientUser2.request(`
|
||||
mutation {
|
||||
CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('I shout a post of another user', async () => {
|
||||
const res = await clientUser1.request(
|
||||
mutationShoutPost('p2')
|
||||
)
|
||||
const expected = {
|
||||
shout: true
|
||||
}
|
||||
expect(res).toMatchObject(expected)
|
||||
|
||||
const { Post } = await clientUser1.request(`{
|
||||
Post(id: "p2") {
|
||||
shoutedByCurrentUser
|
||||
}
|
||||
}`)
|
||||
const expected2 = {
|
||||
shoutedByCurrentUser: true
|
||||
}
|
||||
expect(Post[0]).toMatchObject(expected2)
|
||||
})
|
||||
|
||||
it('I unshout a post of another user', async () => {
|
||||
// shout
|
||||
await clientUser1.request(
|
||||
mutationShoutPost('p2')
|
||||
)
|
||||
const expected = {
|
||||
unshout: true
|
||||
}
|
||||
// unshout
|
||||
const res = await clientUser1.request(mutationUnshoutPost('p2'))
|
||||
expect(res).toMatchObject(expected)
|
||||
|
||||
const { Post } = await clientUser1.request(`{
|
||||
Post(id: "p2") {
|
||||
shoutedByCurrentUser
|
||||
}
|
||||
}`)
|
||||
const expected2 = {
|
||||
shoutedByCurrentUser: false
|
||||
}
|
||||
expect(Post[0]).toMatchObject(expected2)
|
||||
})
|
||||
|
||||
it('I can`t shout my own post', async () => {
|
||||
const res = await clientUser1.request(
|
||||
mutationShoutPost('p1')
|
||||
)
|
||||
const expected = {
|
||||
shout: false
|
||||
}
|
||||
expect(res).toMatchObject(expected)
|
||||
|
||||
const { Post } = await clientUser1.request(`{
|
||||
Post(id: "p1") {
|
||||
shoutedByCurrentUser
|
||||
}
|
||||
}`)
|
||||
const expected2 = {
|
||||
shoutedByCurrentUser: false
|
||||
}
|
||||
expect(Post[0]).toMatchObject(expected2)
|
||||
})
|
||||
})
|
||||
})
|
||||
67
src/resolvers/statistics.js
Normal file
67
src/resolvers/statistics.js
Normal file
@ -0,0 +1,67 @@
|
||||
export const query = (cypher, session) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = []
|
||||
session
|
||||
.run(cypher)
|
||||
.subscribe({
|
||||
onNext: function (record) {
|
||||
let item = {}
|
||||
record.keys.forEach(key => {
|
||||
item[key] = record.get(key)
|
||||
})
|
||||
data.push(item)
|
||||
},
|
||||
onCompleted: function () {
|
||||
session.close()
|
||||
resolve(data)
|
||||
},
|
||||
onError: function (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
const queryOne = (cypher, session) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
query(cypher, session)
|
||||
.then(res => {
|
||||
resolve(res.length ? res.pop() : {})
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
statistics: async (parent, args, { driver, user }) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const session = driver.session()
|
||||
const queries = {
|
||||
countUsers: 'MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers',
|
||||
countPosts: 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts',
|
||||
countComments: 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments',
|
||||
countNotifications: 'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications',
|
||||
countOrganizations: 'MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations',
|
||||
countProjects: 'MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects',
|
||||
countInvites: 'MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites',
|
||||
countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows',
|
||||
countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts'
|
||||
}
|
||||
let data = {
|
||||
countUsers: (await queryOne(queries.countUsers, session)).countUsers.low,
|
||||
countPosts: (await queryOne(queries.countPosts, session)).countPosts.low,
|
||||
countComments: (await queryOne(queries.countComments, session)).countComments.low,
|
||||
countNotifications: (await queryOne(queries.countNotifications, session)).countNotifications.low,
|
||||
countOrganizations: (await queryOne(queries.countOrganizations, session)).countOrganizations.low,
|
||||
countProjects: (await queryOne(queries.countProjects, session)).countProjects.low,
|
||||
countInvites: (await queryOne(queries.countInvites, session)).countInvites.low,
|
||||
countFollows: (await queryOne(queries.countFollows, session)).countFollows.low,
|
||||
countShouts: (await queryOne(queries.countShouts, session)).countShouts.low
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/resolvers/user_management.js
Normal file
51
src/resolvers/user_management.js
Normal file
@ -0,0 +1,51 @@
|
||||
import encode from '../jwt/encode'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { AuthenticationError } from 'apollo-server'
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
isLoggedIn: (parent, args, { driver, user }) => {
|
||||
return Boolean(user && user.id)
|
||||
},
|
||||
currentUser: async (object, params, ctx, resolveInfo) => {
|
||||
const { user } = ctx
|
||||
if (!user) return null
|
||||
return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false)
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
signup: async (parent, { email, password }, { req }) => {
|
||||
// if (data[email]) {
|
||||
// throw new Error('Another User with same email exists.')
|
||||
// }
|
||||
// data[email] = {
|
||||
// password: await bcrypt.hashSync(password, 10),
|
||||
// }
|
||||
|
||||
return true
|
||||
},
|
||||
login: async (parent, { email, password }, { driver, req, user }) => {
|
||||
// if (user && user.id) {
|
||||
// throw new Error('Already logged in.')
|
||||
// }
|
||||
const session = driver.session()
|
||||
return session.run(
|
||||
'MATCH (user:User {email: $userEmail}) ' +
|
||||
'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role} as user LIMIT 1', {
|
||||
userEmail: email
|
||||
})
|
||||
.then(async (result) => {
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map(function (record) {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
if (currentUser && await bcrypt.compareSync(password, currentUser.password)) {
|
||||
delete currentUser.password
|
||||
return encode(currentUser)
|
||||
} else throw new AuthenticationError('Incorrect email address or password.')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/resolvers/user_management.spec.js
Normal file
179
src/resolvers/user_management.spec.js
Normal file
@ -0,0 +1,179 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient, request } from 'graphql-request'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
|
||||
// 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' }
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg',
|
||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
role: 'user',
|
||||
email: 'test@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('isLoggedIn', () => {
|
||||
const query = '{ isLoggedIn }'
|
||||
describe('unauthenticated', () => {
|
||||
it('returns false', async () => {
|
||||
await expect(request(host, query)).resolves.toEqual({ 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('with valid JWT Bearer token', () => {
|
||||
const client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
|
||||
|
||||
it('returns false', async () => {
|
||||
await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false })
|
||||
})
|
||||
|
||||
describe('and a corresponding user in the database', () => {
|
||||
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('currentUser', () => {
|
||||
const query = `{
|
||||
currentUser {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
email
|
||||
role
|
||||
}
|
||||
}`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns null', async () => {
|
||||
const expected = { currentUser: null }
|
||||
await expect(request(host, query)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
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('and corresponding user in the database', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('returns the whole user object', async () => {
|
||||
const expected = {
|
||||
currentUser: {
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg',
|
||||
email: 'test@example.org',
|
||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
role: 'user'
|
||||
}
|
||||
}
|
||||
await expect(client.request(query)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
const mutation = (params) => {
|
||||
const { email, password } = params
|
||||
return `
|
||||
mutation {
|
||||
login(email:"${email}", password:"${password}")
|
||||
}`
|
||||
}
|
||||
|
||||
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
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, data) => {
|
||||
expect(data.email).toEqual('test@example.org')
|
||||
expect(err).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a valid email but incorrect password', () => {
|
||||
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.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a non-existing email', () => {
|
||||
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.')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user