mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1710-list-and-protocol-moderation
This commit is contained in:
commit
085b59bf4d
@ -8,13 +8,13 @@ addons:
|
|||||||
- docker
|
- docker
|
||||||
- chromium
|
- chromium
|
||||||
|
|
||||||
before_install:
|
install:
|
||||||
- yarn global add wait-on
|
- yarn global add wait-on
|
||||||
# Install Codecov
|
# Install Codecov
|
||||||
- yarn install
|
- yarn install
|
||||||
- cp cypress.env.template.json cypress.env.json
|
- cp cypress.env.template.json cypress.env.json
|
||||||
|
|
||||||
install:
|
before_script:
|
||||||
- docker-compose -f docker-compose.yml build --parallel
|
- docker-compose -f docker-compose.yml build --parallel
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
|
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
|
||||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
|
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
|
||||||
@ -30,10 +30,6 @@ script:
|
|||||||
- docker-compose exec backend yarn run test --ci --verbose=false --coverage
|
- docker-compose exec backend yarn run test --ci --verbose=false --coverage
|
||||||
- docker-compose exec backend yarn run db:seed
|
- docker-compose exec backend yarn run db:seed
|
||||||
- docker-compose exec backend yarn run db:reset
|
- docker-compose exec backend yarn run db:reset
|
||||||
# ActivityPub cucumber testing temporarily disabled because it's too buggy
|
|
||||||
# - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
|
|
||||||
# - docker-compose exec backend yarn run db:reset
|
|
||||||
# - docker-compose exec backend yarn run db:seed
|
|
||||||
# Frontend
|
# Frontend
|
||||||
- docker-compose exec webapp yarn run lint
|
- docker-compose exec webapp yarn run lint
|
||||||
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
|
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
|
||||||
@ -42,6 +38,7 @@ script:
|
|||||||
- docker-compose -f docker-compose.yml up -d
|
- docker-compose -f docker-compose.yml up -d
|
||||||
- wait-on http://localhost:7474
|
- wait-on http://localhost:7474
|
||||||
- yarn run cypress:run --record
|
- yarn run cypress:run --record
|
||||||
|
- yarn run cucumber
|
||||||
# Coverage
|
# Coverage
|
||||||
- yarn run codecov
|
- yarn run codecov
|
||||||
|
|
||||||
|
|||||||
12
babel.config.json
Normal file
12
babel.config.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"node": "10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -10,8 +10,6 @@
|
|||||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -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",
|
"lint": "eslint src --config .eslintrc.js",
|
||||||
"test": "jest --forceExit --detectOpenHandles --runInBand",
|
"test": "jest --forceExit --detectOpenHandles --runInBand",
|
||||||
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
|
||||||
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
|
||||||
"db:reset": "babel-node src/seed/reset-db.js",
|
"db:reset": "babel-node src/seed/reset-db.js",
|
||||||
"db:seed": "babel-node src/seed/seed-db.js"
|
"db:seed": "babel-node src/seed/seed-db.js"
|
||||||
},
|
},
|
||||||
@ -99,7 +97,7 @@
|
|||||||
"xregexp": "^4.2.4"
|
"xregexp": "^4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "~7.7.0",
|
"@babel/cli": "~7.7.4",
|
||||||
"@babel/core": "~7.7.4",
|
"@babel/core": "~7.7.4",
|
||||||
"@babel/node": "~7.7.4",
|
"@babel/node": "~7.7.4",
|
||||||
"@babel/plugin-proposal-throw-expressions": "^7.7.4",
|
"@babel/plugin-proposal-throw-expressions": "^7.7.4",
|
||||||
@ -111,11 +109,11 @@
|
|||||||
"babel-jest": "~24.9.0",
|
"babel-jest": "~24.9.0",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"cucumber": "~6.0.5",
|
"cucumber": "~6.0.5",
|
||||||
"eslint": "~6.7.1",
|
"eslint": "~6.7.2",
|
||||||
"eslint-config-prettier": "~6.7.0",
|
"eslint-config-prettier": "~6.7.0",
|
||||||
"eslint-config-standard": "~14.1.0",
|
"eslint-config-standard": "~14.1.0",
|
||||||
"eslint-plugin-import": "~2.18.2",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~23.0.5",
|
"eslint-plugin-jest": "~23.1.1",
|
||||||
"eslint-plugin-node": "~10.0.0",
|
"eslint-plugin-node": "~10.0.0",
|
||||||
"eslint-plugin-prettier": "~3.1.1",
|
"eslint-plugin-prettier": "~3.1.1",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
|
|||||||
@ -1,27 +1,29 @@
|
|||||||
import user from './user'
|
import user from './user'
|
||||||
import inbox from './inbox'
|
import inbox from './inbox'
|
||||||
import webFinger from './webFinger'
|
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import verify from './verify'
|
import verify from './verify'
|
||||||
|
|
||||||
|
export default function() {
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
|
|
||||||
router.use(
|
router.use(
|
||||||
'/activitypub/users',
|
'/activitypub/users',
|
||||||
cors(),
|
cors(),
|
||||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
express.json({
|
||||||
|
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||||
|
}),
|
||||||
express.urlencoded({ extended: true }),
|
express.urlencoded({ extended: true }),
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
router.use(
|
router.use(
|
||||||
'/activitypub/inbox',
|
'/activitypub/inbox',
|
||||||
cors(),
|
cors(),
|
||||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
express.json({
|
||||||
|
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||||
|
}),
|
||||||
express.urlencoded({ extended: true }),
|
express.urlencoded({ extended: true }),
|
||||||
verify,
|
verify,
|
||||||
inbox,
|
inbox,
|
||||||
)
|
)
|
||||||
|
return router
|
||||||
export default router
|
}
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { createWebFinger } from '../utils/actor'
|
|
||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
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.',
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const nameAndDomain = resource.replace('acct:', '')
|
|
||||||
const name = nameAndDomain.split('@')[0]
|
|
||||||
|
|
||||||
let result
|
|
||||||
try {
|
|
||||||
result = await req.app.get('ap').dataSource.client.query({
|
|
||||||
query: gql`
|
|
||||||
query {
|
|
||||||
User(slug: "${name}") {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(500).json({ error })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data && result.data.User.length > 0) {
|
|
||||||
const webFinger = createWebFinger(name)
|
|
||||||
return res.contentType('application/jrd+json').json(webFinger)
|
|
||||||
} else {
|
|
||||||
return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
59
backend/src/activitypub/routes/webfinger.js
Normal file
59
backend/src/activitypub/routes/webfinger.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import CONFIG from '../../config/'
|
||||||
|
import cors from 'cors'
|
||||||
|
|
||||||
|
const debug = require('debug')('ea:webfinger')
|
||||||
|
const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/
|
||||||
|
|
||||||
|
const createWebFinger = name => {
|
||||||
|
const { host } = new URL(CONFIG.CLIENT_URI)
|
||||||
|
return {
|
||||||
|
subject: `acct:${name}@${host}`,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
rel: 'self',
|
||||||
|
type: 'application/activity+json',
|
||||||
|
href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handler(req, res) {
|
||||||
|
const { resource = '' } = req.query
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [_, name, domain] = resource.match(regex) || []
|
||||||
|
if (!(name && domain))
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = req.app.get('driver').session()
|
||||||
|
try {
|
||||||
|
const [slug] = await session.readTransaction(async t => {
|
||||||
|
const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', {
|
||||||
|
slug: name,
|
||||||
|
})
|
||||||
|
return result.records.map(record => record.get('slug'))
|
||||||
|
})
|
||||||
|
if (!slug)
|
||||||
|
return res.status(404).json({
|
||||||
|
error: `No record found for "${name}@${domain}".`,
|
||||||
|
})
|
||||||
|
const webFinger = createWebFinger(name)
|
||||||
|
return res.contentType('application/jrd+json').json(webFinger)
|
||||||
|
} catch (error) {
|
||||||
|
debug(error)
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Something went terribly wrong. Please contact support@human-connection.org',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
const router = express.Router()
|
||||||
|
router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler)
|
||||||
|
return router
|
||||||
|
}
|
||||||
113
backend/src/activitypub/routes/webfinger.spec.js
Normal file
113
backend/src/activitypub/routes/webfinger.spec.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { handler } from './webfinger'
|
||||||
|
import Factory from '../../seed/factories'
|
||||||
|
import { getDriver } from '../../bootstrap/neo4j'
|
||||||
|
|
||||||
|
let resource, res, json, status, contentType
|
||||||
|
|
||||||
|
const factory = Factory()
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
const request = () => {
|
||||||
|
json = jest.fn()
|
||||||
|
status = jest.fn(() => ({ json }))
|
||||||
|
contentType = jest.fn(() => ({ json }))
|
||||||
|
res = { status, contentType }
|
||||||
|
const req = {
|
||||||
|
app: {
|
||||||
|
get: key => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
}[key]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
resource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return handler(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('webfinger', () => {
|
||||||
|
describe('no ressource', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resource = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends HTTP 400', async () => {
|
||||||
|
await request()
|
||||||
|
expect(status).toHaveBeenCalledWith(400)
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('?resource query param', () => {
|
||||||
|
describe('is missing acct:', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resource = 'some-user@domain'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends HTTP 400', async () => {
|
||||||
|
await request()
|
||||||
|
expect(status).toHaveBeenCalledWith(400)
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('has no domain', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resource = 'acct:some-user@'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends HTTP 400', async () => {
|
||||||
|
await request()
|
||||||
|
expect(status).toHaveBeenCalledWith(400)
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with acct:', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resource = 'acct:some-user@domain'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns error as json', async () => {
|
||||||
|
await request()
|
||||||
|
expect(status).toHaveBeenCalledWith(404)
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
error: 'No record found for "some-user@domain".',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a user for acct', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await factory.create('User', { slug: 'some-user' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns user object', async () => {
|
||||||
|
await request()
|
||||||
|
expect(contentType).toHaveBeenCalledWith('application/jrd+json')
|
||||||
|
expect(json).toHaveBeenCalledWith({
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href: 'http://localhost:3000/activitypub/users/some-user',
|
||||||
|
rel: 'self',
|
||||||
|
type: 'application/activity+json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subject: 'acct:some-user@localhost:3000',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -22,17 +22,3 @@ export function createActor(name, pubkey) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWebFinger(name) {
|
|
||||||
const { host } = new URL(activityPub.endpoint)
|
|
||||||
return {
|
|
||||||
subject: `acct:${name}@${host}`,
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
rel: 'self',
|
|
||||||
type: 'application/activity+json',
|
|
||||||
href: `${activityPub.endpoint}/activitypub/users/${name}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config({ path: path.resolve(__dirname, '../../.env') })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
MAPBOX_TOKEN,
|
MAPBOX_TOKEN,
|
||||||
|
|||||||
@ -11,15 +11,21 @@ export default async (driver, authorizationHeader) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const session = driver.session()
|
|
||||||
const query = `
|
const query = `
|
||||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||||
SET user.lastActiveAt = toString(datetime())
|
SET user.lastActiveAt = toString(datetime())
|
||||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
const result = await session.run(query, { id })
|
const session = driver.session()
|
||||||
|
let result
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await session.run(query, { id })
|
||||||
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
const [currentUser] = await result.records.map(record => {
|
const [currentUser] = await result.records.map(record => {
|
||||||
return record.get('user')
|
return record.get('user')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import extractHashtags from '../hashtags/extractHashtags'
|
|||||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||||
if (!hashtags.length) return
|
if (!hashtags.length) return
|
||||||
|
|
||||||
const session = context.driver.session()
|
|
||||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||||
// and no new Hashtags and relations will be created.
|
// and no new Hashtags and relations will be created.
|
||||||
@ -19,6 +18,8 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
|||||||
MERGE (p)-[:TAGGED]->(t)
|
MERGE (p)-[:TAGGED]->(t)
|
||||||
RETURN p, t
|
RETURN p, t
|
||||||
`
|
`
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
await session.run(cypherDeletePreviousRelations, {
|
await session.run(cypherDeletePreviousRelations, {
|
||||||
postId,
|
postId,
|
||||||
})
|
})
|
||||||
@ -26,8 +27,10 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
|||||||
postId,
|
postId,
|
||||||
hashtags,
|
hashtags,
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||||
const hashtags = extractHashtags(args.content)
|
const hashtags = extractHashtags(args.content)
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||||
|
|
||||||
const postAuthorOfComment = async (comment, { context }) => {
|
const postAuthorOfComment = async (comment, { context }) => {
|
||||||
const session = context.driver.session()
|
|
||||||
const cypherFindUser = `
|
const cypherFindUser = `
|
||||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||||
RETURN user { .id }
|
RETURN user { .id }
|
||||||
`
|
`
|
||||||
const result = await session.run(cypherFindUser, {
|
const session = context.driver.session()
|
||||||
|
let result
|
||||||
|
try {
|
||||||
|
result = await session.run(cypherFindUser, {
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
|
}
|
||||||
const [postAuthor] = await result.records.map(record => {
|
const [postAuthor] = await result.records.map(record => {
|
||||||
return record.get('user')
|
return record.get('user')
|
||||||
})
|
})
|
||||||
@ -31,7 +35,6 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
throw new Error('Notification does not fit the reason!')
|
throw new Error('Notification does not fit the reason!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = context.driver.session()
|
|
||||||
let cypher
|
let cypher
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'mentioned_in_post': {
|
case 'mentioned_in_post': {
|
||||||
@ -85,13 +88,17 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
await session.run(cypher, {
|
await session.run(cypher, {
|
||||||
id,
|
id,
|
||||||
idsOfUsers,
|
idsOfUsers,
|
||||||
reason,
|
reason,
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||||
const idsOfUsers = extractMentionedUsers(args.content)
|
const idsOfUsers = extractMentionedUsers(args.content)
|
||||||
@ -123,15 +130,19 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
|
|||||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||||
|
|
||||||
if (comment) {
|
if (comment) {
|
||||||
const session = context.driver.session()
|
|
||||||
const cypherFindUser = `
|
const cypherFindUser = `
|
||||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||||
RETURN user { .id }
|
RETURN user { .id }
|
||||||
`
|
`
|
||||||
const result = await session.run(cypherFindUser, {
|
const session = context.driver.session()
|
||||||
|
let result
|
||||||
|
try {
|
||||||
|
result = await session.run(cypherFindUser, {
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
|
}
|
||||||
const [postAuthor] = await result.records.map(record => {
|
const [postAuthor] = await result.records.map(record => {
|
||||||
return record.get('user')
|
return record.get('user')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -45,8 +45,8 @@ const isAuthor = rule({
|
|||||||
cache: 'no_cache',
|
cache: 'no_cache',
|
||||||
})(async (_parent, args, { user, driver }) => {
|
})(async (_parent, args, { user, driver }) => {
|
||||||
if (!user) return false
|
if (!user) return false
|
||||||
const session = driver.session()
|
|
||||||
const { id: resourceId } = args
|
const { id: resourceId } = args
|
||||||
|
const session = driver.session()
|
||||||
try {
|
try {
|
||||||
const result = await session.run(
|
const result = await session.run(
|
||||||
`
|
`
|
||||||
|
|||||||
@ -3,11 +3,14 @@ import uniqueSlug from './slugify/uniqueSlug'
|
|||||||
const isUniqueFor = (context, type) => {
|
const isUniqueFor = (context, type) => {
|
||||||
return async slug => {
|
return async slug => {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||||
slug,
|
slug,
|
||||||
})
|
})
|
||||||
session.close()
|
|
||||||
return response.records.length === 0
|
return response.records.length === 0
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
|||||||
const NO_CATEGORIES_ERR_MESSAGE =
|
const NO_CATEGORIES_ERR_MESSAGE =
|
||||||
'You cannot save a post without at least one category or more than three'
|
'You cannot save a post without at least one category or more than three'
|
||||||
|
|
||||||
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
const { postId } = args
|
const { postId } = args
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
|
|||||||
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 session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const postQueryRes = await session.run(
|
const postQueryRes = await session.run(
|
||||||
`
|
`
|
||||||
MATCH (post:Post {id: $postId})
|
MATCH (post:Post {id: $postId})
|
||||||
@ -21,7 +22,6 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
|
|||||||
postId,
|
postId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
const [post] = postQueryRes.records.map(record => {
|
const [post] = postQueryRes.records.map(record => {
|
||||||
return record.get('post')
|
return record.get('post')
|
||||||
})
|
})
|
||||||
@ -31,10 +31,12 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
|
|||||||
} else {
|
} else {
|
||||||
return resolve(root, args, context, info)
|
return resolve(root, args, context, info)
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateUpdateComment = async (resolve, root, args, context, info) => {
|
const validateUpdateComment = async (resolve, root, args, context, info) => {
|
||||||
const COMMENT_MIN_LENGTH = 1
|
|
||||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
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!`)
|
||||||
@ -115,7 +117,7 @@ const validateReview = async (resolve, root, args, context, info) => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateComment: validateCommentCreation,
|
CreateComment: validateCreateComment,
|
||||||
UpdateComment: validateUpdateComment,
|
UpdateComment: validateUpdateComment,
|
||||||
CreatePost: validatePost,
|
CreatePost: validatePost,
|
||||||
UpdatePost: validateUpdatePost,
|
UpdatePost: validateUpdatePost,
|
||||||
|
|||||||
@ -14,8 +14,44 @@ let authenticatedUser,
|
|||||||
reportVariables,
|
reportVariables,
|
||||||
disableVariables,
|
disableVariables,
|
||||||
reportingUser,
|
reportingUser,
|
||||||
moderatingUser
|
moderatingUser,
|
||||||
|
commentingUser
|
||||||
|
|
||||||
|
const createCommentMutation = gql`
|
||||||
|
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||||
|
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const updateCommentMutation = gql`
|
||||||
|
mutation($content: String!, $id: ID!) {
|
||||||
|
UpdateComment(content: $content, id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const createPostMutation = gql`
|
||||||
|
mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
|
||||||
|
CreatePost(
|
||||||
|
id: $id
|
||||||
|
title: $title
|
||||||
|
content: $content
|
||||||
|
language: $language
|
||||||
|
categoryIds: $categoryIds
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const updatePostMutation = gql`
|
||||||
|
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||||
|
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
const reportMutation = gql`
|
const reportMutation = gql`
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(
|
fileReport(
|
||||||
@ -57,6 +93,9 @@ beforeEach(async () => {
|
|||||||
id: 'moderating-user',
|
id: 'moderating-user',
|
||||||
role: 'moderator',
|
role: 'moderator',
|
||||||
}),
|
}),
|
||||||
|
factory.create('User', {
|
||||||
|
id: 'commenting-user',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
reportVariables = {
|
reportVariables = {
|
||||||
resourceId: 'whatever',
|
resourceId: 'whatever',
|
||||||
@ -70,16 +109,202 @@ beforeEach(async () => {
|
|||||||
}
|
}
|
||||||
reportingUser = users[0]
|
reportingUser = users[0]
|
||||||
moderatingUser = users[1]
|
moderatingUser = users[1]
|
||||||
offensivePost = await factory.create('Post', {
|
commentingUser = users[2]
|
||||||
|
const posts = await Promise.all([
|
||||||
|
factory.create('Post', {
|
||||||
id: 'offensive-post',
|
id: 'offensive-post',
|
||||||
authorId: 'moderating-user',
|
authorId: 'moderating-user',
|
||||||
})
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
id: 'post-4-commenting',
|
||||||
|
authorId: 'commenting-user',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
offensivePost = posts[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('validateCreateComment', () => {
|
||||||
|
let createCommentVariables
|
||||||
|
beforeEach(async () => {
|
||||||
|
createCommentVariables = {
|
||||||
|
postId: 'whatever',
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
authenticatedUser = await commentingUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if content is empty', async () => {
|
||||||
|
createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { CreateComment: null },
|
||||||
|
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||||
|
createCommentVariables = { postId: 'post-4-commenting', content: '<a></a>' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { CreateComment: null },
|
||||||
|
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if there is no post with given id in the database', async () => {
|
||||||
|
createCommentVariables = {
|
||||||
|
...createCommentVariables,
|
||||||
|
postId: 'non-existent-post',
|
||||||
|
content: 'valid content',
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { CreateComment: null },
|
||||||
|
errors: [{ message: 'Comment cannot be created without a post!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateUpdateComment', () => {
|
||||||
|
let updateCommentVariables
|
||||||
|
beforeEach(async () => {
|
||||||
|
await factory.create('Comment', {
|
||||||
|
id: 'comment-id',
|
||||||
|
authorId: 'commenting-user',
|
||||||
|
})
|
||||||
|
updateCommentVariables = {
|
||||||
|
id: 'whatever',
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
authenticatedUser = await commentingUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if content is empty', async () => {
|
||||||
|
updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { UpdateComment: null },
|
||||||
|
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||||
|
updateCommentVariables = { id: 'comment-id', content: '<a></a>' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { UpdateComment: null },
|
||||||
|
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validatePost', () => {
|
||||||
|
let createPostVariables
|
||||||
|
beforeEach(async () => {
|
||||||
|
createPostVariables = {
|
||||||
|
title: 'I am a title',
|
||||||
|
content: 'Some content',
|
||||||
|
}
|
||||||
|
authenticatedUser = await commentingUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('categories', () => {
|
||||||
|
describe('null', () => {
|
||||||
|
it('throws UserInputError', async () => {
|
||||||
|
createPostVariables = { ...createPostVariables, categoryIds: null }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { CreatePost: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'You cannot save a post without at least one category or more than three',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('empty', () => {
|
||||||
|
it('throws UserInputError', async () => {
|
||||||
|
createPostVariables = { ...createPostVariables, categoryIds: [] }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { CreatePost: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'You cannot save a post without at least one category or more than three',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('more than 3 categoryIds', () => {
|
||||||
|
it('throws UserInputError', async () => {
|
||||||
|
createPostVariables = {
|
||||||
|
...createPostVariables,
|
||||||
|
categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'],
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { CreatePost: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'You cannot save a post without at least one category or more than three',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateUpdatePost', () => {
|
||||||
|
describe('post created without categories somehow', () => {
|
||||||
|
let owner, updatePostVariables
|
||||||
|
beforeEach(async () => {
|
||||||
|
const postSomehowCreated = await neode.create('Post', {
|
||||||
|
id: 'how-was-this-created',
|
||||||
|
})
|
||||||
|
owner = await neode.create('User', {
|
||||||
|
id: 'author-of-post-without-category',
|
||||||
|
slug: 'hacker',
|
||||||
|
})
|
||||||
|
await postSomehowCreated.relateTo(owner, 'author')
|
||||||
|
authenticatedUser = await owner.toJson()
|
||||||
|
updatePostVariables = {
|
||||||
|
id: 'how-was-this-created',
|
||||||
|
title: 'I am a title',
|
||||||
|
content: 'Some content',
|
||||||
|
categoryIds: [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requires at least one category for successful update', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: updatePostMutation, variables: updatePostVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { UpdatePost: null },
|
||||||
|
errors: [
|
||||||
|
{ message: 'You cannot save a post without at least one category or more than three' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('validateReport', () => {
|
describe('validateReport', () => {
|
||||||
it('throws an error if a user tries to report themself', async () => {
|
it('throws an error if a user tries to report themself', async () => {
|
||||||
authenticatedUser = await reportingUser.toJson()
|
authenticatedUser = await reportingUser.toJson()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default {
|
|||||||
params.id = params.id || uuid()
|
params.id = params.id || uuid()
|
||||||
|
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const createCommentCypher = `
|
const createCommentCypher = `
|
||||||
MATCH (post:Post {id: $postId})
|
MATCH (post:Post {id: $postId})
|
||||||
MATCH (author:User {id: $userId})
|
MATCH (author:User {id: $userId})
|
||||||
@ -28,14 +29,17 @@ export default {
|
|||||||
postId,
|
postId,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
session.close()
|
|
||||||
|
|
||||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||||
|
|
||||||
return comment
|
return comment
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
UpdateComment: async (_parent, params, context, _resolveInfo) => {
|
UpdateComment: async (_parent, params, context, _resolveInfo) => {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const updateCommentCypher = `
|
const updateCommentCypher = `
|
||||||
MATCH (comment:Comment {id: $params.id})
|
MATCH (comment:Comment {id: $params.id})
|
||||||
SET comment += $params
|
SET comment += $params
|
||||||
@ -43,12 +47,15 @@ export default {
|
|||||||
RETURN comment
|
RETURN comment
|
||||||
`
|
`
|
||||||
const transactionRes = await session.run(updateCommentCypher, { params })
|
const transactionRes = await session.run(updateCommentCypher, { params })
|
||||||
session.close()
|
|
||||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||||
return comment
|
return comment
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
DeleteComment: async (_parent, args, context, _resolveInfo) => {
|
DeleteComment: async (_parent, args, context, _resolveInfo) => {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`
|
`
|
||||||
MATCH (comment:Comment {id: $commentId})
|
MATCH (comment:Comment {id: $commentId})
|
||||||
@ -59,9 +66,11 @@ export default {
|
|||||||
`,
|
`,
|
||||||
{ commentId: args.id },
|
{ commentId: args.id },
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||||
return comment
|
return comment
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Comment: {
|
Comment: {
|
||||||
|
|||||||
@ -111,42 +111,6 @@ describe('CreateComment', () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('comment content is empty', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, content: '<p></p>' }
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throw UserInput error', async () => {
|
|
||||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
|
||||||
expect(data).toEqual({ CreateComment: null })
|
|
||||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('comment content contains only whitespaces', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, content: ' <p> </p> ' }
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throw UserInput error', async () => {
|
|
||||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
|
||||||
expect(data).toEqual({ CreateComment: null })
|
|
||||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('invalid post id', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, postId: 'does-not-exist' }
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throw UserInput error', async () => {
|
|
||||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
|
||||||
expect(data).toEqual({ CreateComment: null })
|
|
||||||
expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -226,17 +190,6 @@ describe('UpdateComment', () => {
|
|||||||
expect(newlyCreatedComment.updatedAt).not.toEqual(UpdateComment.updatedAt)
|
expect(newlyCreatedComment.updatedAt).not.toEqual(UpdateComment.updatedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('if `content` empty', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, content: ' <p> </p>' }
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws InputError', async () => {
|
|
||||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
|
||||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('if comment does not exist for given id', () => {
|
describe('if comment does not exist for given id', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
variables = { ...variables, id: 'does-not-exist' }
|
variables = { ...variables, id: 'does-not-exist' }
|
||||||
|
|||||||
@ -2,8 +2,8 @@ export default {
|
|||||||
Mutation: {
|
Mutation: {
|
||||||
UpdateDonations: async (_parent, params, context, _resolveInfo) => {
|
UpdateDonations: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { driver } = context
|
const { driver } = context
|
||||||
const session = driver.session()
|
|
||||||
let donations
|
let donations
|
||||||
|
const session = driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||||
const updateDonationsTransactionResponse = await txc.run(
|
const updateDonationsTransactionResponse = await txc.run(
|
||||||
`
|
`
|
||||||
|
|||||||
@ -4,7 +4,6 @@ export default async function createPasswordReset(options) {
|
|||||||
const { driver, nonce, email, issuedAt = new Date() } = options
|
const { driver, nonce, email, issuedAt = new Date() } = options
|
||||||
const normalizedEmail = normalizeEmail(email)
|
const normalizedEmail = normalizeEmail(email)
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
let response = {}
|
|
||||||
try {
|
try {
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
||||||
@ -23,9 +22,8 @@ export default async function createPasswordReset(options) {
|
|||||||
const { name } = record.get('u').properties
|
const { name } = record.get('u').properties
|
||||||
return { email, nonce, name }
|
return { email, nonce, name }
|
||||||
})
|
})
|
||||||
response = records[0] || {}
|
return records[0] || {}
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default {
|
|||||||
notifications: async (_parent, args, context, _resolveInfo) => {
|
notifications: async (_parent, args, context, _resolveInfo) => {
|
||||||
const { user: currentUser } = context
|
const { user: currentUser } = context
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
let notifications, whereClause, orderByClause
|
let whereClause, orderByClause
|
||||||
|
|
||||||
switch (args.read) {
|
switch (args.read) {
|
||||||
case true:
|
case true:
|
||||||
@ -42,7 +42,6 @@ export default {
|
|||||||
}
|
}
|
||||||
const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : ''
|
const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : ''
|
||||||
const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : ''
|
const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : ''
|
||||||
try {
|
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
|
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
|
||||||
${whereClause}
|
${whereClause}
|
||||||
@ -50,19 +49,18 @@ export default {
|
|||||||
${orderByClause}
|
${orderByClause}
|
||||||
${offset} ${limit}
|
${offset} ${limit}
|
||||||
`
|
`
|
||||||
|
try {
|
||||||
const result = await session.run(cypher, { id: currentUser.id })
|
const result = await session.run(cypher, { id: currentUser.id })
|
||||||
notifications = await result.records.map(transformReturnType)
|
return result.records.map(transformReturnType)
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
return notifications
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
markAsRead: async (parent, args, context, resolveInfo) => {
|
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||||
const { user: currentUser } = context
|
const { user: currentUser } = context
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
let notification
|
|
||||||
try {
|
try {
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||||
@ -71,11 +69,10 @@ export default {
|
|||||||
`
|
`
|
||||||
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||||
const notifications = await result.records.map(transformReturnType)
|
const notifications = await result.records.map(transformReturnType)
|
||||||
notification = notifications[0]
|
return notifications[0]
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
return notification
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NOTIFIED: {
|
NOTIFIED: {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ export default {
|
|||||||
return createPasswordReset({ driver, nonce, email })
|
return createPasswordReset({ driver, nonce, email })
|
||||||
},
|
},
|
||||||
resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => {
|
resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => {
|
||||||
const session = driver.session()
|
|
||||||
const stillValid = new Date()
|
const stillValid = new Date()
|
||||||
stillValid.setDate(stillValid.getDate() - 1)
|
stillValid.setDate(stillValid.getDate() - 1)
|
||||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||||
@ -21,6 +20,8 @@ export default {
|
|||||||
SET u.encryptedPassword = $encryptedNewPassword
|
SET u.encryptedPassword = $encryptedNewPassword
|
||||||
RETURN pr
|
RETURN pr
|
||||||
`
|
`
|
||||||
|
const session = driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(cypher, {
|
const transactionRes = await session.run(cypher, {
|
||||||
stillValid,
|
stillValid,
|
||||||
email,
|
email,
|
||||||
@ -29,8 +30,10 @@ export default {
|
|||||||
})
|
})
|
||||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||||
const response = !!(reset && reset.properties.usedAt)
|
const response = !!(reset && reset.properties.usedAt)
|
||||||
session.close()
|
|
||||||
return response
|
return response
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,10 +15,13 @@ let variables
|
|||||||
|
|
||||||
const getAllPasswordResets = async () => {
|
const getAllPasswordResets = async () => {
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||||
const resets = transactionRes.records.map(record => record.get('r'))
|
const resets = transactionRes.records.map(record => record.get('r'))
|
||||||
session.close()
|
|
||||||
return resets
|
return resets
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -55,37 +55,41 @@ export default {
|
|||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
},
|
},
|
||||||
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||||
const session = context.driver.session()
|
|
||||||
const { postId, data } = params
|
const { postId, data } = params
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||||
`,
|
`,
|
||||||
{ postId, data },
|
{ postId, data },
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
|
|
||||||
const [emotionsCount] = transactionRes.records.map(record => {
|
const [emotionsCount] = transactionRes.records.map(record => {
|
||||||
return record.get('emotionsCount').low
|
return record.get('emotionsCount').low
|
||||||
})
|
})
|
||||||
|
|
||||||
return emotionsCount
|
return emotionsCount
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||||
const session = context.driver.session()
|
|
||||||
const { postId } = params
|
const { postId } = params
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||||
RETURN collect(emoted.emotion) as emotion`,
|
RETURN collect(emoted.emotion) as emotion`,
|
||||||
{ userId: context.user.id, postId },
|
{ userId: context.user.id, postId },
|
||||||
)
|
)
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
const [emotions] = transactionRes.records.map(record => {
|
const [emotions] = transactionRes.records.map(record => {
|
||||||
return record.get('emotion')
|
return record.get('emotion')
|
||||||
})
|
})
|
||||||
return emotions
|
return emotions
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -94,8 +98,6 @@ export default {
|
|||||||
delete params.categoryIds
|
delete params.categoryIds
|
||||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||||
params.id = params.id || uuid()
|
params.id = params.id || uuid()
|
||||||
let post
|
|
||||||
|
|
||||||
const createPostCypher = `CREATE (post:Post {params})
|
const createPostCypher = `CREATE (post:Post {params})
|
||||||
SET post.createdAt = toString(datetime())
|
SET post.createdAt = toString(datetime())
|
||||||
SET post.updatedAt = toString(datetime())
|
SET post.updatedAt = toString(datetime())
|
||||||
@ -114,7 +116,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||||
const posts = transactionRes.records.map(record => record.get('post').properties)
|
const posts = transactionRes.records.map(record => record.get('post').properties)
|
||||||
post = posts[0]
|
return posts[0]
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||||
throw new UserInputError('Post with this slug already exists!')
|
throw new UserInputError('Post with this slug already exists!')
|
||||||
@ -122,20 +124,19 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return post
|
|
||||||
},
|
},
|
||||||
UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { categoryIds } = params
|
const { categoryIds } = params
|
||||||
delete params.categoryIds
|
delete params.categoryIds
|
||||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||||
const session = context.driver.session()
|
|
||||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||||
SET post += $params
|
SET post += $params
|
||||||
SET post.updatedAt = toString(datetime())
|
SET post.updatedAt = toString(datetime())
|
||||||
WITH post
|
WITH post
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
if (categoryIds && categoryIds.length) {
|
if (categoryIds && categoryIds.length) {
|
||||||
const cypherDeletePreviousRelations = `
|
const cypherDeletePreviousRelations = `
|
||||||
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||||
@ -160,14 +161,15 @@ export default {
|
|||||||
const [post] = transactionRes.records.map(record => {
|
const [post] = transactionRes.records.map(record => {
|
||||||
return record.get('post').properties
|
return record.get('post').properties
|
||||||
})
|
})
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return post
|
return post
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
DeletePost: async (object, args, context, resolveInfo) => {
|
DeletePost: async (object, args, context, resolveInfo) => {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`
|
`
|
||||||
@ -183,21 +185,24 @@ export default {
|
|||||||
`,
|
`,
|
||||||
{ postId: args.id },
|
{ postId: args.id },
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||||
return post
|
return post
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
||||||
const session = context.driver.session()
|
|
||||||
const { to, data } = params
|
const { to, data } = params
|
||||||
const { user } = context
|
const { user } = context
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||||
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
||||||
RETURN userFrom, postTo, emotedRelation`,
|
RETURN userFrom, postTo, emotedRelation`,
|
||||||
{ user, to, data },
|
{ user, to, data },
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
const [emoted] = transactionRes.records.map(record => {
|
const [emoted] = transactionRes.records.map(record => {
|
||||||
return {
|
return {
|
||||||
from: { ...record.get('userFrom').properties },
|
from: { ...record.get('userFrom').properties },
|
||||||
@ -206,18 +211,21 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return emoted
|
return emoted
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
RemovePostEmotions: async (object, params, context, resolveInfo) => {
|
RemovePostEmotions: async (object, params, context, resolveInfo) => {
|
||||||
const session = context.driver.session()
|
|
||||||
const { to, data } = params
|
const { to, data } = params
|
||||||
const { id: from } = context.user
|
const { id: from } = context.user
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||||
DELETE emotedRelation
|
DELETE emotedRelation
|
||||||
RETURN userFrom, postTo`,
|
RETURN userFrom, postTo`,
|
||||||
{ from, to, data },
|
{ from, to, data },
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
const [emoted] = transactionRes.records.map(record => {
|
const [emoted] = transactionRes.records.map(record => {
|
||||||
return {
|
return {
|
||||||
from: { ...record.get('userFrom').properties },
|
from: { ...record.get('userFrom').properties },
|
||||||
@ -226,6 +234,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return emoted
|
return emoted
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
pinPost: async (_parent, params, context, _resolveInfo) => {
|
pinPost: async (_parent, params, context, _resolveInfo) => {
|
||||||
let pinnedPostWithNestedAttributes
|
let pinnedPostWithNestedAttributes
|
||||||
@ -243,6 +254,7 @@ export default {
|
|||||||
)
|
)
|
||||||
return deletePreviousRelationsResponse.records.map(record => record.get('post').properties)
|
return deletePreviousRelationsResponse.records.map(record => record.get('post').properties)
|
||||||
})
|
})
|
||||||
|
try {
|
||||||
await writeTxResultPromise
|
await writeTxResultPromise
|
||||||
|
|
||||||
writeTxResultPromise = session.writeTransaction(async transaction => {
|
writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||||
@ -261,7 +273,6 @@ export default {
|
|||||||
pinnedAt: record.get('pinnedAt'),
|
pinnedAt: record.get('pinnedAt'),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
try {
|
|
||||||
const [transactionResult] = await writeTxResultPromise
|
const [transactionResult] = await writeTxResultPromise
|
||||||
const { pinnedPost, pinnedAt } = transactionResult
|
const { pinnedPost, pinnedAt } = transactionResult
|
||||||
pinnedPostWithNestedAttributes = {
|
pinnedPostWithNestedAttributes = {
|
||||||
|
|||||||
@ -316,53 +316,6 @@ describe('CreatePost', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('categories', () => {
|
|
||||||
describe('null', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, categoryIds: null }
|
|
||||||
})
|
|
||||||
it('throws UserInputError', async () => {
|
|
||||||
const {
|
|
||||||
errors: [error],
|
|
||||||
} = await mutate({ mutation: createPostMutation, variables })
|
|
||||||
expect(error).toHaveProperty(
|
|
||||||
'message',
|
|
||||||
'You cannot save a post without at least one category or more than three',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('empty', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, categoryIds: [] }
|
|
||||||
})
|
|
||||||
it('throws UserInputError', async () => {
|
|
||||||
const {
|
|
||||||
errors: [error],
|
|
||||||
} = await mutate({ mutation: createPostMutation, variables })
|
|
||||||
expect(error).toHaveProperty(
|
|
||||||
'message',
|
|
||||||
'You cannot save a post without at least one category or more than three',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('more than 3 items', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
|
|
||||||
})
|
|
||||||
it('throws UserInputError', async () => {
|
|
||||||
const {
|
|
||||||
errors: [error],
|
|
||||||
} = await mutate({ mutation: createPostMutation, variables })
|
|
||||||
expect(error).toHaveProperty(
|
|
||||||
'message',
|
|
||||||
'You cannot save a post without at least one category or more than three',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -493,74 +446,6 @@ describe('UpdatePost', () => {
|
|||||||
expected,
|
expected,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('more than 3 categories', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows a maximum of three category for a successful update', async () => {
|
|
||||||
const {
|
|
||||||
errors: [error],
|
|
||||||
} = await mutate({ mutation: updatePostMutation, variables })
|
|
||||||
expect(error).toHaveProperty(
|
|
||||||
'message',
|
|
||||||
'You cannot save a post without at least one category or more than three',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('post created without categories somehow', () => {
|
|
||||||
let owner
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const postSomehowCreated = await neode.create('Post', {
|
|
||||||
id: 'how-was-this-created',
|
|
||||||
})
|
|
||||||
owner = await neode.create('User', {
|
|
||||||
id: 'author-of-post-without-category',
|
|
||||||
name: 'Hacker',
|
|
||||||
slug: 'hacker',
|
|
||||||
email: 'hacker@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
await postSomehowCreated.relateTo(owner, 'author')
|
|
||||||
authenticatedUser = await owner.toJson()
|
|
||||||
variables = { ...variables, id: 'how-was-this-created' }
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws an error if categoryIds is not an array', async () => {
|
|
||||||
const {
|
|
||||||
errors: [error],
|
|
||||||
} = await mutate({
|
|
||||||
mutation: updatePostMutation,
|
|
||||||
variables: {
|
|
||||||
...variables,
|
|
||||||
categoryIds: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(error).toHaveProperty(
|
|
||||||
'message',
|
|
||||||
'You cannot save a post without at least one category or more than three',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('requires at least one category for successful update', async () => {
|
|
||||||
const {
|
|
||||||
errors: [error],
|
|
||||||
} = await mutate({
|
|
||||||
mutation: updatePostMutation,
|
|
||||||
variables: {
|
|
||||||
...variables,
|
|
||||||
categoryIds: [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(error).toHaveProperty(
|
|
||||||
'message',
|
|
||||||
'You cannot save a post without at least one category or more than three',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export default {
|
|||||||
const { id, type } = params
|
const { id, type } = params
|
||||||
|
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
|
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
|
||||||
WHERE $type IN labels(node) AND NOT userWritten.id = $userId
|
WHERE $type IN labels(node) AND NOT userWritten.id = $userId
|
||||||
@ -20,15 +21,16 @@ export default {
|
|||||||
return record.get('isShouted')
|
return record.get('isShouted')
|
||||||
})
|
})
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return isShouted
|
return isShouted
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
unshout: async (_object, params, context, _resolveInfo) => {
|
unshout: async (_object, params, context, _resolveInfo) => {
|
||||||
const { id, type } = params
|
const { id, type } = params
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
const transactionRes = await session.run(
|
const transactionRes = await session.run(
|
||||||
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||||
WHERE $type IN labels(node)
|
WHERE $type IN labels(node)
|
||||||
@ -43,9 +45,10 @@ export default {
|
|||||||
const [isShouted] = transactionRes.records.map(record => {
|
const [isShouted] = transactionRes.records.map(record => {
|
||||||
return record.get('isShouted')
|
return record.get('isShouted')
|
||||||
})
|
})
|
||||||
session.close()
|
|
||||||
|
|
||||||
return isShouted
|
return isShouted
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
statistics: async (parent, args, { driver, user }) => {
|
statistics: async (_parent, _args, { driver }) => {
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const response = {}
|
const response = {}
|
||||||
try {
|
try {
|
||||||
@ -33,10 +33,10 @@ export default {
|
|||||||
* Note: invites count is calculated this way because invitation codes are not in use yet
|
* Note: invites count is calculated this way because invitation codes are not in use yet
|
||||||
*/
|
*/
|
||||||
response.countInvites = response.countEmails - response.countUsers
|
response.countInvites = response.countEmails - response.countUsers
|
||||||
|
return response
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
return response
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
140
backend/src/schema/resolvers/statistics.spec.js
Normal file
140
backend/src/schema/resolvers/statistics.spec.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import Factory from '../../seed/factories'
|
||||||
|
import { gql } from '../../helpers/jest'
|
||||||
|
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
let query, authenticatedUser
|
||||||
|
const factory = Factory()
|
||||||
|
const instance = getNeode()
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
const statisticsQuery = gql`
|
||||||
|
query {
|
||||||
|
statistics {
|
||||||
|
countUsers
|
||||||
|
countPosts
|
||||||
|
countComments
|
||||||
|
countNotifications
|
||||||
|
countInvites
|
||||||
|
countFollows
|
||||||
|
countShouts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beforeAll(() => {
|
||||||
|
authenticatedUser = undefined
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode: instance,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('statistics', () => {
|
||||||
|
describe('countUsers', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
[...Array(6).keys()].map(() => {
|
||||||
|
return factory.create('User')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the count of all users', async () => {
|
||||||
|
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||||
|
data: { statistics: { countUsers: 6 } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('countPosts', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
[...Array(3).keys()].map(() => {
|
||||||
|
return factory.create('Post')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the count of all posts', async () => {
|
||||||
|
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||||
|
data: { statistics: { countPosts: 3 } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('countComments', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
[...Array(2).keys()].map(() => {
|
||||||
|
return factory.create('Comment')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the count of all comments', async () => {
|
||||||
|
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||||
|
data: { statistics: { countComments: 2 } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('countFollows', () => {
|
||||||
|
let users
|
||||||
|
beforeEach(async () => {
|
||||||
|
users = await Promise.all(
|
||||||
|
[...Array(2).keys()].map(() => {
|
||||||
|
return factory.create('User')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await users[0].relateTo(users[1], 'following')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the count of all follows', async () => {
|
||||||
|
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||||
|
data: { statistics: { countFollows: 1 } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('countShouts', () => {
|
||||||
|
let users, posts
|
||||||
|
beforeEach(async () => {
|
||||||
|
users = await Promise.all(
|
||||||
|
[...Array(2).keys()].map(() => {
|
||||||
|
return factory.create('User')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
posts = await Promise.all(
|
||||||
|
[...Array(3).keys()].map(() => {
|
||||||
|
return factory.create('Post')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await Promise.all([
|
||||||
|
users[0].relateTo(posts[1], 'shouted'),
|
||||||
|
users[1].relateTo(posts[0], 'shouted'),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the count of all shouts', async () => {
|
||||||
|
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||||
|
data: { statistics: { countShouts: 2 } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -24,6 +24,7 @@ export default {
|
|||||||
// }
|
// }
|
||||||
email = normalizeEmail(email)
|
email = normalizeEmail(email)
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
|
try {
|
||||||
const result = await session.run(
|
const result = await session.run(
|
||||||
`
|
`
|
||||||
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
||||||
@ -31,7 +32,6 @@ export default {
|
|||||||
`,
|
`,
|
||||||
{ userEmail: email },
|
{ userEmail: email },
|
||||||
)
|
)
|
||||||
session.close()
|
|
||||||
const [currentUser] = await result.records.map(record => {
|
const [currentUser] = await result.records.map(record => {
|
||||||
return record.get('user')
|
return record.get('user')
|
||||||
})
|
})
|
||||||
@ -48,6 +48,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
throw new AuthenticationError('Incorrect email address or password.')
|
throw new AuthenticationError('Incorrect email address or password.')
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
||||||
const currentUser = await instance.find('User', user.id)
|
const currentUser = await instance.find('User', user.id)
|
||||||
|
|||||||
@ -29,8 +29,8 @@ const factories = {
|
|||||||
|
|
||||||
export const cleanDatabase = async (options = {}) => {
|
export const cleanDatabase = async (options = {}) => {
|
||||||
const { driver = getDriver() } = options
|
const { driver = getDriver() } = options
|
||||||
const session = driver.session()
|
|
||||||
const cypher = 'MATCH (n) DETACH DELETE n'
|
const cypher = 'MATCH (n) DETACH DELETE n'
|
||||||
|
const session = driver.session()
|
||||||
try {
|
try {
|
||||||
return await session.run(cypher)
|
return await session.run(cypher)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import middleware from './middleware'
|
|||||||
import { neode as getNeode, getDriver } from './bootstrap/neo4j'
|
import { neode as getNeode, getDriver } from './bootstrap/neo4j'
|
||||||
import decode from './jwt/decode'
|
import decode from './jwt/decode'
|
||||||
import schema from './schema'
|
import schema from './schema'
|
||||||
|
import webfinger from './activitypub/routes/webfinger'
|
||||||
|
|
||||||
// check required configs and throw error
|
// check required configs and throw error
|
||||||
// TODO check this directly in config file - currently not possible due to testsetup
|
// TODO check this directly in config file - currently not possible due to testsetup
|
||||||
@ -41,7 +42,10 @@ const createServer = options => {
|
|||||||
const server = new ApolloServer(Object.assign({}, defaults, options))
|
const server = new ApolloServer(Object.assign({}, defaults, options))
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
app.set('driver', driver)
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
|
app.use('/.well-known/', webfinger())
|
||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
server.applyMiddleware({ app, path: '/' })
|
server.applyMiddleware({ app, path: '/' })
|
||||||
|
|
||||||
|
|||||||
@ -9,32 +9,6 @@ Feature: Webfinger discovery
|
|||||||
| Slug |
|
| Slug |
|
||||||
| peter-lustiger |
|
| peter-lustiger |
|
||||||
|
|
||||||
Scenario: Search
|
|
||||||
When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
|
|
||||||
Then I receive the following json:
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"subject": "acct:peter-lustiger@localhost:4123",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"rel": "self",
|
|
||||||
"type": "application/activity+json",
|
|
||||||
"href": "http://localhost:4123/activitypub/users/peter-lustiger"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
And I expect the Content-Type to be "application/jrd+json; charset=utf-8"
|
|
||||||
|
|
||||||
Scenario: User does not exist
|
|
||||||
When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
|
|
||||||
Then I receive the following json:
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"error": "No record found for nonexisting@localhost."
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
Scenario: Receiving an actor object
|
Scenario: Receiving an actor object
|
||||||
When I send a GET request to "/activitypub/users/peter-lustiger"
|
When I send a GET request to "/activitypub/users/peter-lustiger"
|
||||||
Then I receive the following json:
|
Then I receive the following json:
|
||||||
|
|||||||
@ -33,12 +33,12 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc"
|
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc"
|
||||||
integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ==
|
integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ==
|
||||||
|
|
||||||
"@babel/cli@~7.7.0":
|
"@babel/cli@~7.7.4":
|
||||||
version "7.7.0"
|
version "7.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.7.0.tgz#8d10c9acb2acb362d7614a9493e1791c69100d89"
|
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.7.4.tgz#38804334c8db40209f88c69a5c90998e60cca18b"
|
||||||
integrity sha512-jECEqAq6Ngf3pOhLSg7od9WKyrIacyh1oNNYtRXNn+ummSHCTXBamGywOAtiae34Vk7zKuQNnLvo2BKTMCoV4A==
|
integrity sha512-O7mmzaWdm+VabWQmxuM8hqNrWGGihN83KfhPUzp2lAW4kzIMwBxujXkZbD4fMwKMYY9FXTbDvXsJqU+5XHXi4A==
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "^2.8.1"
|
commander "^4.0.1"
|
||||||
convert-source-map "^1.1.0"
|
convert-source-map "^1.1.0"
|
||||||
fs-readdir-recursive "^1.1.0"
|
fs-readdir-recursive "^1.1.0"
|
||||||
glob "^7.0.0"
|
glob "^7.0.0"
|
||||||
@ -2570,6 +2570,11 @@ commander@^3.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
||||||
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
||||||
|
|
||||||
|
commander@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c"
|
||||||
|
integrity sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==
|
||||||
|
|
||||||
commondir@^1.0.1:
|
commondir@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||||
@ -3329,10 +3334,10 @@ eslint-plugin-import@~2.18.2:
|
|||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
resolve "^1.11.0"
|
resolve "^1.11.0"
|
||||||
|
|
||||||
eslint-plugin-jest@~23.0.5:
|
eslint-plugin-jest@~23.1.1:
|
||||||
version "23.0.5"
|
version "23.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.5.tgz#3c7c5e636c5a21677d2dfc8ba5424233ee2b9f27"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.1.1.tgz#1220ab53d5a4bf5c3c4cd07c0dabc6199d4064dd"
|
||||||
integrity sha512-etxXrWsFWzxsrxKwJnFC38uppH/vlJ3oF7Wmp/cxedqxRIxVhXup8e5y5MmtVXelevgxrgA1QS1vo8j889iK5Q==
|
integrity sha512-2oPxHKNh4j1zmJ6GaCBuGcb8FVZU7YjFUOJzGOPnl9ic7VA/MGAskArLJiRIlnFUmi1EUxY+UiATAy8dv8s5JA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/experimental-utils" "^2.5.0"
|
"@typescript-eslint/experimental-utils" "^2.5.0"
|
||||||
|
|
||||||
@ -3385,10 +3390,10 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
|
||||||
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
|
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
|
||||||
|
|
||||||
eslint@~6.7.1:
|
eslint@~6.7.2:
|
||||||
version "6.7.1"
|
version "6.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.1.tgz#269ccccec3ef60ab32358a44d147ac209154b919"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1"
|
||||||
integrity sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA==
|
integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0"
|
"@babel/code-frame" "^7.0.0"
|
||||||
ajv "^6.10.0"
|
ajv "^6.10.0"
|
||||||
|
|||||||
@ -9,7 +9,7 @@ open your minikube dashboard:
|
|||||||
$ minikube dashboard
|
$ minikube dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
This will give you an overview. Some of the steps below need some timing to make ressources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
|
This will give you an overview. Some of the steps below need some timing to make resources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
|
||||||
|
|
||||||
Follow the installation instruction for [Human Connection](../human-connection/README.md).
|
Follow the installation instruction for [Human Connection](../human-connection/README.md).
|
||||||
If all the pods and services have settled and everything looks green in your
|
If all the pods and services have settled and everything looks green in your
|
||||||
|
|||||||
@ -15,7 +15,7 @@ services:
|
|||||||
- ./webapp:/nitro-web
|
- ./webapp:/nitro-web
|
||||||
environment:
|
environment:
|
||||||
- NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
|
- NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
|
||||||
- PUBLIC_REGISTRATION=true
|
- PUBLIC_REGISTRATION=false
|
||||||
command: yarn run dev
|
command: yarn run dev
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
@ -29,7 +29,7 @@ services:
|
|||||||
- SMTP_PORT=25
|
- SMTP_PORT=25
|
||||||
- SMTP_IGNORE_TLS=true
|
- SMTP_IGNORE_TLS=true
|
||||||
- "DEBUG=${DEBUG}"
|
- "DEBUG=${DEBUG}"
|
||||||
- PUBLIC_REGISTRATION=true
|
- PUBLIC_REGISTRATION=false
|
||||||
maintenance:
|
maintenance:
|
||||||
image: humanconnection/maintenance:latest
|
image: humanconnection/maintenance:latest
|
||||||
build:
|
build:
|
||||||
|
|||||||
48
features/support/steps.js
Normal file
48
features/support/steps.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// features/support/steps.js
|
||||||
|
import { Given, When, Then, After, AfterAll } from 'cucumber'
|
||||||
|
import Factory from '../../backend/src/seed/factories'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import expect from 'expect'
|
||||||
|
|
||||||
|
const debug = require('debug')('ea:test:steps')
|
||||||
|
const factory = Factory()
|
||||||
|
|
||||||
|
|
||||||
|
After(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
Given('our CLIENT_URI is {string}', function (string) {
|
||||||
|
expect(string).toEqual('http://localhost:3000')
|
||||||
|
// This is just for documentation. When you see URLs in the response of
|
||||||
|
// scenarios you, should be able to tell that it's coming from this
|
||||||
|
// environment variable.
|
||||||
|
});
|
||||||
|
|
||||||
|
Given('we have the following users in our database:', function (dataTable) {
|
||||||
|
return Promise.all(dataTable.hashes().map(({ slug, name }) => {
|
||||||
|
return factory.create('User', {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
When('I send a GET request to {string}', async function (pathname) {
|
||||||
|
const response = await this.get(pathname)
|
||||||
|
this.lastContentType = response.lastContentType
|
||||||
|
|
||||||
|
this.lastResponses.push(response.lastResponse)
|
||||||
|
this.statusCode = response.statusCode
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('the server responds with a HTTP Status {int} and the following json:', function (statusCode, docString) {
|
||||||
|
expect(this.statusCode).toEqual(statusCode)
|
||||||
|
const [ lastResponse ] = this.lastResponses
|
||||||
|
expect(JSON.parse(lastResponse)).toMatchObject(JSON.parse(docString))
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('the Content-Type is {string}', function (contentType) {
|
||||||
|
expect(this.lastContentType).toEqual(contentType)
|
||||||
|
})
|
||||||
|
|
||||||
36
features/webfinger.feature
Normal file
36
features/webfinger.feature
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
Feature: Webfinger discovery
|
||||||
|
From an external server, e.g. Mastodon
|
||||||
|
I want to search for an actor alias
|
||||||
|
In order to follow the actor
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given our CLIENT_URI is "http://localhost:3000"
|
||||||
|
And we have the following users in our database:
|
||||||
|
| name | slug |
|
||||||
|
| Peter Lustiger | peter-lustiger |
|
||||||
|
|
||||||
|
Scenario: Search a user
|
||||||
|
When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
|
||||||
|
Then the server responds with a HTTP Status 200 and the following json:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"subject": "acct:peter-lustiger@localhost:3000",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "http://localhost:3000/activitypub/users/peter-lustiger"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
And the Content-Type is "application/jrd+json; charset=utf-8"
|
||||||
|
|
||||||
|
Scenario: Search without result
|
||||||
|
When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
|
||||||
|
Then the server responds with a HTTP Status 404 and the following json:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"error": "No record found for \"nonexisting@localhost\"."
|
||||||
|
}
|
||||||
|
"""
|
||||||
38
features/world.js
Normal file
38
features/world.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { setWorldConstructor } from 'cucumber'
|
||||||
|
import request from 'request'
|
||||||
|
|
||||||
|
class CustomWorld {
|
||||||
|
constructor () {
|
||||||
|
// webFinger.feature
|
||||||
|
this.lastResponses = []
|
||||||
|
this.lastContentType = null
|
||||||
|
this.lastInboxUrl = null
|
||||||
|
this.lastActivity = null
|
||||||
|
// object-article.feature
|
||||||
|
this.statusCode = null
|
||||||
|
}
|
||||||
|
get (pathname) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request(`http://localhost:4000/${this.replaceSlashes(pathname)}`, {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/activity+json'
|
||||||
|
}}, (error, response, body) => {
|
||||||
|
if (!error) {
|
||||||
|
resolve({
|
||||||
|
lastResponse: body,
|
||||||
|
lastContentType: response.headers['content-type'],
|
||||||
|
statusCode: response.statusCode
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceSlashes (pathname) {
|
||||||
|
return pathname.replace(/^\/+/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorldConstructor(CustomWorld)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FROM neo4j:3.5.12-enterprise
|
FROM neo4j:3.5.13-enterprise
|
||||||
LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
|
LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
|
||||||
|
|
||||||
ARG BUILD_COMMIT
|
ARG BUILD_COMMIT
|
||||||
|
|||||||
15
package.json
15
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "nitro-cypress",
|
"name": "human-connection",
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"description": "Fullstack tests with cypress for Human Connection",
|
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||||
"author": "Human Connection gGmbh",
|
"author": "Human Connection gGmbh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"cypress-cucumber-preprocessor": {
|
"cypress-cucumber-preprocessor": {
|
||||||
@ -16,19 +16,26 @@
|
|||||||
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
||||||
"cypress:run": "cross-env cypress run --browser chromium",
|
"cypress:run": "cross-env cypress run --browser chromium",
|
||||||
"cypress:open": "cross-env cypress open --browser chromium",
|
"cypress:open": "cross-env cypress open --browser chromium",
|
||||||
|
"cucumber:setup": "cd backend && yarn run dev",
|
||||||
|
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||||
"version": "auto-changelog -p"
|
"version": "auto-changelog -p"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.7.2",
|
||||||
|
"@babel/preset-env": "^7.7.4",
|
||||||
|
"@babel/register": "^7.7.4",
|
||||||
"auto-changelog": "^1.16.2",
|
"auto-changelog": "^1.16.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"codecov": "^3.6.1",
|
"codecov": "^3.6.1",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
|
"cucumber": "^6.0.5",
|
||||||
"cypress": "^3.7.0",
|
"cypress": "^3.7.0",
|
||||||
"cypress-cucumber-preprocessor": "^1.16.2",
|
"cypress-cucumber-preprocessor": "^1.17.0",
|
||||||
"cypress-file-upload": "^3.5.0",
|
"cypress-file-upload": "^3.5.0",
|
||||||
"cypress-plugin-retries": "^1.4.0",
|
"cypress-plugin-retries": "^1.5.0",
|
||||||
"date-fns": "^2.8.1",
|
"date-fns": "^2.8.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
"expect": "^24.9.0",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql-request": "^1.8.2",
|
"graphql-request": "^1.8.2",
|
||||||
"neo4j-driver": "^1.7.6",
|
"neo4j-driver": "^1.7.6",
|
||||||
|
|||||||
@ -12,27 +12,25 @@
|
|||||||
<hc-avatar v-if="showAvatar" class="avatar" :user="user" />
|
<hc-avatar v-if="showAvatar" class="avatar" :user="user" />
|
||||||
<div>
|
<div>
|
||||||
<ds-text class="userinfo">
|
<ds-text class="userinfo">
|
||||||
<b class="username">{{ userName | truncate(18) }}</b>
|
<b>{{ userSlug }}</b>
|
||||||
<ds-text v-if="positionDatetime === 'sideward' && dateTime" size="small" color="soft">
|
|
||||||
<base-icon name="clock" />
|
|
||||||
<client-only>
|
|
||||||
<hc-relative-date-time :date-time="dateTime" />
|
|
||||||
</client-only>
|
|
||||||
<slot name="dateTime"></slot>
|
|
||||||
</ds-text>
|
|
||||||
</ds-text>
|
</ds-text>
|
||||||
</div>
|
</div>
|
||||||
<ds-text class="user-slug" align="left" size="small" color="soft">
|
<ds-text class="username" align="left" size="small" color="soft">
|
||||||
{{ userSlug }}
|
{{ userName | truncate(18) }}
|
||||||
|
<base-icon name="clock" />
|
||||||
|
<template v-if="dateTime">
|
||||||
|
<hc-relative-date-time :date-time="dateTime" />
|
||||||
|
<slot name="dateTime"></slot>
|
||||||
|
</template>
|
||||||
</ds-text>
|
</ds-text>
|
||||||
<!-- dateTime: kind of same as above: make own component? -->
|
<!-- dateTime: kind of same as above: make own component?
|
||||||
<ds-text v-if="positionDatetime === 'below' && dateTime" size="small" color="soft">
|
<ds-text v-if="positionDatetime === 'below' && dateTime" size="small" color="soft">
|
||||||
<base-icon name="clock" />
|
<base-icon name="clock" />
|
||||||
<client-only>
|
<client-only>
|
||||||
<hc-relative-date-time :date-time="dateTime" />
|
<hc-relative-date-time :date-time="dateTime" />
|
||||||
</client-only>
|
</client-only>
|
||||||
<slot name="dateTime"></slot>
|
<slot name="dateTime"></slot>
|
||||||
</ds-text>
|
</ds-text> -->
|
||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
@ -116,7 +114,7 @@ export default {
|
|||||||
showAvatar: { type: Boolean, default: true },
|
showAvatar: { type: Boolean, default: true },
|
||||||
trunc: { type: Number, default: 18 }, // "-1" is no trunc
|
trunc: { type: Number, default: 18 }, // "-1" is no trunc
|
||||||
dateTime: { type: [Date, String], default: null },
|
dateTime: { type: [Date, String], default: null },
|
||||||
positionDatetime: { type: String, default: 'sideward' }, // 'below' is the otherone
|
// positionDatetime: { type: String, default: 'sideward' }, // 'below' is the otherone
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|||||||
@ -46,7 +46,6 @@
|
|||||||
:showAvatar="false"
|
:showAvatar="false"
|
||||||
:trunc="30"
|
:trunc="30"
|
||||||
:date-time="report.updatedAt"
|
:date-time="report.updatedAt"
|
||||||
positionDatetime="below"
|
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
</td>
|
</td>
|
||||||
@ -59,7 +58,6 @@
|
|||||||
<ds-button
|
<ds-button
|
||||||
v-else
|
v-else
|
||||||
danger
|
danger
|
||||||
class="confirm"
|
|
||||||
size="small"
|
size="small"
|
||||||
:icon="statusIconName"
|
:icon="statusIconName"
|
||||||
@click="$emit('confirm-report')"
|
@click="$emit('confirm-report')"
|
||||||
|
|||||||
@ -101,7 +101,7 @@ describe('ReportsTable', () => {
|
|||||||
describe('give report has not been closed', () => {
|
describe('give report has not been closed', () => {
|
||||||
let confirmButton
|
let confirmButton
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
confirmButton = commentRow.find('button.confirm')
|
confirmButton = commentRow.find('[data-test="confirm"]')
|
||||||
})
|
})
|
||||||
it('renders a confirm button', () => {
|
it('renders a confirm button', () => {
|
||||||
expect(confirmButton.exists()).toBe(true)
|
expect(confirmButton.exists()).toBe(true)
|
||||||
@ -153,7 +153,7 @@ describe('ReportsTable', () => {
|
|||||||
describe('give report has not been closed', () => {
|
describe('give report has not been closed', () => {
|
||||||
let confirmButton
|
let confirmButton
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
confirmButton = postRow.find('button.confirm')
|
confirmButton = postRow.find('[data-test="confirm"]')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders a confirm button', () => {
|
it('renders a confirm button', () => {
|
||||||
|
|||||||
@ -184,7 +184,7 @@
|
|||||||
"projects": "Projekte",
|
"projects": "Projekte",
|
||||||
"invites": "Einladungen",
|
"invites": "Einladungen",
|
||||||
"follows": "Folgen",
|
"follows": "Folgen",
|
||||||
"shouts": "Zurufe"
|
"shouts": "Empfehlungen"
|
||||||
},
|
},
|
||||||
"organizations": {
|
"organizations": {
|
||||||
"name": "Organisationen"
|
"name": "Organisationen"
|
||||||
@ -790,6 +790,10 @@
|
|||||||
"title": "Fehler und Rückmeldungen",
|
"title": "Fehler und Rückmeldungen",
|
||||||
"description": "Wir sind sehr bemüht, unser Netzwerk und unsere Daten sicher und abrufbar zu erhalten. Jede neue Version der Software durchläuft sowohl automatisierte als auch manuelle Tests. Es können jedoch unvorhergesehene Fehler auftreten. Deshalb sind wir dankbar für jeden gemeldeten Fehler. Du kannst gerne jeden von Dir entdeckten Fehler dem Support\/der Hilfe-Assistenz mitteilen: support@human-connection.org"
|
"description": "Wir sind sehr bemüht, unser Netzwerk und unsere Daten sicher und abrufbar zu erhalten. Jede neue Version der Software durchläuft sowohl automatisierte als auch manuelle Tests. Es können jedoch unvorhergesehene Fehler auftreten. Deshalb sind wir dankbar für jeden gemeldeten Fehler. Du kannst gerne jeden von Dir entdeckten Fehler dem Support\/der Hilfe-Assistenz mitteilen: support@human-connection.org"
|
||||||
},
|
},
|
||||||
|
"no-commercial-use" : {
|
||||||
|
"title": "Keine kommerzielle Nutzung",
|
||||||
|
"description": "Die Nutzung des Human Connection Netzwerkes ist nicht gestattet für kommerzielle Nutzung. Darunter fällt unter anderem das Bewerben von Produkten mit kommerzieller Absicht, das Einstellen von Affiliate-Links, direkter Aufruf zu Spenden oder finanzieller Unterstützung für Zwecke, die steuerlich nicht als gemeinnützig anerkannt sind."
|
||||||
|
},
|
||||||
"help-and-questions": {
|
"help-and-questions": {
|
||||||
"title": "Hilfe und Fragen",
|
"title": "Hilfe und Fragen",
|
||||||
"description": "Für Hilfe und Fragen haben wir Dir eine umfassende Sammlung an häufig gestellten Fragen und Antworten (FAQ) zusammengestellt. Du findest diese hier: <a href=\"https:\/\/support.human-connection.org\/kb\/\" target=\"_blank\" > https:\/\/support.human-connection.org\/kb\/ <\/a>"
|
"description": "Für Hilfe und Fragen haben wir Dir eine umfassende Sammlung an häufig gestellten Fragen und Antworten (FAQ) zusammengestellt. Du findest diese hier: <a href=\"https:\/\/support.human-connection.org\/kb\/\" target=\"_blank\" > https:\/\/support.human-connection.org\/kb\/ <\/a>"
|
||||||
|
|||||||
@ -783,6 +783,10 @@
|
|||||||
"title": "Errors and Feedback",
|
"title": "Errors and Feedback",
|
||||||
"description": "We make every effort to keep our network and data secure and available. Each new release of the software goes through both automated and manual testing. However, unforeseen errors may occur. Therefore, we are grateful for any reported bugs. You are welcome to report any bugs you discover by emailing Support at support@human-connection.org"
|
"description": "We make every effort to keep our network and data secure and available. Each new release of the software goes through both automated and manual testing. However, unforeseen errors may occur. Therefore, we are grateful for any reported bugs. You are welcome to report any bugs you discover by emailing Support at support@human-connection.org"
|
||||||
},
|
},
|
||||||
|
"no-commercial-use" : {
|
||||||
|
"title": "No Commercial Use",
|
||||||
|
"description": "The use of the Human Connection Network is not permitted for commercial purposes. This includes, but is not limited to, advertising products with commercial intent, posting affiliate links, directly soliciting donations, or providing financial support for purposes that are not recognized as charitable for tax purposes."
|
||||||
|
},
|
||||||
"help-and-questions" : {
|
"help-and-questions" : {
|
||||||
"title": "Help and Questions",
|
"title": "Help and Questions",
|
||||||
"description": "For help and questions we have compiled a comprehensive collection of frequently asked questions and answers (FAQ) for you. You can find them here: <a href=\"https://support.human-connection.org/kb/\" target=\"_blank\" > https://support.human-connection.org/kb/ </a>"
|
"description": "For help and questions we have compiled a comprehensive collection of frequently asked questions and answers (FAQ) for you. You can find them here: <a href=\"https://support.human-connection.org/kb/\" target=\"_blank\" > https://support.human-connection.org/kb/ </a>"
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"no-account": "Ainda não tem uma conta?",
|
"no-account": "Ainda não tem uma conta?",
|
||||||
"register": "Cadastrar-se",
|
"register": "Cadastrar-se",
|
||||||
"moreInfoURL": "https:\/\/human-connection.org\/en\/",
|
"moreInfoURL": "https:\/\/human-connection.org\/en\/",
|
||||||
"moreInfoHint": "",
|
"moreInfoHint": "para a página de apresentação",
|
||||||
"success": "Você está conectado!",
|
"success": "Você está conectado!",
|
||||||
"failure": "Endereço de e-mail ou senha incorretos."
|
"failure": "Endereço de e-mail ou senha incorretos."
|
||||||
},
|
},
|
||||||
@ -125,15 +125,15 @@
|
|||||||
"name": "Fornecedores de terceiros",
|
"name": "Fornecedores de terceiros",
|
||||||
"info-description": "Se você concordar, as publicações da seguinte lista de provedores incluirão automaticamente código de terceiros de outros provedores (terceiros) na forma de vídeos, imagens ou texto incorporados.",
|
"info-description": "Se você concordar, as publicações da seguinte lista de provedores incluirão automaticamente código de terceiros de outros provedores (terceiros) na forma de vídeos, imagens ou texto incorporados.",
|
||||||
"status": {
|
"status": {
|
||||||
"description": "",
|
"description": "Como padrão para você, o código incorporado de provedores de terceiros é",
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"off": "",
|
"off": "não exibido inicialmente",
|
||||||
"on": ""
|
"on": "exibido imediatamente"
|
||||||
},
|
},
|
||||||
"change": {
|
"change": {
|
||||||
"question": "",
|
"question": "O código-fonte incorporado de terceiros deve sempre ser exibido para você?",
|
||||||
"allow": "",
|
"allow": "Certeza",
|
||||||
"deny": ""
|
"deny": "Não, obrigado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -158,13 +158,13 @@
|
|||||||
"columns": {
|
"columns": {
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"slug": "Slug",
|
"slug": "Slug",
|
||||||
"unblock": ""
|
"unblock": "Desbloquear"
|
||||||
},
|
},
|
||||||
"empty": "Até agora, você não bloqueou ninguém.",
|
"empty": "Até agora, você não bloqueou ninguém.",
|
||||||
"how-to": "Você pode bloquear outros usuários em suas páginas de perfil através do menu de conteúdo.",
|
"how-to": "Você pode bloquear outros usuários em suas páginas de perfil através do menu de conteúdo.",
|
||||||
"block": "Bloquear usuário",
|
"block": "Bloquear usuário",
|
||||||
"unblock": "Desbloquear usuário",
|
"unblock": "Desbloquear usuário",
|
||||||
"unblocked": ""
|
"unblocked": "{name} está desbloqueado novamente"
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"name": "Privacidade",
|
"name": "Privacidade",
|
||||||
@ -261,10 +261,10 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"edit": "Editar publicação",
|
"edit": "Editar publicação",
|
||||||
"delete": "Excluir publicação",
|
"delete": "Excluir publicação",
|
||||||
"pin": "",
|
"pin": "Fixar publicação",
|
||||||
"pinnedSuccessfully": "",
|
"pinnedSuccessfully": "Publicação fixada com sucesso!",
|
||||||
"unpin": "",
|
"unpin": "Desafixar publicação",
|
||||||
"unpinnedSuccessfully": ""
|
"unpinnedSuccessfully": "Publicação desafixada com sucesso!"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"submit": "Commentar",
|
"submit": "Commentar",
|
||||||
@ -299,7 +299,7 @@
|
|||||||
"validations": {
|
"validations": {
|
||||||
"email": "deve ser um endereço de e-mail válido",
|
"email": "deve ser um endereço de e-mail válido",
|
||||||
"url": "deve ser uma URL válida",
|
"url": "deve ser uma URL válida",
|
||||||
"categories": ""
|
"categories": "devem ser seleccionadas, no mínimo uma e, no máximo três categorias"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
@ -430,7 +430,7 @@
|
|||||||
"teaserImage": {
|
"teaserImage": {
|
||||||
"cropperConfirm": "Confirmar"
|
"cropperConfirm": "Confirmar"
|
||||||
},
|
},
|
||||||
"languageSelectText": ""
|
"languageSelectText": "Selecionar Idioma"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"edit": "Editar Comentário",
|
"edit": "Editar Comentário",
|
||||||
@ -745,8 +745,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"donations": {
|
"donations": {
|
||||||
"donations-for": "",
|
"donations-for": "Doações para",
|
||||||
"donate-now": "",
|
"donate-now": "Doe agora",
|
||||||
"amount-of-total": ""
|
"amount-of-total": "{amount} dos {total} € foram coletados"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@
|
|||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"tippy.js": "^4.3.5",
|
"tippy.js": "^4.3.5",
|
||||||
"tiptap": "~1.26.3",
|
"tiptap": "~1.26.3",
|
||||||
"tiptap-extensions": "~1.28.4",
|
"tiptap-extensions": "~1.28.5",
|
||||||
"trunc-html": "^1.1.2",
|
"trunc-html": "^1.1.2",
|
||||||
"v-tooltip": "~2.0.2",
|
"v-tooltip": "~2.0.2",
|
||||||
"validator": "^12.1.0",
|
"validator": "^12.1.0",
|
||||||
@ -100,13 +100,13 @@
|
|||||||
"@babel/core": "~7.7.4",
|
"@babel/core": "~7.7.4",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.7.4",
|
"@babel/preset-env": "~7.7.4",
|
||||||
"@storybook/addon-a11y": "^5.2.6",
|
"@storybook/addon-a11y": "^5.2.8",
|
||||||
"@storybook/addon-actions": "^5.2.6",
|
"@storybook/addon-actions": "^5.2.8",
|
||||||
"@storybook/addon-notes": "^5.2.5",
|
"@storybook/addon-notes": "^5.2.8",
|
||||||
"@storybook/vue": "~5.2.6",
|
"@storybook/vue": "~5.2.8",
|
||||||
"@vue/cli-shared-utils": "~4.0.5",
|
"@vue/cli-shared-utils": "~4.1.1",
|
||||||
"@vue/eslint-config-prettier": "~6.0.0",
|
"@vue/eslint-config-prettier": "~6.0.0",
|
||||||
"@vue/server-test-utils": "~1.0.0-beta.29",
|
"@vue/server-test-utils": "~1.0.0-beta.30",
|
||||||
"@vue/test-utils": "~1.0.0-beta.29",
|
"@vue/test-utils": "~1.0.0-beta.29",
|
||||||
"async-validator": "^3.2.2",
|
"async-validator": "^3.2.2",
|
||||||
"babel-core": "~7.0.0-bridge.0",
|
"babel-core": "~7.0.0-bridge.0",
|
||||||
@ -116,13 +116,13 @@
|
|||||||
"babel-plugin-require-context-hook": "^1.0.0",
|
"babel-plugin-require-context-hook": "^1.0.0",
|
||||||
"babel-preset-vue": "~2.0.2",
|
"babel-preset-vue": "~2.0.2",
|
||||||
"core-js": "~2.6.10",
|
"core-js": "~2.6.10",
|
||||||
"css-loader": "~3.2.0",
|
"css-loader": "~3.2.1",
|
||||||
"eslint": "~6.7.1",
|
"eslint": "~6.7.2",
|
||||||
"eslint-config-prettier": "~6.7.0",
|
"eslint-config-prettier": "~6.7.0",
|
||||||
"eslint-config-standard": "~14.1.0",
|
"eslint-config-standard": "~14.1.0",
|
||||||
"eslint-loader": "~3.0.2",
|
"eslint-loader": "~3.0.2",
|
||||||
"eslint-plugin-import": "~2.18.2",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~23.0.5",
|
"eslint-plugin-jest": "~23.1.1",
|
||||||
"eslint-plugin-node": "~10.0.0",
|
"eslint-plugin-node": "~10.0.0",
|
||||||
"eslint-plugin-prettier": "~3.1.1",
|
"eslint-plugin-prettier": "~3.1.1",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export default {
|
|||||||
'code-of-conduct',
|
'code-of-conduct',
|
||||||
'moderation',
|
'moderation',
|
||||||
'errors-and-feedback',
|
'errors-and-feedback',
|
||||||
|
'no-commercial-use',
|
||||||
'help-and-questions',
|
'help-and-questions',
|
||||||
'addition',
|
'addition',
|
||||||
],
|
],
|
||||||
|
|||||||
742
webapp/yarn.lock
742
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user