merge upstream/552-update_comment

This commit is contained in:
ALau2088 2019-06-08 15:55:51 -07:00
commit 22f166adac
333 changed files with 8457 additions and 5188 deletions

View File

@ -5,7 +5,7 @@ before submitting a new issue. Following one of the issue templates will ensure
Thanks!
-->
## Issue
## 💬 Issue
<!-- Describe your Issue in detail. -->
<!-- Attach screenshots and drawings if needed. -->

View File

@ -1,7 +1,8 @@
---
name: 🐛 Bug report
about: Create a report to help us improve
labels: bug
title: 🐛 [Bug]
---
## :bug: Bugreport

View File

@ -1,22 +1,12 @@
---
name: 🚀 Feature request
about: Suggest an idea for this project
labels: feature
title: 🚀 [Feature]
---
## :rocket: Feature
<!-- Describe the Feature. -->
### Is your feature request related to a problem? Please describe.
<!-- A clear and concise description of what the problem is.
Ex. I'm always frustrated when [...] -->
### Describe the prefered solution and alternatives you've considered
<!-- A clear and concise description of what you want to happen.
Are there any alternative solutions or features you've considered? -->
<!-- Describe the Feature. Use Screenshots if possible. -->
### Design & Layout
<!-- Attach Screenshots and Drawings. -->

View File

@ -1,6 +1,8 @@
---
name: 💬 Question
about: If you need help understanding HumanConnection.
labels: question
title: 💬 [Question]
---
<!-- Chat with Team HumanConnection -->
<!-- If you need an answer right away, visit the HumanConnection Discord:

View File

@ -1,27 +1,12 @@
## Pullrequest
<!-- Describe the Pullrequest. -->
## 🍰 Pullrequest
<!-- Describe the Pullrequest. Use Screenshots if possible. -->
### Issues
<!-- Which Issues does this fix, which are related?
- fixes #XXX
- relates #XXX
-->
- [X] None
### Checklist
<!-- Anything important to be thought of when deploying?
- [ ] Env-Variables adjustment needed
- [ ] Breaking/critical change
-->
- [X] None
### How2Test
<!-- Give a detailed description how to test your PR and confirm it is working as expected. -->
<!-- Maintainers will check the Tests
- [ ] Test1
- [ ] Test2
-->
- [X] None
- None
### Todo
<!-- In case some parts are still missing, list them here. -->

1
.gitignore vendored
View File

@ -1,7 +1,6 @@
.env
.idea
*.iml
.vscode
.DS_Store
npm-debug.log*
yarn-debug.log*

View File

@ -17,12 +17,13 @@ before_install:
install:
- docker-compose -f docker-compose.yml -f docker-compose.travis.yml up --build -d
- wait-on http://localhost:7474 && docker-compose exec neo4j migrate
# avoid "Database constraints have changed after this transaction started"
- wait-on http://localhost:7474
script:
# Backend
- docker-compose exec backend yarn run lint
- docker-compose exec backend yarn run test:jest --ci --verbose=false
- docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
- docker-compose exec backend yarn run db:reset
- docker-compose exec backend yarn run db:seed
- docker-compose exec backend yarn run test:cucumber
@ -30,7 +31,7 @@ script:
- docker-compose exec backend yarn run db:seed
# Frontend
- docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test --ci --verbose=false
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
- docker-compose exec -d backend yarn run test:before:seeder
# Fullstack
- CYPRESS_RETRIES=1 yarn run cypress:run

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"octref.vetur",
"gruntfuggly.todo-tree",
]
}

12
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": true
}
],
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true
}

View File

@ -3,6 +3,7 @@
* [Introduction](README.md)
* [Edit this Documentation](edit-this-documentation.md)
* [Installation](installation.md)
* [Neo4J](neo4j/README.md)
* [Backend](backend/README.md)
* [GraphQL](backend/graphql.md)
* [Webapp](webapp/README.md)

View File

@ -4,7 +4,7 @@ NEO4J_PASSWORD=letmein
GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
MOCK=false
MOCKS=false
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"

View File

@ -1,20 +1,25 @@
module.exports = {
"extends": "standard",
"parser": "babel-eslint",
"env": {
"es6": true,
"node": true,
"jest/globals": true
env: {
es6: true,
node: true,
jest: true
},
"rules": {
"indent": [
"error",
2
],
"quotes": [
"error",
"single"
]
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'standard',
'plugin:prettier/recommended'
],
plugins: [
'jest'
],
rules: {
//'indent': [ 'error', 2 ],
//'quotes': [ "error", "single"],
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-console': ['error'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'prettier/prettier': ['error'],
},
"plugins": ["jest"]
};

9
backend/.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
semi: false,
printWidth: 100,
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true
};

View File

@ -1,4 +1,4 @@
FROM node:10-alpine as base
FROM node:12.4-alpine as base
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
EXPOSE 4000
@ -7,6 +7,9 @@ ENV BUILD_COMMIT=$BUILD_COMMIT
ARG WORKDIR=/nitro-backend
RUN mkdir -p $WORKDIR
WORKDIR $WORKDIR
RUN apk --no-cache add git
COPY package.json yarn.lock ./
COPY .env.template .env
CMD ["yarn", "run", "start"]

View File

@ -1,8 +1,6 @@
# Backend
## Installation
{% tabs %}
{% tab title="Docker" %}
## Installation with Docker
Run the following command to install everything through docker.
@ -14,28 +12,15 @@ $ docker-compose up
# rebuild the containers for a cleanup
$ docker-compose up --build
```
Open another terminal and create unique indices with:
```bash
$ docker-compose exec neo4j migrate
```
Wait a little until your backend is up and running at [http://localhost:4000/](http://localhost:4000/).
{% endtab %}
{% tab title="Without Docker" %}
## Installation without Docker
For the local installation you need a recent version of [node](https://nodejs.org/en/)
(&gt;= `v10.12.0`) and [Neo4J](https://neo4j.com/) along with
[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) plugin installed
on your system.
(&gt;= `v10.12.0`).
Download [Neo4j Community Edition](https://neo4j.com/download-center/#releases) and unpack the files.
Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases) and drop the file into the `plugins` folder of the just extracted Neo4j-Server
Note that grand-stack-starter does not currently bundle a distribution of Neo4j. You can download [Neo4j Desktop](https://neo4j.com/download/) and run locally for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/) or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). Just be sure to update the Neo4j connection string and credentials accordingly in `.env`.
Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474).
Now install node dependencies with [yarn](https://yarnpkg.com/en/):
Install node dependencies with [yarn](https://yarnpkg.com/en/):
```bash
$ cd backend
$ yarn install
@ -46,14 +31,8 @@ Copy Environment Variables:
# in backend/
$ cp .env.template .env
```
Configure the new files according to your needs and your local setup.
Create unique indices with:
```bash
$ ./neo4j/migrate.sh
```
Configure the new file according to your needs and your local setup. Make sure
a [local Neo4J](http://localhost:7474) instance is up and running.
Start the backend for development with:
```bash
@ -65,17 +44,12 @@ or start the backend in production environment with:
yarn run start
```
{% endtab %}
{% endtabs %}
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
This will start the GraphQL service \(by default on localhost:4000\) where you can issue GraphQL requests or access GraphQL Playground in the browser.
This will start the GraphQL service \(by default on localhost:4000\) where you
can issue GraphQL requests or access GraphQL Playground in the browser.
![GraphQL Playground](../.gitbook/assets/graphql-playground.png)
You can access Neo4J through [http://localhost:7474/](http://localhost:7474/)
for an interactive `cypher` shell and a visualization of the graph.
#### Seed Database
@ -114,7 +88,8 @@ $ yarn run db:reset
# Testing
**Beware**: We have no multiple database setup at the moment. We clean the database after each test, running the tests will wipe out all your data!
**Beware**: We have no multiple database setup at the moment. We clean the
database after each test, running the tests will wipe out all your data!
{% tabs %}

View File

@ -6,8 +6,8 @@
"scripts": {
"build": "babel src/ -d dist/ --copy-files",
"start": "node dist/",
"dev": "nodemon --exec babel-node src/ -e js,graphql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql",
"dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
"lint": "eslint src --config .eslintrc.js",
"test": "run-s test:jest test:cucumber",
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
@ -19,14 +19,13 @@
"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": "babel-node src/seed/reset-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"
},
"author": "Human Connection gGmbH",
"license": "MIT",
"jest": {
"verbose": true,
"collectCoverage": true,
"collectCoverageFrom": [
"**/*.js",
"!**/node_modules/**",
@ -44,34 +43,34 @@
},
"dependencies": {
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.5.1",
"apollo-client": "~2.5.1",
"apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.2",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14",
"apollo-server": "~2.5.0",
"apollo-server": "~2.6.2",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27",
"date-fns": "2.0.0-alpha.31",
"debug": "~4.1.1",
"dotenv": "~8.0.0",
"express": "~4.17.0",
"express": "~4.17.1",
"faker": "~4.1.0",
"graphql": "~14.3.0",
"graphql": "~14.3.1",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.5",
"graphql-shield": "~5.3.6",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.11",
"ms": "~2.1.1",
"merge-graphql-schemas": "^1.5.8",
"neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "~2.6.0",
"neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
"node-fetch": "~2.6.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
@ -83,27 +82,30 @@
},
"devDependencies": {
"@babel/cli": "~7.4.4",
"@babel/core": "~7.4.4",
"@babel/node": "~7.2.2",
"@babel/core": "~7.4.5",
"@babel/node": "~7.4.5",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.4",
"@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.5.0",
"apollo-server-testing": "~2.6.2",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.8.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~5.16.0",
"eslint-config-prettier": "~4.3.0",
"eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.17.2",
"eslint-plugin-jest": "~22.5.1",
"eslint-plugin-node": "~9.0.1",
"eslint-plugin-import": "~2.17.3",
"eslint-plugin-jest": "~22.6.4",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",
"eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2",
"jest": "~24.8.0",
"nodemon": "~1.19.0",
"nodemon": "~1.19.1",
"prettier": "~1.17.1",
"supertest": "~4.0.2"
}
}
}

View File

View File

@ -1,20 +1,12 @@
import {
extractNameFromId,
extractDomainFromUrl,
signAndSend
} from './utils'
import {
isPublicAddressed,
sendAcceptActivity,
sendRejectActivity
} from './utils/activity'
import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
import request from 'request'
import as from 'activitystrea.ms'
import NitroDataSource from './NitroDataSource'
import router from './routes'
import dotenv from 'dotenv'
import Collections from './Collections'
import uuid from 'uuid/v4'
import CONFIG from '../config'
const debug = require('debug')('ea')
let activityPub = null
@ -22,182 +14,199 @@ let activityPub = null
export { activityPub }
export default class ActivityPub {
constructor (activityPubEndpointUri, internalGraphQlUri) {
constructor(activityPubEndpointUri, internalGraphQlUri) {
this.endpoint = activityPubEndpointUri
this.dataSource = new NitroDataSource(internalGraphQlUri)
this.collections = new Collections(this.dataSource)
}
static init (server) {
static init(server) {
if (!activityPub) {
dotenv.config()
activityPub = new ActivityPub(process.env.CLIENT_URI || 'http://localhost:3000', process.env.GRAPHQL_URI || 'http://localhost:4000')
activityPub = new ActivityPub(CONFIG.CLIENT_URI, CONFIG.GRAPHQL_URI)
// integrate into running graphql express server
server.express.set('ap', activityPub)
server.express.use(router)
console.log('-> ActivityPub middleware added to the graphql express server')
console.log('-> ActivityPub middleware added to the graphql express server') // eslint-disable-line no-console
} else {
console.log('-> ActivityPub middleware already added to the graphql express server')
console.log('-> ActivityPub middleware already added to the graphql express server') // eslint-disable-line no-console
}
}
handleFollowActivity (activity) {
handleFollowActivity(activity) {
debug(`inside FOLLOW ${activity.actor}`)
let toActorName = extractNameFromId(activity.object)
let fromDomain = extractDomainFromUrl(activity.actor)
const dataSource = this.dataSource
return new Promise((resolve, reject) => {
request({
url: activity.actor,
headers: {
'Accept': 'application/activity+json'
}
}, async (err, response, toActorObject) => {
if (err) return reject(err)
// save shared inbox
toActorObject = JSON.parse(toActorObject)
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
request(
{
url: activity.actor,
headers: {
Accept: 'application/activity+json',
},
},
async (err, response, toActorObject) => {
if (err) return reject(err)
// save shared inbox
toActorObject = JSON.parse(toActorObject)
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(activity.object)
let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
activity.object,
)
const followActivity = as.follow()
.id(activity.id)
.actor(activity.actor)
.object(activity.object)
const followActivity = as
.follow()
.id(activity.id)
.actor(activity.actor)
.object(activity.object)
// add follower if not already in collection
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
debug('follower already in collection!')
// add follower if not already in collection
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
debug('follower already in collection!')
debug(`inbox = ${toActorObject.inbox}`)
resolve(
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
} else {
followersCollectionPage.orderedItems.push(activity.actor)
}
debug(`toActorObject = ${toActorObject}`)
toActorObject =
typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
debug(`inbox = ${toActorObject.inbox}`)
resolve(sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox))
} else {
followersCollectionPage.orderedItems.push(activity.actor)
}
debug(`toActorObject = ${toActorObject}`)
toActorObject = typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
debug(`inbox = ${toActorObject.inbox}`)
debug(`outbox = ${toActorObject.outbox}`)
debug(`followers = ${toActorObject.followers}`)
debug(`following = ${toActorObject.following}`)
debug(`outbox = ${toActorObject.outbox}`)
debug(`followers = ${toActorObject.followers}`)
debug(`following = ${toActorObject.following}`)
try {
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
debug('follow activity saved')
resolve(sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox))
} catch (e) {
debug('followers update error!', e)
resolve(sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox))
}
})
try {
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
debug('follow activity saved')
resolve(
sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
} catch (e) {
debug('followers update error!', e)
resolve(
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
}
},
)
})
}
handleUndoActivity (activity) {
handleUndoActivity(activity) {
debug('inside UNDO')
switch (activity.object.type) {
case 'Follow':
const followActivity = activity.object
return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object)
case 'Like':
return this.dataSource.deleteShouted(activity)
default:
case 'Follow':
const followActivity = activity.object
return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object)
case 'Like':
return this.dataSource.deleteShouted(activity)
default:
}
}
handleCreateActivity (activity) {
handleCreateActivity(activity) {
debug('inside create')
switch (activity.object.type) {
case 'Article':
case 'Note':
const articleObject = activity.object
if (articleObject.inReplyTo) {
return this.dataSource.createComment(activity)
} else {
return this.dataSource.createPost(activity)
}
default:
case 'Article':
case 'Note':
const articleObject = activity.object
if (articleObject.inReplyTo) {
return this.dataSource.createComment(activity)
} else {
return this.dataSource.createPost(activity)
}
default:
}
}
handleDeleteActivity (activity) {
handleDeleteActivity(activity) {
debug('inside delete')
switch (activity.object.type) {
case 'Article':
case 'Note':
return this.dataSource.deletePost(activity)
default:
case 'Article':
case 'Note':
return this.dataSource.deletePost(activity)
default:
}
}
handleUpdateActivity (activity) {
handleUpdateActivity(activity) {
debug('inside update')
switch (activity.object.type) {
case 'Note':
case 'Article':
return this.dataSource.updatePost(activity)
default:
case 'Note':
case 'Article':
return this.dataSource.updatePost(activity)
default:
}
}
handleLikeActivity (activity) {
handleLikeActivity(activity) {
// TODO differ if activity is an Article/Note/etc.
return this.dataSource.createShouted(activity)
}
handleDislikeActivity (activity) {
handleDislikeActivity(activity) {
// TODO differ if activity is an Article/Note/etc.
return this.dataSource.deleteShouted(activity)
}
async handleAcceptActivity (activity) {
async handleAcceptActivity(activity) {
debug('inside accept')
switch (activity.object.type) {
case 'Follow':
const followObject = activity.object
const followingCollectionPage = await this.collections.getFollowingCollectionPage(followObject.actor)
followingCollectionPage.orderedItems.push(followObject.object)
await this.dataSource.saveFollowingCollectionPage(followingCollectionPage)
case 'Follow':
const followObject = activity.object
const followingCollectionPage = await this.collections.getFollowingCollectionPage(
followObject.actor,
)
followingCollectionPage.orderedItems.push(followObject.object)
await this.dataSource.saveFollowingCollectionPage(followingCollectionPage)
}
}
getActorObject (url) {
getActorObject(url) {
return new Promise((resolve, reject) => {
request({
url: url,
headers: {
'Accept': 'application/json'
}
}, (err, response, body) => {
if (err) {
reject(err)
}
resolve(JSON.parse(body))
})
request(
{
url: url,
headers: {
Accept: 'application/json',
},
},
(err, response, body) => {
if (err) {
reject(err)
}
resolve(JSON.parse(body))
},
)
})
}
generateStatusId (slug) {
generateStatusId(slug) {
return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}`
}
async sendActivity (activity) {
async sendActivity(activity) {
delete activity.send
const fromName = extractNameFromId(activity.actor)
if (Array.isArray(activity.to) && isPublicAddressed(activity)) {
debug('is public addressed')
const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints()
// serve shared inbox endpoints
sharedInboxEndpoints.map((sharedInbox) => {
sharedInboxEndpoints.map(sharedInbox => {
return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox)
})
activity.to = activity.to.filter((recipient) => {
return !(isPublicAddressed({ to: recipient }))
activity.to = activity.to.filter(recipient => {
return !isPublicAddressed({ to: recipient })
})
// serve the rest
activity.to.map(async (recipient) => {
activity.to.map(async recipient => {
debug('serve rest')
const actorObject = await this.getActorObject(recipient)
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
@ -207,18 +216,18 @@ export default class ActivityPub {
const actorObject = await this.getActorObject(activity.to)
return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox)
} else if (Array.isArray(activity.to)) {
activity.to.map(async (recipient) => {
activity.to.map(async recipient => {
const actorObject = await this.getActorObject(recipient)
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
})
}
}
async trySend (activity, fromName, host, url, tries = 5) {
async trySend(activity, fromName, host, url, tries = 5) {
try {
return await signAndSend(activity, fromName, host, url)
} catch (e) {
if (tries > 0) {
setTimeout(function () {
setTimeout(function() {
return this.trySend(activity, fromName, host, url, --tries)
}, 20000)
}

View File

@ -1,28 +1,28 @@
export default class Collections {
constructor (dataSource) {
constructor(dataSource) {
this.dataSource = dataSource
}
getFollowersCollection (actorId) {
getFollowersCollection(actorId) {
return this.dataSource.getFollowersCollection(actorId)
}
getFollowersCollectionPage (actorId) {
getFollowersCollectionPage(actorId) {
return this.dataSource.getFollowersCollectionPage(actorId)
}
getFollowingCollection (actorId) {
getFollowingCollection(actorId) {
return this.dataSource.getFollowingCollection(actorId)
}
getFollowingCollectionPage (actorId) {
getFollowingCollectionPage(actorId) {
return this.dataSource.getFollowingCollectionPage(actorId)
}
getOutboxCollection (actorId) {
getOutboxCollection(actorId) {
return this.dataSource.getOutboxCollection(actorId)
}
getOutboxCollectionPage (actorId) {
getOutboxCollectionPage(actorId) {
return this.dataSource.getOutboxCollectionPage(actorId)
}
}

View File

@ -2,16 +2,10 @@ import {
throwErrorIfApolloErrorOccurred,
extractIdFromActivityId,
extractNameFromId,
constructIdFromName
constructIdFromName,
} from './utils'
import {
createOrderedCollection,
createOrderedCollectionPage
} from './utils/collection'
import {
createArticleObject,
isPublicAddressed
} from './utils/activity'
import { createOrderedCollection, createOrderedCollectionPage } from './utils/collection'
import { createArticleObject, isPublicAddressed } from './utils/activity'
import crypto from 'crypto'
import gql from 'graphql-tag'
import { createHttpLink } from 'apollo-link-http'
@ -23,35 +17,36 @@ import trunc from 'trunc-html'
const debug = require('debug')('ea:nitro-datasource')
export default class NitroDataSource {
constructor (uri) {
constructor(uri) {
this.uri = uri
const defaultOptions = {
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all'
}
errorPolicy: 'all',
},
}
const link = createHttpLink({ uri: this.uri, fetch: fetch }) // eslint-disable-line
const cache = new InMemoryCache()
const authLink = setContext((_, { headers }) => {
// generate the authentication token (maybe from env? Which user?)
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw'
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw'
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : ''
}
Authorization: token ? `Bearer ${token}` : '',
},
}
})
this.client = new ApolloClient({
link: authLink.concat(link),
cache: cache,
defaultOptions
defaultOptions,
})
}
async getFollowersCollection (actorId) {
async getFollowersCollection(actorId) {
const slug = extractNameFromId(actorId)
debug(`slug= ${slug}`)
const result = await this.client.query({
@ -61,7 +56,7 @@ export default class NitroDataSource {
followedByCount
}
}
`
`,
})
debug('successfully fetched followers')
debug(result.data)
@ -78,7 +73,7 @@ export default class NitroDataSource {
}
}
async getFollowersCollectionPage (actorId) {
async getFollowersCollectionPage(actorId) {
const slug = extractNameFromId(actorId)
debug(`getFollowersPage slug = ${slug}`)
const result = await this.client.query({
@ -91,7 +86,7 @@ export default class NitroDataSource {
followedByCount
}
}
`
`,
})
debug(result.data)
@ -104,9 +99,9 @@ export default class NitroDataSource {
followersCollection.totalItems = followersCount
debug(`followers = ${JSON.stringify(followers, null, 2)}`)
await Promise.all(
followers.map(async (follower) => {
followers.map(async follower => {
followersCollection.orderedItems.push(constructIdFromName(follower.slug))
})
}),
)
return followersCollection
@ -115,7 +110,7 @@ export default class NitroDataSource {
}
}
async getFollowingCollection (actorId) {
async getFollowingCollection(actorId) {
const slug = extractNameFromId(actorId)
const result = await this.client.query({
query: gql`
@ -124,7 +119,7 @@ export default class NitroDataSource {
followingCount
}
}
`
`,
})
debug(result.data)
@ -141,7 +136,7 @@ export default class NitroDataSource {
}
}
async getFollowingCollectionPage (actorId) {
async getFollowingCollectionPage(actorId) {
const slug = extractNameFromId(actorId)
const result = await this.client.query({
query: gql`
@ -153,7 +148,7 @@ export default class NitroDataSource {
followingCount
}
}
`
`,
})
debug(result.data)
@ -166,9 +161,9 @@ export default class NitroDataSource {
followingCollection.totalItems = followingCount
await Promise.all(
following.map(async (user) => {
following.map(async user => {
followingCollection.orderedItems.push(await constructIdFromName(user.slug))
})
}),
)
return followingCollection
@ -177,7 +172,7 @@ export default class NitroDataSource {
}
}
async getOutboxCollection (actorId) {
async getOutboxCollection(actorId) {
const slug = extractNameFromId(actorId)
const result = await this.client.query({
query: gql`
@ -192,7 +187,7 @@ export default class NitroDataSource {
}
}
}
`
`,
})
debug(result.data)
@ -209,7 +204,7 @@ export default class NitroDataSource {
}
}
async getOutboxCollectionPage (actorId) {
async getOutboxCollectionPage(actorId) {
const slug = extractNameFromId(actorId)
debug(`inside getting outbox collection page => ${slug}`)
const result = await this.client.query({
@ -232,7 +227,7 @@ export default class NitroDataSource {
}
}
}
`
`,
})
debug(result.data)
@ -243,9 +238,18 @@ export default class NitroDataSource {
const outboxCollection = createOrderedCollectionPage(slug, 'outbox')
outboxCollection.totalItems = posts.length
await Promise.all(
posts.map(async (post) => {
outboxCollection.orderedItems.push(await createArticleObject(post.activityId, post.objectId, post.content, post.author.slug, post.id, post.createdAt))
})
posts.map(async post => {
outboxCollection.orderedItems.push(
await createArticleObject(
post.activityId,
post.objectId,
post.content,
post.author.slug,
post.id,
post.createdAt,
),
)
}),
)
debug('after createNote')
@ -255,7 +259,7 @@ export default class NitroDataSource {
}
}
async undoFollowActivity (fromActorId, toActorId) {
async undoFollowActivity(fromActorId, toActorId) {
const fromUserId = await this.ensureUser(fromActorId)
const toUserId = await this.ensureUser(toActorId)
const result = await this.client.mutate({
@ -265,13 +269,13 @@ export default class NitroDataSource {
from { name }
}
}
`
`,
})
debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`)
throwErrorIfApolloErrorOccurred(result)
}
async saveFollowersCollectionPage (followersCollection, onlyNewestItem = true) {
async saveFollowersCollectionPage(followersCollection, onlyNewestItem = true) {
debug('inside saveFollowers')
let orderedItems = followersCollection.orderedItems
const toUserName = extractNameFromId(followersCollection.id)
@ -279,7 +283,7 @@ export default class NitroDataSource {
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
return Promise.all(
orderedItems.map(async (follower) => {
orderedItems.map(async follower => {
debug(`follower = ${follower}`)
const fromUserId = await this.ensureUser(follower)
debug(`fromUserId = ${fromUserId}`)
@ -291,22 +295,22 @@ export default class NitroDataSource {
from { name }
}
}
`
`,
})
debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`)
throwErrorIfApolloErrorOccurred(result)
debug('saveFollowers: added follow edge successfully')
})
}),
)
}
async saveFollowingCollectionPage (followingCollection, onlyNewestItem = true) {
async saveFollowingCollectionPage(followingCollection, onlyNewestItem = true) {
debug('inside saveFollowers')
let orderedItems = followingCollection.orderedItems
const fromUserName = extractNameFromId(followingCollection.id)
const fromUserId = await this.ensureUser(constructIdFromName(fromUserName))
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
return Promise.all(
orderedItems.map(async (following) => {
orderedItems.map(async following => {
debug(`follower = ${following}`)
const toUserId = await this.ensureUser(following)
debug(`fromUserId = ${fromUserId}`)
@ -318,33 +322,45 @@ export default class NitroDataSource {
from { name }
}
}
`
`,
})
debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`)
throwErrorIfApolloErrorOccurred(result)
debug('saveFollowing: added follow edge successfully')
})
}),
)
}
async createPost (activity) {
async createPost(activity) {
// TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient
// createPost
const postObject = activity.object
if (!isPublicAddressed(postObject)) {
return debug('createPost: not send to public (sending to specific persons is not implemented yet)')
return debug(
'createPost: not send to public (sending to specific persons is not implemented yet)',
)
}
const title = postObject.summary ? postObject.summary : postObject.content.split(' ').slice(0, 5).join(' ')
const title = postObject.summary
? postObject.summary
: postObject.content
.split(' ')
.slice(0, 5)
.join(' ')
const postId = extractIdFromActivityId(postObject.id)
debug('inside create post')
let result = await this.client.mutate({
mutation: gql`
mutation {
CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(postObject.content, 120)}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${activity.id}") {
CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(
postObject.content,
120,
)}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${
activity.id
}") {
id
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
@ -362,13 +378,13 @@ export default class NitroDataSource {
}
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
}
async deletePost (activity) {
async deletePost(activity) {
const result = await this.client.mutate({
mutation: gql`
mutation {
@ -376,28 +392,30 @@ export default class NitroDataSource {
title
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
}
async updatePost (activity) {
async updatePost(activity) {
const postObject = activity.object
const postId = extractIdFromActivityId(postObject.id)
const date = postObject.updated ? postObject.updated : new Date().toISOString()
const result = await this.client.mutate({
mutation: gql`
mutation {
UpdatePost(content: "${postObject.content}", contentExcerpt: "${trunc(postObject.content, 120).html}", id: "${postId}", updatedAt: "${date}") {
UpdatePost(content: "${postObject.content}", contentExcerpt: "${
trunc(postObject.content, 120).html
}", id: "${postId}", updatedAt: "${date}") {
title
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
}
async createShouted (activity) {
async createShouted(activity) {
const userId = await this.ensureUser(activity.actor)
const postId = extractIdFromActivityId(activity.object)
const result = await this.client.mutate({
@ -409,7 +427,7 @@ export default class NitroDataSource {
}
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
if (!result.data.AddUserShouted) {
@ -418,7 +436,7 @@ export default class NitroDataSource {
}
}
async deleteShouted (activity) {
async deleteShouted(activity) {
const userId = await this.ensureUser(activity.actor)
const postId = extractIdFromActivityId(activity.object)
const result = await this.client.mutate({
@ -430,7 +448,7 @@ export default class NitroDataSource {
}
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
if (!result.data.AddUserShouted) {
@ -439,27 +457,27 @@ export default class NitroDataSource {
}
}
async getSharedInboxEndpoints () {
async getSharedInboxEndpoints() {
const result = await this.client.query({
query: gql`
query {
SharedInboxEndpoint {
uri
}
SharedInboxEndpoint {
uri
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
return result.data.SharedInboxEnpoint
}
async addSharedInboxEndpoint (uri) {
async addSharedInboxEndpoint(uri) {
try {
const result = await this.client.mutate({
mutation: gql`
mutation {
CreateSharedInboxEndpoint(uri: "${uri}")
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
return true
@ -468,16 +486,18 @@ export default class NitroDataSource {
}
}
async createComment (activity) {
async createComment(activity) {
const postObject = activity.object
let result = await this.client.mutate({
mutation: gql`
mutation {
CreateComment(content: "${postObject.content}", activityId: "${extractIdFromActivityId(activity.id)}") {
CreateComment(content: "${
postObject.content
}", activityId: "${extractIdFromActivityId(activity.id)}") {
id
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
@ -485,11 +505,13 @@ export default class NitroDataSource {
const result2 = await this.client.mutate({
mutation: gql`
mutation {
AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) {
AddCommentAuthor(from: {id: "${
result.data.CreateComment.id
}"}, to: {id: "${toUserId}"}) {
id
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result2)
@ -497,11 +519,13 @@ export default class NitroDataSource {
result = await this.client.mutate({
mutation: gql`
mutation {
AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) {
AddCommentPost(from: { id: "${
result.data.CreateComment.id
}", to: { id: "${postId}" }}) {
id
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
@ -513,7 +537,7 @@ export default class NitroDataSource {
* @param actorId
* @returns {Promise<*>}
*/
async ensureUser (actorId) {
async ensureUser(actorId) {
debug(`inside ensureUser = ${actorId}`)
const name = extractNameFromId(actorId)
const queryResult = await this.client.query({
@ -523,10 +547,14 @@ export default class NitroDataSource {
id
}
}
`
`,
})
if (queryResult.data && Array.isArray(queryResult.data.User) && queryResult.data.User.length > 0) {
if (
queryResult.data &&
Array.isArray(queryResult.data.User) &&
queryResult.data.User.length > 0
) {
debug('ensureUser: user exists.. return id')
// user already exists.. return the id
return queryResult.data.User[0].id
@ -534,7 +562,10 @@ export default class NitroDataSource {
debug('ensureUser: user not exists.. createUser')
// user does not exist.. create it
const pw = crypto.randomBytes(16).toString('hex')
const slug = name.toLowerCase().split(' ').join('-')
const slug = name
.toLowerCase()
.split(' ')
.join('-')
const result = await this.client.mutate({
mutation: gql`
mutation {
@ -542,7 +573,7 @@ export default class NitroDataSource {
id
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)

View File

@ -7,24 +7,24 @@ const router = express.Router()
// Shared Inbox endpoint (federated Server)
// For now its only able to handle Note Activities!!
router.post('/', async function (req, res, next) {
router.post('/', async function(req, res, next) {
debug(`Content-Type = ${req.get('Content-Type')}`)
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`)
switch (req.body.type) {
case 'Create':
await activityPub.handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break
/* eslint-disable */
case 'Create':
await activityPub.handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break
/* eslint-disable */
case 'Update':
await activityPub.handleUpdateActivity(req.body).catch(next)
break

View File

@ -7,23 +7,21 @@ import verify from './verify'
const router = express.Router()
router.use('/.well-known/webFinger',
cors(),
express.urlencoded({ extended: true }),
webFinger
)
router.use('/activitypub/users',
router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
router.use(
'/activitypub/users',
cors(),
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
express.urlencoded({ extended: true }),
user
user,
)
router.use('/activitypub/inbox',
router.use(
'/activitypub/inbox',
cors(),
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
express.urlencoded({ extended: true }),
verify,
inbox
inbox,
)
export default router

View File

@ -2,7 +2,7 @@ import { createActor } from '../utils/actor'
const gql = require('graphql-tag')
const debug = require('debug')('ea:serveUser')
export async function serveUser (req, res, next) {
export async function serveUser(req, res, next) {
let name = req.params.name
if (name.startsWith('@')) {
@ -10,21 +10,32 @@ export async function serveUser (req, res, next) {
}
debug(`name = ${name}`)
const result = await req.app.get('ap').dataSource.client.query({
query: gql`
const result = await req.app
.get('ap')
.dataSource.client.query({
query: gql`
query {
User(slug: "${name}") {
publicKey
}
}
`
}).catch(reason => { debug(`serveUser User fetch error: ${reason}`) })
`,
})
.catch(reason => {
debug(`serveUser User fetch error: ${reason}`)
})
if (result.data && Array.isArray(result.data.User) && result.data.User.length > 0) {
const publicKey = result.data.User[0].publicKey
const actor = createActor(name, publicKey)
debug(`actor = ${JSON.stringify(actor, null, 2)}`)
debug(`accepts json = ${req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])}`)
debug(
`accepts json = ${req.accepts([
'application/activity+json',
'application/ld+json',
'application/json',
])}`,
)
if (req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])) {
return res.json(actor)
} else if (req.accepts('text/html')) {

View File

@ -7,7 +7,7 @@ import verify from './verify'
const router = express.Router()
const debug = require('debug')('ea:user')
router.get('/:name', async function (req, res, next) {
router.get('/:name', async function(req, res, next) {
debug('inside user.js -> serveUser')
await serveUser(req, res, next)
})
@ -45,24 +45,24 @@ router.get('/:name/outbox', (req, res) => {
}
})
router.post('/:name/inbox', verify, async function (req, res, next) {
router.post('/:name/inbox', verify, async function(req, res, next) {
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`actorId = ${req.body.actor}`)
// const result = await saveActorId(req.body.actor)
switch (req.body.type) {
case 'Create':
await activityPub.handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break
/* eslint-disable */
case 'Create':
await activityPub.handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break
/* eslint-disable */
case 'Update':
await activityPub.handleUpdateActivity(req.body).catch(next)
break

View File

@ -4,7 +4,12 @@ const debug = require('debug')('ea:verify')
export default async (req, res, next) => {
debug(`actorId = ${req.body.actor}`)
// TODO stop if signature validation fails
if (await verifySignature(`${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`, req.headers)) {
if (
await verifySignature(
`${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`,
req.headers,
)
) {
debug('verify = true')
next()
} else {

View File

@ -4,10 +4,14 @@ import gql from 'graphql-tag'
const router = express.Router()
router.get('/', async function (req, res) {
router.get('/', async function(req, res) {
const resource = req.query.resource
if (!resource || !resource.includes('acct:')) {
return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.')
return res
.status(400)
.send(
'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.',
)
} else {
const nameAndDomain = resource.replace('acct:', '')
const name = nameAndDomain.split('@')[0]
@ -21,7 +25,7 @@ router.get('/', async function (req, res) {
slug
}
}
`
`,
})
} catch (error) {
return res.status(500).json({ error })

View File

@ -14,9 +14,9 @@ describe('activityPub/security', () => {
privateKey = pair.privateKey
publicKey = pair.publicKey
headers = {
'Date': '2019-03-08T14:35:45.759Z',
'Host': 'democracy-app.de',
'Content-Type': 'application/json'
Date: '2019-03-08T14:35:45.759Z',
Host: 'democracy-app.de',
'Content-Type': 'application/json',
}
})
@ -27,13 +27,23 @@ describe('activityPub/security', () => {
beforeEach(() => {
const signer = crypto.createSign('rsa-sha256')
signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json')
signer.update(
'(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json',
)
signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
httpSignature = createSignature({ privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase })
httpSignature = createSignature({
privateKey,
keyId: 'https://human-connection.org/activitypub/users/lea#main-key',
url: 'https://democracy-app.de/activitypub/users/max/inbox',
headers,
passphrase,
})
})
it('contains keyId', () => {
expect(httpSignature).toContain('keyId="https://human-connection.org/activitypub/users/lea#main-key"')
expect(httpSignature).toContain(
'keyId="https://human-connection.org/activitypub/users/lea#main-key"',
)
})
it('contains default algorithm "rsa-sha256"', () => {
@ -54,13 +64,19 @@ describe('activityPub/security', () => {
let httpSignature
beforeEach(() => {
httpSignature = createSignature({ privateKey, keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase })
httpSignature = createSignature({
privateKey,
keyId: 'http://localhost:4001/activitypub/users/test-user#main-key',
url: 'https://democracy-app.de/activitypub/users/max/inbox',
headers,
passphrase,
})
const body = {
'publicKey': {
'id': 'https://localhost:4001/activitypub/users/test-user#main-key',
'owner': 'https://localhost:4001/activitypub/users/test-user',
'publicKeyPem': publicKey
}
publicKey: {
id: 'https://localhost:4001/activitypub/users/test-user#main-key',
owner: 'https://localhost:4001/activitypub/users/test-user',
publicKeyPem: publicKey,
},
}
const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body)))
@ -68,7 +84,9 @@ describe('activityPub/security', () => {
})
it('resolves false', async () => {
await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(false)
await expect(
verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
).resolves.toEqual(false)
})
describe('valid signature', () => {
@ -77,7 +95,9 @@ describe('activityPub/security', () => {
})
it('resolves true', async () => {
await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(true)
await expect(
verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
).resolves.toEqual(true)
})
})
})

View File

@ -1,47 +1,55 @@
import dotenv from 'dotenv'
import { resolve } from 'path'
// import dotenv from 'dotenv'
// import { resolve } from 'path'
import crypto from 'crypto'
import request from 'request'
import CONFIG from './../../config'
const debug = require('debug')('ea:security')
dotenv.config({ path: resolve('src', 'activitypub', '.env') })
// TODO Does this reference a local config? Why?
// dotenv.config({ path: resolve('src', 'activitypub', '.env') })
export function generateRsaKeyPair (options = {}) {
const { passphrase = process.env.PRIVATE_KEY_PASSPHRASE } = options
export function generateRsaKeyPair(options = {}) {
const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options
return crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase
}
passphrase,
},
})
}
// signing
export function createSignature (options) {
export function createSignature(options) {
const {
privateKey, keyId, url,
privateKey,
keyId,
url,
headers = {},
algorithm = 'rsa-sha256',
passphrase = process.env.PRIVATE_KEY_PASSPHRASE
passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE,
} = options
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
}
const signer = crypto.createSign(algorithm)
const signingString = constructSigningString(url, headers)
signer.update(signingString)
const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '')
const headersString = Object.keys(headers).reduce((result, key) => {
return result + ' ' + key.toLowerCase()
}, '')
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
}
// verifying
export function verifySignature (url, headers) {
export function verifySignature(url, headers) {
return new Promise((resolve, reject) => {
const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature']
if (!signatureHeader) {
@ -61,40 +69,47 @@ export function verifySignature (url, headers) {
const usedHeaders = headersString.split(' ')
const verifyHeaders = {}
Object.keys(headers).forEach((key) => {
Object.keys(headers).forEach(key => {
if (usedHeaders.includes(key.toLowerCase())) {
verifyHeaders[key.toLowerCase()] = headers[key]
}
})
const signingString = constructSigningString(url, verifyHeaders)
debug(`keyId= ${keyId}`)
request({
url: keyId,
headers: {
'Accept': 'application/json'
}
}, (err, response, body) => {
if (err) reject(err)
debug(`body = ${body}`)
const actor = JSON.parse(body)
const publicKeyPem = actor.publicKey.publicKeyPem
resolve(httpVerify(publicKeyPem, signature, signingString, algorithm))
})
request(
{
url: keyId,
headers: {
Accept: 'application/json',
},
},
(err, response, body) => {
if (err) reject(err)
debug(`body = ${body}`)
const actor = JSON.parse(body)
const publicKeyPem = actor.publicKey.publicKeyPem
resolve(httpVerify(publicKeyPem, signature, signingString, algorithm))
},
)
})
}
// private: signing
function constructSigningString (url, headers) {
function constructSigningString(url, headers) {
const urlObj = new URL(url)
let signingString = `(request-target): post ${urlObj.pathname}${urlObj.search !== '' ? urlObj.search : ''}`
let signingString = `(request-target): post ${urlObj.pathname}${
urlObj.search !== '' ? urlObj.search : ''
}`
return Object.keys(headers).reduce((result, key) => {
return result + `\n${key.toLowerCase()}: ${headers[key]}`
}, signingString)
}
// private: verifying
function httpVerify (pubKey, signature, signingString, algorithm) {
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
function httpVerify(pubKey, signature, signingString, algorithm) {
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
}
const verifier = crypto.createVerify(algorithm)
verifier.update(signingString)
return verifier.verify(pubKey, signature, 'base64')
@ -103,14 +118,16 @@ function httpVerify (pubKey, signature, signingString, algorithm) {
// private: verifying
// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header.
// Just pass what you want as key
function extractKeyValueFromSignatureHeader (signatureHeader, key) {
const keyString = signatureHeader.split(',').filter((el) => {
function extractKeyValueFromSignatureHeader(signatureHeader, key) {
const keyString = signatureHeader.split(',').filter(el => {
return !!el.startsWith(key)
})[0]
let firstEqualIndex = keyString.search('=')
// When headers are requested add 17 to the index to remove "(request-target) " from the string
if (key === 'headers') { firstEqualIndex += 17 }
if (key === 'headers') {
firstEqualIndex += 17
}
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
}
@ -151,4 +168,5 @@ export const SUPPORTED_HASH_ALGORITHMS = [
'sha512WithRSAEncryption',
'ssl3-md5',
'ssl3-sha1',
'whirlpool']
'whirlpool',
]

View File

@ -6,45 +6,45 @@ import as from 'activitystrea.ms'
import gql from 'graphql-tag'
const debug = require('debug')('ea:utils:activity')
export function createNoteObject (text, name, id, published) {
export function createNoteObject(text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`,
'type': 'Create',
'actor': `${activityPub.endpoint}/activitypub/users/${name}`,
'object': {
'id': `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`,
'type': 'Note',
'published': published,
'attributedTo': `${activityPub.endpoint}/activitypub/users/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
id: `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`,
type: 'Create',
actor: `${activityPub.endpoint}/activitypub/users/${name}`,
object: {
id: `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`,
type: 'Note',
published: published,
attributedTo: `${activityPub.endpoint}/activitypub/users/${name}`,
content: text,
to: 'https://www.w3.org/ns/activitystreams#Public',
},
}
}
export async function createArticleObject (activityId, objectId, text, name, id, published) {
export async function createArticleObject(activityId, objectId, text, name, id, published) {
const actorId = await getActorId(name)
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `${activityId}`,
'type': 'Create',
'actor': `${actorId}`,
'object': {
'id': `${objectId}`,
'type': 'Article',
'published': published,
'attributedTo': `${actorId}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
id: `${activityId}`,
type: 'Create',
actor: `${actorId}`,
object: {
id: `${objectId}`,
type: 'Article',
published: published,
attributedTo: `${actorId}`,
content: text,
to: 'https://www.w3.org/ns/activitystreams#Public',
},
}
}
export async function getActorId (name) {
export async function getActorId(name) {
const result = await activityPub.dataSource.client.query({
query: gql`
query {
@ -52,7 +52,7 @@ export async function getActorId (name) {
actorId
}
}
`
`,
})
throwErrorIfApolloErrorOccurred(result)
if (Array.isArray(result.data.User) && result.data.User[0]) {
@ -62,9 +62,12 @@ export async function getActorId (name) {
}
}
export function sendAcceptActivity (theBody, name, targetDomain, url) {
export function sendAcceptActivity(theBody, name, targetDomain, url) {
as.accept()
.id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.id(
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
crypto.randomBytes(16).toString('hex'),
)
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
@ -77,9 +80,12 @@ export function sendAcceptActivity (theBody, name, targetDomain, url) {
})
}
export function sendRejectActivity (theBody, name, targetDomain, url) {
export function sendRejectActivity(theBody, name, targetDomain, url) {
as.reject()
.id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.id(
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
crypto.randomBytes(16).toString('hex'),
)
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
@ -92,7 +98,7 @@ export function sendRejectActivity (theBody, name, targetDomain, url) {
})
}
export function isPublicAddressed (postObject) {
export function isPublicAddressed(postObject) {
if (typeof postObject.to === 'string') {
postObject.to = [postObject.to]
}
@ -102,7 +108,9 @@ export function isPublicAddressed (postObject) {
if (Array.isArray(postObject)) {
postObject.to = postObject
}
return postObject.to.includes('Public') ||
return (
postObject.to.includes('Public') ||
postObject.to.includes('as:Public') ||
postObject.to.includes('https://www.w3.org/ns/activitystreams#Public')
)
}

View File

@ -1,41 +1,38 @@
import { activityPub } from '../ActivityPub'
export function createActor (name, pubkey) {
export function createActor(name, pubkey) {
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
'id': `${activityPub.endpoint}/activitypub/users/${name}`,
'type': 'Person',
'preferredUsername': `${name}`,
'name': `${name}`,
'following': `${activityPub.endpoint}/activitypub/users/${name}/following`,
'followers': `${activityPub.endpoint}/activitypub/users/${name}/followers`,
'inbox': `${activityPub.endpoint}/activitypub/users/${name}/inbox`,
'outbox': `${activityPub.endpoint}/activitypub/users/${name}/outbox`,
'url': `${activityPub.endpoint}/activitypub/@${name}`,
'endpoints': {
'sharedInbox': `${activityPub.endpoint}/activitypub/inbox`
'@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
id: `${activityPub.endpoint}/activitypub/users/${name}`,
type: 'Person',
preferredUsername: `${name}`,
name: `${name}`,
following: `${activityPub.endpoint}/activitypub/users/${name}/following`,
followers: `${activityPub.endpoint}/activitypub/users/${name}/followers`,
inbox: `${activityPub.endpoint}/activitypub/users/${name}/inbox`,
outbox: `${activityPub.endpoint}/activitypub/users/${name}/outbox`,
url: `${activityPub.endpoint}/activitypub/@${name}`,
endpoints: {
sharedInbox: `${activityPub.endpoint}/activitypub/inbox`,
},
publicKey: {
id: `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
owner: `${activityPub.endpoint}/activitypub/users/${name}`,
publicKeyPem: pubkey,
},
'publicKey': {
'id': `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
'owner': `${activityPub.endpoint}/activitypub/users/${name}`,
'publicKeyPem': pubkey
}
}
}
export function createWebFinger (name) {
export function createWebFinger(name) {
const { host } = new URL(activityPub.endpoint)
return {
'subject': `acct:${name}@${host}`,
'links': [
subject: `acct:${name}@${host}`,
links: [
{
'rel': 'self',
'type': 'application/activity+json',
'href': `${activityPub.endpoint}/activitypub/users/${name}`
}
]
rel: 'self',
type: 'application/activity+json',
href: `${activityPub.endpoint}/activitypub/users/${name}`,
},
],
}
}

View File

@ -2,68 +2,71 @@ import { activityPub } from '../ActivityPub'
import { constructIdFromName } from './index'
const debug = require('debug')('ea:utils:collections')
export function createOrderedCollection (name, collectionName) {
export function createOrderedCollection(name, collectionName) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollection',
'first': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
'totalItems': 0
id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
summary: `${name}s ${collectionName} collection`,
type: 'OrderedCollection',
first: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
totalItems: 0,
}
}
export function createOrderedCollectionPage (name, collectionName) {
export function createOrderedCollectionPage(name, collectionName) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollectionPage',
'totalItems': 0,
'partOf': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
'orderedItems': []
id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
summary: `${name}s ${collectionName} collection`,
type: 'OrderedCollectionPage',
totalItems: 0,
partOf: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
orderedItems: [],
}
}
export function sendCollection (collectionName, req, res) {
export function sendCollection(collectionName, req, res) {
const name = req.params.name
const id = constructIdFromName(name)
switch (collectionName) {
case 'followers':
attachThenCatch(activityPub.collections.getFollowersCollection(id), res)
break
case 'followers':
attachThenCatch(activityPub.collections.getFollowersCollection(id), res)
break
case 'followersPage':
attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
break
case 'followersPage':
attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
break
case 'following':
attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
break
case 'following':
attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
break
case 'followingPage':
attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
break
case 'followingPage':
attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
break
case 'outbox':
attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
break
case 'outbox':
attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
break
case 'outboxPage':
attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
break
case 'outboxPage':
attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
break
default:
res.status(500).end()
default:
res.status(500).end()
}
}
function attachThenCatch (promise, res) {
function attachThenCatch(promise, res) {
return promise
.then((collection) => {
res.status(200).contentType('application/activity+json').send(collection)
.then(collection => {
res
.status(200)
.contentType('application/activity+json')
.send(collection)
})
.catch((err) => {
.catch(err => {
debug(`error getting a Collection: = ${err}`)
res.status(500).end()
})

View File

@ -2,9 +2,10 @@ import { activityPub } from '../ActivityPub'
import gql from 'graphql-tag'
import { createSignature } from '../security'
import request from 'request'
import CONFIG from './../../config'
const debug = require('debug')('ea:utils')
export function extractNameFromId (uri) {
export function extractNameFromId(uri) {
const urlObject = new URL(uri)
const pathname = urlObject.pathname
const splitted = pathname.split('/')
@ -12,31 +13,33 @@ export function extractNameFromId (uri) {
return splitted[splitted.indexOf('users') + 1]
}
export function extractIdFromActivityId (uri) {
export function extractIdFromActivityId(uri) {
const urlObject = new URL(uri)
const pathname = urlObject.pathname
const splitted = pathname.split('/')
return splitted[splitted.indexOf('status') + 1]
}
export function constructIdFromName (name, fromDomain = activityPub.endpoint) {
export function constructIdFromName(name, fromDomain = activityPub.endpoint) {
return `${fromDomain}/activitypub/users/${name}`
}
export function extractDomainFromUrl (url) {
export function extractDomainFromUrl(url) {
return new URL(url).host
}
export function throwErrorIfApolloErrorOccurred (result) {
export function throwErrorIfApolloErrorOccurred(result) {
if (result.error && (result.error.message || result.error.errors)) {
throw new Error(`${result.error.message ? result.error.message : result.error.errors[0].message}`)
throw new Error(
`${result.error.message ? result.error.message : result.error.errors[0].message}`,
)
}
}
export function signAndSend (activity, fromName, targetDomain, url) {
export function signAndSend(activity, fromName, targetDomain, url) {
// fix for development: replace with http
url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
debug(`passhprase = ${process.env.PRIVATE_KEY_PASSPHRASE}`)
debug(`passhprase = ${CONFIG.PRIVATE_KEY_PASSPHRASE}`)
return new Promise(async (resolve, reject) => {
debug('inside signAndSend')
// get the private key
@ -47,7 +50,7 @@ export function signAndSend (activity, fromName, targetDomain, url) {
privateKey
}
}
`
`,
})
if (result.error) {
@ -69,34 +72,38 @@ export function signAndSend (activity, fromName, targetDomain, url) {
const date = new Date().toUTCString()
debug(`url = ${url}`)
request({
url: url,
headers: {
'Host': targetDomain,
'Date': date,
'Signature': createSignature({ privateKey,
keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`,
url,
headers: {
'Host': targetDomain,
'Date': date,
'Content-Type': 'application/activity+json'
}
}),
'Content-Type': 'application/activity+json'
request(
{
url: url,
headers: {
Host: targetDomain,
Date: date,
Signature: createSignature({
privateKey,
keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`,
url,
headers: {
Host: targetDomain,
Date: date,
'Content-Type': 'application/activity+json',
},
}),
'Content-Type': 'application/activity+json',
},
method: 'POST',
body: JSON.stringify(parsedActivity),
},
method: 'POST',
body: JSON.stringify(parsedActivity)
}, (error, response) => {
if (error) {
debug(`Error = ${JSON.stringify(error, null, 2)}`)
reject(error)
} else {
debug('Response Headers:', JSON.stringify(response.headers, null, 2))
debug('Response Body:', JSON.stringify(response.body, null, 2))
resolve()
}
})
(error, response) => {
if (error) {
debug(`Error = ${JSON.stringify(error, null, 2)}`)
reject(error)
} else {
debug('Response Headers:', JSON.stringify(response.headers, null, 2))
debug('Response Body:', JSON.stringify(response.body, null, 2))
resolve()
}
},
)
}
})
}

View File

@ -1,15 +1,11 @@
import {
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective
GraphQLDefaultToDirective,
} from 'graphql-custom-directives'
export default function applyDirectives (augmentedSchema) {
const directives = [
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective
]
export default function applyDirectives(augmentedSchema) {
const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
return augmentedSchema

View File

@ -1,15 +1,13 @@
import { v1 as neo4j } from 'neo4j-driver'
import dotenv from 'dotenv'
dotenv.config()
import CONFIG from './../config'
let driver
export function getDriver (options = {}) {
export function getDriver(options = {}) {
const {
uri = process.env.NEO4J_URI || 'bolt://localhost:7687',
username = process.env.NEO4J_USERNAME || 'neo4j',
password = process.env.NEO4J_PASSWORD || 'neo4j'
uri = CONFIG.NEO4J_URI,
username = CONFIG.NEO4J_USERNAME,
password = CONFIG.NEO4J_PASSWORD,
} = options
if (!driver) {
driver = neo4j.driver(uri, neo4j.auth.basic(username, password))

View File

@ -1,10 +1,6 @@
import {
GraphQLDate,
GraphQLTime,
GraphQLDateTime
} from 'graphql-iso-date'
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
export default function applyScalars (augmentedSchema) {
export default function applyScalars(augmentedSchema) {
augmentedSchema._typeMap.Date = GraphQLDate
augmentedSchema._typeMap.Time = GraphQLTime
augmentedSchema._typeMap.DateTime = GraphQLDateTime

View File

@ -0,0 +1,35 @@
import dotenv from 'dotenv'
dotenv.config()
export const requiredConfigs = {
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
JWT_SECRET: process.env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
}
export const neo4jConfigs = {
NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
}
export const serverConfigs = {
GRAPHQL_PORT: process.env.GRAPHQL_PORT || 4000,
CLIENT_URI: process.env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
}
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
MOCKS: process.env.MOCKS === 'true',
DISABLED_MIDDLEWARES:
(process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '',
}
export default {
...requiredConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,
}

View File

@ -1,41 +0,0 @@
import fs from 'fs'
import path from 'path'
import userManagement from './resolvers/user_management.js'
import statistics from './resolvers/statistics.js'
import reports from './resolvers/reports.js'
import posts from './resolvers/posts.js'
import moderation from './resolvers/moderation.js'
import follow from './resolvers/follow.js'
import shout from './resolvers/shout.js'
import rewards from './resolvers/rewards.js'
import socialMedia from './resolvers/socialMedia.js'
import notifications from './resolvers/notifications'
import comments from './resolvers/comments'
export const typeDefs = fs
.readFileSync(
process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')
)
.toString('utf-8')
export const resolvers = {
Query: {
...statistics.Query,
...userManagement.Query,
...notifications.Query,
...comments.Query
},
Mutation: {
...userManagement.Mutation,
...reports.Mutation,
...posts.Mutation,
...moderation.Mutation,
...follow.Mutation,
...shout.Mutation,
...rewards.Mutation,
...socialMedia.Mutation,
...notifications.Mutation,
...comments.Mutation
}
}

View File

@ -5,7 +5,7 @@
* @param callback
* @returns {Promise<void>}
*/
async function asyncForEach (array, callback) {
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}

View File

@ -1,10 +1,10 @@
/**
* iterate through all fields and replace it with the callback result
* @property data Array
* @property fields Array
* @property callback Function
*/
function walkRecursive (data, fields, callback, _key) {
* iterate through all fields and replace it with the callback result
* @property data Array
* @property fields Array
* @property callback Function
*/
function walkRecursive(data, fields, callback, _key) {
if (!Array.isArray(fields)) {
throw new Error('please provide an fields array for the walkRecursive helper')
}

View File

@ -1,17 +1,18 @@
import createServer from './server'
import ActivityPub from './activitypub/ActivityPub'
import CONFIG from './config'
const serverConfig = {
port: process.env.GRAPHQL_PORT || 4000
port: CONFIG.GRAPHQL_PORT,
// cors: {
// credentials: true,
// origin: [process.env.CLIENT_URI] // your frontend url.
// origin: [CONFIG.CLIENT_URI] // your frontend url.
// }
}
const server = createServer()
server.start(serverConfig, options => {
/* eslint-disable-next-line no-console */
console.log(`GraphQLServer ready at ${process.env.GRAPHQL_URI} 🚀`)
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
ActivityPub.init(server)
})

View File

@ -4,13 +4,13 @@ import { request } from 'graphql-request'
// not to be confused with the seeder host
export const host = 'http://127.0.0.1:4123'
export async function login ({ email, password }) {
export async function login({ email, password }) {
const mutation = `
mutation {
login(email:"${email}", password:"${password}")
}`
const response = await request(host, mutation)
return {
authorization: `Bearer ${response.login}`
authorization: `Bearer ${response.login}`,
}
}

View File

@ -1,13 +1,14 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
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)
const decoded = await jwt.verify(token, CONFIG.JWT_SECRET)
id = decoded.sub
} catch {
} catch (err) {
return null
}
const session = driver.session()
@ -18,13 +19,13 @@ export default async (driver, authorizationHeader) => {
`
const result = await session.run(query, { id })
session.close()
const [currentUser] = await result.records.map((record) => {
const [currentUser] = await result.records.map(record => {
return record.get('user')
})
if (!currentUser) return null
if (currentUser.disabled) return null
return {
token,
...currentUser
...currentUser,
}
}

View File

@ -1,16 +1,15 @@
import jwt from 'jsonwebtoken'
import ms from 'ms'
import CONFIG from './../config'
// 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()
export default function encode(user) {
const token = jwt.sign(user, CONFIG.JWT_SECRET, {
expiresIn: 24 * 60 * 60 * 1000, // one day
issuer: CONFIG.GRAPHQL_URI,
audience: CONFIG.CLIENT_URI,
subject: user.id.toString(),
})
// jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => {
// jwt.verifySignature(token, CONFIG.JWT_SECRET, (err, data) => {
// console.log('token verification:', err, data)
// })
return token

View File

@ -1,10 +1,8 @@
import { generateRsaKeyPair } from '../activitypub/security'
import { activityPub } from '../activitypub/ActivityPub'
import as from 'activitystrea.ms'
import dotenv from 'dotenv'
const debug = require('debug')('backend:schema')
dotenv.config()
export default {
Mutation: {
@ -22,13 +20,15 @@ export default {
.id(`${actorId}/status/${args.activityId}`)
.actor(`${actorId}`)
.object(
as.article()
as
.article()
.id(`${actorId}/status/${post.id}`)
.content(post.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`)
).prettyWrite((err, doc) => {
.attributedTo(`${actorId}`),
)
.prettyWrite((err, doc) => {
if (err) {
reject(err)
} else {
@ -51,6 +51,6 @@ export default {
Object.assign(args, keys)
args.actorId = `${activityPub.host}/activitypub/users/${args.slug}`
return resolve(root, args, context, info)
}
}
},
},
}

View File

@ -1,9 +1,9 @@
const setCreatedAt = (resolve, root, args, context, info) => {
args.createdAt = (new Date()).toISOString()
args.createdAt = new Date().toISOString()
return resolve(root, args, context, info)
}
const setUpdatedAt = (resolve, root, args, context, info) => {
args.updatedAt = (new Date()).toISOString()
args.updatedAt = new Date().toISOString()
return resolve(root, args, context, info)
}
@ -18,6 +18,6 @@ export default {
UpdatePost: setUpdatedAt,
UpdateComment: setUpdatedAt,
UpdateOrganization: setUpdatedAt,
UpdateNotification: setUpdatedAt
}
UpdateNotification: setUpdatedAt,
},
}

View File

@ -31,6 +31,6 @@ export default {
args.descriptionExcerpt = trunc(args.description, 120).html
const result = await resolve(root, args, context, info)
return result
}
}
},
},
}

View File

@ -0,0 +1,77 @@
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../../jest/helpers'
import Factory from '../../seed/factories'
const factory = Factory()
const currentUserParams = {
id: 'u1',
email: 'you@example.org',
name: 'This is you',
password: '1234',
}
const followedAuthorParams = {
id: 'u2',
email: 'followed@example.org',
name: 'Followed User',
password: '1234',
}
const randomAuthorParams = {
email: 'someone@example.org',
name: 'Someone else',
password: 'else',
}
beforeEach(async () => {
await Promise.all([
factory.create('User', currentUserParams),
factory.create('User', followedAuthorParams),
factory.create('User', randomAuthorParams),
])
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' })
await asSomeoneElse.create('Post', { title: 'This is some random post' })
})
afterEach(async () => {
await factory.cleanDatabase()
})
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 })
})
describe('no filter bubble', () => {
it('returns all posts', async () => {
const query = '{ Post(filter: { }) { title } }'
const expected = {
Post: [
{ title: 'This is some random post' },
{ title: 'This is the post of a followed user' },
],
}
await expect(authenticatedClient.request(query)).resolves.toEqual(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 expected = {
Post: [{ title: 'This is the post of a followed user' }],
}
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
})
})
})
})

View File

@ -1,21 +1,24 @@
const legacyUrls = [
'https://api-alpha.human-connection.org',
'https://staging-api.human-connection.org',
'http://localhost:3000'
'http://localhost:3000',
]
export const fixUrl = (url) => {
legacyUrls.forEach((legacyUrl) => {
export const fixUrl = url => {
legacyUrls.forEach(legacyUrl => {
url = url.replace(legacyUrl, '/api')
})
return url
}
const checkUrl = (thing) => {
return thing && typeof thing === 'string' && legacyUrls.find((legacyUrl) => {
return thing.indexOf(legacyUrl) === 0
})
const checkUrl = thing => {
return (
thing &&
typeof thing === 'string' &&
legacyUrls.find(legacyUrl => {
return thing.indexOf(legacyUrl) === 0
})
)
}
export const fixImageURLs = (result, recursive) => {
@ -41,5 +44,5 @@ export default {
Query: async (resolve, root, args, context, info) => {
let result = await resolve(root, args, context, info)
return fixImageURLs(result)
}
},
}

View File

@ -3,15 +3,21 @@ import { fixImageURLs } from './fixImageUrlsMiddleware'
describe('fixImageURLs', () => {
describe('image url of legacy alpha', () => {
it('removes domain', () => {
const url = 'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png'
expect(fixImageURLs(url)).toEqual('/api/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png')
const url =
'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png'
expect(fixImageURLs(url)).toEqual(
'/api/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png',
)
})
})
describe('image url of legacy staging', () => {
it('removes domain', () => {
const url = 'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg'
expect(fixImageURLs(url)).toEqual('/api/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg')
const url =
'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg'
expect(fixImageURLs(url)).toEqual(
'/api/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg',
)
})
})
@ -24,7 +30,7 @@ describe('fixImageURLs', () => {
describe('some string', () => {
it('returns untouched', () => {})
const string = 'Yeah I\'m a String'
const string = "Yeah I'm a String"
expect(fixImageURLs(string)).toEqual(string)
})
})

View File

@ -2,21 +2,21 @@ import cloneDeep from 'lodash/cloneDeep'
const _includeFieldsRecursively = (selectionSet, includedFields) => {
if (!selectionSet) return
includedFields.forEach((includedField) => {
includedFields.forEach(includedField => {
selectionSet.selections.unshift({
kind: 'Field',
name: { kind: 'Name', value: includedField }
name: { kind: 'Name', value: includedField },
})
})
selectionSet.selections.forEach((selection) => {
selectionSet.selections.forEach(selection => {
_includeFieldsRecursively(selection.selectionSet, includedFields)
})
}
const includeFieldsRecursively = (includedFields) => {
const includeFieldsRecursively = includedFields => {
return (resolve, root, args, context, resolveInfo) => {
const copy = cloneDeep(resolveInfo)
copy.fieldNodes.forEach((fieldNode) => {
copy.fieldNodes.forEach(fieldNode => {
_includeFieldsRecursively(fieldNode.selectionSet, includedFields)
})
return resolve(root, args, context, copy)
@ -25,5 +25,5 @@ const includeFieldsRecursively = (includedFields) => {
export default {
Query: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']),
Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted'])
Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']),
}

View File

@ -1,41 +1,63 @@
import activityPubMiddleware from './activityPubMiddleware'
import passwordMiddleware from './passwordMiddleware'
import softDeleteMiddleware from './softDeleteMiddleware'
import sluggifyMiddleware from './sluggifyMiddleware'
import fixImageUrlsMiddleware from './fixImageUrlsMiddleware'
import excerptMiddleware from './excerptMiddleware'
import dateTimeMiddleware from './dateTimeMiddleware'
import xssMiddleware from './xssMiddleware'
import permissionsMiddleware from './permissionsMiddleware'
import userMiddleware from './userMiddleware'
import includedFieldsMiddleware from './includedFieldsMiddleware'
import orderByMiddleware from './orderByMiddleware'
import validationMiddleware from './validation'
import notificationsMiddleware from './notifications'
import CONFIG from './../config'
import activityPub from './activityPubMiddleware'
import password from './passwordMiddleware'
import softDelete from './softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import fixImageUrls from './fixImageUrlsMiddleware'
import excerpt from './excerptMiddleware'
import dateTime from './dateTimeMiddleware'
import xss from './xssMiddleware'
import permissions from './permissionsMiddleware'
import user from './userMiddleware'
import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware'
import validation from './validation'
import notifications from './notifications'
export default schema => {
let middleware = [
passwordMiddleware,
dateTimeMiddleware,
validationMiddleware,
sluggifyMiddleware,
excerptMiddleware,
notificationsMiddleware,
xssMiddleware,
fixImageUrlsMiddleware,
softDeleteMiddleware,
userMiddleware,
includedFieldsMiddleware,
orderByMiddleware
const middlewares = {
permissions: permissions,
activityPub: activityPub,
password: password,
dateTime: dateTime,
validation: validation,
sluggify: sluggify,
excerpt: excerpt,
notifications: notifications,
xss: xss,
fixImageUrls: fixImageUrls,
softDelete: softDelete,
user: user,
includedFields: includedFields,
orderBy: orderBy,
}
let order = [
'permissions',
'activityPub',
'password',
'dateTime',
'validation',
'sluggify',
'excerpt',
'notifications',
'xss',
'fixImageUrls',
'softDelete',
'user',
'includedFields',
'orderBy',
]
// add permisions middleware at the first position (unless we're seeding)
// NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF
if (process.env.NODE_ENV !== 'production') {
const DISABLED_MIDDLEWARES = process.env.DISABLED_MIDDLEWARES || ''
const disabled = DISABLED_MIDDLEWARES.split(',')
if (!disabled.includes('activityPub')) middleware.unshift(activityPubMiddleware)
if (!disabled.includes('permissions')) middleware.unshift(permissionsMiddleware.generate(schema))
if (CONFIG.DISABLED_MIDDLEWARES) {
const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',')
order = order.filter(key => {
return !disabledMiddlewares.includes(key)
})
/* eslint-disable-next-line no-console */
console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`)
}
return middleware
return order.map(key => middlewares[key])
}

View File

@ -1,12 +1,12 @@
import request from 'request'
import { UserInputError } from 'apollo-server'
import isEmpty from 'lodash/isEmpty'
import asyncForEach from '../../helpers/asyncForEach'
import CONFIG from './../../config'
const fetch = url => {
return new Promise((resolve, reject) => {
request(url, function (error, response, body) {
request(url, function(error, response, body) {
if (error) {
reject(error)
} else {
@ -16,16 +16,7 @@ const fetch = url => {
})
}
const locales = [
'en',
'de',
'fr',
'nl',
'it',
'es',
'pt',
'pl'
]
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl']
const createLocation = async (session, mapboxData) => {
const data = {
@ -39,21 +30,22 @@ const createLocation = async (session, mapboxData) => {
namePT: mapboxData.text_pt,
namePL: mapboxData.text_pl,
type: mapboxData.id.split('.')[0].toLowerCase(),
lat: (mapboxData.center && mapboxData.center.length) ? mapboxData.center[0] : null,
lng: (mapboxData.center && mapboxData.center.length) ? mapboxData.center[1] : null
lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null,
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
}
let query = 'MERGE (l:Location {id: $id}) ' +
'SET l.name = $nameEN, ' +
'l.nameEN = $nameEN, ' +
'l.nameDE = $nameDE, ' +
'l.nameFR = $nameFR, ' +
'l.nameNL = $nameNL, ' +
'l.nameIT = $nameIT, ' +
'l.nameES = $nameES, ' +
'l.namePT = $namePT, ' +
'l.namePL = $namePL, ' +
'l.type = $type'
let query =
'MERGE (l:Location {id: $id}) ' +
'SET l.name = $nameEN, ' +
'l.nameEN = $nameEN, ' +
'l.nameDE = $nameDE, ' +
'l.nameFR = $nameFR, ' +
'l.nameNL = $nameNL, ' +
'l.nameIT = $nameIT, ' +
'l.nameES = $nameES, ' +
'l.namePT = $namePT, ' +
'l.namePL = $namePL, ' +
'l.type = $type'
if (data.lat && data.lng) {
query += ', l.lat = $lat, l.lng = $lng'
@ -67,8 +59,13 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
if (isEmpty(locationName)) {
return
}
const mapboxToken = process.env.MAPBOX_TOKEN
const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=${locales.join(',')}`)
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
locationName,
)}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${locales.join(
',',
)}`,
)
if (!res || !res.features || !res.features[0]) {
throw new UserInputError('locationName is invalid')
@ -100,24 +97,29 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
await session.run(
'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' +
'MERGE (child)<-[:IS_IN]-(parent) ' +
'RETURN child.id, parent.id', {
'MERGE (child)<-[:IS_IN]-(parent) ' +
'RETURN child.id, parent.id',
{
parentId: parent.id,
childId: ctx.id
})
childId: ctx.id,
},
)
parent = ctx
})
}
// delete all current locations from user
await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', {
userId: userId
userId: userId,
})
// connect user with location
await session.run('MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', {
userId: userId,
locationId: data.id
})
await session.run(
'MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id',
{
userId: userId,
locationId: data.id,
},
)
session.close()
}

View File

@ -1,13 +1,16 @@
import cheerio from 'cheerio'
const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g
export default function (content) {
export default function(content) {
if (!content) return []
const $ = cheerio.load(content)
const urls = $('.mention').map((_, el) => {
return $(el).attr('href')
}).get()
const urls = $('.mention')
.map((_, el) => {
return $(el).attr('href')
})
.get()
const ids = []
urls.forEach((url) => {
urls.forEach(url => {
let match
while ((match = ID_REGEX.exec(url)) != null) {
ids.push(match[1])

View File

@ -0,0 +1,59 @@
import extractIds from '.'
describe('extractIds', () => {
describe('content undefined', () => {
it('returns empty array', () => {
expect(extractIds()).toEqual([])
})
})
describe('searches through links', () => {
it('ignores links without .mention class', () => {
const content =
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
describe('given a link with .mention class', () => {
it('extracts ids', () => {
const content =
'<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
describe('handles links', () => {
it('with slug and id', () => {
const content =
'<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
it('with domains', () => {
const content =
'<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
it('special characters', () => {
const content =
'<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3'])
})
})
describe('does not crash if', () => {
it('`href` contains no user id', () => {
const content =
'<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
it('`href` is empty or invalid', () => {
const content =
'<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
})
})
})
})

View File

@ -1,46 +0,0 @@
import extractIds from './extractMentions'
describe('extract', () => {
describe('searches through links', () => {
it('ignores links without .mention class', () => {
const content = '<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
describe('given a link with .mention class', () => {
it('extracts ids', () => {
const content = '<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
describe('handles links', () => {
it('with slug and id', () => {
const content = '<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
it('with domains', () => {
const content = '<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
it('special characters', () => {
const content = '<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3'])
})
})
describe('does not crash if', () => {
it('`href` contains no user id', () => {
const content = '<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
it('`href` is empty or invalid', () => {
const content = '<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
})
})
})
})

View File

@ -1,4 +1,4 @@
import extractIds from './extractMentions'
import extractIds from './extractIds'
const notify = async (resolve, root, args, context, resolveInfo) => {
// extract user ids before xss-middleware removes link classes
@ -8,7 +8,7 @@ const notify = async (resolve, root, args, context, resolveInfo) => {
const session = context.driver.session()
const { id: postId } = post
const createdAt = (new Date()).toISOString()
const createdAt = new Date().toISOString()
const cypher = `
match(u:User) where u.id in $ids
match(p:Post) where p.id = $postId
@ -25,6 +25,6 @@ const notify = async (resolve, root, args, context, resolveInfo) => {
export default {
Mutation: {
CreatePost: notify,
UpdatePost: notify
}
UpdatePost: notify,
},
}

View File

@ -11,7 +11,7 @@ beforeEach(async () => {
name: 'Al Capone',
slug: 'al-capone',
email: 'test@example.org',
password: '1234'
password: '1234',
})
})
@ -47,7 +47,7 @@ describe('currentUser { notifications }', () => {
authorParams = {
email: 'author@example.org',
password: '1234',
id: 'author'
id: 'author',
}
await factory.create('User', authorParams)
authorHeaders = await login(authorParams)
@ -56,7 +56,8 @@ describe('currentUser { notifications }', () => {
describe('who mentions me in a post', () => {
let post
const title = 'Mentioning Al Capone'
const content = 'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
const content =
'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
beforeEach(async () => {
const createPostMutation = `
@ -74,20 +75,21 @@ describe('currentUser { notifications }', () => {
})
it('sends you a notification', async () => {
const expectedContent = 'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expectedContent =
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expected = {
currentUser: {
notifications: [
{ read: false, post: { content: expectedContent } }
]
}
notifications: [{ read: false, post: { content: expectedContent } }],
},
}
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
})
describe('who mentions me again', () => {
beforeEach(async () => {
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
const updatedContent = `${
post.content
} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
// The response `post.content` contains a link but the XSSmiddleware
// should have the `mention` CSS class removed. I discovered this
// during development and thought: A feature not a bug! This way we
@ -106,14 +108,15 @@ describe('currentUser { notifications }', () => {
})
it('creates exactly one more notification', async () => {
const expectedContent = 'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
const expectedContent =
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
const expected = {
currentUser: {
notifications: [
{ read: false, post: { content: expectedContent } },
{ read: false, post: { content: expectedContent } }
]
}
{ read: false, post: { content: expectedContent } },
],
},
}
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
})

View File

@ -5,7 +5,7 @@ const defaultOrderBy = (resolve, root, args, context, resolveInfo) => {
const newestFirst = {
kind: 'Argument',
name: { kind: 'Name', value: 'orderBy' },
value: { kind: 'EnumValue', value: 'createdAt_desc' }
value: { kind: 'EnumValue', value: 'createdAt_desc' },
}
const [fieldNode] = copy.fieldNodes
if (fieldNode) fieldNode.arguments.push(newestFirst)
@ -14,6 +14,6 @@ const defaultOrderBy = (resolve, root, args, context, resolveInfo) => {
export default {
Query: {
Post: defaultOrderBy
}
Post: defaultOrderBy,
},
}

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories'
import { host } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
let client
let headers
@ -35,7 +35,7 @@ describe('Query', () => {
{ title: 'last' },
{ title: 'third' },
{ title: 'second' },
{ title: 'first' }
{ title: 'first' },
]
const expected = { Post: posts }
await expect(client.request(query)).resolves.toEqual(expected)
@ -51,7 +51,7 @@ describe('Query', () => {
{ title: 'first' },
{ title: 'second' },
{ title: 'third' },
{ title: 'last' }
{ title: 'last' },
]
const expected = { Post: posts }
await expect(client.request(query)).resolves.toEqual(expected)

View File

@ -8,7 +8,7 @@ export default {
const result = await resolve(root, args, context, info)
result.password = '*****'
return result
}
},
},
Query: async (resolve, root, args, context, info) => {
let result = await resolve(root, args, context, info)
@ -17,5 +17,5 @@ export default {
return '*****'
})
return result
}
},
}

View File

@ -1,9 +1,9 @@
import { rule, shield, allow, or } from 'graphql-shield'
/*
* TODO: implement
* See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363
*/
* TODO: implement
* See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363
*/
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
return ctx.user !== null
})
@ -13,45 +13,69 @@ const isModerator = rule()(async (parent, args, { user }, info) => {
})
const isAdmin = rule()(async (parent, args, { user }, info) => {
return user && (user.role === 'admin')
return user && user.role === 'admin'
})
const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) => {
const isMyOwn = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
return context.user.id === parent.id
})
const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
const { driver, user: { id: userId } } = context
const belongsToMe = rule({
cache: 'no_cache',
})(async (_, args, context) => {
const {
driver,
user: { id: userId },
} = context
const { id: notificationId } = args
const session = driver.session()
const result = await session.run(`
const result = await session.run(
`
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
RETURN n
`, { userId, notificationId })
const [notification] = result.records.map((record) => {
`,
{
userId,
notificationId,
},
)
const [notification] = result.records.map(record => {
return record.get('n')
})
session.close()
return Boolean(notification)
})
const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => {
const onlyEnabledContent = rule({
cache: 'strict',
})(async (parent, args, ctx, info) => {
const { disabled, deleted } = args
return !(disabled || deleted)
})
const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => {
const isAuthor = rule({
cache: 'no_cache',
})(async (parent, args, { user, driver }) => {
if (!user) return false
const session = driver.session()
const { id: postId } = args
const result = await session.run(`
MATCH (post:Post {id: $postId})<-[:WROTE]-(author)
const { id: resourceId } = args
const result = await session.run(
`
MATCH (resource {id: $resourceId})<-[:WROTE]-(author)
RETURN author
`, { postId })
const [author] = result.records.map((record) => {
`,
{
resourceId,
},
)
const [author] = result.records.map(record => {
return record.get('author')
})
const { properties: { id: authorId } } = author
const {
properties: { id: authorId },
} = author
session.close()
return authorId === user.id
})
@ -62,7 +86,7 @@ const permissions = shield({
Notification: isAdmin,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator)
Post: or(onlyEnabledContent, isModerator),
},
Mutation: {
UpdateNotification: belongsToMe,
@ -88,14 +112,15 @@ const permissions = shield({
changePassword: isAuthenticated,
enable: isModerator,
disable: isModerator,
CreateComment: isAuthenticated
CreateComment: isAuthenticated,
DeleteComment: isAuthor,
// CreateUser: allow,
},
User: {
email: isMyOwn,
password: isMyOwn,
privateKey: isMyOwn
}
privateKey: isMyOwn,
},
})
export default permissions

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories'
import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
const factory = Factory()
@ -10,12 +10,12 @@ describe('authorization', () => {
await factory.create('User', {
email: 'owner@example.org',
name: 'Owner',
password: 'iamtheowner'
password: 'iamtheowner',
})
await factory.create('User', {
email: 'someone@example.org',
name: 'Someone else',
password: 'else'
password: 'else',
})
})
@ -39,14 +39,14 @@ describe('authorization', () => {
await expect(action()).rejects.toThrow('Not Authorised!')
})
it('does not expose the owner\'s email address', async () => {
it("does not expose the owner's email address", async () => {
let response = {}
try {
await action()
} catch (error) {
response = error.response.data
} finally {
expect(response).toEqual({ User: [ null ] })
expect(response).toEqual({ User: [null] })
}
})
})
@ -55,12 +55,12 @@ describe('authorization', () => {
beforeEach(() => {
loginCredentials = {
email: 'owner@example.org',
password: 'iamtheowner'
password: 'iamtheowner',
}
})
it('exposes the owner\'s email address', async () => {
await expect(action()).resolves.toEqual({ User: [ { email: 'owner@example.org' } ] })
it("exposes the owner's email address", async () => {
await expect(action()).resolves.toEqual({ User: [{ email: 'owner@example.org' }] })
})
})
@ -68,7 +68,7 @@ describe('authorization', () => {
beforeEach(async () => {
loginCredentials = {
email: 'someone@example.org',
password: 'else'
password: 'else',
}
})
@ -76,14 +76,14 @@ describe('authorization', () => {
await expect(action()).rejects.toThrow('Not Authorised!')
})
it('does not expose the owner\'s email address', async () => {
it("does not expose the owner's email address", async () => {
let response
try {
await action()
} catch (error) {
response = error.response.data
}
expect(response).toEqual({ User: [ null ] })
expect(response).toEqual({ User: [null] })
})
})
})

View File

@ -3,12 +3,9 @@ import uniqueSlug from './slugify/uniqueSlug'
const isUniqueFor = (context, type) => {
return async slug => {
const session = context.driver.session()
const response = await session.run(
`MATCH(p:${type} {slug: $slug }) return p.slug`,
{
slug
}
)
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
slug,
})
session.close()
return response.records.length === 0
}
@ -17,28 +14,20 @@ const isUniqueFor = (context, type) => {
export default {
Mutation: {
CreatePost: async (resolve, root, args, context, info) => {
args.slug =
args.slug ||
(await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},
CreateUser: async (resolve, root, args, context, info) => {
args.slug =
args.slug ||
(await uniqueSlug(args.name, isUniqueFor(context, 'User')))
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
return resolve(root, args, context, info)
},
CreateOrganization: async (resolve, root, args, context, info) => {
args.slug =
args.slug ||
(await uniqueSlug(args.name, isUniqueFor(context, 'Organization')))
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization')))
return resolve(root, args, context, info)
},
CreateCategory: async (resolve, root, args, context, info) => {
args.slug =
args.slug ||
(await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
return resolve(root, args, context, info)
}
}
},
},
}

View File

@ -1,7 +1,7 @@
import slugify from 'slug'
export default async function uniqueSlug (string, isUnique) {
export default async function uniqueSlug(string, isUnique) {
let slug = slugify(string || 'anonymous', {
lower: true
lower: true,
})
if (await isUnique(slug)) return slug
@ -10,6 +10,6 @@ export default async function uniqueSlug (string, isUnique) {
do {
count += 1
uniqueSlug = `${slug}-${count}`
} while (!await isUnique(uniqueSlug))
} while (!(await isUnique(uniqueSlug)))
return uniqueSlug
}

View File

@ -3,14 +3,14 @@ import uniqueSlug from './uniqueSlug'
describe('uniqueSlug', () => {
it('slugifies given string', () => {
const string = 'Hello World'
const isUnique = jest.fn()
.mockResolvedValue(true)
const isUnique = jest.fn().mockResolvedValue(true)
expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world')
})
it('increments slugified string until unique', () => {
const string = 'Hello World'
const isUnique = jest.fn()
const isUnique = jest
.fn()
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true)
expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1')
@ -18,8 +18,7 @@ describe('uniqueSlug', () => {
it('slugify null string', () => {
const string = null
const isUnique = jest.fn()
.mockResolvedValue(true)
const isUnique = jest.fn().mockResolvedValue(true)
expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous')
})
})

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories'
import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
let authenticatedClient
let headers
@ -10,7 +10,7 @@ beforeEach(async () => {
await factory.create('User', { email: 'user@example.org', password: '1234' })
await factory.create('User', {
email: 'someone@example.org',
password: '1234'
password: '1234',
})
headers = await login({ email: 'user@example.org', password: '1234' })
authenticatedClient = new GraphQLClient(host, { headers })
@ -30,7 +30,7 @@ describe('slugify', () => {
) { slug }
}`)
expect(response).toEqual({
CreatePost: { slug: 'i-am-a-brand-new-post' }
CreatePost: { slug: 'i-am-a-brand-new-post' },
})
})
@ -38,11 +38,11 @@ describe('slugify', () => {
beforeEach(async () => {
const asSomeoneElse = await Factory().authenticateAs({
email: 'someone@example.org',
password: '1234'
password: '1234',
})
await asSomeoneElse.create('Post', {
title: 'Pre-existing post',
slug: 'pre-existing-post'
slug: 'pre-existing-post',
})
})
@ -54,7 +54,7 @@ describe('slugify', () => {
) { slug }
}`)
expect(response).toEqual({
CreatePost: { slug: 'pre-existing-post-1' }
CreatePost: { slug: 'pre-existing-post-1' },
})
})
@ -67,7 +67,7 @@ describe('slugify', () => {
content: "Some content",
slug: "pre-existing-post"
) { slug }
}`)
}`),
).rejects.toThrow('already exists')
})
})
@ -81,32 +81,26 @@ describe('slugify', () => {
}`)
}
it('generates a slug based on name', async () => {
await expect(
action('CreateUser', 'name: "I am a user"')
).resolves.toEqual({ CreateUser: { slug: 'i-am-a-user' } })
await expect(action('CreateUser', 'name: "I am a user"')).resolves.toEqual({
CreateUser: { slug: 'i-am-a-user' },
})
})
describe('if slug exists', () => {
beforeEach(async () => {
await action(
'CreateUser',
'name: "Pre-existing user", slug: "pre-existing-user"'
)
await action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"')
})
it('chooses another slug', async () => {
await expect(
action('CreateUser', 'name: "pre-existing-user"')
).resolves.toEqual({ CreateUser: { slug: 'pre-existing-user-1' } })
await expect(action('CreateUser', 'name: "pre-existing-user"')).resolves.toEqual({
CreateUser: { slug: 'pre-existing-user-1' },
})
})
describe('but if the client specifies a slug', () => {
it('rejects CreateUser', async () => {
await expect(
action(
'CreateUser',
'name: "Pre-existing user", slug: "pre-existing-user"'
)
action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"'),
).rejects.toThrow('already exists')
})
})

View File

@ -30,7 +30,7 @@ export default {
Query: {
Post: setDefaultFilters,
Comment: setDefaultFilters,
User: setDefaultFilters
User: setDefaultFilters,
},
Mutation: async (resolve, root, args, context, info) => {
args.disabled = false
@ -42,5 +42,5 @@ export default {
},
Post: obfuscateDisabled,
User: obfuscateDisabled,
Comment: obfuscateDisabled
Comment: obfuscateDisabled,
}

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories'
import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
const factory = Factory()
let client
@ -11,32 +11,51 @@ 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' }),
factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' }),
factory.create('User', { id: 'u2', role: 'user', name: 'Offensive Name', avatar: '/some/offensive/avatar.jpg', about: 'This self description is very offensive', email: 'troll@example.org', password: '1234' })
factory.create('User', {
id: 'm1',
role: 'moderator',
email: 'moderator@example.org',
password: '1234',
}),
factory.create('User', {
id: 'u2',
role: 'user',
name: 'Offensive Name',
avatar: '/some/offensive/avatar.jpg',
about: 'This self description is very offensive',
email: 'troll@example.org',
password: '1234',
}),
])
await factory.authenticateAs({ email: 'user@example.org', password: '1234' })
await Promise.all([
factory.follow({ id: 'u2', type: 'User' }),
factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }),
factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false })
factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }),
])
await Promise.all([
factory.create('Comment', { id: 'c2', postId: 'p3', content: 'Enabled comment on public post' })
factory.create('Comment', {
id: 'c2',
postId: 'p3',
content: 'Enabled comment on public post',
}),
])
await Promise.all([
factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })
])
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', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false })
await asTroll.create('Post', {
id: 'p2',
title: 'Disabled post',
content: 'This is an offensive post content',
image: '/some/offensive/image.jpg',
deleted: false,
})
await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
await Promise.all([
asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })
])
await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })])
const asModerator = Factory()
await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' })
@ -65,7 +84,8 @@ describe('softDeleteMiddleware', () => {
user = response.User[0].following[0]
}
const beforePost = async () => {
query = '{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }'
query =
'{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }'
const response = await action()
post = response.User[0].following[0].contributions[0]
}
@ -84,7 +104,8 @@ describe('softDeleteMiddleware', () => {
beforeEach(beforeUser)
it('displays name', () => expect(user.name).toEqual('Offensive Name'))
it('displays about', () => expect(user.about).toEqual('This self description is very offensive'))
it('displays about', () =>
expect(user.about).toEqual('This self description is very offensive'))
it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg'))
})
@ -92,8 +113,10 @@ describe('softDeleteMiddleware', () => {
beforeEach(beforePost)
it('displays title', () => expect(post.title).toEqual('Disabled post'))
it('displays content', () => expect(post.content).toEqual('This is an offensive post content'))
it('displays contentExcerpt', () => expect(post.contentExcerpt).toEqual('This is an offensive post content'))
it('displays content', () =>
expect(post.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'))
})
@ -101,7 +124,8 @@ describe('softDeleteMiddleware', () => {
beforeEach(beforeComment)
it('displays content', () => expect(comment.content).toEqual('Disabled comment'))
it('displays contentExcerpt', () => expect(comment.contentExcerpt).toEqual('Disabled comment'))
it('displays contentExcerpt', () =>
expect(comment.contentExcerpt).toEqual('Disabled comment'))
})
})
@ -162,10 +186,7 @@ describe('softDeleteMiddleware', () => {
})
it('shows disabled but hides deleted posts', async () => {
const expected = [
{ title: 'Disabled post' },
{ title: 'Publicly visible post' }
]
const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }]
const { Post } = await action()
await expect(Post).toEqual(expect.arrayContaining(expected))
})
@ -185,9 +206,11 @@ describe('softDeleteMiddleware', () => {
it('conceals disabled comments', async () => {
const expected = [
{ content: 'Enabled comment on public post' },
{ content: 'UNAVAILABLE' }
{ content: 'UNAVAILABLE' },
]
const { Post: [{ comments }] } = await action()
const {
Post: [{ comments }],
} = await action()
await expect(comments).toEqual(expect.arrayContaining(expected))
})
})
@ -201,9 +224,11 @@ describe('softDeleteMiddleware', () => {
it('shows disabled comments', async () => {
const expected = [
{ content: 'Enabled comment on public post' },
{ content: 'Disabled comment' }
{ content: 'Disabled comment' },
]
const { Post: [{ comments }] } = await action()
const {
Post: [{ comments }],
} = await action()
await expect(comments).toEqual(expect.arrayContaining(expected))
})
})

View File

@ -1,9 +1,5 @@
import dotenv from 'dotenv'
import createOrUpdateLocations from './nodes/locations'
dotenv.config()
export default {
Mutation: {
CreateUser: async (resolve, root, args, context, info) => {
@ -15,6 +11,6 @@ export default {
const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver)
return result
}
}
},
},
}

View File

@ -3,13 +3,11 @@ import { UserInputError } from 'apollo-server'
const USERNAME_MIN_LENGTH = 3
const validateUsername = async (resolve, root, args, context, info) => {
if (args.name && args.name.length >= USERNAME_MIN_LENGTH) {
if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) {
/* eslint-disable-next-line no-return-await */
return await resolve(root, args, context, info)
} else {
throw new UserInputError(
`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`
)
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`)
}
}
@ -28,9 +26,7 @@ const validateComment = async (resolve, root, args, context, info) => {
const COMMENT_MIN_LENGTH = 1
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
throw new UserInputError(
`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`
)
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
}
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
const { postId } = args
@ -47,6 +43,6 @@ export default {
UpdateUser: validateUsername,
CreateSocialMedia: validateUrl,
CreateComment: validateComment,
UpdateComment: validateComment
}
UpdateComment: validateComment,
},
}

View File

@ -5,7 +5,7 @@ import sanitizeHtml from 'sanitize-html'
import cheerio from 'cheerio'
import linkifyHtml from 'linkifyjs/html'
const embedToAnchor = (content) => {
const embedToAnchor = content => {
const $ = cheerio.load(content)
$('div[data-url-embed]').each((i, el) => {
let url = el.attribs['data-url-embed']
@ -15,7 +15,7 @@ const embedToAnchor = (content) => {
return $('body').html()
}
function clean (dirty) {
function clean(dirty) {
if (!dirty) {
return dirty
}
@ -24,27 +24,48 @@ function clean (dirty) {
dirty = embedToAnchor(dirty)
dirty = linkifyHtml(dirty)
dirty = sanitizeHtml(dirty, {
allowedTags: ['iframe', 'img', 'p', 'h3', 'h4', 'br', 'hr', 'b', 'i', 'em', 'strong', 'a', 'pre', 'ul', 'li', 'ol', 's', 'strike', 'span', 'blockquote'],
allowedTags: [
'iframe',
'img',
'p',
'h3',
'h4',
'br',
'hr',
'b',
'i',
'em',
'strong',
'a',
'pre',
'ul',
'li',
'ol',
's',
'strike',
'span',
'blockquote',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
span: ['contenteditable', 'class', 'data-*'],
img: ['src'],
iframe: ['src', 'class', 'frameborder', 'allowfullscreen']
iframe: ['src', 'class', 'frameborder', 'allowfullscreen'],
},
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
parser: {
lowerCaseTags: true
lowerCaseTags: true,
},
transformTags: {
iframe: function (tagName, attribs) {
iframe: function(tagName, attribs) {
return {
tagName: 'a',
text: attribs.src,
attribs: {
href: attribs.src,
target: '_blank',
'data-url-embed': ''
}
'data-url-embed': '',
},
}
},
h1: 'h3',
@ -53,19 +74,19 @@ function clean (dirty) {
h4: 'h4',
h5: 'strong',
i: 'em',
a: function (tagName, attribs) {
a: function(tagName, attribs) {
return {
tagName: 'a',
attribs: {
href: attribs.href,
target: '_blank',
rel: 'noopener noreferrer nofollow'
}
rel: 'noopener noreferrer nofollow',
},
}
},
b: 'strong',
s: 'strike',
img: function (tagName, attribs) {
img: function(tagName, attribs) {
let src = attribs.src
if (!src) {
@ -88,11 +109,11 @@ function clean (dirty) {
tagName: 'img',
attribs: {
// TODO: use environment variables
src: `http://localhost:3050/images?url=${src}`
}
src: `http://localhost:3050/images?url=${src}`,
},
}
}
}
},
},
})
// remove empty html tags and duplicated linebreaks and returns
@ -100,10 +121,7 @@ function clean (dirty) {
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
// remove all iframes
.replace(
/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim,
''
)
.replace(/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim, '')
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
@ -111,15 +129,9 @@ function clean (dirty) {
// limit linebreaks to max 2 (equivalent to html "br" linebreak)
.replace(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(
/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim,
'</p><p>'
)
.replace(/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim, '</p><p>')
// remove additional linebreaks inside p tags
.replace(
/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim,
''
)
.replace(/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '')
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
@ -138,5 +150,5 @@ export default {
Query: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
return walkRecursive(result, fields, clean)
}
},
}

View File

@ -1,15 +1,14 @@
import faker from 'faker'
export default {
User: () => ({
name: () => `${faker.name.firstName()} ${faker.name.lastName()}`,
email: () => `${faker.internet.email()}`
email: () => `${faker.internet.email()}`,
}),
Post: () => ({
title: () => faker.lorem.lines(1),
slug: () => faker.lorem.slug(3),
content: () => faker.lorem.paragraphs(5),
contentExcerpt: () => faker.lorem.paragraphs(1)
})
contentExcerpt: () => faker.lorem.paragraphs(1),
}),
}

View File

@ -1,211 +0,0 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let client
let createCommentVariables
let createPostVariables
let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('Comment Functionality', () => {
const createCommentMutation = `
mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
}
}
`
const createPostMutation = `
mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
}
}
`
const commentQueryForPostId = `
query($content: String) {
Comment(content: $content) {
postId
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
createCommentVariables = {
postId: 'p1',
content: 'I\'m not authorised to comment'
}
client = new GraphQLClient(host)
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
createCommentVariables = {
postId: 'p1',
content: 'I\'m authorised to comment'
}
createPostVariables = {
id: 'p1',
title: 'post to comment on',
content: 'please comment on me'
}
await client.request(createPostMutation, createPostVariables)
})
it('creates a comment', async () => {
const expected = {
CreateComment: {
content: 'I\'m authorised to comment'
}
}
await expect(
client.request(createCommentMutation, createCommentVariables)
).resolves.toMatchObject(expected)
})
it('updates a comment', async () => {
await client.request(createCommentMutation, createCommentVariables)
const updateCommentMutation = `
mutation($postId: ID, $content: String!, $id: ID!) {
UpdateComment(postId: $postId, content: $content, id: $id) {
id
content
}
}
`
let updateCommentVariables = {
postId: 'p1',
content: 'Comment is updated',
id: 'c8'
}
const expected = {
UpdateComment: {
content: 'Comment is updated'
}
}
await expect(
client.request(updateCommentMutation, updateCommentVariables)
).resolves.toMatchObject(expected)
})
it('assigns the authenticated user as author', async () => {
await client.request(createCommentMutation, createCommentVariables)
const { User } = await client.request(`{
User(email: "test@example.org") {
comments {
content
}
}
}`)
expect(User).toEqual([ { comments: [ { 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>'
}
await expect(client.request(createCommentMutation, createCommentVariables))
.rejects.toThrow('Comment must be at least 1 character long!')
})
it('throws an error if a comment sent from the editor does not contain a single character', async () => {
createCommentVariables = {
postId: 'p1',
content: '<p> </p>'
}
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 () => {
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 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('does not create the comment with the postId as an attribute', async () => {
const commentQueryVariablesByContent = {
content: 'I\'m authorised to comment'
}
await client.request(createCommentMutation, createCommentVariables)
const { Comment } = await client.request(
commentQueryForPostId,
commentQueryVariablesByContent
)
expect(Comment).toEqual([{ postId: null }])
})
})
})

View File

@ -1,22 +0,0 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
export default {
Mutation: {
CreatePost: async (object, params, context, resolveInfo) => {
const result = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
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
}
)
session.close()
return result
}
}
}

View File

@ -1,101 +0,0 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
describe('CreateSocialMedia', () => {
let client
let headers
const mutationC = `
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
url
}
}
`
const mutationD = `
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url
}
}
`
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'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
const variables = { url: 'http://nsosp.org' }
await expect(
client.request(mutationC, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('creates social media with correct URL', async () => {
const variables = { url: 'http://nsosp.org' }
await expect(
client.request(mutationC, variables)
).resolves.toEqual(expect.objectContaining({
CreateSocialMedia: {
id: expect.any(String),
url: 'http://nsosp.org'
}
}))
})
it('deletes social media', async () => {
const creationVariables = { url: 'http://nsosp.org' }
const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
const { id } = CreateSocialMedia
const deletionVariables = { id }
const expected = {
DeleteSocialMedia: {
id: id,
url: 'http://nsosp.org'
}
}
await expect(
client.request(mutationD, deletionVariables)
).resolves.toEqual(expected)
})
it('rejects empty string', async () => {
const variables = { url: '' }
await expect(
client.request(mutationC, variables)
).rejects.toThrow('Input is not a URL')
})
it('validates URLs', async () => {
const variables = { url: 'not-a-url' }
await expect(
client.request(mutationC, variables)
).rejects.toThrow('Input is not a URL')
})
})
})

View File

@ -1,67 +0,0 @@
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)
})
}
}
}

View File

@ -0,0 +1,24 @@
import { makeAugmentedSchema } from 'neo4j-graphql-js'
import CONFIG from './../config'
import applyScalars from './../bootstrap/scalars'
import applyDirectives from './../bootstrap/directives'
import typeDefs from './types'
import resolvers from './resolvers'
export default applyScalars(
applyDirectives(
makeAugmentedSchema({
typeDefs,
resolvers,
config: {
query: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
},
mutation: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
},
debug: CONFIG.DEBUG,
},
}),
),
)

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
@ -10,17 +10,17 @@ describe('badges', () => {
await factory.create('User', {
email: 'user@example.org',
role: 'user',
password: '1234'
password: '1234',
})
await factory.create('User', {
id: 'u2',
role: 'moderator',
email: 'moderator@example.org'
email: 'moderator@example.org',
})
await factory.create('User', {
id: 'u3',
role: 'admin',
email: 'admin@example.org'
email: 'admin@example.org',
})
})
@ -34,15 +34,15 @@ describe('badges', () => {
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg'
icon: '/img/badges/indiegogo_en_racoon.svg',
}
const mutation = `
mutation(
$id: ID
$key: String!
$type: BadgeTypeEnum!
$status: BadgeStatusEnum!
$type: BadgeType!
$status: BadgeStatus!
$icon: String!
) {
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
@ -58,9 +58,7 @@ describe('badges', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -76,8 +74,8 @@ describe('badges', () => {
id: 'b1',
key: 'indiegogo_en_racoon',
status: 'permanent',
type: 'crowdfunding'
}
type: 'crowdfunding',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
@ -90,9 +88,7 @@ describe('badges', () => {
})
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
})
@ -104,7 +100,7 @@ describe('badges', () => {
})
const variables = {
id: 'b1',
key: 'whatever'
key: 'whatever',
}
const mutation = `
@ -119,9 +115,7 @@ describe('badges', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -132,9 +126,7 @@ describe('badges', () => {
})
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -147,8 +139,8 @@ describe('badges', () => {
const expected = {
UpdateBadge: {
id: 'b1',
key: 'whatever'
}
key: 'whatever',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
@ -161,7 +153,7 @@ describe('badges', () => {
await factory.create('Badge', { id: 'b1' })
})
const variables = {
id: 'b1'
id: 'b1',
}
const mutation = `
@ -175,9 +167,7 @@ describe('badges', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -188,9 +178,7 @@ describe('badges', () => {
})
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -202,8 +190,8 @@ describe('badges', () => {
it('deletes a badge', async () => {
const expected = {
DeleteBadge: {
id: 'b1'
}
id: 'b1',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})

View File

@ -19,8 +19,8 @@ export default {
MATCH (post:Post {id: $postId})
RETURN post`,
{
postId
}
postId,
},
)
const [post] = postQueryRes.records.map(record => {
return record.get('post')
@ -29,14 +29,7 @@ export default {
if (!post) {
throw new UserInputError(NO_POST_ERR_MESSAGE)
}
const comment = await neo4jgraphql(
object,
params,
context,
resolveInfo,
false
)
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
await session.run(
`
@ -46,8 +39,8 @@ export default {
{
userId: context.user.id,
postId,
commentId: comment.id
}
commentId: comment.id,
},
)
session.close()
@ -55,6 +48,11 @@ export default {
},
UpdateComment: async (object, params, context, resolveInfo) => {
await neo4jgraphql(object, params, context, resolveInfo, false)
}
}
},
DeleteComment: async (object, params, context, resolveInfo) => {
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
return comment
},
},
}

View File

@ -0,0 +1,299 @@
import gql from 'graphql-tag'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
let createCommentVariables
let createPostVariables
let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234',
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('CreateComment', () => {
const createCommentMutation = gql`
mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
}
}
`
const createPostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
}
}
`
const commentQueryForPostId = gql`
query($content: String) {
Comment(content: $content) {
postId
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
createCommentVariables = {
postId: 'p1',
content: "I'm not authorised to comment",
}
client = new GraphQLClient(host)
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
createCommentVariables = {
postId: 'p1',
content: "I'm authorised to comment",
}
createPostVariables = {
id: 'p1',
title: 'post to comment on',
content: 'please comment on me',
}
await client.request(createPostMutation, createPostVariables)
})
it('creates a comment', async () => {
const expected = {
CreateComment: {
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(email: "test@example.org") {
comments {
content
}
}
}
`)
expect(User).toEqual([
{
comments: [
{
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>',
}
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
'Comment must be at least 1 character long!',
)
})
it('throws an error if a comment sent from the editor does not contain a single character', async () => {
createCommentVariables = {
postId: 'p1',
content: '<p> </p>',
}
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 () => {
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 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('does not create the comment with the postId as an attribute', async () => {
const commentQueryVariablesByContent = {
content: "I'm authorised to comment",
}
await client.request(createCommentMutation, createCommentVariables)
const { Comment } = await client.request(
commentQueryForPostId,
commentQueryVariablesByContent,
)
expect(Comment).toEqual([
{
postId: null,
},
])
})
})
})
describe('DeleteComment', () => {
const deleteCommentMutation = gql`
mutation($id: ID!) {
DeleteComment(id: $id) {
id
}
}
`
let deleteCommentVariables = {
id: 'c1',
}
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: 'Post to be commented',
})
await asAuthor.create('Comment', {
id: 'c1',
postId: 'p1',
content: 'Comment to be deleted',
})
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
describe('authenticated but not the author', () => {
beforeEach(async () => {
let headers
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('throws authorization error', async () => {
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
describe('authenticated as author', () => {
beforeEach(async () => {
let headers
headers = await login({
email: 'author@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('deletes the comment', async () => {
const expected = {
DeleteComment: {
id: 'c1',
},
}
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).resolves.toEqual(
expected,
)
})
})
})

View File

@ -0,0 +1,27 @@
import { createWriteStream } from 'fs'
import path from 'path'
import slug from 'slug'
const storeUpload = ({ createReadStream, fileLocation }) =>
new Promise((resolve, reject) =>
createReadStream()
.pipe(createWriteStream(`public${fileLocation}`))
.on('finish', resolve)
.on('error', reject),
)
export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) {
const upload = params[file]
if (upload) {
const { createReadStream, filename } = await upload
const { name } = path.parse(filename)
const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
await uploadCallback({ createReadStream, fileLocation })
delete params[file]
params[url] = fileLocation
}
return params
}

View File

@ -0,0 +1,65 @@
import fileUpload from '.'
describe('fileUpload', () => {
let params
let uploadCallback
beforeEach(() => {
params = {
uploadAttribute: {
filename: 'avatar.jpg',
mimetype: 'image/jpeg',
encoding: '7bit',
createReadStream: jest.fn(),
},
}
uploadCallback = jest.fn()
})
it('calls uploadCallback', async () => {
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(uploadCallback).toHaveBeenCalled()
})
describe('file name', () => {
it('saves the upload url in params[url]', async () => {
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/)
})
it('uses the name without file ending', async () => {
params.uploadAttribute.filename = 'somePng.png'
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/)
})
it('creates a url safe name', async () => {
params.uploadAttribute.filename =
'/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/)
})
describe('in case of duplicates', () => {
it('creates unique names to avoid overwriting existing files', async () => {
const { attribute: first } = await fileUpload(
{
...params,
},
{ file: 'uploadAttribute', url: 'attribute' },
uploadCallback,
)
await new Promise(resolve => setTimeout(resolve, 1000))
const { attribute: second } = await fileUpload(
{
...params,
},
{ file: 'uploadAttribute', url: 'attribute' },
uploadCallback,
)
expect(first).not.toEqual(second)
})
})
})
})

View File

@ -12,8 +12,8 @@ export default {
{
id,
type,
userId: context.user.id
}
userId: context.user.id,
},
)
const [isFollowed] = transactionRes.records.map(record => {
@ -37,8 +37,8 @@ export default {
{
id,
type,
userId: context.user.id
}
userId: context.user.id,
},
)
const [isFollowed] = transactionRes.records.map(record => {
return record.get('isFollowed')
@ -46,6 +46,6 @@ export default {
session.close()
return isFollowed
}
}
},
},
}

View File

@ -1,17 +1,17 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let clientUser1
let headersUser1
const mutationFollowUser = (id) => `
const mutationFollowUser = id => `
mutation {
follow(id: "${id}", type: User)
}
`
const mutationUnfollowUser = (id) => `
const mutationUnfollowUser = id => `
mutation {
unfollow(id: "${id}", type: User)
}
@ -21,12 +21,12 @@ beforeEach(async () => {
await factory.create('User', {
id: 'u1',
email: 'test@example.org',
password: '1234'
password: '1234',
})
await factory.create('User', {
id: 'u2',
email: 'test2@example.org',
password: '1234'
password: '1234',
})
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
@ -43,18 +43,14 @@ describe('follow', () => {
it('throws authorization error', async () => {
let client
client = new GraphQLClient(host)
await expect(
client.request(mutationFollowUser('u2'))
).rejects.toThrow('Not Authorised')
await expect(client.request(mutationFollowUser('u2'))).rejects.toThrow('Not Authorised')
})
})
it('I can follow another user', async () => {
const res = await clientUser1.request(
mutationFollowUser('u2')
)
const res = await clientUser1.request(mutationFollowUser('u2'))
const expected = {
follow: true
follow: true,
}
expect(res).toMatchObject(expected)
@ -65,20 +61,16 @@ describe('follow', () => {
}
}`)
const expected2 = {
followedBy: [
{ id: 'u1' }
],
followedByCurrentUser: true
followedBy: [{ id: 'u1' }],
followedByCurrentUser: true,
}
expect(User[0]).toMatchObject(expected2)
})
it('I can`t follow myself', async () => {
const res = await clientUser1.request(
mutationFollowUser('u1')
)
const res = await clientUser1.request(mutationFollowUser('u1'))
const expected = {
follow: false
follow: false,
}
expect(res).toMatchObject(expected)
@ -90,7 +82,7 @@ describe('follow', () => {
}`)
const expected2 = {
followedBy: [],
followedByCurrentUser: false
followedByCurrentUser: false,
}
expect(User[0]).toMatchObject(expected2)
})
@ -99,26 +91,20 @@ describe('follow', () => {
describe('unauthenticated follow', () => {
it('throws authorization error', async () => {
// follow
await clientUser1.request(
mutationFollowUser('u2')
)
await clientUser1.request(mutationFollowUser('u2'))
// unfollow
let client
client = new GraphQLClient(host)
await expect(
client.request(mutationUnfollowUser('u2'))
).rejects.toThrow('Not Authorised')
await expect(client.request(mutationUnfollowUser('u2'))).rejects.toThrow('Not Authorised')
})
})
it('I can unfollow a user', async () => {
// follow
await clientUser1.request(
mutationFollowUser('u2')
)
await clientUser1.request(mutationFollowUser('u2'))
// unfollow
const expected = {
unfollow: true
unfollow: true,
}
const res = await clientUser1.request(mutationUnfollowUser('u2'))
expect(res).toMatchObject(expected)
@ -131,7 +117,7 @@ describe('follow', () => {
}`)
const expected2 = {
followedBy: [],
followedByCurrentUser: false
followedByCurrentUser: false,
}
expect(User[0]).toMatchObject(expected2)
})

View File

@ -0,0 +1,5 @@
import path from 'path'
import { fileLoader, mergeResolvers } from 'merge-graphql-schemas'
const resolversArray = fileLoader(path.join(__dirname, './!(*.spec).js'))
export default mergeResolvers(resolversArray)

View File

@ -14,7 +14,7 @@ export default {
const session = driver.session()
const res = await session.run(cypher, { id, userId })
session.close()
const [resource] = res.records.map((record) => {
const [resource] = res.records.map(record => {
return record.get('resource')
})
if (!resource) return null
@ -31,11 +31,11 @@ export default {
const session = driver.session()
const res = await session.run(cypher, { id })
session.close()
const [resource] = res.records.map((record) => {
const [resource] = res.records.map(record => {
return record.get('resource')
})
if (!resource) return null
return resource.id
}
}
},
},
}

View File

@ -1,11 +1,11 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
const setupAuthenticateClient = (params) => {
const setupAuthenticateClient = params => {
const authenticateClient = async () => {
await factory.create('User', params)
const headers = await login(params)
@ -46,7 +46,7 @@ describe('disable', () => {
beforeEach(() => {
// our defaul set of variables
variables = {
id: 'blabla'
id: 'blabla',
}
})
@ -63,7 +63,7 @@ describe('disable', () => {
beforeEach(() => {
authenticateClient = setupAuthenticateClient({
email: 'user@example.org',
password: '1234'
password: '1234',
})
})
@ -78,19 +78,17 @@ describe('disable', () => {
id: 'u7',
email: 'moderator@example.org',
password: '1234',
role: 'moderator'
role: 'moderator',
})
})
describe('on something that is not a (Comment|Post|User) ', () => {
beforeEach(async () => {
variables = {
id: 't23'
id: 't23',
}
createResource = () => {
return Promise.all([
factory.create('Tag', { id: 't23' })
])
return Promise.all([factory.create('Tag', { id: 't23' })])
}
})
@ -104,21 +102,28 @@ describe('disable', () => {
describe('on a comment', () => {
beforeEach(async () => {
variables = {
id: 'c47'
id: 'c47',
}
createPostVariables = {
id: 'p3',
title: 'post to comment on',
content: 'please comment on me'
content: 'please comment on me',
}
createCommentVariables = {
id: 'c47',
postId: 'p3',
content: 'this comment was created for this post'
content: 'this comment was created for this post',
}
createResource = async () => {
await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' })
const asAuthenticatedUser = await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
await factory.create('User', {
id: 'u45',
email: 'commenter@example.org',
password: '1234',
})
const asAuthenticatedUser = await factory.authenticateAs({
email: 'commenter@example.org',
password: '1234',
})
await asAuthenticatedUser.create('Post', createPostVariables)
await asAuthenticatedUser.create('Comment', createCommentVariables)
}
@ -135,41 +140,39 @@ describe('disable', () => {
const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] }
await setup()
await expect(client.request(
'{ Comment { id, disabledBy { id } } }'
)).resolves.toEqual(before)
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)
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 } ] }
const before = { Comment: [{ id: 'c47', disabled: false }] }
const expected = { Comment: [{ id: 'c47', disabled: true }] }
await setup()
await expect(client.request(
'{ Comment { id disabled } }'
)).resolves.toEqual(before)
await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Comment(disabled: true) { id disabled } }'
)).resolves.toEqual(expected)
await expect(
client.request('{ Comment(disabled: true) { id disabled } }'),
).resolves.toEqual(expected)
})
})
describe('on a post', () => {
beforeEach(async () => {
variables = {
id: 'p9'
id: 'p9',
}
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
id: 'p9', // that's the ID we will look for
})
}
})
@ -185,27 +188,25 @@ describe('disable', () => {
const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] }
await setup()
await expect(client.request(
'{ Post { id, disabledBy { id } } }'
)).resolves.toEqual(before)
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)
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 } ] }
const before = { Post: [{ id: 'p9', disabled: false }] }
const expected = { Post: [{ id: 'p9', disabled: true }] }
await setup()
await expect(client.request(
'{ Post { id disabled } }'
)).resolves.toEqual(before)
await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Post(disabled: true) { id disabled } }'
)).resolves.toEqual(expected)
await expect(client.request('{ Post(disabled: true) { id disabled } }')).resolves.toEqual(
expected,
)
})
})
})
@ -227,7 +228,7 @@ describe('enable', () => {
beforeEach(() => {
// our defaul set of variables
variables = {
id: 'blabla'
id: 'blabla',
}
})
@ -240,7 +241,7 @@ describe('enable', () => {
beforeEach(() => {
authenticateClient = setupAuthenticateClient({
email: 'user@example.org',
password: '1234'
password: '1234',
})
})
@ -254,20 +255,18 @@ describe('enable', () => {
authenticateClient = setupAuthenticateClient({
role: 'moderator',
email: 'someUser@example.org',
password: '1234'
password: '1234',
})
})
describe('on something that is not a (Comment|Post|User) ', () => {
beforeEach(async () => {
variables = {
id: 't23'
id: 't23',
}
createResource = () => {
// we cannot create a :DISABLED relationship here
return Promise.all([
factory.create('Tag', { id: 't23' })
])
return Promise.all([factory.create('Tag', { id: 't23' })])
}
})
@ -281,21 +280,28 @@ describe('enable', () => {
describe('on a comment', () => {
beforeEach(async () => {
variables = {
id: 'c456'
id: 'c456',
}
createPostVariables = {
id: 'p9',
title: 'post to comment on',
content: 'please comment on me'
content: 'please comment on me',
}
createCommentVariables = {
id: 'c456',
postId: 'p9',
content: 'this comment was created for this post'
content: 'this comment was created for this post',
}
createResource = async () => {
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
const asAuthenticatedUser = await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
await factory.create('User', {
id: 'u123',
email: 'author@example.org',
password: '1234',
})
const asAuthenticatedUser = await factory.authenticateAs({
email: 'author@example.org',
password: '1234',
})
await asAuthenticatedUser.create('Post', createPostVariables)
await asAuthenticatedUser.create('Comment', createCommentVariables)
@ -319,41 +325,43 @@ describe('enable', () => {
const expected = { Comment: [{ id: 'c456', disabledBy: null }] }
await setup()
await expect(client.request(
'{ Comment(disabled: true) { id, disabledBy { id } } }'
)).resolves.toEqual(before)
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)
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 } ] }
const before = { Comment: [{ id: 'c456', disabled: true }] }
const expected = { Comment: [{ id: 'c456', disabled: false }] }
await setup()
await expect(client.request(
'{ Comment(disabled: true) { id disabled } }'
)).resolves.toEqual(before)
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)
await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(expected)
})
})
describe('on a post', () => {
beforeEach(async () => {
variables = {
id: 'p9'
id: 'p9',
}
createResource = async () => {
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
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
id: 'p9', // that's the ID we will look for
})
const disableMutation = `
@ -376,27 +384,25 @@ describe('enable', () => {
const expected = { Post: [{ id: 'p9', disabledBy: null }] }
await setup()
await expect(client.request(
'{ Post(disabled: true) { id, disabledBy { id } } }'
)).resolves.toEqual(before)
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)
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 } ] }
const before = { Post: [{ id: 'p9', disabled: true }] }
const expected = { Post: [{ id: 'p9', disabled: false }] }
await setup()
await expect(client.request(
'{ Post(disabled: true) { id disabled } }'
)).resolves.toEqual(before)
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)
await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(expected)
})
})
})

View File

@ -4,11 +4,11 @@ export default {
Query: {
Notification: (object, params, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false)
}
},
},
Mutation: {
UpdateNotification: (object, params, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false)
}
}
},
},
}

View File

@ -1,14 +1,13 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
let userParams = {
id: 'you',
email: 'test@example.org',
password: '1234'
password: '1234',
}
beforeEach(async () => {
@ -49,12 +48,12 @@ describe('currentUser { notifications }', () => {
const neighborParams = {
email: 'neighbor@example.org',
password: '1234',
id: 'neighbor'
id: 'neighbor',
}
await Promise.all([
factory.create('User', neighborParams),
factory.create('Notification', { id: 'not-for-you' }),
factory.create('Notification', { id: 'already-seen', read: true })
factory.create('Notification', { id: 'already-seen', read: true }),
])
await factory.create('Notification', { id: 'unseen' })
await factory.authenticateAs(neighborParams)
@ -65,7 +64,7 @@ describe('currentUser { notifications }', () => {
factory.relate('Notification', 'User', { from: 'unseen', to: 'you' }),
factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }),
factory.relate('Notification', 'User', { from: 'already-seen', to: 'you' }),
factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' })
factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }),
])
})
@ -84,10 +83,8 @@ describe('currentUser { notifications }', () => {
it('returns only unread notifications of current user', async () => {
const expected = {
currentUser: {
notifications: [
{ id: 'unseen', post: { id: 'p1' } }
]
}
notifications: [{ id: 'unseen', post: { id: 'p1' } }],
},
}
await expect(client.request(query, variables)).resolves.toEqual(expected)
})
@ -109,9 +106,9 @@ describe('currentUser { notifications }', () => {
currentUser: {
notifications: [
{ id: 'unseen', post: { id: 'p1' } },
{ id: 'already-seen', post: { id: 'p1' } }
]
}
{ id: 'already-seen', post: { id: 'p1' } },
],
},
}
await expect(client.request(query, variables)).resolves.toEqual(expected)
})
@ -136,7 +133,7 @@ describe('UpdateNotification', () => {
id: 'mentioned-1',
email: 'mentioned@example.org',
password: '1234',
slug: 'mentioned'
slug: 'mentioned',
}
await factory.create('User', mentionedParams)
await factory.create('Notification', { id: 'to-be-updated' })
@ -144,7 +141,7 @@ describe('UpdateNotification', () => {
await factory.create('Post', { id: 'p1' })
await Promise.all([
factory.relate('Notification', 'User', { from: 'to-be-updated', to: 'mentioned-1' }),
factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' })
factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }),
])
})

View File

@ -0,0 +1,30 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload'
export default {
Mutation: {
UpdatePost: async (object, params, context, resolveInfo) => {
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
return neo4jgraphql(object, params, context, resolveInfo, false)
},
CreatePost: async (object, params, context, resolveInfo) => {
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
const result = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
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,
},
)
session.close()
return result
},
},
}

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
@ -8,7 +8,7 @@ let client
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234'
password: '1234',
})
})
@ -47,22 +47,25 @@ describe('CreatePost', () => {
const expected = {
CreatePost: {
title: 'I am a title',
content: 'Some content'
}
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(`{
const { User } = await client.request(
`{
User(email:"test@example.org") {
contributions {
title
}
}
}`, { headers })
expect(User).toEqual([ { contributions: [ { title: 'I am a title' } ] } ])
}`,
{ headers },
)
expect(User).toEqual([{ contributions: [{ title: 'I am a title' }] }])
})
describe('disabled and deleted', () => {
@ -86,22 +89,22 @@ describe('UpdatePost', () => {
let variables = {
id: 'p1',
content: 'New content'
content: 'New content',
}
beforeEach(async () => {
const asAuthor = Factory()
await asAuthor.create('User', {
email: 'author@example.org',
password: '1234'
password: '1234',
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234'
password: '1234',
})
await asAuthor.create('Post', {
id: 'p1',
content: 'Old content'
content: 'Old content',
})
})
@ -149,22 +152,22 @@ describe('DeletePost', () => {
`
let variables = {
id: 'p1'
id: 'p1',
}
beforeEach(async () => {
const asAuthor = Factory()
await asAuthor.create('User', {
email: 'author@example.org',
password: '1234'
password: '1234',
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234'
password: '1234',
})
await asAuthor.create('Post', {
id: 'p1',
content: 'To be deleted'
content: 'To be deleted',
})
})

View File

@ -7,11 +7,12 @@ export default {
const session = driver.session()
const reportData = {
id: reportId,
createdAt: (new Date()).toISOString(),
description: description
createdAt: new Date().toISOString(),
description: description,
}
const res = await session.run(`
const res = await session.run(
`
MATCH (submitter:User {id: $userId})
MATCH (resource {id: $resourceId})
WHERE resource:User OR resource:Comment OR resource:Post
@ -19,11 +20,12 @@ export default {
MERGE (resource)<-[:REPORTED]-(report)
MERGE (report)<-[:REPORTED]-(submitter)
RETURN report, submitter, resource, labels(resource)[0] as type
`, {
resourceId: id,
userId: user.id,
reportData
}
`,
{
resourceId: id,
userId: user.id,
reportData,
},
)
session.close()
@ -32,7 +34,7 @@ export default {
report: r.get('report'),
submitter: r.get('submitter'),
resource: r.get('resource'),
type: r.get('type')
type: r.get('type'),
}
})
if (!dbResponse) return null
@ -44,20 +46,20 @@ export default {
comment: null,
user: null,
submitter: submitter.properties,
type
type,
}
switch (type) {
case 'Post':
response.post = resource.properties
break
case 'Comment':
response.comment = resource.properties
break
case 'User':
response.user = resource.properties
break
case 'Post':
response.post = resource.properties
break
case 'Comment':
response.comment = resource.properties
break
case 'User':
response.user = resource.properties
break
}
return response
}
}
},
},
}

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
@ -18,13 +18,13 @@ describe('report', () => {
await factory.create('User', {
id: 'u1',
email: 'test@example.org',
password: '1234'
password: '1234',
})
await factory.create('User', {
id: 'u2',
name: 'abusive-user',
role: 'user',
email: 'abusive-user@example.org'
email: 'abusive-user@example.org',
})
})
@ -59,7 +59,7 @@ describe('report', () => {
describe('invalid resource id', () => {
it('returns null', async () => {
await expect(action()).resolves.toEqual({
report: null
report: null,
})
})
})
@ -71,14 +71,14 @@ describe('report', () => {
it('creates a report', async () => {
await expect(action()).resolves.toEqual({
report: { description: 'Violates code of conduct' }
report: { description: 'Violates code of conduct' },
})
})
it('returns the submitter', async () => {
returnedObject = '{ submitter { email } }'
await expect(action()).resolves.toEqual({
report: { submitter: { email: 'test@example.org' } }
report: { submitter: { email: 'test@example.org' } },
})
})
@ -86,14 +86,14 @@ describe('report', () => {
it('returns type "User"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
report: { type: 'User' }
report: { type: 'User' },
})
})
it('returns resource in user attribute', async () => {
returnedObject = '{ user { name } }'
await expect(action()).resolves.toEqual({
report: { user: { name: 'abusive-user' } }
report: { user: { name: 'abusive-user' } },
})
})
})
@ -101,28 +101,31 @@ describe('report', () => {
describe('reported resource is a post', () => {
beforeEach(async () => {
await factory.authenticateAs({ email: 'test@example.org', password: '1234' })
await factory.create('Post', { id: 'p23', title: 'Matt and Robert having a pair-programming' })
await factory.create('Post', {
id: 'p23',
title: 'Matt and Robert having a pair-programming',
})
variables = { id: 'p23' }
})
it('returns type "Post"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
report: { type: 'Post' }
report: { type: 'Post' },
})
})
it('returns resource in post attribute', async () => {
returnedObject = '{ post { title } }'
await expect(action()).resolves.toEqual({
report: { post: { title: 'Matt and Robert having a pair-programming' } }
report: { post: { title: 'Matt and Robert having a pair-programming' } },
})
})
it('returns null in user attribute', async () => {
returnedObject = '{ user { name } }'
await expect(action()).resolves.toEqual({
report: { user: null }
report: { user: null },
})
})
})
@ -132,25 +135,32 @@ describe('report', () => {
createPostVariables = {
id: 'p1',
title: 'post to comment on',
content: 'please comment on me'
content: 'please comment on me',
}
const asAuthenticatedUser = await factory.authenticateAs({ email: 'test@example.org', password: '1234' })
const asAuthenticatedUser = await factory.authenticateAs({
email: 'test@example.org',
password: '1234',
})
await asAuthenticatedUser.create('Post', createPostVariables)
await asAuthenticatedUser.create('Comment', { postId: 'p1', id: 'c34', content: 'Robert getting tired.' })
await asAuthenticatedUser.create('Comment', {
postId: 'p1',
id: 'c34',
content: 'Robert getting tired.',
})
variables = { id: 'c34' }
})
it('returns type "Comment"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
report: { type: 'Comment' }
report: { type: 'Comment' },
})
})
it('returns resource in comment attribute', async () => {
returnedObject = '{ comment { content } }'
await expect(action()).resolves.toEqual({
report: { comment: { content: 'Robert getting tired.' } }
report: { comment: { content: 'Robert getting tired.' } },
})
})
})

View File

@ -10,8 +10,8 @@ export default {
RETURN rewardedUser {.id}`,
{
badgeId: fromBadgeId,
rewardedUserId: toUserId
}
rewardedUserId: toUserId,
},
)
const [rewardedUser] = transactionRes.records.map(record => {
@ -33,8 +33,8 @@ export default {
RETURN rewardedUser {.id}`,
{
badgeId: fromBadgeId,
rewardedUserId: toUserId
}
rewardedUserId: toUserId,
},
)
const [rewardedUser] = transactionRes.records.map(record => {
return record.get('rewardedUser')
@ -42,6 +42,6 @@ export default {
session.close()
return rewardedUser.id
}
}
},
},
}

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
@ -10,24 +10,24 @@ describe('rewards', () => {
id: 'u1',
role: 'user',
email: 'user@example.org',
password: '1234'
password: '1234',
})
await factory.create('User', {
id: 'u2',
role: 'moderator',
email: 'moderator@example.org'
email: 'moderator@example.org',
})
await factory.create('User', {
id: 'u3',
role: 'admin',
email: 'admin@example.org'
email: 'admin@example.org',
})
await factory.create('Badge', {
id: 'b6',
key: 'indiegogo_en_rhino',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_rhino.svg'
icon: '/img/badges/indiegogo_en_rhino.svg',
})
})
@ -48,15 +48,13 @@ describe('rewards', () => {
describe('unauthenticated', () => {
const variables = {
from: 'b6',
to: 'u1'
to: 'u1',
}
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -70,14 +68,12 @@ describe('rewards', () => {
it('rewards a badge to user', async () => {
const variables = {
from: 'b6',
to: 'u1'
to: 'u1',
}
const expected = {
reward: 'u1'
reward: 'u1',
}
await expect(
client.request(mutation, variables)
).resolves.toEqual(expected)
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
it('rewards a second different badge to same user', async () => {
await factory.create('Badge', {
@ -85,41 +81,37 @@ describe('rewards', () => {
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg'
icon: '/img/badges/indiegogo_en_racoon.svg',
})
const variables = {
from: 'b1',
to: 'u1'
to: 'u1',
}
const expected = {
reward: 'u1'
reward: 'u1',
}
await expect(
client.request(mutation, variables)
).resolves.toEqual(expected)
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
it('rewards the same badge as well to another user', async () => {
const variables1 = {
from: 'b6',
to: 'u1'
to: 'u1',
}
await client.request(mutation, variables1)
const variables2 = {
from: 'b6',
to: 'u2'
to: 'u2',
}
const expected = {
reward: 'u2'
reward: 'u2',
}
await expect(
client.request(mutation, variables2)
).resolves.toEqual(expected)
await expect(client.request(mutation, variables2)).resolves.toEqual(expected)
})
it('returns the original reward if a reward is attempted a second time', async () => {
const variables = {
from: 'b6',
to: 'u1'
to: 'u1',
}
await client.request(mutation, variables)
await client.request(mutation, variables)
@ -132,16 +124,14 @@ describe('rewards', () => {
`
const expected = { User: [{ badgesCount: 1 }] }
await expect(
client.request(query)
).resolves.toEqual(expected)
await expect(client.request(query)).resolves.toEqual(expected)
})
})
describe('authenticated moderator', () => {
const variables = {
from: 'b6',
to: 'u1'
to: 'u1',
}
let client
beforeEach(async () => {
@ -151,9 +141,7 @@ describe('rewards', () => {
describe('rewards bage to user', () => {
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
})
@ -165,10 +153,10 @@ describe('rewards', () => {
})
const variables = {
from: 'b6',
to: 'u1'
to: 'u1',
}
const expected = {
unreward: 'u1'
unreward: 'u1',
}
const mutation = `
@ -185,9 +173,7 @@ describe('rewards', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
@ -199,17 +185,15 @@ describe('rewards', () => {
})
it('removes a badge from user', async () => {
await expect(
client.request(mutation, variables)
).resolves.toEqual(expected)
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
it('fails to remove a not existing badge from user', async () => {
await client.request(mutation, variables)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Cannot read property \'id\' of undefined')
await expect(client.request(mutation, variables)).rejects.toThrow(
"Cannot read property 'id' of undefined",
)
})
})
@ -222,9 +206,7 @@ describe('rewards', () => {
describe('removes bage from user', () => {
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
})

View File

@ -12,8 +12,8 @@ export default {
{
id,
type,
userId: context.user.id
}
userId: context.user.id,
},
)
const [isShouted] = transactionRes.records.map(record => {
@ -37,8 +37,8 @@ export default {
{
id,
type,
userId: context.user.id
}
userId: context.user.id,
},
)
const [isShouted] = transactionRes.records.map(record => {
return record.get('isShouted')
@ -46,6 +46,6 @@ export default {
session.close()
return isShouted
}
}
},
},
}

View File

@ -1,17 +1,17 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let clientUser1, clientUser2
let headersUser1, headersUser2
const mutationShoutPost = (id) => `
const mutationShoutPost = id => `
mutation {
shout(id: "${id}", type: Post)
}
`
const mutationUnshoutPost = (id) => `
const mutationUnshoutPost = id => `
mutation {
unshout(id: "${id}", type: Post)
}
@ -21,12 +21,12 @@ beforeEach(async () => {
await factory.create('User', {
id: 'u1',
email: 'test@example.org',
password: '1234'
password: '1234',
})
await factory.create('User', {
id: 'u2',
email: 'test2@example.org',
password: '1234'
password: '1234',
})
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
@ -62,18 +62,14 @@ describe('shout', () => {
it('throws authorization error', async () => {
let client
client = new GraphQLClient(host)
await expect(
client.request(mutationShoutPost('p1'))
).rejects.toThrow('Not Authorised')
await expect(client.request(mutationShoutPost('p1'))).rejects.toThrow('Not Authorised')
})
})
it('I shout a post of another user', async () => {
const res = await clientUser1.request(
mutationShoutPost('p2')
)
const res = await clientUser1.request(mutationShoutPost('p2'))
const expected = {
shout: true
shout: true,
}
expect(res).toMatchObject(expected)
@ -83,17 +79,15 @@ describe('shout', () => {
}
}`)
const expected2 = {
shoutedByCurrentUser: true
shoutedByCurrentUser: true,
}
expect(Post[0]).toMatchObject(expected2)
})
it('I can`t shout my own post', async () => {
const res = await clientUser1.request(
mutationShoutPost('p1')
)
const res = await clientUser1.request(mutationShoutPost('p1'))
const expected = {
shout: false
shout: false,
}
expect(res).toMatchObject(expected)
@ -103,7 +97,7 @@ describe('shout', () => {
}
}`)
const expected2 = {
shoutedByCurrentUser: false
shoutedByCurrentUser: false,
}
expect(Post[0]).toMatchObject(expected2)
})
@ -113,25 +107,19 @@ describe('shout', () => {
describe('unauthenticated shout', () => {
it('throws authorization error', async () => {
// shout
await clientUser1.request(
mutationShoutPost('p2')
)
await clientUser1.request(mutationShoutPost('p2'))
// unshout
let client
client = new GraphQLClient(host)
await expect(
client.request(mutationUnshoutPost('p2'))
).rejects.toThrow('Not Authorised')
await expect(client.request(mutationUnshoutPost('p2'))).rejects.toThrow('Not Authorised')
})
})
it('I unshout a post of another user', async () => {
// shout
await clientUser1.request(
mutationShoutPost('p2')
)
await clientUser1.request(mutationShoutPost('p2'))
const expected = {
unshout: true
unshout: true,
}
// unshout
const res = await clientUser1.request(mutationUnshoutPost('p2'))
@ -143,7 +131,7 @@ describe('shout', () => {
}
}`)
const expected2 = {
shoutedByCurrentUser: false
shoutedByCurrentUser: false,
}
expect(Post[0]).toMatchObject(expected2)
})

View File

@ -5,16 +5,17 @@ export default {
CreateSocialMedia: async (object, params, context, resolveInfo) => {
/**
* TODO?: Creates double Nodes!
*/
*/
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
await session.run(
`MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
MERGE (socialMedia)<-[:OWNED]-(owner)
RETURN owner`, {
RETURN owner`,
{
userId: context.user.id,
socialMediaId: socialMedia.id
}
socialMediaId: socialMedia.id,
},
)
session.close()
@ -24,6 +25,6 @@ export default {
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
return socialMedia
}
}
},
},
}

View File

@ -0,0 +1,111 @@
import gql from 'graphql-tag'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
describe('SocialMedia', () => {
let client
let headers
const mutationC = gql`
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
url
}
}
`
const mutationD = gql`
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url
}
}
`
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',
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
const variables = {
url: 'http://nsosp.org',
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('creates social media with correct URL', async () => {
const variables = {
url: 'http://nsosp.org',
}
await expect(client.request(mutationC, variables)).resolves.toEqual(
expect.objectContaining({
CreateSocialMedia: {
id: expect.any(String),
url: 'http://nsosp.org',
},
}),
)
})
it('deletes social media', async () => {
const creationVariables = {
url: 'http://nsosp.org',
}
const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
const { id } = CreateSocialMedia
const deletionVariables = {
id,
}
const expected = {
DeleteSocialMedia: {
id: id,
url: 'http://nsosp.org',
},
}
await expect(client.request(mutationD, deletionVariables)).resolves.toEqual(expected)
})
it('rejects empty string', async () => {
const variables = {
url: '',
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
})
it('validates URLs', async () => {
const variables = {
url: 'not-a-url',
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
})
})
})

View File

@ -0,0 +1,74 @@
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)
})
},
},
}

Some files were not shown because too many files have changed in this diff Show More