Merge branch 'master' into delete-posts

This commit is contained in:
Ulf Gebhardt 2019-05-09 15:05:47 +02:00
commit aa80d7492d
No known key found for this signature in database
GPG Key ID: 44C888923CC8E7F3
60 changed files with 1905 additions and 1193 deletions

169
.codecov.yml Normal file
View File

@ -0,0 +1,169 @@
codecov:
#token: uuid # Your private repository token
#url: "http" # for Codecov Enterprise customers
#slug: "owner/repo" # for Codecov Enterprise customers
#branch: master # override the default branch
#bot: username # set user whom will be the consumer of oauth requests
#ci: # Custom CI domains if Codecov does not identify them automatically
# - ci.domain.com
# - !provider # ignore these providers when checking if CI passed
# # ex. You may test on Travis, Circle, and AppVeyor, but only need
# # to check if Travis passes. Therefore add: !circle and !appveyor
notify:
#after_n_builds: null # number of expected builds to recieve before sending notifications
# # after: check ci status unless disabled via require_ci_to_pass
require_ci_to_pass: yes # yes: will delay sending notifications until all ci is finished
# no: will send notifications without checking ci status and wait till "after_n_builds" are uploaded
#countdown: null # number of seconds to wait before first ci build check
#delay: null # number of seconds to wait between ci build checks
coverage:
precision: 2 # 2 = xx.xx%, 0 = xx%
round: nearest # down|up|nearest - default down
# range: 50...60 # default 70...90. red...green
#notify:
# irc:
# default:
# server: "chat.freenode.net"|encrypted
# branches: null # all branches by default
# threshold: 1%
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
# flags: null
# paths: null
#
# slack:
# default:
# url: "http"|encrypted
# threshold: 1%
# branches: null # all branches by default
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
# attachments: "sunburst, diff"
# only_pulls: false
# flags: null
# paths: null
#
# email:
# default:
# to:
# - example@domain.com
# - &author
# threshold: 1%
# only_pulls: false
# layout: header, diff, trends
# flags: null
# paths: null
#
# hipchat:
# default:
# url: "http"|encrypted
# room: name|id
# threshold: 1%
# token: encrypted
# branches: null # all branches by default
# notify: false # if the hipchat message is silent or loud (default false)
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
# flags: null
# paths: null
#
# gitter:
# url: "http"|encrypted
# threshold: 1%
# branches: null # all branches by default
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
#
# webhooks:
# _name_:
# url: "http"|encrypted
# threshold: 1%
# branches: null # all branches by default
status:
project:
default: false # disable the default status that measures entire project
backend: # declare a new status context "backend"
against: parent
target: auto
threshold: null
#threshold: 1%
base: auto
if_no_uploads: error
if_not_found: success
if_ci_failed: error
only_pulls: false
#branches:
# - master
#flags:
# - integration
paths:
- backend/ # only include coverage in "backend/" folder
webapp: # declare a new status context "frontend"
against: parent
target: auto
threshold: null
#threshold: 1%
base: auto
if_no_uploads: error
if_not_found: success
if_ci_failed: error
only_pulls: false
#branches:
# - master
#flags:
# - integration
paths:
- webapp/ # only include coverage in "webapp/" folder
patch:
default: false
# against: parent
# target: 80%
# branches: null
# if_no_uploads: success
# if_not_found: success
# if_ci_failed: error
# only_pulls: false
# flags:
# - integration
# paths:
# - folder
#changes:
# default:
# against: parent
# branches: null
# if_no_uploads: error
# if_not_found: success
# if_ci_failed: error
# only_pulls: false
# flags:
# - integration
# paths:
# - folder
#flags:
# integration:
# branches:
# - master
# ignore:
# - app/ui
#ignore: # files and folders for processing
# - tests/*
#fixes:
# - "old_path::new_path"
comment:
# layout options are quite limited in v4.x - there have been way more options in v1.0
layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags
behavior: new # default = posts once then update, posts new if delete
# once = post once then updates
# new = delete old, post new
# spammy = post new
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]
require_head: no # [yes :: must have a head report to post]
branches: null # branch names that can post comment
flags: null
paths: null

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ cypress/screenshots/
cypress.env.json cypress.env.json
!.gitkeep !.gitkeep
**/coverage

View File

@ -10,6 +10,8 @@ addons:
before_install: before_install:
- yarn global add wait-on - yarn global add wait-on
# Install Codecov
- yarn global add codecov
- yarn install - yarn install
- cp cypress.env.template.json cypress.env.json - cp cypress.env.template.json cypress.env.json
@ -18,6 +20,7 @@ install:
- wait-on http://localhost:7474 && docker-compose exec neo4j migrate - wait-on http://localhost:7474 && docker-compose exec neo4j migrate
script: script:
# Backend
- docker-compose exec backend yarn run lint - docker-compose exec backend yarn run lint
- docker-compose exec backend yarn run test:jest --ci --verbose=false - docker-compose exec backend yarn run test:jest --ci --verbose=false
- docker-compose exec backend yarn run db:reset - docker-compose exec backend yarn run db:reset
@ -25,10 +28,14 @@ script:
- docker-compose exec backend yarn run test:cucumber - docker-compose exec backend yarn run test:cucumber
- docker-compose exec backend yarn run db:reset - docker-compose exec backend yarn run db:reset
- docker-compose exec backend yarn run db:seed - docker-compose exec backend yarn run db:seed
# 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 - docker-compose exec webapp yarn run test --ci --verbose=false
- docker-compose exec -d backend yarn run test:before:seeder - docker-compose exec -d backend yarn run test:before:seeder
- yarn run cypress:run # Fullstack
- CYPRESS_RETRIES=1 yarn run cypress:run
# Coverage
- codecov
after_success: after_success:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh

View File

@ -1,6 +1,7 @@
# Human-Connection # Human-Connection
[![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/Human-Connection/Human-Connection) [![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/Human-Connection/Human-Connection)
[![Codecov Coverage](https://img.shields.io/codecov/c/github/Human-Connection/Human-Connection/master.svg?style=flat-square)](https://codecov.io/gh/Human-Connection/Human-Connection/)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md) [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3) [![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3)

View File

@ -1,9 +0,0 @@
version: "3.7"
services:
neo4j:
environment:
- NEO4J_PASSWORD=letmein
backend:
environment:
- NEO4J_PASSWORD=letmein

View File

@ -26,6 +26,18 @@
"license": "MIT", "license": "MIT",
"jest": { "jest": {
"verbose": true, "verbose": true,
"collectCoverage": true,
"collectCoverageFrom": [
"**/*.js",
"!**/node_modules/**",
"!**/test/**",
"!**/dist/**",
"!**/src/**/?(*.)+(spec|test).js?(x)"
],
"coverageReporters": [
"text",
"lcov"
],
"testMatch": [ "testMatch": [
"**/src/**/?(*.)+(spec|test).js?(x)" "**/src/**/?(*.)+(spec|test).js?(x)"
] ]
@ -36,31 +48,31 @@
"apollo-client": "~2.5.1", "apollo-client": "~2.5.1",
"apollo-link-context": "~1.0.14", "apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14", "apollo-link-http": "~1.5.14",
"apollo-server": "~2.4.8", "apollo-server": "~2.5.0",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27", "date-fns": "2.0.0-alpha.27",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~7.0.0", "dotenv": "~8.0.0",
"express": "~4.16.4", "express": "~4.16.4",
"faker": "~4.1.0", "faker": "~4.1.0",
"graphql": "~14.2.1", "graphql": "~14.2.1",
"graphql-custom-directives": "~0.2.14", "graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1", "graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2", "graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.4", "graphql-shield": "~5.3.5",
"graphql-tag": "~2.10.1", "graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4", "graphql-yoga": "~1.17.4",
"helmet": "~3.16.0", "helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8", "linkifyjs": "~2.1.8",
"lodash": "~4.17.11", "lodash": "~4.17.11",
"ms": "~2.1.1", "ms": "~2.1.1",
"neo4j-driver": "~1.7.3", "neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "~2.4.2", "neo4j-graphql-js": "~2.4.2",
"node-fetch": "~2.4.1", "node-fetch": "~2.5.0",
"npm-run-all": "~4.1.5", "npm-run-all": "~4.1.5",
"request": "~2.88.0", "request": "~2.88.0",
"sanitize-html": "~1.20.1", "sanitize-html": "~1.20.1",
@ -79,19 +91,19 @@
"apollo-server-testing": "~2.4.8", "apollo-server-testing": "~2.4.8",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1", "babel-eslint": "~10.0.1",
"babel-jest": "~24.7.1", "babel-jest": "~24.8.0",
"chai": "~4.2.0", "chai": "~4.2.0",
"cucumber": "~5.1.0", "cucumber": "~5.1.0",
"eslint": "~5.16.0", "eslint": "~5.16.0",
"eslint-config-standard": "~12.0.0", "eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.17.2", "eslint-plugin-import": "~2.17.2",
"eslint-plugin-jest": "~22.5.1", "eslint-plugin-jest": "~22.5.1",
"eslint-plugin-node": "~8.0.1", "eslint-plugin-node": "~9.0.1",
"eslint-plugin-promise": "~4.1.1", "eslint-plugin-promise": "~4.1.1",
"eslint-plugin-standard": "~4.0.0", "eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2", "graphql-request": "~1.8.2",
"jest": "~24.7.1", "jest": "~24.8.0",
"nodemon": "~1.18.11", "nodemon": "~1.19.0",
"supertest": "~4.0.2" "supertest": "~4.0.2"
} }
} }

View File

@ -75,6 +75,7 @@ const permissions = shield({
DeleteBadge: isAdmin, DeleteBadge: isAdmin,
AddUserBadges: isAdmin, AddUserBadges: isAdmin,
CreateSocialMedia: isAuthenticated, CreateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin, // AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin,
reward: isAdmin, reward: isAdmin,

View File

@ -2,44 +2,47 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
const COMMENT_MIN_LENGTH = 1 const COMMENT_MIN_LENGTH = 1
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
export default { export default {
Query: {
CommentByPost: async (object, params, context, resolveInfo) => {
const { postId } = params
const session = context.driver.session()
const transactionRes = await session.run(`
MATCH (comment:Comment)-[:COMMENTS]->(post:Post {id: $postId})
RETURN comment {.id, .contentExcerpt, .createdAt} ORDER BY comment.createdAt ASC`, {
postId
})
session.close()
let comments = []
transactionRes.records.map(record => {
comments.push(record.get('comment'))
})
return comments
}
},
Mutation: { Mutation: {
CreateComment: async (object, params, context, resolveInfo) => { CreateComment: async (object, params, context, resolveInfo) => {
const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim() const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim()
const { postId } = params
// Adding relationship from comment to post by passing in the postId,
// but we do not want to create the comment with postId as an attribute
// because we use relationships for this. So, we are deleting it from params
// before comment creation.
delete params.postId
if (!params.content || content.length < COMMENT_MIN_LENGTH) { if (!params.content || content.length < COMMENT_MIN_LENGTH) {
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
} }
const { postId } = params if (!postId.trim()) {
delete params.postId throw new UserInputError(NO_POST_ERR_MESSAGE)
const comment = await neo4jgraphql(object, params, context, resolveInfo, false) }
const session = context.driver.session() const session = context.driver.session()
const postQueryRes = await session.run(`
MATCH (post:Post {id: $postId})
RETURN post`, {
postId
}
)
const [post] = postQueryRes.records.map(record => {
return record.get('post')
})
if (!post) {
throw new UserInputError(NO_POST_ERR_MESSAGE)
}
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
await session.run(` await session.run(`
MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}) MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}), (author:User {id: $userId})
MERGE (post)<-[:COMMENTS]-(comment) MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
RETURN comment {.id, .content}`, { RETURN post`, {
userId: context.user.id,
postId, postId,
commentId: comment.id commentId: comment.id
} }

View File

@ -4,7 +4,10 @@ import { host, login } from '../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client
let variables let createCommentVariables
let createPostVariables
let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { await factory.create('User', {
@ -18,22 +21,36 @@ afterEach(async () => {
}) })
describe('CreateComment', () => { describe('CreateComment', () => {
const mutation = ` const createCommentMutation = `
mutation($postId: ID, $content: String!) { mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) { CreateComment(postId: $postId, content: $content) {
id id
content content
}
}
`
const createPostMutation = `
mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
}
}
`
const commentQueryForPostId = `
query($content: String) {
Comment(content: $content) {
postId
} }
} }
` `
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
variables = { createCommentVariables = {
postId: 'p1', postId: 'p1',
content: 'I\'m not authorised to comment' content: 'I\'m not authorised to comment'
} }
client = new GraphQLClient(host) client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow('Not Authorised')
}) })
}) })
@ -42,40 +59,120 @@ describe('CreateComment', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) createCommentVariables = {
it('creates a comment', async () => {
variables = {
postId: 'p1', postId: 'p1',
content: 'I\'m authorised to comment' content: 'I\'m authorised to comment'
} }
createPostVariables = {
id: 'p1',
title: 'post to comment on',
content: 'please comment on me'
}
await client.request(createPostMutation, createPostVariables)
})
it('creates a comment', async () => {
const expected = { const expected = {
CreateComment: { CreateComment: {
content: 'I\'m authorised to comment' content: 'I\'m authorised to comment'
} }
} }
await expect(client.request(mutation, variables)).resolves.toMatchObject(expected) await expect(client.request(createCommentMutation, createCommentVariables)).resolves.toMatchObject(expected)
}) })
it('throw an error if an empty string is sent as content', async () => { it('assigns the authenticated user as author', async () => {
variables = { await client.request(createCommentMutation, createCommentVariables)
const { User } = await client.request(`{
User(email: "test@example.org") {
comments {
content
}
}
}`)
expect(User).toEqual([ { comments: [ { content: 'I\'m authorised to comment' } ] } ])
})
it('throw an error if an empty string is sent from the editor as content', async () => {
createCommentVariables = {
postId: 'p1', postId: 'p1',
content: '<p></p>' content: '<p></p>'
} }
await expect(client.request(mutation, variables)) await expect(client.request(createCommentMutation, createCommentVariables))
.rejects.toThrow('Comment must be at least 1 character long!') .rejects.toThrow('Comment must be at least 1 character long!')
}) })
it('throws an error if a comment does not contain a single character', async () => { it('throws an error if a comment sent from the editor does not contain a single character', async () => {
variables = { createCommentVariables = {
postId: 'p1', postId: 'p1',
content: '<p> </p>' content: '<p> </p>'
} }
await expect(client.request(mutation, variables)) await expect(client.request(createCommentMutation, createCommentVariables))
.rejects.toThrow('Comment must be at least 1 character long!') .rejects.toThrow('Comment must be at least 1 character long!')
}) })
it('throws an error if postId is sent as an empty string', async () => {
createCommentVariables = {
postId: 'p1',
content: ''
}
await expect(client.request(createCommentMutation, createCommentVariables))
.rejects.toThrow('Comment must be at least 1 character long!')
})
it('throws an error if content is sent as an string of empty characters', async () => {
createCommentVariables = {
postId: 'p1',
content: ' '
}
await expect(client.request(createCommentMutation, createCommentVariables))
.rejects.toThrow('Comment must be at least 1 character long!')
})
it('throws an error if postId is sent as an empty string', async () => {
createCommentVariablesSansPostId = {
postId: '',
content: 'this comment should not be created'
}
await expect(client.request(createCommentMutation, createCommentVariablesSansPostId))
.rejects.toThrow('Comment cannot be created without a post!')
})
it('throws an error if postId is sent as an string of empty characters', async () => {
createCommentVariablesSansPostId = {
postId: ' ',
content: 'this comment should not be created'
}
await expect(client.request(createCommentMutation, createCommentVariablesSansPostId))
.rejects.toThrow('Comment cannot be created without a post!')
})
it('throws an error if the post does not exist in the database', async () => {
createCommentVariablesWithNonExistentPost = {
postId: 'p2',
content: 'comment should not be created cause the post doesn\'t exist'
}
await expect(client.request(createCommentMutation, createCommentVariablesWithNonExistentPost))
.rejects.toThrow('Comment cannot be created without a post!')
})
it('does not create the comment with the postId as an attribute', async () => {
const commentQueryVariablesByContent = {
content: 'I\'m authorised to comment'
}
await client.request(createCommentMutation, createCommentVariables)
const { Comment } = await client.request(commentQueryForPostId, commentQueryVariablesByContent)
expect(Comment).toEqual([{ postId: null }])
})
}) })
}) })

View File

@ -16,6 +16,9 @@ const setupAuthenticateClient = (params) => {
let createResource let createResource
let authenticateClient let authenticateClient
let createPostVariables
let createCommentVariables
beforeEach(() => { beforeEach(() => {
createResource = () => {} createResource = () => {}
authenticateClient = () => { authenticateClient = () => {
@ -103,18 +106,21 @@ describe('disable', () => {
variables = { variables = {
id: 'c47' id: 'c47'
} }
createPostVariables = {
id: 'p3',
title: 'post to comment on',
content: 'please comment on me'
}
createCommentVariables = {
id: 'c47',
postId: 'p3',
content: 'this comment was created for this post'
}
createResource = async () => { createResource = async () => {
await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' })
await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) const asAuthenticatedUser = await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
await Promise.all([ await asAuthenticatedUser.create('Post', createPostVariables)
factory.create('Post', { id: 'p3' }), await asAuthenticatedUser.create('Comment', createCommentVariables)
factory.create('Comment', { id: 'c47', postId: 'p3', content: 'this comment was created for this post' })
])
await Promise.all([
factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' })
])
} }
}) })
@ -277,17 +283,21 @@ describe('enable', () => {
variables = { variables = {
id: 'c456' id: 'c456'
} }
createPostVariables = {
id: 'p9',
title: 'post to comment on',
content: 'please comment on me'
}
createCommentVariables = {
id: 'c456',
postId: 'p9',
content: 'this comment was created for this post'
}
createResource = async () => { createResource = async () => {
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) const asAuthenticatedUser = await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
await Promise.all([ await asAuthenticatedUser.create('Post', createPostVariables)
factory.create('Post', { id: 'p9' }), await asAuthenticatedUser.create('Comment', createCommentVariables)
factory.create('Comment', { id: 'c456' })
])
await Promise.all([
factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' })
])
const disableMutation = ` const disableMutation = `
mutation { mutation {

View File

@ -9,6 +9,7 @@ describe('report', () => {
let headers let headers
let returnedObject let returnedObject
let variables let variables
let createPostVariables
beforeEach(async () => { beforeEach(async () => {
returnedObject = '{ description }' returnedObject = '{ description }'
@ -128,8 +129,14 @@ describe('report', () => {
describe('reported resource is a comment', () => { describe('reported resource is a comment', () => {
beforeEach(async () => { beforeEach(async () => {
await factory.authenticateAs({ email: 'test@example.org', password: '1234' }) createPostVariables = {
await factory.create('Comment', { id: 'c34', content: 'Robert getting tired.' }) id: 'p1',
title: 'post to comment on',
content: 'please comment on me'
}
const asAuthenticatedUser = await factory.authenticateAs({ email: 'test@example.org', password: '1234' })
await asAuthenticatedUser.create('Post', createPostVariables)
await asAuthenticatedUser.create('Comment', { postId: 'p1', id: 'c34', content: 'Robert getting tired.' })
variables = { id: 'c34' } variables = { id: 'c34' }
}) })

View File

@ -3,6 +3,9 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
export default { export default {
Mutation: { Mutation: {
CreateSocialMedia: async (object, params, context, resolveInfo) => { CreateSocialMedia: async (object, params, context, resolveInfo) => {
/**
* TODO?: Creates double Nodes!
*/
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session() const session = context.driver.session()
await session.run( await session.run(
@ -15,6 +18,11 @@ export default {
) )
session.close() session.close()
return socialMedia
},
DeleteSocialMedia: async (object, params, context, resolveInfo) => {
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
return socialMedia return socialMedia
} }
} }

View File

@ -7,9 +7,18 @@ const factory = Factory()
describe('CreateSocialMedia', () => { describe('CreateSocialMedia', () => {
let client let client
let headers let headers
const mutation = ` const mutationC = `
mutation($url: String!) { mutation($url: String!) {
CreateSocialMedia(url: $url) { CreateSocialMedia(url: $url) {
id
url
}
}
`
const mutationD = `
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url url
} }
} }
@ -30,20 +39,63 @@ describe('CreateSocialMedia', () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
const variables = { url: 'http://nsosp.org' }
await expect(
client.request(mutationC, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
it('creates social media with correct URL', async () => {
const variables = { url: 'http://nsosp.org' }
await expect(
client.request(mutationC, variables)
).resolves.toEqual(expect.objectContaining({
CreateSocialMedia: {
id: expect.any(String),
url: 'http://nsosp.org'
}
}))
})
it('deletes social media', async () => {
const creationVariables = { url: 'http://nsosp.org' }
const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
const { id } = CreateSocialMedia
const deletionVariables = { id }
const expected = {
DeleteSocialMedia: {
id: id,
url: 'http://nsosp.org'
}
}
await expect(
client.request(mutationD, deletionVariables)
).resolves.toEqual(expected)
})
it('rejects empty string', async () => { it('rejects empty string', async () => {
const variables = { url: '' } const variables = { url: '' }
await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') await expect(
client.request(mutationC, variables)
).rejects.toThrow('Input is not a URL')
}) })
it('validates URLs', async () => { it('validates URLs', async () => {
const variables = { url: 'not-a-url' } const variables = { url: 'not-a-url' }
await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') await expect(
client.request(mutationC, variables)
).rejects.toThrow('Input is not a URL')
}) })
}) })
}) })

View File

@ -189,33 +189,18 @@ import Factory from './factories'
]) ])
await Promise.all([ await Promise.all([
f.create('Comment', { id: 'c1', postId: 'p1' }), asUser.create('Comment', { id: 'c1', postId: 'p1' }),
f.create('Comment', { id: 'c2', postId: 'p1' }), asTick.create('Comment', { id: 'c2', postId: 'p1' }),
f.create('Comment', { id: 'c3', postId: 'p3' }), asTrack.create('Comment', { id: 'c3', postId: 'p3' }),
f.create('Comment', { id: 'c4', postId: 'p2' }), asTrick.create('Comment', { id: 'c4', postId: 'p2' }),
f.create('Comment', { id: 'c5', postId: 'p3' }), asModerator.create('Comment', { id: 'c5', postId: 'p3' }),
f.create('Comment', { id: 'c6', postId: 'p4' }), asAdmin.create('Comment', { id: 'c6', postId: 'p4' }),
f.create('Comment', { id: 'c7', postId: 'p2' }), asUser.create('Comment', { id: 'c7', postId: 'p2' }),
f.create('Comment', { id: 'c8', postId: 'p15' }), asTick.create('Comment', { id: 'c8', postId: 'p15' }),
f.create('Comment', { id: 'c9', postId: 'p15' }), asTrick.create('Comment', { id: 'c9', postId: 'p15' }),
f.create('Comment', { id: 'c10', postId: 'p15' }), asTrack.create('Comment', { id: 'c10', postId: 'p15' }),
f.create('Comment', { id: 'c11', postId: 'p15' }), asUser.create('Comment', { id: 'c11', postId: 'p15' }),
f.create('Comment', { id: 'c12', postId: 'p15' }) asUser.create('Comment', { id: 'c12', postId: 'p15' })
])
await Promise.all([
f.relate('Comment', 'Author', { from: 'u3', to: 'c1' }),
f.relate('Comment', 'Author', { from: 'u1', to: 'c2' }),
f.relate('Comment', 'Author', { from: 'u1', to: 'c3' }),
f.relate('Comment', 'Author', { from: 'u4', to: 'c4' }),
f.relate('Comment', 'Author', { from: 'u4', to: 'c5' }),
f.relate('Comment', 'Author', { from: 'u3', to: 'c6' }),
f.relate('Comment', 'Author', { from: 'u2', to: 'c7' }),
f.relate('Comment', 'Author', { from: 'u5', to: 'c8' }),
f.relate('Comment', 'Author', { from: 'u6', to: 'c9' }),
f.relate('Comment', 'Author', { from: 'u7', to: 'c10' }),
f.relate('Comment', 'Author', { from: 'u5', to: 'c11' }),
f.relate('Comment', 'Author', { from: 'u6', to: 'c12' })
]) ])
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }' const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -1,5 +1,7 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps' import { When, Then } from 'cypress-cucumber-preprocessor/steps'
const narratorAvatar = 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg'
Then('I click on the {string} button', text => { Then('I click on the {string} button', text => {
cy.get('button').contains(text).click() cy.get('button').contains(text).click()
}) })
@ -12,6 +14,9 @@ Then('my comment should be successfully created', () => {
Then('I should see my comment', () => { Then('I should see my comment', () => {
cy.get('div.comment p') cy.get('div.comment p')
.should('contain', 'Human Connection rocks') .should('contain', 'Human Connection rocks')
.get('.ds-avatar img')
.should('have.attr', 'src')
.and('contain', narratorAvatar)
}) })
Then('the editor should be cleared', () => { Then('the editor should be cleared', () => {

View File

@ -77,7 +77,7 @@ Then('I should be on the {string} page', page => {
.should('contain', 'Social media') .should('contain', 'Social media')
}) })
Then('I add a social media link', () => { When('I add a social media link', () => {
cy.get("input[name='social-media']") cy.get("input[name='social-media']")
.type('https://freeradical.zone/peter-pan') .type('https://freeradical.zone/peter-pan')
.get('button') .get('button')
@ -87,7 +87,7 @@ Then('I add a social media link', () => {
Then('it gets saved successfully', () => { Then('it gets saved successfully', () => {
cy.get('.iziToast-message') cy.get('.iziToast-message')
.should('contain', 'Updated user') .should('contain', 'Added social media')
}) })
Then('the new social media link shows up on the page', () => { Then('the new social media link shows up on the page', () => {
@ -110,3 +110,13 @@ Then('they should be able to see my social media links', () => {
.get('a[href="https://freeradical.zone/peter-pan"]') .get('a[href="https://freeradical.zone/peter-pan"]')
.should('have.length', 1) .should('have.length', 1)
}) })
When('I delete a social media link', () => {
cy.get("a[name='delete']")
.click()
})
Then('it gets deleted successfully', () => {
cy.get('.iziToast-message')
.should('contain', 'Deleted social media')
})

View File

@ -11,6 +11,7 @@ let loginCredentials = {
} }
const narratorParams = { const narratorParams = {
name: 'Peter Pan', name: 'Peter Pan',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg',
...loginCredentials ...loginCredentials
} }

View File

@ -19,3 +19,11 @@ Feature: List Social Media Accounts
Given I have added a social media link Given I have added a social media link
When people visit my profile page When people visit my profile page
Then they should be able to see my social media links Then they should be able to see my social media links
Scenario: Deleting Social Media
Given I am on the "settings" page
And I click on the "Social media" link
Then I should be on the "/settings/my-social-media" page
Given I have added a social media link
When I delete a social media link
Then it gets deleted successfully

View File

@ -14,8 +14,13 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import './commands'
import './factories' import './factories'
// intermittent failing tests
import 'cypress-plugin-retries'
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -e
mkdir -p ~/.ssh
echo $SSH_PRIVATE_KEY | base64 -d > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa

View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
tail -f /dev/null

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
for var in "SSH_USERNAME" "SSH_HOST" "MONGODB_USERNAME" "MONGODB_PASSWORD" "MONGODB_DATABASE" "MONGODB_AUTH_DB" "NEO4J_URI" for var in "SSH_USERNAME" "SSH_HOST" "MONGODB_USERNAME" "MONGODB_PASSWORD" "MONGODB_DATABASE" "MONGODB_AUTH_DB"
do do
if [[ -z "${!var}" ]]; then if [[ -z "${!var}" ]]; then
echo "${var} is undefined" echo "${var} is undefined"

View File

@ -9,5 +9,4 @@ do
fi fi
done done
[ -z "$SSH_PRIVATE_KEY" ] || create_private_ssh_key_from_env rsync --archive --update --verbose ${SSH_USERNAME}@${SSH_HOST}:${UPLOADS_DIRECTORY}/ /uploads/
rsync --archive --update --verbose ${SSH_USERNAME}@${SSH_HOST}:${UPLOADS_DIRECTORY}/* /uploads/

View File

@ -9,16 +9,17 @@ echo "MONGODB_DATABASE ${MONGODB_DATABASE}"
echo "MONGODB_AUTH_DB ${MONGODB_AUTH_DB}" echo "MONGODB_AUTH_DB ${MONGODB_AUTH_DB}"
echo "-------------------------------------------------" echo "-------------------------------------------------"
[ -z "$SSH_PRIVATE_KEY" ] || create_private_ssh_key_from_env
rm -rf /tmp/mongo-export/* rm -rf /tmp/mongo-export/*
mkdir -p /tmp/mongo-export mkdir -p /tmp/mongo-export/
ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST} ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST}
for collection in "categories" "badges" "users" "contributions" "comments" "follows" "shouts" for collection in "categories" "badges" "users" "contributions" "comments" "follows" "shouts"
do do
mongoexport --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --db ${MONGODB_DATABASE} --collection $collection --out "/tmp/mongo-export/$collection.json" mongoexport --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $collection --collection $collection --out "/tmp/mongo-export/$collection.json"
mkdir -p /tmp/mongo-export/splits/$collection/
split -l 1000 -a 3 /tmp/mongo-export/$collection.json /tmp/mongo-export/splits/$collection/
done done
ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST} ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST}

View File

@ -1,4 +1,4 @@
CALL apoc.load.json('file:/tmp/mongo-export/badges.json') YIELD value as badge CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as badge
MERGE(b:Badge {id: badge._id["$oid"]}) MERGE(b:Badge {id: badge._id["$oid"]})
ON CREATE SET ON CREATE SET
b.key = badge.key, b.key = badge.key,

View File

@ -1,4 +1,4 @@
CALL apoc.load.json('file:/tmp/mongo-export/categories.json') YIELD value as category CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as category
MERGE(c:Category {id: category._id["$oid"]}) MERGE(c:Category {id: category._id["$oid"]})
ON CREATE SET ON CREATE SET
c.name = category.title, c.name = category.title,

View File

@ -1,4 +1,5 @@
CALL apoc.load.json('file:/tmp/mongo-export/comments.json') YIELD value as json CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as json
MERGE (comment:Comment {id: json._id["$oid"]}) MERGE (comment:Comment {id: json._id["$oid"]})
ON CREATE SET ON CREATE SET
comment.content = json.content, comment.content = json.content,

View File

@ -1,4 +1,4 @@
CALL apoc.load.json('file:/tmp/mongo-export/contributions.json') YIELD value as post CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as post
MERGE (p:Post {id: post._id["$oid"]}) MERGE (p:Post {id: post._id["$oid"]})
ON CREATE SET ON CREATE SET
p.title = post.title, p.title = post.title,

View File

@ -1,4 +1,4 @@
CALL apoc.load.json('file:/tmp/mongo-export/follows.json') YIELD value as follow CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as follow
MATCH (u1:User {id: follow.userId}), (u2:User {id: follow.foreignId}) MATCH (u1:User {id: follow.userId}), (u2:User {id: follow.foreignId})
MERGE (u1)-[:FOLLOWS]->(u2) MERGE (u1)-[:FOLLOWS]->(u2)
; ;

View File

@ -1,9 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
SECONDS=0
SCRIPT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SCRIPT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
echo "MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r;" | cypher-shell -a $NEO4J_URI
echo "MATCH (n) DETACH DELETE n;" | cypher-shell
for collection in "badges" "categories" "users" "follows" "contributions" "shouts" "comments" for collection in "badges" "categories" "users" "follows" "contributions" "shouts" "comments"
do do
echo "Import ${collection}..." && cypher-shell -a $NEO4J_URI < $SCRIPT_DIRECTORY/$collection.cql for chunk in /tmp/mongo-export/splits/$collection/*
do
mv $chunk /tmp/mongo-export/splits/current-chunk.json
echo "Import ${chunk}" && cypher-shell < $SCRIPT_DIRECTORY/$collection.cql
done
done done
echo "Time elapsed: $SECONDS seconds"

View File

@ -1,4 +1,4 @@
CALL apoc.load.json('file:/tmp/mongo-export/shouts.json') YIELD value as shout CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as shout
MATCH (u:User {id: shout.userId}), (p:Post {id: shout.foreignId}) MATCH (u:User {id: shout.userId}), (p:Post {id: shout.foreignId})
MERGE (u)-[:SHOUTED]->(p) MERGE (u)-[:SHOUTED]->(p)
; ;

View File

@ -1,4 +1,4 @@
CALL apoc.load.json('file:/tmp/mongo-export/users.json') YIELD value as user CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as user
MERGE(u:User {id: user._id["$oid"]}) MERGE(u:User {id: user._id["$oid"]})
ON CREATE SET ON CREATE SET
u.name = user.name, u.name = user.name,

View File

@ -4,14 +4,17 @@ services:
maintenance: maintenance:
image: humanconnection/maintenance-worker:latest image: humanconnection/maintenance-worker:latest
build: build:
context: . context: deployment/legacy-migration/maintenance-worker
volumes: volumes:
- uploads:/uploads - uploads:/uploads
- neo4j-data:/data - neo4j-data:/data
- ./migration/:/migration - ./deployment/legacy-migration/maintenance-worker/migration/:/migration
- ./deployment/legacy-migration/maintenance-worker/ssh/:/root/.ssh
networks: networks:
- hc-network - hc-network
environment: environment:
- NEO4J_dbms_security_auth__enabled=false
- NEO4J_dbms_memory_heap_max__size=2G
- GRAPHQL_PORT=4000 - GRAPHQL_PORT=4000
- GRAPHQL_URI=http://localhost:4000 - GRAPHQL_URI=http://localhost:4000
- CLIENT_URI=http://localhost:3000 - CLIENT_URI=http://localhost:3000
@ -19,12 +22,9 @@ services:
- MOCK=false - MOCK=false
- MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78 - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
- NEO4J_URI=bolt://localhost:7687
- NEO4J_apoc_import_file_enabled=true - NEO4J_apoc_import_file_enabled=true
- NEO4J_AUTH=none
- "SSH_USERNAME=${SSH_USERNAME}" - "SSH_USERNAME=${SSH_USERNAME}"
- "SSH_HOST=${SSH_HOST}" - "SSH_HOST=${SSH_HOST}"
- "SSH_PRIVATE_KEY=${SSH_PRIVATE_KEY}"
- "MONGODB_USERNAME=${MONGODB_USERNAME}" - "MONGODB_USERNAME=${MONGODB_USERNAME}"
- "MONGODB_PASSWORD=${MONGODB_PASSWORD}" - "MONGODB_PASSWORD=${MONGODB_PASSWORD}"
- "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}" - "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}"
@ -34,9 +34,11 @@ services:
- 7687:7687 - 7687:7687
- 7474:7474 - 7474:7474
volumes:
uploads:
neo4j-data:
networks: networks:
hc-network: hc-network:
volumes:
webapp_node_modules:
backend_node_modules:
neo4j-data:
uploads:

View File

@ -18,6 +18,7 @@ services:
volumes: volumes:
- ./backend:/nitro-backend - ./backend:/nitro-backend
- backend_node_modules:/nitro-backend/node_modules - backend_node_modules:/nitro-backend/node_modules
- uploads:/nitro-backend/public/uploads
command: yarn run dev command: yarn run dev
neo4j: neo4j:
environment: environment:
@ -32,3 +33,4 @@ volumes:
webapp_node_modules: webapp_node_modules:
backend_node_modules: backend_node_modules:
neo4j-data: neo4j-data:
uploads:

View File

@ -11,6 +11,9 @@ services:
build: build:
context: webapp context: webapp
target: build-and-test target: build-and-test
volumes:
#/nitro-web
- ./webapp/coverage:/nitro-web/coverage
environment: environment:
- GRAPHQL_URI=http://backend:4000 - GRAPHQL_URI=http://backend:4000
backend: backend:
@ -18,6 +21,8 @@ services:
build: build:
context: backend context: backend
target: builder target: builder
volumes:
- ./backend/coverage:/nitro-backend/coverage
ports: ports:
- 4001:4001 - 4001:4001
- 4123:4123 - 4123:4123

View File

@ -1,3 +1,3 @@
FROM neo4j:3.5.4 FROM neo4j:3.5.5
RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/ RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/
COPY migrate.sh /usr/local/bin/migrate COPY migrate.sh /usr/local/bin/migrate

View File

@ -4,15 +4,30 @@
# the initial default user. Before we can create constraints, we have to change # the initial default user. Before we can create constraints, we have to change
# the default password. This is a security feature of neo4j. # the default password. This is a security feature of neo4j.
if echo ":exit" | cypher-shell --password neo4j 2> /dev/null ; then if echo ":exit" | cypher-shell --password neo4j 2> /dev/null ; then
echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --password neo4j if [[ -z "${NEO4J_PASSWORD}" ]]; then
echo "NEO4J_PASSWORD environment variable is undefined. I cannot set the initial password."
else
echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --password neo4j
fi
fi fi
set -e set -e
echo ' echo '
CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]); CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);
CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE;
CREATE CONSTRAINT ON (o:Organization) ASSERT o.id IS UNIQUE;
CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE;
CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE; CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE;
' | cypher-shell ' | cypher-shell
echo "Successfully created all indices and unique constraints:"
echo 'CALL db.indexes();' | cypher-shell

View File

@ -15,16 +15,19 @@
"cypress:webapp": "cd webapp && cross-env GRAPHQL_URI=http://localhost:4123 yarn run dev", "cypress:webapp": "cd webapp && cross-env GRAPHQL_URI=http://localhost:4123 yarn run dev",
"cypress:setup": "run-p cypress:backend:* cypress:webapp", "cypress:setup": "run-p cypress:backend:* cypress:webapp",
"cypress:run": "cypress run --browser chromium", "cypress:run": "cypress run --browser chromium",
"cypress:open": "cypress open --browser chromium" "cypress:open": "cypress open --browser chromium",
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov"
}, },
"devDependencies": { "devDependencies": {
"codecov": "^3.3.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"cypress": "^3.2.0", "cypress": "^3.2.0",
"cypress-cucumber-preprocessor": "^1.11.0", "cypress-cucumber-preprocessor": "^1.11.0",
"dotenv": "^7.0.0", "cypress-plugin-retries": "^1.2.0",
"dotenv": "^8.0.0",
"faker": "^4.1.0", "faker": "^4.1.0",
"graphql-request": "^1.8.2", "graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.3", "neo4j-driver": "^1.7.4",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
} }
} }

View File

@ -1,4 +1,7 @@
{ {
"plugins": [
"@babel/plugin-syntax-dynamic-import"
],
"presets": [ "presets": [
[ [
"@babel/preset-env", "@babel/preset-env",
@ -21,4 +24,4 @@
] ]
} }
} }
} }

View File

@ -20,6 +20,7 @@
<ds-button <ds-button
:disabled="disabled" :disabled="disabled"
ghost ghost
class="cancelBtn"
@click.prevent="clear" @click.prevent="clear"
> >
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
@ -28,6 +29,7 @@
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '40%', xs: '40%' }"> <ds-flex-item :width="{ base: '40%', md: '20%', sm: '40%', xs: '40%' }">
<ds-button <ds-button
type="submit" type="submit"
:loading="loading"
:disabled="disabled || errors" :disabled="disabled || errors"
primary primary
> >
@ -55,6 +57,7 @@ export default {
data() { data() {
return { return {
disabled: true, disabled: true,
loading: false,
form: { form: {
content: '' content: ''
}, },
@ -75,6 +78,8 @@ export default {
this.$refs.editor.clear() this.$refs.editor.clear()
}, },
handleSubmit() { handleSubmit() {
this.loading = true
this.disabled = true
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: gql` mutation: gql`
@ -91,9 +96,11 @@ export default {
} }
}) })
.then(res => { .then(res => {
this.$emit('addComment', res.data.CreateComment) this.loading = false
this.$root.$emit('refetchPostComments', res.data.CreateComment)
this.$refs.editor.clear() this.$refs.editor.clear()
this.$toast.success(this.$t('post.comment.submitted')) this.$toast.success(this.$t('post.comment.submitted'))
this.disabled = false
}) })
.catch(err => { .catch(err => {
this.$toast.error(err.message) this.$toast.error(err.message)

View File

@ -0,0 +1,69 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import CommentList from '.'
import Empty from '~/components/Empty'
import Vue from 'vue'
import Vuex from 'vuex'
import Filters from '~/plugins/vue-filters'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(Vuex)
localVue.filter('truncate', string => string)
config.stubs['v-popover'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
config.stubs['no-ssr'] = '<span><slot /></span>'
describe('CommentList.vue', () => {
let mocks
let store
let wrapper
let propsData
let data
propsData = {
post: { id: 1 }
}
store = new Vuex.Store({
getters: {
'auth/user': () => {
return {}
}
}
})
mocks = {
$t: jest.fn()
}
data = () => {
return {
comments: []
}
}
describe('shallowMount', () => {
const Wrapper = () => {
return mount(CommentList, { store, mocks, localVue, propsData, data })
}
beforeEach(() => {
wrapper = Wrapper()
wrapper.setData({
comments: [{ id: 'c1', contentExcerpt: 'this is a comment' }]
})
})
it('displays a message icon when there are no comments to display', () => {
expect(Wrapper().findAll(Empty)).toHaveLength(1)
})
it('displays a comments counter', () => {
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
it('displays comments when there are comments to display', () => {
expect(wrapper.find('div#comments').text()).toEqual('this is a comment')
})
})
})

View File

@ -0,0 +1,80 @@
<template>
<div>
<h3 style="margin-top: -10px;">
<span>
<ds-icon name="comments" />
<ds-tag
v-if="comments"
style="margin-top: -4px; margin-left: -12px; position: absolute;"
color="primary"
size="small"
round
>{{ comments.length }}</ds-tag>&nbsp; Comments
</span>
</h3>
<ds-space margin-bottom="large" />
<div
v-if="comments && comments.length"
id="comments"
class="comments"
>
<comment
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
<hc-empty
v-else
name="empty"
icon="messages"
/>
</div>
</template>
<script>
import Comment from '~/components/Comment.vue'
import HcEmpty from '~/components/Empty.vue'
export default {
components: {
Comment,
HcEmpty
},
props: {
post: { type: Object, default: () => {} }
},
data() {
return {
comments: []
}
},
watch: {
Post(post) {
this.comments = post[0].comments || []
}
},
mounted() {
this.$root.$on('refetchPostComments', comment => {
this.refetchPostComments(comment)
})
},
methods: {
refetchPostComments(comment) {
this.$apollo.queries.Post.refetch()
}
},
apollo: {
Post: {
query() {
return require('~/graphql/PostCommentsQuery.js').default(this)
},
variables() {
return {
slug: this.post.slug
}
},
fetchPolicy: 'cache-and-network'
}
}
}
</script>

View File

@ -1,16 +0,0 @@
version: '3.7'
services:
webapp:
build:
context: .
target: build-and-test
volumes:
- .:/nitro-web
- node_modules:/nitro-web/node_modules
- nuxt:/nitro-web/.nuxt
command: yarn run dev
volumes:
node_modules:
nuxt:

View File

@ -1,10 +0,0 @@
version: "3.7"
services:
webapp:
build:
context: .
target: build-and-test
environment:
- GRAPHQL_URI=http://backend:4123
- NODE_ENV=test

View File

@ -1,23 +0,0 @@
version: '3.7'
services:
webapp:
image: humanconnection/nitro-web:latest
build:
context: .
target: production
ports:
- 3000:3000
networks:
- hc-network
environment:
- HOST=0.0.0.0
- GRAPHQL_URI=http://backend:4000
- MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ"
networks:
hc-network:
name: hc-network
volumes:
node_modules:

View File

@ -1,12 +1,34 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export default app => { export default app => {
const lang = app.$i18n.locale().toUpperCase()
return gql(` return gql(`
query CommentByPost($postId: ID!) { query Comment($postId: ID) {
CommentByPost(postId: $postId) { Comment(postId: $postId) {
id id
contentExcerpt contentExcerpt
createdAt createdAt
author {
id
slug
name
avatar
disabled
deleted
shoutedCount
contributionsCount
commentsCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
key
icon
}
}
} }
} }
`) `)

View File

@ -0,0 +1,39 @@
import gql from 'graphql-tag'
export default app => {
const lang = app.$i18n.locale().toUpperCase()
return gql(`
query Post($slug: String!) {
Post(slug: $slug) {
comments(orderBy: createdAt_asc) {
id
contentExcerpt
createdAt
disabled
deleted
author {
id
slug
name
avatar
disabled
deleted
shoutedCount
contributionsCount
commentsCount
followedByCount
followedByCurrentUser
location {
name: name${lang}
}
badges {
id
key
icon
}
}
}
}
}
`)
}

View File

@ -6,6 +6,8 @@
"email": "Deine E-Mail", "email": "Deine E-Mail",
"password": "Dein Passwort", "password": "Dein Passwort",
"moreInfo": "Was ist Human Connection?", "moreInfo": "Was ist Human Connection?",
"moreInfoURL": "https://human-connection.org",
"moreInfoHint": "zur Präsentationsseite",
"hello": "Hallo" "hello": "Hallo"
}, },
"editor": { "editor": {
@ -63,8 +65,10 @@
}, },
"social-media": { "social-media": {
"name": "Soziale Medien", "name": "Soziale Medien",
"placeholder": "Füge eine Social-Media URL hinzu",
"submit": "Link hinzufügen", "submit": "Link hinzufügen",
"success": "Profil aktualisiert" "successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
} }
}, },
"admin": { "admin": {

View File

@ -6,6 +6,8 @@
"email": "Your Email", "email": "Your Email",
"password": "Your Password", "password": "Your Password",
"moreInfo": "What is Human Connection?", "moreInfo": "What is Human Connection?",
"moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "to the presentation page",
"hello": "Hello" "hello": "Hello"
}, },
"editor": { "editor": {
@ -63,8 +65,10 @@
}, },
"social-media": { "social-media": {
"name": "Social media", "name": "Social media",
"placeholder": "Add social media url",
"submit": "Add link", "submit": "Add link",
"success": "Updated user profile" "successAdd": "Added social media. Updated user profile!",
"successDelete": "Deleted social media. Updated user profile!"
} }
}, },
"admin": { "admin": {

View File

@ -17,19 +17,33 @@
}, },
"jest": { "jest": {
"verbose": true, "verbose": true,
"moduleFileExtensions": [ "collectCoverage": true,
"js", "collectCoverageFrom": [
"json", "**/*.{js,vue}",
"vue" "!**/node_modules/**",
"!**/.nuxt/**",
"!**/?(*.)+(spec|test).js?(x)"
],
"coverageReporters": [
"text",
"lcov"
], ],
"transform": { "transform": {
".*\\.(vue)$": "vue-jest", ".*\\.(vue)$": "vue-jest",
"^.+\\.js$": "<rootDir>/node_modules/babel-jest" "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
}, },
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"moduleNameMapper": { "moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1", "^@/(.*)$": "<rootDir>/src/$1",
"^~/(.*)$": "<rootDir>/$1" "^~/(.*)$": "<rootDir>/$1"
} },
"testMatch": [
"**/?(*.)+(spec|test).js?(x)"
]
}, },
"dependencies": { "dependencies": {
"@human-connection/styleguide": "0.5.15", "@human-connection/styleguide": "0.5.15",
@ -44,15 +58,15 @@
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27", "date-fns": "2.0.0-alpha.27",
"express": "~4.16.4", "express": "~4.16.4",
"graphql": "~14.2.1", "graphql": "~14.3.0",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"linkify-it": "~2.1.0", "linkify-it": "~2.1.0",
"nuxt": "~2.6.3", "nuxt": "~2.6.3",
"nuxt-env": "~0.1.0", "nuxt-env": "~0.1.0",
"stack-utils": "^1.0.2", "stack-utils": "^1.0.2",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"tiptap": "^1.17.0", "tiptap": "^1.18.0",
"tiptap-extensions": "^1.17.0", "tiptap-extensions": "^1.18.1",
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2", "vue-izitoast": "1.1.2",
@ -61,6 +75,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "~7.4.4", "@babel/core": "~7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "~7.4.4", "@babel/preset-env": "~7.4.4",
"@vue/cli-shared-utils": "~3.7.0", "@vue/cli-shared-utils": "~3.7.0",
"@vue/eslint-config-prettier": "~4.0.1", "@vue/eslint-config-prettier": "~4.0.1",
@ -68,20 +83,20 @@
"@vue/test-utils": "~1.0.0-beta.29", "@vue/test-utils": "~1.0.0-beta.29",
"babel-core": "~7.0.0-bridge.0", "babel-core": "~7.0.0-bridge.0",
"babel-eslint": "~10.0.1", "babel-eslint": "~10.0.1",
"babel-jest": "~24.7.1", "babel-jest": "~24.8.0",
"eslint": "~5.16.0", "eslint": "~5.16.0",
"eslint-config-prettier": "~4.2.0", "eslint-config-prettier": "~4.2.0",
"eslint-loader": "~2.1.2", "eslint-loader": "~2.1.2",
"eslint-plugin-prettier": "~3.0.1", "eslint-plugin-prettier": "~3.0.1",
"eslint-plugin-vue": "~5.2.2", "eslint-plugin-vue": "~5.2.2",
"fuse.js": "^3.4.4", "fuse.js": "^3.4.4",
"jest": "~24.7.1", "jest": "~24.8.0",
"node-sass": "~4.11.0", "node-sass": "~4.12.0",
"nodemon": "~1.18.11", "nodemon": "~1.19.0",
"prettier": "~1.14.3", "prettier": "~1.14.3",
"sass-loader": "~7.1.0", "sass-loader": "~7.1.0",
"tippy.js": "^4.3.0", "tippy.js": "^4.3.0",
"vue-jest": "~3.0.4", "vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0" "vue-svg-loader": "~0.12.0"
} }
} }

View File

@ -79,8 +79,8 @@
</ds-button> </ds-button>
<ds-space margin="x-small"> <ds-space margin="x-small">
<a <a
href="https://human-connection.org" :href="$t('login.moreInfoURL')"
title="zur Präsentationsseite" :title="$t('login.moreInfoHint')"
target="_blank" target="_blank"
> >
{{ $t('login.moreInfo') }} {{ $t('login.moreInfo') }}

View File

@ -96,39 +96,9 @@
<ds-space margin="small" /> <ds-space margin="small" />
<!-- Comments --> <!-- Comments -->
<ds-section slot="footer"> <ds-section slot="footer">
<h3 style="margin-top: -10px;"> <hc-comment-list :post="post" />
<span>
<ds-icon name="comments" />
<ds-tag
v-if="comments"
style="margin-top: -4px; margin-left: -12px; position: absolute;"
color="primary"
size="small"
round
>{{ comments.length }}</ds-tag>&nbsp; Comments
</span>
</h3>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
<div <hc-comment-form :post="post" />
v-if="comments && comments.length"
id="comments"
class="comments"
>
<comment
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
<hc-empty
v-else
icon="messages"
/>
<ds-space margin-bottom="large" />
<hc-comment-form
:post="post"
@addComment="addComment"
/>
</ds-section> </ds-section>
</ds-card> </ds-card>
</transition> </transition>
@ -142,9 +112,8 @@ import HcTag from '~/components/Tag'
import ContentMenu from '~/components/ContentMenu' import ContentMenu from '~/components/ContentMenu'
import HcUser from '~/components/User' import HcUser from '~/components/User'
import HcShoutButton from '~/components/ShoutButton.vue' import HcShoutButton from '~/components/ShoutButton.vue'
import HcEmpty from '~/components/Empty.vue' import HcCommentForm from '~/components/comments/CommentForm'
import HcCommentForm from '~/components/CommentForm' import HcCommentList from '~/components/comments/CommentList'
import Comment from '~/components/Comment.vue'
export default { export default {
transition: { transition: {
@ -156,10 +125,9 @@ export default {
HcCategory, HcCategory,
HcUser, HcUser,
HcShoutButton, HcShoutButton,
HcEmpty,
Comment,
ContentMenu, ContentMenu,
HcCommentForm HcCommentForm,
HcCommentList
}, },
head() { head() {
return { return {
@ -169,7 +137,6 @@ export default {
data() { data() {
return { return {
post: null, post: null,
comments: null,
ready: false, ready: false,
title: 'loading' title: 'loading'
} }
@ -178,9 +145,6 @@ export default {
Post(post) { Post(post) {
this.post = post[0] || {} this.post = post[0] || {}
this.title = this.post.title this.title = this.post.title
},
CommentByPost(comments) {
this.comments = comments || []
} }
}, },
async asyncData(context) { async asyncData(context) {
@ -289,22 +253,6 @@ export default {
methods: { methods: {
isAuthor(id) { isAuthor(id) {
return this.$store.getters['auth/user'].id === id return this.$store.getters['auth/user'].id === id
},
addComment(comment) {
this.$apollo.queries.CommentByPost.refetch()
}
},
apollo: {
CommentByPost: {
query() {
return require('~/graphql/CommentQuery.js').default(this)
},
variables() {
return {
postId: this.post.id
}
},
fetchPolicy: 'cache-and-network'
} }
} }
} }

View File

@ -71,6 +71,40 @@ describe('my-social-media.vue', () => {
const socialMediaLink = wrapper.find('a').attributes().href const socialMediaLink = wrapper.find('a').attributes().href
expect(socialMediaLink).toBe(socialMediaUrl) expect(socialMediaLink).toBe(socialMediaUrl)
}) })
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockRejectedValue({ message: 'Ouch!' })
.mockResolvedValueOnce({
data: { DeleteSocialMeda: { id: 's1', url: socialMediaUrl } }
})
},
$toast: {
error: jest.fn(),
success: jest.fn()
}
}
getters = {
'auth/user': () => {
return {
socialMedia: [{ id: 's1', url: socialMediaUrl }]
}
}
}
})
it('displays a trash sympol after a social media and allows the user to delete it', () => {
wrapper = Wrapper()
const deleteSelector = wrapper.find({ name: 'delete' })
expect(deleteSelector).toEqual({ selector: 'Component' })
const icon = wrapper.find({ name: 'trash' })
icon.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
}) })
describe('currentUser does not have a social media account linked', () => { describe('currentUser does not have a social media account linked', () => {

View File

@ -8,9 +8,12 @@
<ds-list> <ds-list>
<ds-list-item <ds-list-item
v-for="link in socialMediaLinks" v-for="link in socialMediaLinks"
:key="link.url" :key="link.id"
> >
<a :href="link.url"> <a
:href="link.url"
target="_blank"
>
<img <img
:src="link.favicon" :src="link.favicon"
alt="Social Media link" alt="Social Media link"
@ -19,26 +22,39 @@
> >
{{ link.url }} {{ link.url }}
</a> </a>
&nbsp;&nbsp; <span class="layout-leave-active">|</span> &nbsp;&nbsp;
<ds-icon
name="edit"
class="layout-leave-active"
/>
<a
name="delete"
@click="handleDeleteSocialMedia(link)"
>
<ds-icon name="trash" />
</a>
</ds-list-item> </ds-list-item>
</ds-list> </ds-list>
</ds-space> </ds-space>
<div>
<ds-input
v-model="value"
placeholder="Add social media url"
name="social-media"
:schema="{type: 'url'}"
/>
</div>
<ds-space margin-top="base"> <ds-space margin-top="base">
<div> <div>
<ds-button <ds-input
primary v-model="value"
@click="handleAddSocialMedia" :placeholder="$t('settings.social-media.placeholder')"
> name="social-media"
{{ $t('settings.social-media.submit') }} :schema="{type: 'url'}"
</ds-button> />
</div> </div>
<ds-space margin-top="base">
<div>
<ds-button
primary
@click="handleAddSocialMedia"
>
{{ $t('settings.social-media.submit') }}
</ds-button>
</div>
</ds-space>
</ds-space> </ds-space>
</ds-card> </ds-card>
</template> </template>
@ -59,13 +75,13 @@ export default {
socialMediaLinks() { socialMediaLinks() {
const { socialMedia = [] } = this.currentUser const { socialMedia = [] } = this.currentUser
return socialMedia.map(socialMedia => { return socialMedia.map(socialMedia => {
const { url } = socialMedia const { id, url } = socialMedia
const matches = url.match( const matches = url.match(
/^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:\/\n?]+)/g /^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:\/\n?]+)/g
) )
const [domain] = matches || [] const [domain] = matches || []
const favicon = domain ? `${domain}/favicon.ico` : null const favicon = domain ? `${domain}/favicon.ico` : null
return { url, favicon } return { id, url, favicon }
}) })
} }
}, },
@ -79,6 +95,7 @@ export default {
mutation: gql` mutation: gql`
mutation($url: String!) { mutation($url: String!) {
CreateSocialMedia(url: $url) { CreateSocialMedia(url: $url) {
id
url url
} }
} }
@ -97,11 +114,51 @@ export default {
}) })
} }
}) })
.then( .then(() => {
this.$toast.success(this.$t('settings.social-media.success')), this.$toast.success(this.$t('settings.social-media.successAdd')),
(this.value = '') (this.value = '')
) })
.catch(error => {
this.$toast.error(error.message)
})
},
handleDeleteSocialMedia(link) {
this.$apollo
.mutate({
mutation: gql`
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url
}
}
`,
variables: {
id: link.id
},
update: (store, { data }) => {
const socialMedia = this.currentUser.socialMedia.filter(
element => element.id !== link.id
)
this.setCurrentUser({
...this.currentUser,
socialMedia
})
}
})
.then(() => {
this.$toast.success(this.$t('settings.social-media.successDelete'))
})
.catch(error => {
this.$toast.error(error.message)
})
} }
} }
} }
</script> </script>
<style lang="scss">
.layout-leave-active {
opacity: 0.4;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -762,6 +762,13 @@ acorn@^6.0.2:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
agent-base@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
dependencies:
es6-promisify "^5.0.0"
ajv@^6.5.5: ajv@^6.5.5:
version "6.10.0" version "6.10.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
@ -840,6 +847,11 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
argv@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab"
integrity sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=
arr-diff@^2.0.0: arr-diff@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
@ -1491,6 +1503,17 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codecov@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.3.0.tgz#7bf337b3f7b0474606b5c31c56dd9e44e395e15d"
integrity sha512-S70c3Eg9SixumOvxaKE/yKUxb9ihu/uebD9iPO2IR73IdP4i6ZzjXEULj3d0HeyWPr0DqBfDkjNBWxURjVO5hw==
dependencies:
argv "^0.0.2"
ignore-walk "^3.0.1"
js-yaml "^3.12.0"
teeny-request "^3.7.0"
urlgrey "^0.4.4"
coffee-react-transform@^3.1.0: coffee-react-transform@^3.1.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/coffee-react-transform/-/coffee-react-transform-3.3.0.tgz#f1f90fa22de8d767fca2793e3b70f0f7d7a2e467" resolved "https://registry.yarnpkg.com/coffee-react-transform/-/coffee-react-transform-3.3.0.tgz#f1f90fa22de8d767fca2793e3b70f0f7d7a2e467"
@ -1787,6 +1810,11 @@ cypress-cucumber-preprocessor@^1.11.0:
glob "^7.1.2" glob "^7.1.2"
through "^2.3.8" through "^2.3.8"
cypress-plugin-retries@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.2.0.tgz#a4e120c1bc417d1be525632e7d38e52a87bc0578"
integrity sha512-seQFI/0j5WCqX7IVN2k0tbd3FLdhbPuSCWdDtdzDmU9oJfUkRUlluV47TYD+qQ/l+fJYkQkpw8csLg8/LohfRg==
cypress@^3.2.0: cypress@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.2.0.tgz#c2d5befd5077dab6fb52ad70721e0868ac057001" resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.2.0.tgz#c2d5befd5077dab6fb52ad70721e0868ac057001"
@ -1999,10 +2027,10 @@ domain-browser@^1.2.0:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
dotenv@^7.0.0: dotenv@^8.0.0:
version "7.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
version "0.1.4" version "0.1.4"
@ -2103,6 +2131,18 @@ es6-iterator@~2.0.3:
es5-ext "^0.10.35" es5-ext "^0.10.35"
es6-symbol "^3.1.1" es6-symbol "^3.1.1"
es6-promise@^4.0.3:
version "4.2.6"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
dependencies:
es6-promise "^4.0.3"
es6-symbol@^3.1.1, es6-symbol@~3.1.1: es6-symbol@^3.1.1, es6-symbol@~3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
@ -2632,6 +2672,14 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
dependencies:
agent-base "^4.1.0"
debug "^3.1.0"
iconv-lite@^0.4.4: iconv-lite@^0.4.4:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -3007,6 +3055,14 @@ js-levenshtein@^1.1.3:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.12.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.9.0: js-yaml@^3.9.0:
version "3.13.0" version "3.13.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e"
@ -3473,10 +3529,10 @@ needle@^2.2.1:
iconv-lite "^0.4.4" iconv-lite "^0.4.4"
sax "^1.2.4" sax "^1.2.4"
neo4j-driver@^1.7.3: neo4j-driver@^1.7.4:
version "1.7.3" version "1.7.4"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.3.tgz#1c1108ab26b7243975f1b20045daf31d8f685207" resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.4.tgz#9661cf643b63818bff85e82c4691918e75098c1e"
integrity sha512-UCNOFiQdouq14PvZGTr+psy657BJsBpO6O2cJpP+NprZnEF4APrDzAcydPZSFxE1nfooLNc50vfuZ0q54UyY2Q== integrity sha512-pbK1HbXh92zNSwMlXL8aNynkHohg9Jx/Tk+EewdJawGm8n8sKIY4NpRkp0nRw6RHvVBU3u9cQXt01ftFVe7j+A==
dependencies: dependencies:
babel-runtime "^6.26.0" babel-runtime "^6.26.0"
text-encoding "^0.6.4" text-encoding "^0.6.4"
@ -3509,6 +3565,11 @@ node-fetch@2.1.2:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
node-fetch@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==
node-pre-gyp@^0.10.0: node-pre-gyp@^0.10.0:
version "0.10.3" version "0.10.3"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
@ -4621,6 +4682,15 @@ tar@^4:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
yallist "^3.0.2" yallist "^3.0.2"
teeny-request@^3.7.0:
version "3.11.3"
resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-3.11.3.tgz#335c629f7645e5d6599362df2f3230c4cbc23a55"
integrity sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==
dependencies:
https-proxy-agent "^2.2.1"
node-fetch "^2.2.0"
uuid "^3.3.2"
text-encoding@^0.6.4: text-encoding@^0.6.4:
version "0.6.4" version "0.6.4"
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
@ -4847,6 +4917,11 @@ url@0.11.0, url@~0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
urlgrey@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f"
integrity sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=
use@^3.1.0: use@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"