Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 552-update_comment

This commit is contained in:
ALau2088 2019-06-27 10:18:53 -07:00
commit 5e9f46405e
158 changed files with 3881 additions and 1357 deletions

View File

@ -29,7 +29,7 @@ script:
- docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
- docker-compose exec backend yarn run db:reset
- docker-compose exec backend yarn run db:seed
- docker-compose exec backend yarn run test:cucumber
- 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
@ -37,9 +37,7 @@ script:
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
- docker-compose exec -d backend yarn run test:before:seeder
# Fullstack
# Disable recording cypress tests if we just update dependencies. This is to
# avoid running out of quota.
- if [[ $BRANCH == *"dependabot"* ]]; then yarn run cypress:run; else yarn run cypress:run --record; fi
- yarn run cypress:run
# Coverage
- codecov

View File

@ -9,4 +9,4 @@
],
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true
}
}

View File

@ -27,6 +27,7 @@
* [Kubernetes Dashboard](deployment/digital-ocean/dashboard/README.md)
* [HTTPS](deployment/digital-ocean/https/README.md)
* [Human Connection](deployment/human-connection/README.md)
* [Mailserver](deployment/human-connection/mailserver/README.md)
* [Volumes](deployment/volumes/README.md)
* [Neo4J Offline-Backups](deployment/volumes/neo4j-offline-backup/README.md)
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)

View File

@ -5,6 +5,11 @@ GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
MOCKS=false
SMTP_HOST=
SMTP_PORT=
SMTP_IGNORE_TLS=true
SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"

View File

@ -44,6 +44,9 @@ or start the backend in production environment with:
yarn run start
```
For e-mail delivery, please configure at least `SMTP_HOST` and `SMTP_PORT` in
your `.env` configuration file.
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
This will start the GraphQL service \(by default on localhost:4000\) where you
can issue GraphQL requests or access GraphQL Playground in the browser.

View File

@ -44,34 +44,35 @@
"dependencies": {
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.2",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14",
"apollo-server": "~2.6.2",
"apollo-client": "~2.6.3",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.6.6",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.31",
"date-fns": "2.0.0-alpha.37",
"debug": "~4.1.1",
"dotenv": "~8.0.0",
"express": "~4.17.1",
"faker": "~4.1.0",
"faker": "Marak/faker.js#master",
"graphql": "~14.3.1",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.8",
"graphql-shield": "~5.7.1",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"graphql-yoga": "~1.18.0",
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.11",
"merge-graphql-schemas": "^1.5.8",
"neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
"neo4j-graphql-js": "^2.6.3",
"node-fetch": "~2.6.0",
"nodemailer": "^6.2.1",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.1",
@ -87,20 +88,20 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.6.2",
"apollo-server-testing": "~2.6.6",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-eslint": "~10.0.2",
"babel-jest": "~24.8.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~5.16.0",
"eslint-config-prettier": "~4.3.0",
"eslint": "~6.0.1",
"eslint-config-prettier": "~5.0.0",
"eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.17.3",
"eslint-plugin-jest": "~22.6.4",
"eslint-plugin-import": "~2.18.0",
"eslint-plugin-jest": "~22.7.1",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2",
"jest": "~24.8.0",

View File

@ -2,23 +2,33 @@ import dotenv from 'dotenv'
dotenv.config()
export const requiredConfigs = {
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
JWT_SECRET: process.env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
}
const {
MAPBOX_TOKEN,
JWT_SECRET,
PRIVATE_KEY_PASSPHRASE,
SMTP_IGNORE_TLS = true,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
NEO4J_URI = 'bolt://localhost:7687',
NEO4J_USERNAME = 'neo4j',
NEO4J_PASSWORD = 'neo4j',
GRAPHQL_PORT = 4000,
CLIENT_URI = 'http://localhost:3000',
GRAPHQL_URI = 'http://localhost:4000',
} = process.env
export const neo4jConfigs = {
NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
}
export const serverConfigs = {
GRAPHQL_PORT: process.env.GRAPHQL_PORT || 4000,
CLIENT_URI: process.env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
export const requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
export const smtpConfigs = {
SMTP_HOST,
SMTP_PORT,
SMTP_IGNORE_TLS,
SMTP_USERNAME,
SMTP_PASSWORD,
}
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
@ -29,6 +39,7 @@ export const developmentConfigs = {
export default {
...requiredConfigs,
...smtpConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,

View File

@ -1,51 +0,0 @@
const legacyUrls = [
'https://api-alpha.human-connection.org',
'https://staging-api.human-connection.org',
'http://localhost:3000',
]
export const fixUrl = url => {
legacyUrls.forEach(legacyUrl => {
url = url.replace(legacyUrl, '')
})
if (!url.startsWith('/')) {
url = `/${url}`
}
return url
}
const checkUrl = thing => {
return (
thing &&
typeof thing === 'string' &&
legacyUrls.find(legacyUrl => {
return thing.indexOf(legacyUrl) === 0
})
)
}
export const fixImageURLs = (result, recursive) => {
if (checkUrl(result)) {
result = fixUrl(result)
} else if (result && Array.isArray(result)) {
result.forEach((res, index) => {
result[index] = fixImageURLs(result[index], true)
})
} else if (result && typeof result === 'object') {
Object.keys(result).forEach(key => {
result[key] = fixImageURLs(result[key], true)
})
}
return result
}
export default {
Mutation: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
return fixImageURLs(result)
},
Query: async (resolve, root, args, context, info) => {
let result = await resolve(root, args, context, info)
return fixImageURLs(result)
},
}

View File

@ -1,43 +0,0 @@
import { fixImageURLs } from './fixImageUrlsMiddleware'
describe('fixImageURLs', () => {
describe('edge case: image url is exact match of legacy url', () => {
it('replaces it with `/`', () => {
const url = 'https://api-alpha.human-connection.org'
expect(fixImageURLs(url)).toEqual('/')
})
})
describe('image url of legacy alpha', () => {
it('removes domain', () => {
const url =
'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png'
expect(fixImageURLs(url)).toEqual(
'/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png',
)
})
})
describe('image url of legacy staging', () => {
it('removes domain', () => {
const url =
'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg'
expect(fixImageURLs(url)).toEqual(
'/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg',
)
})
})
describe('object', () => {
it('returns untouched', () => {
const object = { some: 'thing' }
expect(fixImageURLs(object)).toEqual(object)
})
})
describe('some string', () => {
it('returns untouched', () => {})
const string = "Yeah I'm a String"
expect(fixImageURLs(string)).toEqual(string)
})
})

View File

@ -3,7 +3,6 @@ import activityPub from './activityPubMiddleware'
import password from './passwordMiddleware'
import softDelete from './softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import fixImageUrls from './fixImageUrlsMiddleware'
import excerpt from './excerptMiddleware'
import dateTime from './dateTimeMiddleware'
import xss from './xssMiddleware'
@ -25,7 +24,6 @@ export default schema => {
excerpt: excerpt,
notifications: notifications,
xss: xss,
fixImageUrls: fixImageUrls,
softDelete: softDelete,
user: user,
includedFields: includedFields,
@ -42,7 +40,6 @@ export default schema => {
'excerpt',
'notifications',
'xss',
'fixImageUrls',
'softDelete',
'user',
'includedFields',

View File

@ -1,4 +1,4 @@
import { rule, shield, allow, or } from 'graphql-shield'
import { rule, shield, deny, allow, or } from 'graphql-shield'
/*
* TODO: implement
@ -16,6 +16,12 @@ const isAdmin = rule()(async (parent, args, { user }, info) => {
return user && user.role === 'admin'
})
const onlyYourself = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
return context.user.id === args.id
})
const isMyOwn = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
@ -48,6 +54,13 @@ const belongsToMe = rule({
return Boolean(notification)
})
/* TODO: decide if we want to remove this check: the check
* `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter
* parameters. Soft-delete middleware obfuscates data on its way out
* anyways. Furthermore, `neo4j-graphql-js` offers many ways to filter for
* data so I believe, this is not a good check anyways.
*/
const onlyEnabledContent = rule({
cache: 'strict',
})(async (parent, args, ctx, info) => {
@ -80,48 +93,73 @@ const isAuthor = rule({
return authorId === user.id
})
// Permissions
const permissions = shield({
Query: {
Notification: isAdmin,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator),
},
Mutation: {
UpdateNotification: belongsToMe,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,
DeletePost: isAuthor,
report: isAuthenticated,
CreateBadge: isAdmin,
UpdateBadge: isAdmin,
DeleteBadge: isAdmin,
AddUserBadges: isAdmin,
CreateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin,
reward: isAdmin,
unreward: isAdmin,
// addFruitToBasket: isAuthenticated
follow: isAuthenticated,
unfollow: isAuthenticated,
shout: isAuthenticated,
unshout: isAuthenticated,
changePassword: isAuthenticated,
enable: isModerator,
disable: isModerator,
CreateComment: isAuthenticated,
UpdateComment: isAuthor,
DeleteComment: isAuthor,
// CreateUser: allow,
},
User: {
email: isMyOwn,
password: isMyOwn,
privateKey: isMyOwn,
},
const isDeletingOwnAccount = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
return context.user.id === args.id
})
// Permissions
const permissions = shield(
{
Query: {
'*': deny,
findPosts: allow,
Category: isAdmin,
Tag: isAdmin,
Report: isModerator,
Notification: isAdmin,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator),
Comment: allow,
User: allow,
isLoggedIn: allow,
},
Mutation: {
'*': deny,
login: allow,
UpdateNotification: belongsToMe,
CreateUser: isAdmin,
UpdateUser: onlyYourself,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,
DeletePost: isAuthor,
report: isAuthenticated,
CreateBadge: isAdmin,
UpdateBadge: isAdmin,
DeleteBadge: isAdmin,
AddUserBadges: isAdmin,
CreateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin,
reward: isAdmin,
unreward: isAdmin,
// addFruitToBasket: isAuthenticated
follow: isAuthenticated,
unfollow: isAuthenticated,
shout: isAuthenticated,
unshout: isAuthenticated,
changePassword: isAuthenticated,
enable: isModerator,
disable: isModerator,
CreateComment: isAuthenticated,
UpdateComment: isAuthor,
DeleteComment: isAuthor,
DeleteUser: isDeletingOwnAccount,
requestPasswordReset: allow,
resetPassword: allow,
},
User: {
email: isMyOwn,
password: isMyOwn,
privateKey: isMyOwn,
},
},
{
fallbackRule: allow,
},
)
export default permissions

View File

@ -7,12 +7,14 @@ let headers
const factory = Factory()
beforeEach(async () => {
await factory.create('User', { email: 'user@example.org', password: '1234' })
const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' }
await factory.create('User', adminParams)
await factory.create('User', {
email: 'someone@example.org',
password: '1234',
})
headers = await login({ email: 'user@example.org', password: '1234' })
// we need to be an admin, otherwise we're not authorized to create a user
headers = await login(adminParams)
authenticatedClient = new GraphQLClient(host, { headers })
})

View File

@ -38,22 +38,41 @@ export default {
if (!post) {
throw new UserInputError(NO_POST_ERR_MESSAGE)
}
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
const commentWithoutRelationships = await neo4jgraphql(
object,
params,
context,
resolveInfo,
false,
)
await session.run(
let transactionRes = await session.run(
`
MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}), (author:User {id: $userId})
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
RETURN post`,
RETURN comment, author`,
{
userId: context.user.id,
postId,
commentId: comment.id,
commentId: commentWithoutRelationships.id,
},
)
session.close()
return comment
const [commentWithAuthor] = transactionRes.records.map(record => {
return {
comment: record.get('comment'),
author: record.get('author'),
}
})
const { comment, author } = commentWithAuthor
const commentReturnedWithAuthor = {
...comment.properties,
author: author.properties,
}
session.close()
return commentReturnedWithAuthor
},
UpdateComment: async (object, params, context, resolveInfo) => {
await neo4jgraphql(object, params, context, resolveInfo, false)

View File

@ -0,0 +1,72 @@
import uuid from 'uuid/v4'
import bcrypt from 'bcryptjs'
import CONFIG from '../../config'
import nodemailer from 'nodemailer'
import { resetPasswordMail, wrongAccountMail } from './passwordReset/emailTemplates'
const transporter = () => {
const configs = {
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: false, // true for 465, false for other ports
}
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
if (user && pass) {
configs.auth = { user, pass }
}
return nodemailer.createTransport(configs)
}
export async function createPasswordReset(options) {
const { driver, code, email, issuedAt = new Date() } = options
const session = driver.session()
const cypher = `
MATCH (u:User) WHERE u.email = $email
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
MERGE (u)-[:REQUESTED]->(pr)
RETURN u
`
const transactionRes = await session.run(cypher, {
issuedAt: issuedAt.toISOString(),
code,
email,
})
const users = transactionRes.records.map(record => record.get('u'))
session.close()
return users
}
export default {
Mutation: {
requestPasswordReset: async (_, { email }, { driver }) => {
const code = uuid().substring(0, 6)
const [user] = await createPasswordReset({ driver, code, email })
if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) {
const name = (user && user.name) || ''
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
await transporter().sendMail(mailTemplate({ email, code, name }))
}
return true
},
resetPassword: async (_, { email, code, newPassword }, { driver }) => {
const session = driver.session()
const stillValid = new Date()
stillValid.setDate(stillValid.getDate() - 1)
const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
const cypher = `
MATCH (pr:PasswordReset {code: $code})
MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
SET pr.usedAt = datetime()
SET u.password = $newHashedPassword
RETURN pr
`
let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword })
const [reset] = transactionRes.records.map(record => record.get('pr'))
const result = !!(reset && reset.properties.usedAt)
session.close()
return result
},
},
}

View File

@ -0,0 +1,180 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host } from '../../jest/helpers'
import { getDriver } from '../../bootstrap/neo4j'
import { createPasswordReset } from './passwordReset'
const factory = Factory()
let client
const driver = getDriver()
const getAllPasswordResets = async () => {
const session = driver.session()
let transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
const resets = transactionRes.records.map(record => record.get('r'))
session.close()
return resets
}
describe('passwordReset', () => {
beforeEach(async () => {
client = new GraphQLClient(host)
await factory.create('User', {
email: 'user@example.org',
role: 'user',
password: '1234',
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('requestPasswordReset', () => {
const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }`
describe('with invalid email', () => {
const variables = { email: 'non-existent@example.org' }
it('resolves anyways', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
requestPasswordReset: true,
})
})
it('creates no node', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
expect(resets).toHaveLength(0)
})
})
describe('with a valid email', () => {
const variables = { email: 'user@example.org' }
it('resolves', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
requestPasswordReset: true,
})
})
it('creates node with label `PasswordReset`', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
expect(resets).toHaveLength(1)
})
it('creates a reset code', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
const [reset] = resets
const { code } = reset.properties
expect(code).toHaveLength(6)
})
})
})
describe('resetPassword', () => {
const setup = async (options = {}) => {
const { email = 'user@example.org', issuedAt = new Date(), code = 'abcdef' } = options
const session = driver.session()
await createPasswordReset({ driver, email, issuedAt, code })
session.close()
}
const mutation = `mutation($code: String!, $email: String!, $newPassword: String!) { resetPassword(code: $code, email: $email, newPassword: $newPassword) }`
let email = 'user@example.org'
let code = 'abcdef'
let newPassword = 'supersecret'
let variables
describe('invalid email', () => {
it('resolves to false', async () => {
await setup()
variables = { newPassword, email: 'non-existent@example.org', code }
await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false })
})
})
describe('valid email', () => {
describe('but invalid code', () => {
it('resolves to false', async () => {
await setup()
variables = { newPassword, email, code: 'slkdjf' }
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: false,
})
})
})
describe('and valid code', () => {
beforeEach(() => {
variables = {
newPassword,
email: 'user@example.org',
code: 'abcdef',
}
})
describe('and code not expired', () => {
beforeEach(async () => {
await setup()
})
it('resolves to true', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: true,
})
})
it('updates PasswordReset `usedAt` property', async () => {
await client.request(mutation, variables)
const requests = await getAllPasswordResets()
const [request] = requests
const { usedAt } = request.properties
expect(usedAt).not.toBeFalsy()
})
it('updates password of the user', async () => {
await client.request(mutation, variables)
const checkLoginMutation = `
mutation($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
const expected = expect.objectContaining({ login: expect.any(String) })
await expect(
client.request(checkLoginMutation, {
email: 'user@example.org',
password: 'supersecret',
}),
).resolves.toEqual(expected)
})
})
describe('but expired code', () => {
beforeEach(async () => {
const issuedAt = new Date()
issuedAt.setDate(issuedAt.getDate() - 1)
await setup({ issuedAt })
})
it('resolves to false', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: false,
})
})
it('does not update PasswordReset `usedAt` property', async () => {
await client.request(mutation, variables)
const requests = await getAllPasswordResets()
const [request] = requests
const { usedAt } = request.properties
expect(usedAt).toBeUndefined()
})
})
})
})
})
})

View File

@ -0,0 +1,85 @@
import CONFIG from '../../../config'
export const from = '"Human Connection" <info@human-connection.org>'
export const resetPasswordMail = options => {
const {
name,
email,
code,
subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('code', code)
actionUrl.searchParams.set('email', email)
return {
to: email,
subject,
text: `
Hi ${name}!
You recently requested to reset your password for your Human Connection account.
Use the link below to reset it. This password reset is only valid for the next
24 hours.
${actionUrl}
If you did not request a password reset, please ignore this email or contact
support if you have questions:
${supportUrl}
Thanks,
The Human Connection Team
If you're having trouble with the link above, you can manually copy and
paste the following code into your browser window:
${code}
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}
export const wrongAccountMail = options => {
const {
email,
subject = `We received a request to reset your password with this email address (${email})`,
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
to: email,
subject,
text: `
We received a request to reset the password to access Human Connection with your
email address, but we were unable to find an account associated with this
address.
If you use Human Connection and were expecting this email, consider trying to
request a password reset using the email address associated with your account.
Try a different email:
${actionUrl}
If you do not use Human Connection or did not request a password reset, please
ignore this email. Feel free to contact support if you have further questions:
${supportUrl}
Thanks,
The Human Connection Team
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}

View File

@ -74,6 +74,22 @@ describe('CreatePost', () => {
await expect(client.request(mutation)).resolves.toMatchObject(expected)
})
})
describe('language', () => {
it('allows a user to set the language of the post', async () => {
const createPostWithLanguageMutation = `
mutation {
CreatePost(title: "I am a title", content: "Some content", language: "en") {
language
}
}
`
const expected = { CreatePost: { language: 'en' } }
await expect(client.request(createPostWithLanguageMutation)).resolves.toEqual(
expect.objectContaining(expected),
)
})
})
})
})

View File

@ -59,7 +59,7 @@ export default {
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
const session = driver.session()
let result = await session.run(
`MATCH (user:User {email: $userEmail})
`MATCH (user:User {email: $userEmail})
RETURN user {.id, .email, .password}`,
{
userEmail: user.email,

View File

@ -315,6 +315,8 @@ describe('change password', () => {
describe('do not expose private RSA key', () => {
let headers
let client
let authenticatedClient
const queryUserPuplicKey = gql`
query($queriedUserSlug: String) {
User(slug: $queriedUserSlug) {
@ -332,7 +334,7 @@ describe('do not expose private RSA key', () => {
}
`
const actionGenUserWithKeys = async () => {
const generateUserWithKeys = async authenticatedClient => {
// Generate user with "privateKey" via 'CreateUser' mutation instead of using the factories "factory.create('User', {...})", see above.
const variables = {
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
@ -341,7 +343,7 @@ describe('do not expose private RSA key', () => {
name: 'Apfel Strudel',
email: 'apfel-strudel@test.org',
}
await client.request(
await authenticatedClient.request(
gql`
mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String!) {
CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) {
@ -353,14 +355,23 @@ describe('do not expose private RSA key', () => {
)
}
// not authenticate
beforeEach(async () => {
const adminParams = {
role: 'admin',
email: 'admin@example.org',
password: '1234',
}
// create an admin user who has enough permissions to create other users
await factory.create('User', adminParams)
const headers = await login(adminParams)
authenticatedClient = new GraphQLClient(host, { headers })
// but also create an unauthenticated client to issue the `User` query
client = new GraphQLClient(host)
})
describe('unauthenticated query of "publicKey" (does the RSA key pair get generated at all?)', () => {
it('returns publicKey', async () => {
await actionGenUserWithKeys()
await generateUserWithKeys(authenticatedClient)
await expect(
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
).toEqual(
@ -378,7 +389,7 @@ describe('do not expose private RSA key', () => {
describe('unauthenticated query of "privateKey"', () => {
it('throws "Not Authorised!"', async () => {
await actionGenUserWithKeys()
await generateUserWithKeys(authenticatedClient)
await expect(
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
).rejects.toThrow('Not Authorised')
@ -393,7 +404,7 @@ describe('do not expose private RSA key', () => {
describe('authenticated query of "publicKey"', () => {
it('returns publicKey', async () => {
await actionGenUserWithKeys()
await generateUserWithKeys(authenticatedClient)
await expect(
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
).toEqual(
@ -411,7 +422,7 @@ describe('do not expose private RSA key', () => {
describe('authenticated query of "privateKey"', () => {
it('throws "Not Authorised!"', async () => {
await actionGenUserWithKeys()
await generateUserWithKeys(authenticatedClient)
await expect(
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
).rejects.toThrow('Not Authorised')

View File

@ -11,5 +11,27 @@ export default {
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
return neo4jgraphql(object, params, context, resolveInfo, false)
},
DeleteUser: async (object, params, context, resolveInfo) => {
const { resource } = params
const session = context.driver.session()
if (resource && resource.length) {
await Promise.all(
resource.map(async node => {
await session.run(
`
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
SET resource.deleted = true
RETURN author`,
{
userId: context.user.id,
},
)
}),
)
session.close()
}
return neo4jgraphql(object, params, context, resolveInfo, false)
},
},
}

View File

@ -1,6 +1,7 @@
import { GraphQLClient } from 'graphql-request'
import { host } from '../../jest/helpers'
import { login, host } from '../../jest/helpers'
import Factory from '../../seed/factories'
import gql from 'graphql-tag'
const factory = Factory()
let client
@ -18,27 +19,58 @@ describe('users', () => {
}
}
`
client = new GraphQLClient(host)
it('with password and email', async () => {
describe('given valid password and email', () => {
const variables = {
name: 'John Doe',
password: '123',
email: '123@123.de',
}
const expected = {
CreateUser: {
id: expect.any(String),
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
describe('unauthenticated', () => {
beforeEach(async () => {
client = new GraphQLClient(host)
})
it('is not allowed to create users', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
beforeEach(async () => {
const adminParams = {
role: 'admin',
email: 'admin@example.org',
password: '1234',
}
await factory.create('User', adminParams)
const headers = await login(adminParams)
client = new GraphQLClient(host, { headers })
})
it('is allowed to create new users', async () => {
const expected = {
CreateUser: {
id: expect.any(String),
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})
})
describe('UpdateUser', () => {
beforeEach(async () => {
await factory.create('User', { id: 'u47', name: 'John Doe' })
})
const userParams = {
email: 'user@example.org',
password: '1234',
id: 'u47',
name: 'John Doe',
}
const variables = {
id: 'u47',
name: 'John Doughnut',
}
const mutation = `
mutation($id: ID!, $name: String) {
@ -48,38 +80,199 @@ describe('users', () => {
}
}
`
client = new GraphQLClient(host)
it('name within specifications', async () => {
const variables = {
id: 'u47',
name: 'James Doe',
}
const expected = {
UpdateUser: {
id: 'u47',
beforeEach(async () => {
await factory.create('User', userParams)
})
describe('as another user', () => {
beforeEach(async () => {
const someoneElseParams = {
email: 'someoneElse@example.org',
password: '1234',
name: 'James Doe',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
}
await factory.create('User', someoneElseParams)
const headers = await login(someoneElseParams)
client = new GraphQLClient(host, { headers })
})
it('is not allowed to change other user accounts', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
it('with no name', async () => {
const variables = {
id: 'u47',
name: null,
describe('as the same user', () => {
beforeEach(async () => {
const headers = await login(userParams)
client = new GraphQLClient(host, { headers })
})
it('name within specifications', async () => {
const expected = {
UpdateUser: {
id: 'u47',
name: 'John Doughnut',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
it('with no name', async () => {
const variables = {
id: 'u47',
name: null,
}
const expected = 'Username must be at least 3 characters long!'
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
})
it('with too short name', async () => {
const variables = {
id: 'u47',
name: ' ',
}
const expected = 'Username must be at least 3 characters long!'
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
})
})
})
describe('DeleteUser', () => {
let deleteUserVariables
let asAuthor
const deleteUserMutation = gql`
mutation($id: ID!, $resource: [String]) {
DeleteUser(id: $id, resource: $resource) {
id
contributions {
id
deleted
}
comments {
id
deleted
}
}
}
const expected = 'Username must be at least 3 characters long!'
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
`
beforeEach(async () => {
asAuthor = await factory.create('User', {
email: 'test@example.org',
password: '1234',
id: 'u343',
})
await factory.create('User', {
email: 'friendsAccount@example.org',
password: '1234',
id: 'u565',
})
deleteUserVariables = { id: 'u343', resource: [] }
})
it('with too short name', async () => {
const variables = {
id: 'u47',
name: ' ',
}
const expected = 'Username must be at least 3 characters long!'
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, { headers })
})
describe("attempting to delete another user's account", () => {
it('throws an authorization error', async () => {
deleteUserVariables = { id: 'u565' }
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
describe('attempting to delete my own account', () => {
let expectedResponse
beforeEach(async () => {
await asAuthor.authenticateAs({
email: 'test@example.org',
password: '1234',
})
await asAuthor.create('Post', {
id: 'p139',
content: 'Post by user u343',
})
await asAuthor.create('Comment', {
id: 'c155',
postId: 'p139',
content: 'Comment by user u343',
})
expectedResponse = {
DeleteUser: {
id: 'u343',
contributions: [{ id: 'p139', deleted: false }],
comments: [{ id: 'c155', deleted: false }],
},
}
})
it("deletes my account, but doesn't delete posts or comments by default", async () => {
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
expectedResponse,
)
})
describe("deletes a user's", () => {
it('posts on request', async () => {
deleteUserVariables = { id: 'u343', resource: ['Post'] }
expectedResponse = {
DeleteUser: {
id: 'u343',
contributions: [{ id: 'p139', deleted: true }],
comments: [{ id: 'c155', deleted: false }],
},
}
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
expectedResponse,
)
})
it('comments on request', async () => {
deleteUserVariables = { id: 'u343', resource: ['Comment'] }
expectedResponse = {
DeleteUser: {
id: 'u343',
contributions: [{ id: 'p139', deleted: false }],
comments: [{ id: 'c155', deleted: true }],
},
}
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
expectedResponse,
)
})
it('posts and comments on request', async () => {
deleteUserVariables = { id: 'u343', resource: ['Post', 'Comment'] }
expectedResponse = {
DeleteUser: {
id: 'u343',
contributions: [{ id: 'p139', deleted: true }],
comments: [{ id: 'c155', deleted: true }],
},
}
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
expectedResponse,
)
})
})
})
})
})
})

View File

@ -4,8 +4,9 @@ type Query {
currentUser: User
# Get the latest Network Statistics
statistics: Statistics!
findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
statement: """
findPosts(filter: String!, limit: Int = 10): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
YIELD node as post, score
MATCH (post)<-[:WROTE]-(user:User)
@ -14,8 +15,8 @@ type Query {
AND NOT post.deleted = true AND NOT post.disabled = true
RETURN post
LIMIT $limit
"""
)
"""
)
CommentByPost(postId: ID!): [Comment]!
}
@ -23,7 +24,9 @@ type Mutation {
# Get a JWT Token for the given Email and password
login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean!
changePassword(oldPassword:String!, newPassword: String!): String!
changePassword(oldPassword: String!, newPassword: String!): String!
requestPasswordReset(email: String!): Boolean!
resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID
@ -37,6 +40,7 @@ type Mutation {
follow(id: ID!, type: FollowTypeEnum): Boolean!
# Unfollow the given Type and ID
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
DeleteUser(id: ID!, resource: [String]): User
}
type Statistics {
@ -53,7 +57,7 @@ type Statistics {
type Notification {
id: ID!
read: Boolean,
read: Boolean
user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN")
createdAt: String
@ -80,7 +84,8 @@ type Report {
id: ID!
submitter: User @relation(name: "REPORTED", direction: "IN")
description: String
type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
type: String!
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
createdAt: String
comment: Comment @relation(name: "REPORTED", direction: "OUT")
post: Post @relation(name: "REPORTED", direction: "OUT")
@ -131,4 +136,3 @@ type SocialMedia {
url: String
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
}

View File

@ -15,29 +15,37 @@ type Post {
disabledBy: User @relation(name: "DISABLED", direction: "IN")
createdAt: String
updatedAt: String
relatedContributions: [Post]! @cypher(
statement: """
language: String
relatedContributions: [Post]!
@cypher(
statement: """
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
RETURN DISTINCT post
LIMIT 10
"""
)
"""
)
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
commentsCount: Int!
@cypher(
statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)"
)
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
shoutedCount: Int!
@cypher(
statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
)
# Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean! @cypher(
statement: """
shoutedByCurrentUser: Boolean!
@cypher(
statement: """
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
"""
)
}

View File

@ -13,7 +13,7 @@ export default function(params) {
faker.lorem.sentence(),
faker.lorem.sentence(),
].join('. '),
image = faker.image.image(),
image = faker.image.unsplash.imageUrl(),
visibility = 'public',
deleted = false,
} = params

View File

@ -214,21 +214,21 @@ import Factory from './factories'
'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
await Promise.all([
asAdmin.create('Post', { id: 'p0' }),
asModerator.create('Post', { id: 'p1' }),
asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food() }),
asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology() }),
asUser.create('Post', { id: 'p2' }),
asTick.create('Post', { id: 'p3' }),
asTrick.create('Post', { id: 'p4' }),
asTrack.create('Post', { id: 'p5' }),
asAdmin.create('Post', { id: 'p6' }),
asAdmin.create('Post', { id: 'p6', image: faker.image.unsplash.buildings() }),
asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
asUser.create('Post', { id: 'p8' }),
asUser.create('Post', { id: 'p8', image: faker.image.unsplash.nature() }),
asTick.create('Post', { id: 'p9' }),
asTrick.create('Post', { id: 'p10' }),
asTrack.create('Post', { id: 'p11' }),
asTrack.create('Post', { id: 'p11', image: faker.image.unsplash.people() }),
asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
asModerator.create('Post', { id: 'p13' }),
asUser.create('Post', { id: 'p14' }),
asUser.create('Post', { id: 'p14', image: faker.image.unsplash.objects() }),
asTick.create('Post', { id: 'p15' }),
])

View File

@ -10,6 +10,7 @@ Feature: Follow a user
| stuart-little |
| tero-vota |
@wip
Scenario: Send a follow to a user inbox and make sure it's added to the right followers collection
When I send a POST request with the following activity to "/activitypub/users/tero-vota/inbox":
"""

View File

@ -27,6 +27,7 @@ Feature: Like an object like an article or note
}
"""
@wip
Scenario: Send a like of a person to an users inbox and make sure it's added to the likes collection
When I send a POST request with the following activity to "/activitypub/users/karl-heinz/inbox":
"""

View File

@ -1020,19 +1020,10 @@
"@types/node" "*"
"@types/range-parser" "*"
"@types/express@*", "@types/express@^4.11.1":
version "4.16.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19"
integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/express@4.16.1":
version "4.16.1"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.1.tgz#d756bd1a85c34d87eaf44c888bad27ba8a4b7cf0"
integrity sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==
"@types/express@*", "@types/express@4.17.0", "@types/express@^4.11.1":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287"
integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
@ -1119,10 +1110,10 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
"@types/yup@0.26.16":
version "0.26.16"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.16.tgz#75c428236207c48d9f8062dd1495cda8c5485a15"
integrity sha512-E2RNc7DSeQ+2EIJ1H3+yFjYu6YiyQBUJ7yNpIxomrYJ3oFizLZ5yDS3T1JTUNBC2OCRkgnhLS0smob5UuCHfNA==
"@types/yup@0.26.17":
version "0.26.17"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc"
integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q==
"@types/zen-observable@^0.5.3":
version "0.5.4"
@ -1226,10 +1217,10 @@ activitystreams-context@>=3.0.0, activitystreams-context@^3.0.0:
resolved "https://registry.yarnpkg.com/activitystreams-context/-/activitystreams-context-3.1.0.tgz#28334e129f17cfb937e8c702c52c1bcb1d2830c7"
integrity sha512-KBQ+igwf1tezMXGVw5MvRSEm0gp97JI1hTZ45I6MEkWv25lEgNoA9L6wqfaOiCX8wnMRWw9pwRsPZKypdtxAtg==
ajv@^6.5.5, ajv@^6.9.1:
version "6.9.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b"
integrity sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg==
ajv@^6.10.0, ajv@^6.5.5, ajv@^6.9.1:
version "6.10.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
@ -1288,13 +1279,13 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
apollo-cache-control@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.2.tgz#b8852422d973c582493e85c776abc9c660090162"
integrity sha512-7prjFN8H9lRE0npqGG8kM3XICvNCcgQt6eCy8kkcPOIZwM+F8m8ShjEfNF9UWW32i+poOk3G67HegPRyjCc6/Q==
apollo-cache-control@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.4.tgz#0cb5c7be0e0dd0c44b1257144cd7f9f2a3c374e6"
integrity sha512-TVACHwcEF4wfHo5H9FLnoNjo0SLDo2jPW+bXs9aw0Y4Z2UisskSAPnIYOqUPnU8SoeNvs7zWgbLizq11SRTJtg==
dependencies:
apollo-server-env "2.4.0"
graphql-extensions "0.7.2"
graphql-extensions "0.7.4"
apollo-cache-control@^0.1.0:
version "0.1.1"
@ -1322,10 +1313,10 @@ apollo-cache@1.3.2, apollo-cache@^1.3.2:
apollo-utilities "^1.3.2"
tslib "^1.9.3"
apollo-client@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.2.tgz#03b6af651e09b6e413e486ddc87464c85bd6e514"
integrity sha512-oks1MaT5x7gHcPeC8vPC1UzzsKaEIC0tye+jg72eMDt5OKc7BobStTeS/o2Ib3e0ii40nKxGBnMdl/Xa/p56Yg==
apollo-client@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
dependencies:
"@types/zen-observable" "^0.8.0"
apollo-cache "1.3.2"
@ -1351,17 +1342,17 @@ apollo-engine-reporting-protobuf@0.3.1:
dependencies:
protobufjs "^6.8.6"
apollo-engine-reporting@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.0.tgz#50151811a0f5e70f4a73e7092a61fec422d8e722"
integrity sha512-xP+Z+wdQH4ee7xfuP3WsJcIe30AH68gpp2lQm2+rnW5JfjIqD5YehSoO2Svi2jK3CSv8Y561i3QMW9i34P7hEQ==
apollo-engine-reporting@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.4.tgz#65e12f94221d80b3b1740c26e82ce9bb6bdfb7ee"
integrity sha512-DJdYghyUBzT0/LcPLwuQNXDCw06r1RfxkVfNTGKoTv6a+leVvjhDJmXvc+jSuBPwaNsc+RYRnfyQ2qUn9fmfyA==
dependencies:
apollo-engine-reporting-protobuf "0.3.1"
apollo-graphql "^0.3.0"
apollo-server-core "2.6.2"
apollo-graphql "^0.3.2"
apollo-server-core "2.6.6"
apollo-server-env "2.4.0"
async-retry "^1.2.1"
graphql-extensions "0.7.2"
graphql-extensions "0.7.5"
apollo-env@0.5.1:
version "0.5.1"
@ -1380,49 +1371,49 @@ apollo-errors@^1.9.0:
assert "^1.4.1"
extendable-error "^0.1.5"
apollo-graphql@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7"
integrity sha512-tbhtzNAAhNI34v4XY9OlZGnH7U0sX4BP1cJrUfSiNzQnZRg1UbQYZ06riHSOHpi5RSndFcA9LDM5C1ZKKOUeBg==
apollo-graphql@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
integrity sha512-YbzYGR14GV0023m//EU66vOzZ3i7c04V/SF8Qk+60vf1sOWyKgO6mxZJ4BKhw10qWUayirhSDxq3frYE+qSG0A==
dependencies:
apollo-env "0.5.1"
lodash.sortby "^4.7.0"
apollo-link-context@~1.0.14:
version "1.0.17"
resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.17.tgz#439272cfb43ec1891506dd175ed907845b7de36c"
integrity sha512-W5UUfHcrrlP5uqJs5X1zbf84AMXhPZGAqX/7AQDgR6wY/7//sMGfJvm36KDkpIeSOElztGtM9z6zdPN1NbT41Q==
apollo-link-context@~1.0.18:
version "1.0.18"
resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.18.tgz#9e700e3314da8ded50057fee0a18af2bfcedbfc3"
integrity sha512-aG5cbUp1zqOHHQjAJXG7n/izeMQ6LApd/whEF5z6qZp5ATvcyfSNkCfy3KRJMMZZ3iNfVTs6jF+IUA8Zvf+zeg==
dependencies:
apollo-link "^1.2.11"
apollo-link "^1.2.12"
tslib "^1.9.3"
apollo-link-http-common@^0.2.13:
version "0.2.13"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350"
integrity sha512-Uyg1ECQpTTA691Fwx5e6Rc/6CPSu4TB4pQRTGIpwZ4l5JDOQ+812Wvi/e3IInmzOZpwx5YrrOfXrtN8BrsDXoA==
apollo-link-http-common@^0.2.14:
version "0.2.14"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg==
dependencies:
apollo-link "^1.2.11"
ts-invariant "^0.3.2"
apollo-link "^1.2.12"
ts-invariant "^0.4.0"
tslib "^1.9.3"
apollo-link-http@~1.5.14:
version "1.5.14"
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.14.tgz#ed6292248d1819ccd16523e346d35203a1b31109"
integrity sha512-XEoPXmGpxFG3wioovgAlPXIarWaW4oWzt8YzjTYZ87R4R7d1A3wKR/KcvkdMV1m5G7YSAHcNkDLe/8hF2nH6cg==
apollo-link-http@~1.5.15:
version "1.5.15"
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf"
integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ==
dependencies:
apollo-link "^1.2.11"
apollo-link-http-common "^0.2.13"
apollo-link "^1.2.12"
apollo-link-http-common "^0.2.14"
tslib "^1.9.3"
apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3:
version "1.2.11"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d"
integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==
apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
version "1.2.12"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==
dependencies:
apollo-utilities "^1.2.1"
ts-invariant "^0.3.2"
apollo-utilities "^1.3.0"
ts-invariant "^0.4.0"
tslib "^1.9.3"
zen-observable-ts "^0.8.18"
zen-observable-ts "^0.8.19"
apollo-server-caching@0.4.0:
version "0.4.0"
@ -1431,24 +1422,24 @@ apollo-server-caching@0.4.0:
dependencies:
lru-cache "^5.0.0"
apollo-server-core@2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.2.tgz#a792b50d4df9e26ec03759a31fbcbce38361b218"
integrity sha512-AbAnfoQ26NPsNIyBa/BVKBtA/wRsNL/E6eEem1VIhzitfgO25bVXFbEZDLxbgz6wvJ+veyRFpse7Qi1bvRpxOw==
apollo-server-core@2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.6.tgz#55fea7980a943948c49dea20d81b9bbfc0e04f87"
integrity sha512-PFSjJbqkV1eetfFJxu11gzklQYC8BrF0RZfvC1d1mhvtxAOKl25uhPHxltN0Omyjp7LW4YeoC6zwl9rLWuhZFQ==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
"@apollographql/graphql-playground-html" "1.6.20"
"@types/ws" "^6.0.0"
apollo-cache-control "0.7.2"
apollo-cache-control "0.7.4"
apollo-datasource "0.5.0"
apollo-engine-reporting "1.3.0"
apollo-engine-reporting "1.3.4"
apollo-server-caching "0.4.0"
apollo-server-env "2.4.0"
apollo-server-errors "2.3.0"
apollo-server-plugin-base "0.5.2"
apollo-tracing "0.7.2"
apollo-server-plugin-base "0.5.5"
apollo-tracing "0.7.3"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.7.2"
graphql-extensions "0.7.5"
graphql-subscriptions "^1.0.0"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
@ -1479,18 +1470,18 @@ apollo-server-errors@2.3.0:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
apollo-server-express@2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.2.tgz#526297c01a7a32fe9215566f9fd7ff92e82f1fa0"
integrity sha512-nbL3noJ5KxKGg+hT8UsAA7++oHWq/KNSevfdCluWTfUNqH1vYRTvAnARx/6JM06S9zcPTfOLcqwHnDnY9zYFxA==
apollo-server-express@2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.6.tgz#ec2b955354d7dd4d12fe01ea7e983d302071d5b9"
integrity sha512-bY/xrr9lZH+hsjchiQuSXpW3ivXfL1h81M5VE9Ppus1PVwwEIar/irBN+PFp97WxERZPDjVZzrRKa+lRHjtJsA==
dependencies:
"@apollographql/graphql-playground-html" "1.6.20"
"@types/accepts" "^1.3.5"
"@types/body-parser" "1.17.0"
"@types/cors" "^2.8.4"
"@types/express" "4.16.1"
"@types/express" "4.17.0"
accepts "^1.3.5"
apollo-server-core "2.6.2"
apollo-server-core "2.6.6"
body-parser "^1.18.3"
cors "^2.8.4"
graphql-subscriptions "^1.0.0"
@ -1518,36 +1509,36 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
apollo-server-plugin-base@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.2.tgz#f97ba983f1e825fec49cba8ff6a23d00e1901819"
integrity sha512-j81CpadRLhxikBYHMh91X4aTxfzFnmmebEiIR9rruS6dywWCxV2aLW87l9ocD1MiueNam0ysdwZkX4F3D4csNw==
apollo-server-plugin-base@0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.5.tgz#364e4a2fca4d95ddeb9fd3e78940ed1da58865c2"
integrity sha512-agiuhknyu3lnnEsqUh99tzxwPCGp+TuDK+TSRTkXU1RUG6lY4C3uJp0JGJw03cP+M6ze73TbRjMA4E68g/ks5A==
apollo-server-testing@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.2.tgz#e0ecddd565fce1c38a346f9fbe6118f543ccf6a6"
integrity sha512-I9QLFk4I/z9oOIXfnLc8RPBYAKih6Olrg3RDeRvWhDjLQ8gfALXVhCO+7WuvM35wNZcZVn7aXBeZ8Y3mlgkj8w==
apollo-server-testing@~2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.6.tgz#3d96486ebdb151219183fcb715973a8385c66e0a"
integrity sha512-GfzEAqXGzhWp1YgNJONAijC3mC34E6cI5XiOdLX9FGAnBmZvDURlZwloZbdNgvqvXnwuxuNgo4xvCnTe7kndqg==
dependencies:
apollo-server-core "2.6.2"
apollo-server-core "2.6.6"
apollo-server@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.2.tgz#33fe894b740588f059a7679346516ffce50377d5"
integrity sha512-fMXaAKIb0dX0lzcZ4zlu7ay1L596d9HTNkdn8cKuM7zmTpugZSAL966COguJUDSjUS9CaB1Kh5hl1yRuRqHXSA==
apollo-server@~2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.6.tgz#0570fce4a682eb1de8bc1b86dbe2543de440cd4e"
integrity sha512-7Bulb3RnOO4/SGA66LXu3ZHCXIK8MYMrsxy4yti1/adDIUmcniolDqJwOYUGoTmv1AQjRxgJb4TVZ0Dk9nrrYg==
dependencies:
apollo-server-core "2.6.2"
apollo-server-express "2.6.2"
apollo-server-core "2.6.6"
apollo-server-express "2.6.6"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
apollo-tracing@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.2.tgz#7730159a4670bca465ac1bfa01f9902610a7aba4"
integrity sha512-bT4/n8Vy9DweC3+XWJelJD41FBlKMXR0OVxjLMiCe9clb4yTgKhYxRGTyh9KjmhWsng9gG/DphO0ixWsOgdXmA==
apollo-tracing@0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.3.tgz#8533e3e2dca2d5a25e8439ce498ea33ff4d159ee"
integrity sha512-H6fSC+awQGnfDyYdGIB0UQUhcUC3n5Vy+ujacJ0bY6R+vwWeZOQvu7wRHNjk/rbOSTLCo9A0OcVX7huRyu9SZg==
dependencies:
apollo-server-env "2.4.0"
graphql-extensions "0.7.2"
graphql-extensions "0.7.4"
apollo-tracing@^0.1.0:
version "0.1.4"
@ -1566,7 +1557,7 @@ apollo-upload-server@^7.0.0:
http-errors "^1.7.0"
object-path "^0.11.4"
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, apollo-utilities@^1.3.2:
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
@ -1790,10 +1781,10 @@ babel-core@~7.0.0-0:
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
babel-eslint@~10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
integrity sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==
babel-eslint@~10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/parser" "^7.0.0"
@ -2070,6 +2061,13 @@ busboy@^0.2.14:
dicer "0.2.5"
readable-stream "1.1.x"
busboy@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
dependencies:
dicer "0.3.0"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
@ -2586,10 +2584,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.31:
version "2.0.0-alpha.31"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.31.tgz#51bcfdca25dfc9bea334a556ab33dfc0bb00421c"
integrity sha512-S19PwMqnbYsqcbCg02Yj9gv4veVNZ0OX7v2+zcd+Mq0RI7LoDKJipJjnMrTZ3Cc6blDuTce5G/pHXcVIGRwJWQ==
date-fns@2.0.0-alpha.37:
version "2.0.0-alpha.37"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.37.tgz#c58b3e827da4f860ec8dc123e54019efb4a610e0"
integrity sha512-fyIv/h6fkFd1u2NHXni5LPRPoa9FFh3hY67JSjNfa+k/u4EKvfrpGtoTM16Y/BJOqTb4W05UjcmwBna1ElyxDA==
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
@ -2721,6 +2719,13 @@ dicer@0.2.5:
readable-stream "1.1.x"
streamsearch "0.1.2"
dicer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
dependencies:
streamsearch "0.1.2"
diff-sequences@^24.3.0:
version "24.3.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975"
@ -2995,10 +3000,10 @@ escodegen@^1.9.1:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@~4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz#c55c1fcac8ce4518aeb77906984e134d9eb5a4f0"
integrity sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA==
eslint-config-prettier@~5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-5.0.0.tgz#f7a94b2b8ae7cbf25842c36fa96c6d32cd0a697c"
integrity sha512-c17Aqiz5e8LEqoc/QPmYnaxQFAHTx2KlCZBPxXXjEMmNchOLnV/7j0HoPZuC+rL/tDC9bazUYOKJW9bOhftI/w==
dependencies:
get-stdin "^6.0.0"
@ -3031,10 +3036,10 @@ eslint-plugin-es@^1.4.0:
eslint-utils "^1.3.0"
regexpp "^2.0.1"
eslint-plugin-import@~2.17.3:
version "2.17.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz#00548b4434c18faebaba04b24ae6198f280de189"
integrity sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q==
eslint-plugin-import@~2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz#7a5ba8d32622fb35eb9c8db195c2090bd18a3678"
integrity sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig==
dependencies:
array-includes "^3.0.3"
contains-path "^0.1.0"
@ -3048,10 +3053,10 @@ eslint-plugin-import@~2.17.3:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.6.4:
version "22.6.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
eslint-plugin-jest@~22.7.1:
version "22.7.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1"
integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw==
eslint-plugin-node@~9.1.0:
version "9.1.0"
@ -3072,10 +3077,10 @@ eslint-plugin-prettier@~3.1.0:
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-promise@~4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==
eslint-plugin-promise@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
eslint-plugin-standard@~4.0.0:
version "4.0.0"
@ -3108,13 +3113,13 @@ eslint-visitor-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
eslint@~5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
eslint@~6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.0.1.tgz#4a32181d72cb999d6f54151df7d337131f81cda7"
integrity sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.9.1"
ajv "^6.10.0"
chalk "^2.1.0"
cross-spawn "^6.0.5"
debug "^4.0.1"
@ -3122,18 +3127,19 @@ eslint@~5.16.0:
eslint-scope "^4.0.3"
eslint-utils "^1.3.1"
eslint-visitor-keys "^1.0.0"
espree "^5.0.1"
espree "^6.0.0"
esquery "^1.0.1"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob "^7.1.2"
glob-parent "^3.1.0"
globals "^11.7.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.2.2"
js-yaml "^3.13.0"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.11"
@ -3141,7 +3147,6 @@ eslint@~5.16.0:
mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
path-is-inside "^1.0.2"
progress "^2.0.0"
regexpp "^2.0.1"
semver "^5.5.1"
@ -3150,10 +3155,10 @@ eslint@~5.16.0:
table "^5.2.3"
text-table "^0.2.0"
espree@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==
espree@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.0.0.tgz#716fc1f5a245ef5b9a7fdb1d7b0d3f02322e75f6"
integrity sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==
dependencies:
acorn "^6.0.7"
acorn-jsx "^5.0.0"
@ -3368,10 +3373,9 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
faker@~4.1.0:
faker@Marak/faker.js#master:
version "4.1.0"
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/10bfb9f467b0ac2b8912ffc15690b50ef3244f09"
fast-deep-equal@^2.0.1:
version "2.0.1"
@ -3535,6 +3539,11 @@ fs-capacitor@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0"
integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ==
fs-capacitor@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
fs-minipass@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
@ -3711,10 +3720,17 @@ graphql-deduplicator@^2.0.1:
resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3"
integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA==
graphql-extensions@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.2.tgz#8711543f835661eaf24b48d6ac2aad44dbbd5506"
integrity sha512-TuVINuAOrEtzQAkAlCZMi9aP5rcZ+pVaqoBI5fD2k5O9fmb8OuXUQOW028MUhC66tg4E7h4YSF1uYUIimbu4SQ==
graphql-extensions@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.4.tgz#78327712822281d5778b9210a55dc59c93a9c184"
integrity sha512-Ly+DiTDU+UtlfPGQkqmBX2SWMr9OT3JxMRwpB9K86rDNDBTJtG6AE2kliQKKE+hg1+945KAimO7Ep+YAvS7ywg==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@0.7.5:
version "0.7.5"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.5.tgz#fab2b9e53cf6014952e6547456d50680ff0ea579"
integrity sha512-B1m+/WEJa3IYKWqBPS9W/7OasfPmlHOSz5hpEAq2Jbn6T0FQ/d2YWFf2HBETHR3RR2qfT+55VMiYovl2ga3qcg==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
@ -3772,12 +3788,12 @@ graphql-request@~1.8.2:
dependencies:
cross-fetch "2.2.2"
graphql-shield@~5.3.8:
version "5.3.8"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.8.tgz#f9e7ad2285f6cfbe20a8a49154ce6c1b184e3893"
integrity sha512-33rQ8U5jMurHIapctHk7hBcUg3nxC7fmMIMtyWiomJXhBmztFq/SG7jNaapnL5M7Q/0BmoaSQd3FLSpelP9KPw==
graphql-shield@~5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98"
integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA==
dependencies:
"@types/yup" "0.26.16"
"@types/yup" "0.26.17"
lightercollective "^0.3.0"
object-hash "^1.3.1"
yup "^0.27.0"
@ -3812,20 +3828,20 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.4:
iterall "^1.1.3"
uuid "^3.1.0"
graphql-upload@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.2.tgz#1c1f116f15b7f8485cf40ff593a21368f0f58856"
integrity sha512-u8a5tKPfJ0rU4MY+B3skabL8pEjMkm3tUzq25KBx6nT0yEWmqUO7Z5tdwvwYLFpkLwew94Gue0ARbZtar3gLTw==
graphql-upload@^8.0.0, graphql-upload@^8.0.2:
version "8.0.7"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.7.tgz#8644264e241529552ea4b3797e7ee15809cf01a3"
integrity sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g==
dependencies:
busboy "^0.2.14"
fs-capacitor "^1.0.0"
http-errors "^1.7.1"
busboy "^0.3.1"
fs-capacitor "^2.0.4"
http-errors "^1.7.2"
object-path "^0.11.4"
graphql-yoga@~1.17.4:
version "1.17.4"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.17.4.tgz#6d325a6270399edf0776fb5f60a2e9e19512e63c"
integrity sha512-zOXFtmS43xDLoECKiuA3xVWH/wLDvLH1D/5fBKcaMFdF43ifDfnA9N6dlGggqAoOhqBnrqHwDpoKlJ6sI1LuxA==
graphql-yoga@~1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.18.0.tgz#2668278e94a0bd1b2ff8c60f928c4e18d62e381a"
integrity sha512-WEibitQA2oFTmD7XBO8/ps8DWeVpkzOzgbB3EvtM2oIpyGhPCzRZYrC7OS9MmijvRwLRXsgHImHWUm82ZrIOWA==
dependencies:
"@types/cors" "^2.8.4"
"@types/express" "^4.11.1"
@ -3847,6 +3863,7 @@ graphql-yoga@~1.17.4:
graphql-playground-middleware-lambda "1.7.12"
graphql-subscriptions "^0.5.8"
graphql-tools "^4.0.0"
graphql-upload "^8.0.0"
subscriptions-transport-ws "^0.9.8"
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.3.1:
@ -4045,7 +4062,7 @@ htmlparser2@^3.10.0, htmlparser2@^3.9.1:
inherits "^2.0.1"
readable-stream "^3.0.6"
http-errors@1.7.2, http-errors@~1.7.2:
http-errors@1.7.2, http-errors@^1.7.2, http-errors@~1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@ -4056,7 +4073,7 @@ http-errors@1.7.2, http-errors@~1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@^1.7.0, http-errors@^1.7.1:
http-errors@^1.7.0:
version "1.7.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027"
integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==
@ -4923,10 +4940,10 @@ js-levenshtein@^1.1.3:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.0:
version "3.13.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e"
integrity sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==
js-yaml@^3.13.1:
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"
@ -5596,9 +5613,10 @@ neo4j-driver@^1.7.3, neo4j-driver@~1.7.4:
text-encoding "^0.6.4"
uri-js "^4.2.1"
"neo4j-graphql-js@git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes":
version "2.6.1"
resolved "git+https://github.com/Human-Connection/neo4j-graphql-js.git#84d529b9ecbc5c284cce4f86238c6d19b192cf0f"
neo4j-graphql-js@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.6.3.tgz#8f28c2479adda08c90abcc32a784587ef49b8b95"
integrity sha512-WZdEqQ8EL9GOIB1ZccbLk1BZz5Dqdbk9i8BDXqxhp1SOI07P9y2cZ244f2Uz4zyES9AVXGmv+861N5xLhrSL2A==
dependencies:
graphql "^14.2.1"
graphql-auth-directives "^2.1.0"
@ -5693,6 +5711,11 @@ node-releases@^1.1.19:
dependencies:
semver "^5.3.0"
nodemailer@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3"
integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==
nodemon@~1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"
@ -6099,7 +6122,7 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-is-inside@^1.0.1, path-is-inside@^1.0.2:
path-is-inside@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
@ -7543,13 +7566,6 @@ trunc-text@1.0.1:
resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5"
integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU=
ts-invariant@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0"
integrity sha512-QsY8BCaRnHiB5T6iE4DPlJMAKEG3gzMiUco9FEt1jUXQf0XP6zi0idT0i0rMTu8A326JqNSDsmlkA9dRSh1TRg==
dependencies:
tslib "^1.9.3"
ts-invariant@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.2.tgz#8685131b8083e67c66d602540e78763408be9113"
@ -8112,10 +8128,10 @@ yup@^0.27.0:
synchronous-promise "^2.0.6"
toposort "^2.0.2"
zen-observable-ts@^0.8.18:
version "0.8.18"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8"
integrity sha512-q7d05s75Rn1j39U5Oapg3HI2wzriVwERVo4N7uFGpIYuHB9ff02P/E92P9B8T7QVC93jCMHpbXH7X0eVR5LA7A==
zen-observable-ts@^0.8.19:
version "0.8.19"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"
integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==
dependencies:
tslib "^1.9.3"
zen-observable "^0.8.0"

View File

@ -1,347 +1,361 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { getLangByName } from '../../support/helpers'
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
import { getLangByName } from "../../support/helpers";
/* global cy */
let lastPost = {}
let lastPost = {};
let loginCredentials = {
email: 'peterpan@example.org',
password: '1234'
}
email: "peterpan@example.org",
password: "1234"
};
const narratorParams = {
name: 'Peter Pan',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg',
name: "Peter Pan",
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
...loginCredentials
}
};
Given('I am logged in', () => {
cy.login(loginCredentials)
})
Given("I am logged in", () => {
cy.login(loginCredentials);
});
Given('we have a selection of tags and categories as well as posts', () => {
Given("we have a selection of tags and categories as well as posts", () => {
cy.factory()
.authenticateAs(loginCredentials)
.create('Category', {
id: 'cat1',
name: 'Just For Fun',
slug: 'justforfun',
icon: 'smile'
.create("Category", {
id: "cat1",
name: "Just For Fun",
slug: "justforfun",
icon: "smile"
})
.create('Category', {
id: 'cat2',
name: 'Happyness & Values',
slug: 'happyness-values',
icon: 'heart-o'
.create("Category", {
id: "cat2",
name: "Happyness & Values",
slug: "happyness-values",
icon: "heart-o"
})
.create('Category', {
id: 'cat3',
name: 'Health & Wellbeing',
slug: 'health-wellbeing',
icon: 'medkit'
.create("Category", {
id: "cat3",
name: "Health & Wellbeing",
slug: "health-wellbeing",
icon: "medkit"
})
.create('Tag', { id: 't1', name: 'Ecology' })
.create('Tag', { id: 't2', name: 'Nature' })
.create('Tag', { id: 't3', name: 'Democracy' })
.create("Tag", { id: "t1", name: "Ecology" })
.create("Tag", { id: "t2", name: "Nature" })
.create("Tag", { id: "t3", name: "Democracy" });
const someAuthor = {
id: 'authorId',
email: 'author@example.org',
password: '1234'
}
id: "authorId",
email: "author@example.org",
password: "1234"
};
const yetAnotherAuthor = {
id: 'yetAnotherAuthor',
email: 'yet-another-author@example.org',
password: '1234'
}
id: "yetAnotherAuthor",
email: "yet-another-author@example.org",
password: "1234"
};
cy.factory()
.create('User', someAuthor)
.create("User", someAuthor)
.authenticateAs(someAuthor)
.create('Post', { id: 'p0' })
.create('Post', { id: 'p1' })
.create("Post", { id: "p0" })
.create("Post", { id: "p1" });
cy.factory()
.create('User', yetAnotherAuthor)
.create("User", yetAnotherAuthor)
.authenticateAs(yetAnotherAuthor)
.create('Post', { id: 'p2' })
.create("Post", { id: "p2" });
cy.factory()
.authenticateAs(loginCredentials)
.create('Post', { id: 'p3' })
.relate('Post', 'Categories', { from: 'p0', to: 'cat1' })
.relate('Post', 'Categories', { from: 'p1', to: 'cat2' })
.relate('Post', 'Categories', { from: 'p2', to: 'cat1' })
.relate('Post', 'Tags', { from: 'p0', to: 't1' })
.relate('Post', 'Tags', { from: 'p0', to: 't2' })
.relate('Post', 'Tags', { from: 'p0', to: 't3' })
.relate('Post', 'Tags', { from: 'p1', to: 't2' })
.relate('Post', 'Tags', { from: 'p1', to: 't3' })
.relate('Post', 'Tags', { from: 'p2', to: 't2' })
.relate('Post', 'Tags', { from: 'p2', to: 't3' })
.relate('Post', 'Tags', { from: 'p3', to: 't3' })
})
.create("Post", { id: "p3" })
.relate("Post", "Categories", { from: "p0", to: "cat1" })
.relate("Post", "Categories", { from: "p1", to: "cat2" })
.relate("Post", "Categories", { from: "p2", to: "cat1" })
.relate("Post", "Tags", { from: "p0", to: "t1" })
.relate("Post", "Tags", { from: "p0", to: "t2" })
.relate("Post", "Tags", { from: "p0", to: "t3" })
.relate("Post", "Tags", { from: "p1", to: "t2" })
.relate("Post", "Tags", { from: "p1", to: "t3" })
.relate("Post", "Tags", { from: "p2", to: "t2" })
.relate("Post", "Tags", { from: "p2", to: "t3" })
.relate("Post", "Tags", { from: "p3", to: "t3" });
});
Given('we have the following user accounts:', table => {
Given("we have the following user accounts:", table => {
table.hashes().forEach(params => {
cy.factory().create('User', params)
})
})
cy.factory().create("User", params);
});
});
Given('I have a user account', () => {
cy.factory().create('User', narratorParams)
})
Given("I have a user account", () => {
cy.factory().create("User", narratorParams);
});
Given('my user account has the role {string}', role => {
cy.factory().create('User', {
Given("my user account has the role {string}", role => {
cy.factory().create("User", {
role,
...loginCredentials
})
})
});
});
When('I log out', cy.logout)
When("I log out", cy.logout);
When('I visit {string}', page => {
cy.openPage(page)
})
When("I visit {string}", page => {
cy.openPage(page);
});
When('I visit the {string} page', page => {
cy.openPage(page)
})
When("I visit the {string} page", page => {
cy.openPage(page);
});
Given('I am on the {string} page', page => {
cy.openPage(page)
})
Given("I am on the {string} page", page => {
cy.openPage(page);
});
When('I fill in my email and password combination and click submit', () => {
cy.login(loginCredentials)
})
When("I fill in my email and password combination and click submit", () => {
cy.login(loginCredentials);
});
When(/(?:when )?I refresh the page/, () => {
cy.reload()
})
cy.reload();
});
When('I log out through the menu in the top right corner', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
When("I log out through the menu in the top right corner", () => {
cy.get(".avatar-menu").click();
cy.get(".avatar-menu-popover")
.find('a[href="/logout"]')
.click()
})
.click();
});
Then('I can see my name {string} in the dropdown menu', () => {
cy.get('.avatar-menu-popover').should('contain', narratorParams.name)
})
Then("I can see my name {string} in the dropdown menu", () => {
cy.get(".avatar-menu-popover").should("contain", narratorParams.name);
});
Then('I see the login screen again', () => {
cy.location('pathname').should('contain', '/login')
})
Then("I see the login screen again", () => {
cy.location("pathname").should("contain", "/login");
});
Then('I can click on my profile picture in the top right corner', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
})
Then("I can click on my profile picture in the top right corner", () => {
cy.get(".avatar-menu").click();
cy.get(".avatar-menu-popover");
});
Then('I am still logged in', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover').contains(narratorParams.name)
})
Then("I am still logged in", () => {
cy.get(".avatar-menu").click();
cy.get(".avatar-menu-popover").contains(narratorParams.name);
});
When('I select {string} in the language menu', name => {
cy.switchLanguage(name, true)
})
Given('I previously switched the language to {string}', name => {
cy.switchLanguage(name, true)
})
Then('the whole user interface appears in {string}', name => {
const lang = getLangByName(name)
cy.get(`html[lang=${lang.code}]`)
cy.getCookie('locale').should('have.property', 'value', lang.code)
})
Then('I see a button with the label {string}', label => {
cy.contains('button', label)
})
When("I select {string} in the language menu", name => {
cy.switchLanguage(name, true);
});
Given("I previously switched the language to {string}", name => {
cy.switchLanguage(name, true);
});
Then("the whole user interface appears in {string}", name => {
const lang = getLangByName(name);
cy.get(`html[lang=${lang.code}]`);
cy.getCookie("locale").should("have.property", "value", lang.code);
});
Then("I see a button with the label {string}", label => {
cy.contains("button", label);
});
When(`I click on {string}`, linkOrButton => {
cy.contains(linkOrButton).click()
})
cy.contains(linkOrButton).click();
});
When(`I click on the menu item {string}`, linkOrButton => {
cy.contains('.ds-menu-item', linkOrButton).click()
})
cy.contains(".ds-menu-item", linkOrButton).click();
});
When('I press {string}', label => {
cy.contains(label).click()
})
When("I press {string}", label => {
cy.contains(label).click();
});
Given('we have the following posts in our database:', table => {
Given("we have the following posts in our database:", table => {
table.hashes().forEach(({ Author, ...postAttributes }) => {
const userAttributes = {
name: Author,
email: `${Author}@example.org`,
password: '1234'
}
postAttributes.deleted = Boolean(postAttributes.deleted)
const disabled = Boolean(postAttributes.disabled)
password: "1234"
};
postAttributes.deleted = Boolean(postAttributes.deleted);
const disabled = Boolean(postAttributes.disabled);
cy.factory()
.create('User', userAttributes)
.create("User", userAttributes)
.authenticateAs(userAttributes)
.create('Post', postAttributes)
.create("Post", postAttributes);
if (disabled) {
const moderatorParams = {
email: 'moderator@example.org',
role: 'moderator',
password: '1234'
}
email: "moderator@example.org",
role: "moderator",
password: "1234"
};
cy.factory()
.create('User', moderatorParams)
.create("User", moderatorParams)
.authenticateAs(moderatorParams)
.mutate('mutation($id: ID!) { disable(id: $id) }', postAttributes)
.mutate("mutation($id: ID!) { disable(id: $id) }", postAttributes);
}
})
})
});
});
Then('I see a success message:', message => {
cy.contains(message)
})
Then("I see a success message:", message => {
cy.contains(message);
});
When('I click on the avatar menu in the top right corner', () => {
cy.get('.avatar-menu').click()
})
When("I click on the avatar menu in the top right corner", () => {
cy.get(".avatar-menu").click();
});
When(
'I click on the big plus icon in the bottom right corner to create post',
"I click on the big plus icon in the bottom right corner to create post",
() => {
cy.get('.post-add-button').click()
cy.get(".post-add-button").click();
}
)
);
Given('I previously created a post', () => {
Given("I previously created a post", () => {
lastPost.title = "previously created post";
lastPost.content = "with some content";
cy.factory()
.authenticateAs(loginCredentials)
.create('Post', lastPost)
})
.create("Post", lastPost);
});
When('I choose {string} as the title of the post', title => {
lastPost.title = title.replace('\n', ' ')
cy.get('input[name="title"]').type(lastPost.title)
})
When("I choose {string} as the title of the post", title => {
lastPost.title = title.replace("\n", " ");
cy.get('input[name="title"]').type(lastPost.title);
});
When('I type in the following text:', text => {
lastPost.content = text.replace('\n', ' ')
cy.get('.ProseMirror').type(lastPost.content)
})
When("I type in the following text:", text => {
lastPost.content = text.replace("\n", " ");
cy.get(".ProseMirror").type(lastPost.content);
});
Then('the post shows up on the landing page at position {int}', index => {
cy.openPage('landing')
const selector = `.post-card:nth-child(${index}) > .ds-card-content`
cy.get(selector).should('contain', lastPost.title)
cy.get(selector).should('contain', lastPost.content)
})
Then("the post shows up on the landing page at position {int}", index => {
cy.openPage("landing");
const selector = `.post-card:nth-child(${index}) > .ds-card-content`;
cy.get(selector).should("contain", lastPost.title);
cy.get(selector).should("contain", lastPost.content);
});
Then('I get redirected to {string}', route => {
cy.location('pathname').should('contain', route.replace('...', ''))
})
Then("I get redirected to {string}", route => {
cy.location("pathname").should("contain", route.replace("...", ""));
});
Then('the post was saved successfully', () => {
cy.get('.ds-card-content > .ds-heading').should('contain', lastPost.title)
cy.get('.content').should('contain', lastPost.content)
})
Then("the post was saved successfully", () => {
cy.get(".ds-card-content > .ds-heading").should("contain", lastPost.title);
cy.get(".content").should("contain", lastPost.content);
});
Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
cy.get('.post-card').should('have.length', postCount)
})
cy.get(".post-card").should("have.length", postCount);
});
Then('the first post on the landing page has the title:', title => {
cy.get('.post-card:first').should('contain', title)
})
Then("the first post on the landing page has the title:", title => {
cy.get(".post-card:first").should("contain", title);
});
Then(
'the page {string} returns a 404 error with a message:',
"the page {string} returns a 404 error with a message:",
(route, message) => {
// TODO: how can we check HTTP codes with cypress?
cy.visit(route, { failOnStatusCode: false })
cy.get('.error').should('contain', message)
cy.visit(route, { failOnStatusCode: false });
cy.get(".error").should("contain", message);
}
)
);
Given('my user account has the following login credentials:', table => {
loginCredentials = table.hashes()[0]
cy.debug()
cy.factory().create('User', loginCredentials)
})
Given("my user account has the following login credentials:", table => {
loginCredentials = table.hashes()[0];
cy.debug();
cy.factory().create("User", loginCredentials);
});
When('I fill the password form with:', table => {
table = table.rowsHash()
cy.get('input[id=oldPassword]')
.type(table['Your old password'])
.get('input[id=newPassword]')
.type(table['Your new passsword'])
.get('input[id=confirmPassword]')
.type(table['Confirm new password'])
})
When("I fill the password form with:", table => {
table = table.rowsHash();
cy.get("input[id=oldPassword]")
.type(table["Your old password"])
.get("input[id=newPassword]")
.type(table["Your new passsword"])
.get("input[id=confirmPassword]")
.type(table["Confirm new password"]);
});
When('submit the form', () => {
cy.get('form').submit()
})
When("submit the form", () => {
cy.get("form").submit();
});
Then('I cannot login anymore with password {string}', password => {
cy.reload()
const { email } = loginCredentials
cy.visit(`/login`)
cy.get('input[name=email]')
.trigger('focus')
.type(email)
cy.get('input[name=password]')
.trigger('focus')
.type(password)
cy.get('button[name=submit]')
.as('submitButton')
.click()
cy.get('.iziToast-wrapper').should('contain', 'Incorrect email address or password.')
})
Then("I cannot login anymore with password {string}", password => {
cy.reload();
const { email } = loginCredentials;
cy.visit(`/login`);
cy.get("input[name=email]")
.trigger("focus")
.type(email);
cy.get("input[name=password]")
.trigger("focus")
.type(password);
cy.get("button[name=submit]")
.as("submitButton")
.click();
cy.get(".iziToast-wrapper").should(
"contain",
"Incorrect email address or password."
);
});
Then('I can login successfully with password {string}', password => {
cy.reload()
Then("I can login successfully with password {string}", password => {
cy.reload();
cy.login({
...loginCredentials,
...{password}
})
cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
})
...{ password }
});
cy.get(".iziToast-wrapper").should("contain", "You are logged in!");
});
When('I log in with the following credentials:', table => {
const { email, password } = table.hashes()[0]
cy.login({ email, password })
})
When("I log in with the following credentials:", table => {
const { email, password } = table.hashes()[0];
cy.login({ email, password });
});
When('open the notification menu and click on the first item', () => {
cy.get('.notifications-menu').click()
cy.get('.notification-mention-post').first().click()
})
When("open the notification menu and click on the first item", () => {
cy.get(".notifications-menu").click();
cy.get(".notification-mention-post")
.first()
.click();
});
Then('see {int} unread notifications in the top menu', count => {
cy.get('.notifications-menu').should('contain', count)
})
Then("see {int} unread notifications in the top menu", count => {
cy.get(".notifications-menu").should("contain", count);
});
Then('I get to the post page of {string}', path => {
path = path.replace('...', '')
cy.url().should('contain', '/post/')
cy.url().should('contain', path)
})
Then("I get to the post page of {string}", path => {
path = path.replace("...", "");
cy.url().should("contain", "/post/");
cy.url().should("contain", path);
});
When('I start to write a new post with the title {string} beginning with:', (title, intro) => {
cy.get('.post-add-button').click()
cy.get('input[name="title"]').type(title)
cy.get('.ProseMirror').type(intro)
})
When(
"I start to write a new post with the title {string} beginning with:",
(title, intro) => {
cy.get(".post-add-button").click();
cy.get('input[name="title"]').type(title);
cy.get(".ProseMirror").type(intro);
}
);
When('mention {string} in the text', (mention) => {
cy.get('.ProseMirror').type(' @')
cy.get('.suggestion-list__item').contains(mention).click()
cy.debug()
})
When("mention {string} in the text", mention => {
cy.get(".ProseMirror").type(" @");
cy.get(".suggestion-list__item")
.contains(mention)
.click();
cy.debug();
});
Then('the notification gets marked as read', () => {
cy.get('.notification').first().should('have.class', 'read')
})
Then("the notification gets marked as read", () => {
cy.get(".notification")
.first()
.should("have.class", "read");
});
Then('there are no notifications in the top menu', () => {
cy.get('.notifications-menu').should('contain', '0')
})
Then("there are no notifications in the top menu", () => {
cy.get(".notifications-menu").should("contain", "0");
});

View File

@ -12,10 +12,10 @@ Feature: Create a post
When I click on the big plus icon in the bottom right corner to create post
And I choose "My first post" as the title of the post
And I type in the following text:
"""
Human Connection is a free and open-source social network
for active citizenship.
"""
"""
Human Connection is a free and open-source social network
for active citizenship.
"""
And I click on "Save"
Then I get redirected to ".../my-first-post"
And the post was saved successfully

View File

@ -10,6 +10,7 @@ metadata:
spec:
tls:
- hosts:
# - nitro-mailserver.human-connection.org
- nitro-staging.human-connection.org
secretName: tls
rules:
@ -20,3 +21,10 @@ spec:
backend:
serviceName: nitro-web
servicePort: 3000
# - host: nitro-mailserver.human-connection.org
# http:
# paths:
# - path: /
# backend:
# serviceName: mailserver
# servicePort: 80

View File

@ -0,0 +1,18 @@
# Development Mail Server
You can deploy a fake smtp server which captures all send mails and displays
them in a web interface. The [sample configuration](../templates/configmap.template.yml)
is assuming such a dummy server in the `SMTP_HOST` configuration and points to
a cluster-internal SMTP server.
To deploy the SMTP server just uncomment the relevant code in the
[ingress server configuration](../../https/templates/ingress.template.yaml) and
run the following:
```bash
# in folder deployment/human-connection
kubectl apply -f mailserver/
```
You might need to refresh the TLS secret to enable HTTPS on the publicly
available web interface.

View File

@ -0,0 +1,34 @@
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailserver
namespace: human-connection
spec:
replicas: 1
minReadySeconds: 15
progressDeadlineSeconds: 60
selector:
matchLabels:
human-connection.org/selector: deployment-human-connection-mailserver
template:
metadata:
labels:
human-connection.org/selector: deployment-human-connection-mailserver
name: "mailserver"
spec:
containers:
- name: mailserver
image: djfarrelly/maildev
imagePullPolicy: Always
ports:
- containerPort: 80
- containerPort: 25
envFrom:
- configMapRef:
name: configmap
- secretRef:
name: human-connection
restartPolicy: Always
terminationGracePeriodSeconds: 30
status: {}

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: mailserver
namespace: human-connection
labels:
human-connection.org/selector: deployment-human-connection-mailserver
spec:
ports:
- name: web
port: 80
targetPort: 80
- name: smtp
port: 25
targetPort: 25
selector:
human-connection.org/selector: deployment-human-connection-mailserver

View File

@ -2,6 +2,10 @@
apiVersion: v1
kind: ConfigMap
data:
SMTP_HOST: "mailserver.human-connection"
SMTP_PORT: "25"
SMTP_USERNAME: ""
SMTP_PASSWORD: ""
GRAPHQL_PORT: "4000"
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
MOCKS: "false"

View File

@ -5,6 +5,11 @@ data:
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
SMTP_HOST:
SMTP_PORT: 587
SMTP_USERNAME:
SMTP_PASSWORD:
SMTP_IGNORE_TLS:
metadata:
name: human-connection
namespace: human-connection

View File

@ -0,0 +1,6 @@
# SSH Access
# SSH_USERNAME='username'
# SSH_HOST='example.org'
# UPLOADS_DIRECTORY=/var/www/api/uploads
OUTPUT_DIRECTORY='/uploads/'

View File

@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -e
# import .env config
set -o allexport
source $(dirname "$0")/.env
set +o allexport
for var in "SSH_USERNAME" "SSH_HOST" "UPLOADS_DIRECTORY"
do
if [[ -z "${!var}" ]]; then
@ -9,4 +14,4 @@ do
fi
done
rsync --archive --update --verbose ${SSH_USERNAME}@${SSH_HOST}:${UPLOADS_DIRECTORY}/ /uploads/
rsync --archive --update --verbose ${SSH_USERNAME}@${SSH_HOST}:${UPLOADS_DIRECTORY}/ ${OUTPUT_DIRECTORY}

View File

@ -8,11 +8,18 @@ set +o allexport
# Export collection function defintion
function export_collection () {
"${EXPORT_MONGOEXPORT_BIN}" --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $1 --collection $1 --out "${EXPORT_PATH}$1.json"
"${EXPORT_MONGOEXPORT_BIN}" --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $1 --out "${EXPORT_PATH}$1.json"
mkdir -p ${EXPORT_PATH}splits/$1/
split -l ${MONGO_EXPORT_SPLIT_SIZE} -a 3 ${EXPORT_PATH}$1.json ${EXPORT_PATH}splits/$1/
}
# Export collection with query function defintion
function export_collection_query () {
"${EXPORT_MONGOEXPORT_BIN}" --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $1 --out "${EXPORT_PATH}$1_$3.json" --query "$2"
mkdir -p ${EXPORT_PATH}splits/$1_$3/
split -l ${MONGO_EXPORT_SPLIT_SIZE} -a 3 ${EXPORT_PATH}$1_$3.json ${EXPORT_PATH}splits/$1_$3/
}
# Delete old export & ensure directory
rm -rf ${EXPORT_PATH}*
mkdir -p ${EXPORT_PATH}
@ -24,9 +31,12 @@ ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${
export_collection "badges"
export_collection "categories"
export_collection "comments"
export_collection "contributions"
export_collection_query "contributions" "{'type': 'DELETED'}" "DELETED"
export_collection_query "contributions" "{'type': 'post'}" "post"
export_collection_query "contributions" "{'type': 'cando'}" "cando"
export_collection "emotions"
export_collection "follows"
export_collection_query "follows" "{'foreignService': 'organizations'}" "organizations"
export_collection_query "follows" "{'foreignService': 'users'}" "users"
export_collection "invites"
export_collection "notifications"
export_collection "organizations"

View File

@ -45,7 +45,7 @@ MERGE(b:Badge {id: badge._id["$oid"]})
ON CREATE SET
b.key = badge.key,
b.type = badge.type,
b.icon = badge.image.path,
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
b.status = badge.status,
b.createdAt = badge.createdAt.`$date`,
b.updatedAt = badge.updatedAt.`$date`

View File

@ -28,7 +28,7 @@
[?] unique: true, // Unique value is not enforced in Nitro?
[-] index: true
},
[ ] type: {
[ ] type: { // db.getCollection('contributions').distinct('type') -> 'DELETED', 'cando', 'post'
[ ] type: String,
[ ] required: true,
[-] index: true
@ -50,7 +50,7 @@
[?] required: true // Not required in Nitro
},
[ ] hasMore: { type: Boolean },
[?] teaserImg: { type: String }, // Path is incorrect in Nitro
[X] teaserImg: { type: String },
[ ] language: {
[ ] type: String,
[ ] required: true,
@ -109,8 +109,8 @@
}
}
},
[?] deleted: {
[X] type: Boolean,
[?] deleted: { // THis field is not always present in the alpha-data
[?] type: Boolean,
[ ] default: false, // Default value is missing in Nitro
[-] index: true
},
@ -131,13 +131,13 @@ MERGE (p:Post {id: post._id["$oid"]})
ON CREATE SET
p.title = post.title,
p.slug = post.slug,
p.image = post.teaserImg,
p.image = replace(post.teaserImg, 'https://api-alpha.human-connection.org', ''),
p.content = post.content,
p.contentExcerpt = post.contentExcerpt,
p.visibility = toLower(post.visibility),
p.createdAt = post.createdAt.`$date`,
p.updatedAt = post.updatedAt.`$date`,
p.deleted = post.deleted,
p.deleted = COALESCE(post.deleted,false),
p.disabled = NOT post.isEnabled
WITH p, post
MATCH (u:User {id: post.userId})

View File

@ -9,10 +9,10 @@ set +o allexport
# Delete collection function defintion
function delete_collection () {
# Delete from Database
echo "Delete $1"
"${IMPORT_CYPHERSHELL_BIN}" < $(dirname "$0")/$1_delete.cql > /dev/null
echo "Delete $2"
"${IMPORT_CYPHERSHELL_BIN}" < $(dirname "$0")/$1/delete.cql > /dev/null
# Delete index file
rm -f "${IMPORT_PATH}splits/$1.index"
rm -f "${IMPORT_PATH}splits/$2.index"
}
# Import collection function defintion
@ -34,7 +34,7 @@ function import_collection () {
# calculate the path of the chunk
export IMPORT_CHUNK_PATH_CQL_FILE="${IMPORT_CHUNK_PATH_CQL}$1/${CHUNK_FILE_NAME}"
# load the neo4j command and replace file variable with actual path
NEO4J_COMMAND="$(envsubst '${IMPORT_CHUNK_PATH_CQL_FILE}' < $(dirname "$0")/$1.cql)"
NEO4J_COMMAND="$(envsubst '${IMPORT_CHUNK_PATH_CQL_FILE}' < $(dirname "$0")/$2)"
# run the import of the chunk
echo "Import $1 ${CHUNK_FILE_NAME} (${chunk})"
echo "${NEO4J_COMMAND}" | "${IMPORT_CYPHERSHELL_BIN}" > /dev/null
@ -52,13 +52,14 @@ SECONDS=0
# Delete all Neo4J Database content
echo "Deleting Database Contents"
delete_collection "badges"
delete_collection "categories"
delete_collection "users"
delete_collection "follows"
delete_collection "contributions"
delete_collection "shouts"
delete_collection "comments"
delete_collection "badges" "badges"
delete_collection "categories" "categories"
delete_collection "users" "users"
delete_collection "follows" "follows_users"
delete_collection "contributions" "contributions_post"
delete_collection "contributions" "contributions_cando"
delete_collection "shouts" "shouts"
delete_collection "comments" "comments"
#delete_collection "emotions"
#delete_collection "invites"
@ -75,26 +76,33 @@ echo "DONE"
# Import Data
echo "Start Importing Data"
import_collection "badges"
import_collection "categories"
import_collection "users"
import_collection "follows"
import_collection "contributions"
import_collection "shouts"
import_collection "comments"
import_collection "badges" "badges/badges.cql"
import_collection "categories" "categories/categories.cql"
import_collection "users" "users/users.cql"
import_collection "follows_users" "follows/follows.cql"
#import_collection "follows_organizations" "follows/follows.cql"
import_collection "contributions_post" "contributions/contributions.cql"
import_collection "contributions_cando" "contributions/contributions.cql"
#import_collection "contributions_DELETED" "contributions/contributions.cql"
import_collection "shouts" "shouts/shouts.cql"
import_collection "comments" "comments/comments.cql"
# import_collection "emotions"
# import_collection "invites"
# import_collection "notifications"
# import_collection "organizations"
# import_collection "pages"
# import_collection "projects"
# import_collection "settings"
# import_collection "status"
# import_collection "systemnotifications"
# import_collection "userscandos"
# import_collection "usersettings"
# does only contain dummy data
# import_collection "projects"
# does only contain alpha specifc data
# import_collection "status
# import_collection "settings""
echo "DONE"
echo "Time elapsed: $SECONDS seconds"

View File

@ -49,8 +49,8 @@
}
},
[ ] timezone: { type: String },
[?] avatar: { type: String }, // Path is incorrect in Nitro
[?] coverImg: { type: String }, // Path is incorrect in Nitro, was not modeled in latest Nitro - do we want this?
[X] avatar: { type: String },
[X] coverImg: { type: String },
[ ] doiToken: { type: String },
[ ] confirmedAt: { type: Date },
[?] badgeIds: [], // Verify this is working properly
@ -102,8 +102,8 @@ u.name = user.name,
u.slug = user.slug,
u.email = user.email,
u.password = user.password,
u.avatar = user.avatar,
u.coverImg = user.coverImg,
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
u.wasInvited = user.wasInvited,
u.wasSeeded = user.wasSeeded,
u.role = toLower(user.role),

View File

@ -1,6 +1,12 @@
version: "3.4"
services:
mailserver:
image: djfarrelly/maildev
ports:
- 1080:80
networks:
- hc-network
webapp:
build:
context: webapp
@ -20,6 +26,10 @@ services:
- backend_node_modules:/nitro-backend/node_modules
- uploads:/nitro-backend/public/uploads
command: yarn run dev
environment:
- SMTP_HOST=mailserver
- SMTP_PORT=25
- SMTP_IGNORE_TLS=true
neo4j:
environment:
- NEO4J_AUTH=none

View File

@ -22,13 +22,13 @@
"codecov": "^3.5.0",
"cross-env": "^5.2.0",
"cypress": "^3.3.1",
"cypress-cucumber-preprocessor": "^1.11.2",
"cypress-file-upload": "^3.1.2",
"cypress-cucumber-preprocessor": "^1.12.0",
"cypress-file-upload": "^3.1.4",
"cypress-plugin-retries": "^1.2.2",
"dotenv": "^8.0.0",
"faker": "^4.1.0",
"faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.5",
"npm-run-all": "^4.1.5"
}
}
}

View File

@ -10,7 +10,7 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
&::before {
@include border-radius($border-radius-x-large);
box-shadow: inset 0 0 0 5px $color-danger;
content: "";
content: '';
display: block;
position: absolute;
width: 100%;
@ -102,10 +102,10 @@ hr {
height: 1px !important;
}
[class$=menu-trigger] {
[class$='menu-trigger'] {
user-select: none;
}
[class$=menu-popover] {
[class$='menu-popover'] {
display: inline-block;
nav {
@ -145,10 +145,11 @@ hr {
}
}
[class$="menu-popover"] {
[class$='menu-popover'] {
min-width: 130px;
a, button {
a,
button {
display: flex;
align-content: center;
align-items: center;

View File

@ -1,9 +1,11 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Styleguide from '@human-connection/styleguide'
import Avatar from './Avatar.vue'
import Filters from '~/plugins/vue-filters'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(Filters)
describe('Avatar.vue', () => {
let propsData = {}
@ -51,7 +53,7 @@ describe('Avatar.vue', () => {
beforeEach(() => {
propsData = {
user: {
avatar: 'http://lorempixel.com/640/480/animals',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
})
@ -62,7 +64,7 @@ describe('Avatar.vue', () => {
Wrapper()
.find('img')
.attributes('src'),
).toBe('http://lorempixel.com/640/480/animals')
).toBe('https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg')
})
})
})

View File

@ -1,5 +1,10 @@
<template>
<ds-avatar :image="avatarUrl" :name="userName" class="avatar" :size="size" />
<ds-avatar
:image="user && user.avatar | proxyApiUrl"
:name="userName"
class="avatar"
:size="size"
/>
</template>
<script>
@ -10,11 +15,6 @@ export default {
size: { type: String, default: 'small' },
},
computed: {
avatarUrl() {
const { avatar: imageSrc } = this.user || {}
if (!imageSrc) return imageSrc
return imageSrc.startsWith('/') ? imageSrc.replace('/', '/api/') : imageSrc
},
userName() {
const { name } = this.user || {}
// The name is used to display the initials in case

View File

@ -1,17 +1,13 @@
<template>
<div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges">
<div v-for="badge in badges" :key="badge.key" class="hc-badge-container">
<hc-image :title="badge.key" :image-props="{ src: badge.icon }" class="hc-badge" />
<img :title="badge.key" :src="badge.icon | proxyApiUrl" class="hc-badge" />
</div>
</div>
</template>
<script>
import HcImage from './Image'
export default {
components: {
HcImage,
},
props: {
badges: {
type: Array,

View File

@ -25,6 +25,9 @@ describe('Comment.vue', () => {
success: jest.fn(),
error: jest.fn(),
},
$filters: {
truncate: a => a,
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},

View File

@ -9,12 +9,13 @@
<ds-space margin-bottom="x-small">
<hc-user :user="author" :date-time="comment.createdAt"/>
</ds-space>
<!-- Content Menu (can open Modals) -->
<no-ssr>
<content-menu
placement="bottom-end"
resource-type="comment"
:resource="comment"
:callbacks="{ confirm: deleteCommentCallback, cancel: null }"
:modalsData="menuModalsData"
style="float-right"
:is-owner="isAuthor(author.id)"
v-on:showEditCommentMenu="editCommentMenu"
@ -75,6 +76,30 @@ export default {
if (this.deleted) return {}
return this.comment.author || {}
},
menuModalsData() {
return {
delete: {
titleIdent: 'delete.comment.title',
messageIdent: 'delete.comment.message',
messageParams: {
name: this.$filters.truncate(this.comment.contentExcerpt, 30),
},
buttons: {
confirm: {
danger: true,
icon: 'trash',
textIdent: 'delete.submit',
callback: this.deleteCommentCallback,
},
cancel: {
icon: 'close',
textIdent: 'delete.cancel',
callback: () => {},
},
},
},
}
},
},
methods: {
isAuthor(id) {

View File

@ -43,7 +43,13 @@ export default {
return value.match(/(contribution|comment|organization|user)/)
},
},
callbacks: { type: Object, required: true },
modalsData: {
type: Object,
required: false,
// default: () => {
// return {}
// },
},
},
computed: {
routes() {
@ -146,7 +152,7 @@ export default {
data: {
type: this.resourceType,
resource: this.resource,
callbacks: this.callbacks,
modalsData: this.modalsData,
},
})
},

View File

@ -0,0 +1,213 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import ContributionForm from './index.vue'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import PostMutations from '~/graphql/PostMutations.js'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['no-ssr'] = '<span><slot /></span>'
describe('ContributionForm.vue', () => {
let wrapper
let postTitleInput
let expectedParams
let deutschOption
let cancelBtn
let mocks
let propsData
const postTitle = 'this is a title for a post'
const postContent = 'this is a post'
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
CreatePost: {
title: postTitle,
slug: 'this-is-a-title-for-a-post',
content: postContent,
contentExcerpt: postContent,
language: 'en',
},
},
})
.mockRejectedValue({ message: 'Not Authorised!' }),
},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
$i18n: {
locale: () => 'en',
},
$router: {
back: jest.fn(),
push: jest.fn(),
},
}
propsData = {}
})
describe('mount', () => {
const getters = {
'editor/placeholder': () => {
return 'some cool placeholder'
},
}
const store = new Vuex.Store({
getters,
})
const Wrapper = () => {
return mount(ContributionForm, { mocks, localVue, store, propsData })
}
beforeEach(() => {
wrapper = Wrapper()
wrapper.setData({ form: { languageOptions: [{ label: 'Deutsch', value: 'de' }] } })
})
describe('CreatePost', () => {
describe('language placeholder', () => {
it("displays the name that corresponds with the user's location code", () => {
expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English')
})
})
describe('invalid form submission', () => {
it('title required for form submission', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content required for form submission', async () => {
wrapper.vm.updateEditorContent(postContent)
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
})
describe('valid form submission', () => {
beforeEach(async () => {
expectedParams = {
mutation: PostMutations().CreatePost,
variables: { title: postTitle, content: postContent, language: 'en', id: null },
}
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent)
await wrapper.find('form').trigger('submit')
})
it('with title and content', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it("sends a fallback language based on a user's locale", () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('supports changing the language', async () => {
expectedParams.variables.language = 'de'
deutschOption = wrapper.findAll('li').at(0)
deutschOption.trigger('click')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it("pushes the user to the post's page", async () => {
expect(mocks.$router.push).toHaveBeenCalledTimes(1)
})
it('shows a success toaster', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
})
describe('cancel', () => {
it('calls $router.back() when cancel button clicked', () => {
cancelBtn = wrapper.find('.cancel-button')
cancelBtn.trigger('click')
expect(mocks.$router.back).toHaveBeenCalledTimes(1)
})
})
describe('handles errors', () => {
beforeEach(async () => {
wrapper = Wrapper()
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent)
// second submission causes mutation to reject
await wrapper.find('form').trigger('submit')
})
it('shows an error toaster when apollo mutation rejects', async () => {
await wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
})
})
})
describe('UpdatePost', () => {
beforeEach(() => {
propsData = {
contribution: {
id: 'p1456',
slug: 'dies-ist-ein-post',
title: 'dies ist ein Post',
content: 'auf Deutsch geschrieben',
language: 'de',
},
}
wrapper = Wrapper()
})
it('sets id equal to contribution id', () => {
expect(wrapper.vm.id).toEqual(propsData.contribution.id)
})
it('sets slug equal to contribution slug', () => {
expect(wrapper.vm.slug).toEqual(propsData.contribution.slug)
})
it('sets title equal to contribution title', () => {
expect(wrapper.vm.form.title).toEqual(propsData.contribution.title)
})
it('sets content equal to contribution content', () => {
expect(wrapper.vm.form.content).toEqual(propsData.contribution.content)
})
it('sets language equal to contribution language', () => {
expect(wrapper.vm.form.language).toEqual({ value: propsData.contribution.language })
})
it('calls the UpdatePost apollo mutation', async () => {
expectedParams = {
mutation: PostMutations().UpdatePost,
variables: {
title: postTitle,
content: postContent,
language: propsData.contribution.language,
id: propsData.contribution.id,
},
}
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent)
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
})
})
})

View File

@ -6,8 +6,27 @@
<no-ssr>
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
</no-ssr>
<ds-space margin-bottom="xxx-large" />
<ds-flex class="contribution-form-footer">
<ds-flex-item :width="{ base: '10%', sm: '10%', md: '10%', lg: '15%' }" />
<ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }">
<ds-space margin-bottom="small" />
<ds-select
model="language"
:options="form.languageOptions"
icon="globe"
:placeholder="locale"
:label="$t('contribution.languageSelectLabel')"
/>
</ds-flex-item>
</ds-flex>
<div slot="footer" style="text-align: right">
<ds-button :disabled="loading || disabled" ghost @click.prevent="$router.back()">
<ds-button
:disabled="loading || disabled"
ghost
class="cancel-button"
@click="$router.back()"
>
{{ $t('actions.cancel') }}
</ds-button>
<ds-button
@ -28,6 +47,9 @@
<script>
import gql from 'graphql-tag'
import HcEditor from '~/components/Editor'
import orderBy from 'lodash/orderBy'
import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js'
export default {
components: {
@ -41,6 +63,8 @@ export default {
form: {
title: '',
content: '',
language: null,
languageOptions: [],
},
formSchema: {
title: { required: true, min: 3, max: 64 },
@ -64,26 +88,38 @@ export default {
this.slug = contribution.slug
this.form.content = contribution.content
this.form.title = contribution.title
this.form.language = { value: contribution.language }
},
},
},
computed: {
locale() {
const locale =
this.contribution && this.contribution.language
? locales.find(loc => this.contribution.language === loc.code)
: locales.find(loc => this.$i18n.locale() === loc.code)
return locale.name
},
},
mounted() {
this.availableLocales()
},
methods: {
submit() {
const postMutations = require('~/graphql/PostMutations.js').default(this)
this.loading = true
this.$apollo
.mutate({
mutation: this.id ? postMutations.UpdatePost : postMutations.CreatePost,
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
variables: {
id: this.id,
title: this.form.title,
content: this.form.content,
language: this.form.language ? this.form.language.value : this.$i18n.locale(),
},
})
.then(res => {
this.loading = false
this.$toast.success('Saved!')
this.$toast.success(this.$t('contribution.success'))
this.disabled = true
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
@ -103,6 +139,11 @@ export default {
// this.form.content = value
this.$refs.contributionForm.update('content', value)
},
availableLocales() {
orderBy(locales, 'name').map(locale => {
this.form.languageOptions.push({ label: locale.name, value: locale.code })
})
},
},
apollo: {
User: {
@ -135,4 +176,8 @@ export default {
padding-right: 0;
}
}
.contribution-form-footer {
border-top: $border-size-base solid $border-color-softest;
}
</style>

View File

@ -0,0 +1,183 @@
import { mount, createLocalVue } from '@vue/test-utils'
import DeleteData from './DeleteData.vue'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('DeleteData.vue', () => {
let mocks
let wrapper
let getters
let actions
let deleteAccountBtn
let enableDeletionInput
let enableContributionDeletionCheckbox
let enableCommentDeletionCheckbox
const deleteAccountName = 'Delete MyAccount'
const deleteContributionsMessage = 'Delete my 2 posts'
const deleteCommentsMessage = 'Delete my 3 comments'
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
DeleteData: {
id: 'u343',
},
},
})
.mockRejectedValue({ message: 'Not authorised!' }),
},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
$router: {
history: {
push: jest.fn(),
},
},
}
getters = {
'auth/user': () => {
return { id: 'u343', name: deleteAccountName, contributionsCount: 2, commentsCount: 3 }
},
}
actions = { 'auth/logout': jest.fn() }
})
describe('mount', () => {
const Wrapper = () => {
const store = new Vuex.Store({
getters,
actions,
})
return mount(DeleteData, { mocks, localVue, store })
}
beforeEach(() => {
wrapper = Wrapper()
})
afterEach(() => {
jest.clearAllMocks()
})
it('defaults to deleteContributions to false', () => {
expect(wrapper.vm.deleteContributions).toEqual(false)
})
it('defaults to deleteComments to false', () => {
expect(wrapper.vm.deleteComments).toEqual(false)
})
it('defaults to deleteEnabled to false', () => {
expect(wrapper.vm.deleteEnabled).toEqual(false)
})
it('does not call the delete user mutation if deleteEnabled is false', () => {
deleteAccountBtn = wrapper.find('.ds-button-danger')
deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
describe('calls the delete user mutation', () => {
beforeEach(() => {
enableDeletionInput = wrapper.find('.enable-deletion-input input')
enableDeletionInput.setValue(deleteAccountName)
deleteAccountBtn = wrapper.find('.ds-button-danger')
})
it('if deleteEnabled is true and only deletes user by default', () => {
deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
id: 'u343',
resource: [],
},
}),
)
})
it("deletes a user's posts if requested", () => {
mocks.$t.mockImplementation(() => deleteContributionsMessage)
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
enableContributionDeletionCheckbox.trigger('click')
deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
id: 'u343',
resource: ['Post'],
},
}),
)
})
it("deletes a user's comments if requested", () => {
mocks.$t.mockImplementation(() => deleteCommentsMessage)
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
enableCommentDeletionCheckbox.trigger('click')
deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
id: 'u343',
resource: ['Comment'],
},
}),
)
})
it("deletes a user's posts and comments if requested", () => {
mocks.$t.mockImplementation(() => deleteContributionsMessage)
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
enableContributionDeletionCheckbox.trigger('click')
mocks.$t.mockImplementation(() => deleteCommentsMessage)
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
enableCommentDeletionCheckbox.trigger('click')
deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
id: 'u343',
resource: ['Post', 'Comment'],
},
}),
)
})
it('shows a success toaster after successful mutation', async () => {
await deleteAccountBtn.trigger('click')
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('redirect the user to the homepage', async () => {
await deleteAccountBtn.trigger('click')
expect(mocks.$router.history.push).toHaveBeenCalledWith('/')
})
})
describe('error handling', () => {
it('shows an error toaster when the mutation rejects', async () => {
enableDeletionInput = wrapper.find('.enable-deletion-input input')
enableDeletionInput.setValue(deleteAccountName)
deleteAccountBtn = wrapper.find('.ds-button-danger')
await deleteAccountBtn.trigger('click')
// second submission causes mutation to reject
await deleteAccountBtn.trigger('click')
await mocks.$apollo.mutate
expect(mocks.$toast.error).toHaveBeenCalledWith('Not authorised!')
})
})
})
})

View File

@ -0,0 +1,225 @@
<template>
<div>
<ds-card hover>
<ds-space />
<ds-container>
<ds-flex>
<ds-flex-item :width="{ base: '22%', sm: '12%', md: '12%', lg: '8%' }">
<ds-icon name="warning" size="xxx-large" class="delete-warning-icon" />
</ds-flex-item>
<ds-flex-item :width="{ base: '78%', sm: '88%', md: '88%', lg: '92%' }">
<ds-heading>{{ $t('settings.deleteUserAccount.name') }}</ds-heading>
</ds-flex-item>
<ds-space />
<ds-heading tag="h4">
{{ $t('settings.deleteUserAccount.accountDescription') }}
</ds-heading>
</ds-flex>
</ds-container>
<ds-space />
<ds-container>
<transition name="slide-up">
<div v-if="deleteEnabled">
<label v-if="currentUser.contributionsCount" class="checkbox-container">
<input type="checkbox" v-model="deleteContributions" />
<span class="checkmark"></span>
{{
$t('settings.deleteUserAccount.contributionsCount', {
count: currentUser.contributionsCount,
})
}}
</label>
<ds-space margin-bottom="small" />
<label v-if="currentUser.commentsCount" class="checkbox-container">
<input type="checkbox" v-model="deleteComments" />
<span class="checkmark"></span>
{{
$t('settings.deleteUserAccount.commentsCount', {
count: currentUser.commentsCount,
})
}}
</label>
<ds-space margin-bottom="small" />
<ds-section id="delete-user-account-warning">
<div v-html="$t('settings.deleteUserAccount.accountWarning')"></div>
</ds-section>
</div>
</transition>
</ds-container>
<template slot="footer" class="delete-data-footer">
<ds-container>
<div
class="delete-input-label"
v-html="$t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name })"
></div>
<ds-space margin-bottom="xx-small" />
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'large' }">
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1.75 }">
<ds-input
v-model="enableDeletionValue"
@input="enableDeletion"
class="enable-deletion-input"
/>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }">
<ds-button icon="trash" danger :disabled="!deleteEnabled" @click="handleSubmit">
{{ $t('settings.deleteUserAccount.name') }}
</ds-button>
</ds-flex-item>
</ds-flex>
</ds-container>
</template>
</ds-card>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import gql from 'graphql-tag'
export default {
name: 'DeleteData',
data() {
return {
deleteContributions: false,
deleteComments: false,
deleteEnabled: false,
enableDeletionValue: null,
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
},
methods: {
...mapActions({
logout: 'auth/logout',
}),
enableDeletion() {
if (this.enableDeletionValue === this.currentUser.name) {
this.deleteEnabled = true
}
},
handleSubmit() {
let resourceArgs = []
if (this.deleteContributions) {
resourceArgs.push('Post')
}
if (this.deleteComments) {
resourceArgs.push('Comment')
}
this.$apollo
.mutate({
mutation: gql`
mutation($id: ID!, $resource: [String]) {
DeleteUser(id: $id, resource: $resource) {
id
}
}
`,
variables: { id: this.currentUser.id, resource: resourceArgs },
})
.then(() => {
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
this.logout()
this.$router.history.push('/')
})
.catch(error => {
this.$toast.error(error.message)
})
},
},
}
</script>
<style lang="scss">
.delete-warning-icon {
color: $color-danger;
}
.checkbox-container {
display: block;
position: relative;
padding-left: 35px;
cursor: pointer;
font-size: $font-size-large;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 20px;
width: 20px;
border: 2px solid $background-color-inverse-softer;
background-color: $background-color-base;
border-radius: $border-radius-x-large;
}
.checkbox-container:hover input ~ .checkmark {
background-color: $background-color-softest;
}
.checkbox-container input:checked ~ .checkmark {
background-color: $background-color-danger-active;
}
.checkmark:after {
content: '';
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 6px;
top: 3px;
width: 5px;
height: 10px;
border: solid $background-color-base;
border-width: 0 $border-size-large $border-size-large 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.enable-deletion-input input:focus {
border-color: $border-color-danger;
}
.delete-input-label {
font-size: $font-size-base;
}
b.is-danger {
color: $text-color-danger;
}
.delete-data-footer {
border-top: $border-size-base solid $border-color-softest;
background-color: $background-color-danger-inverse;
}
#delete-user-account-warning {
background-color: $background-color-danger-inverse;
border-left: $border-size-x-large solid $background-color-danger-active;
color: $text-color-danger;
margin-left: 0px;
margin-right: 0px;
border-radius: $border-radius-x-large;
}
</style>

View File

@ -12,9 +12,7 @@
@{{ user.slug }}
</div>
</template>
<div v-else class="suggestion-list__item is-empty">
No users found
</div>
<div v-else class="suggestion-list__item is-empty">No users found</div>
</div>
<editor-menu-bubble :editor="editor">
@ -175,6 +173,7 @@ import {
History,
} from 'tiptap-extensions'
import Mention from './nodes/Mention.js'
import { mapGetters } from 'vuex'
let throttleInputEvent
@ -212,7 +211,7 @@ export default {
new ListItem(),
new Placeholder({
emptyNodeClass: 'is-empty',
emptyNodeText: this.$t('editor.placeholder'),
emptyNodeText: this.placeholder || this.$t('editor.placeholder'),
}),
new History(),
new Mention({
@ -297,6 +296,7 @@ export default {
}
},
computed: {
...mapGetters({ placeholder: 'editor/placeholder' }),
hasResults() {
return this.filteredUsers.length
},
@ -316,19 +316,20 @@ export default {
this.editor.setContent(content)
},
},
},
mounted() {
this.$root.$on('changeLanguage', () => {
this.changePlaceHolderText()
})
placeholder: {
immediate: true,
handler: function(val) {
if (!val) {
return
}
this.editor.extensions.options.placeholder.emptyNodeText = val
},
},
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
changePlaceHolderText() {
this.editor.extensions.options.placeholder.emptyNodeText = this.$t('editor.placeholder')
},
// navigate to the previous item
// if it's the first item, navigate to the last one
upHandler() {

View File

@ -1,31 +1,43 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Editor from './'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('Editor.vue', () => {
let wrapper
let propsData
let mocks
let getters
beforeEach(() => {
propsData = {}
mocks = {
$t: () => {},
}
getters = {
'editor/placeholder': () => {
return 'some cool placeholder'
},
}
})
describe('mount', () => {
let Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return (wrapper = mount(Editor, {
mocks,
propsData,
localVue,
sync: false,
stubs: { transition: false },
store,
}))
}
@ -43,5 +55,13 @@ describe('Editor.vue', () => {
expect(wrapper.find('.ProseMirror').text()).toContain('I am a piece of text')
})
})
describe('uses the placeholder', () => {
it('from the store', () => {
expect(wrapper.vm.editor.extensions.options.placeholder.emptyNodeText).toEqual(
'some cool placeholder',
)
})
})
})
})

View File

@ -1,10 +1,12 @@
import { mount, createLocalVue } from '@vue/test-utils'
import FilterMenu from './FilterMenu.vue'
import Styleguide from '@human-connection/styleguide'
import VTooltip from 'v-tooltip'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
describe('FilterMenu.vue', () => {
let wrapper

View File

@ -2,13 +2,16 @@
<ds-card>
<ds-flex>
<ds-flex-item class="filter-menu-title">
<ds-heading size="h3">
{{ $t('filter-menu.title') }}
</ds-heading>
<ds-heading size="h3">{{ $t('filter-menu.title') }}</ds-heading>
</ds-flex-item>
<ds-flex-item>
<div class="filter-menu-buttons">
<ds-button
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="!!filterAuthorIsFollowedById"

View File

@ -1,20 +0,0 @@
<template>
<img v-bind="imageProps" :src="imageSrc" />
</template>
<script>
export default {
props: {
imageProps: {
type: Object,
required: true,
},
},
computed: {
imageSrc() {
const src = this.imageProps.src
return src.startsWith('/') ? src.replace('/', '/api/') : src
},
},
}
</script>

View File

@ -1,39 +0,0 @@
import { shallowMount } from '@vue/test-utils'
import Image from '.'
describe('Image', () => {
let propsData = { imageProps: { class: 'hc-badge', src: '' } }
const Wrapper = () => {
return shallowMount(Image, { propsData })
}
it('renders', () => {
expect(Wrapper().is('img')).toBe(true)
})
it('passes properties down to `img`', () => {
expect(Wrapper().classes()).toEqual(['hc-badge'])
})
describe('given a relative `src`', () => {
beforeEach(() => {
propsData.imageProps.src = '/img/badges/fundraisingbox_de_airship.svg'
})
it('adds a prefix to load the image from the backend', () => {
expect(Wrapper().attributes('src')).toBe('/api/img/badges/fundraisingbox_de_airship.svg')
})
})
describe('given an absolute `src`', () => {
beforeEach(() => {
propsData.imageProps.src = 'http://lorempixel.com/640/480/animals'
})
it('keeps the URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(Wrapper().attributes('src')).toBe('http://lorempixel.com/640/480/animals')
})
})
})

View File

@ -0,0 +1,68 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import VTooltip from 'v-tooltip'
import LocaleSwitch from './LocaleSwitch.vue'
import { mutations } from '~/store/editor'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
localVue.use(VTooltip)
describe('LocaleSwitch.vue', () => {
let wrapper
let mocks
let computed
let deutschLanguageItem
beforeEach(() => {
mocks = {
$i18n: {
locale: () => 'de',
set: jest.fn(),
},
$t: jest.fn(),
setPlaceholderText: jest.fn(),
}
computed = {
current: () => {
return { code: 'en' }
},
routes: () => {
return [
{
name: 'English',
path: 'en',
},
{
name: 'Deutsch',
path: 'de',
},
]
},
}
})
describe('mount', () => {
const store = new Vuex.Store({
mutations: {
'editor/SET_PLACEHOLDER_TEXT': mutations.SET_PLACEHOLDER_TEXT,
},
})
const Wrapper = () => {
return mount(LocaleSwitch, { mocks, localVue, store, computed })
}
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('.locale-menu').trigger('click')
deutschLanguageItem = wrapper.findAll('li').at(1)
deutschLanguageItem.trigger('click')
})
it("changes a user's locale", () => {
expect(mocks.$i18n.set).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -36,6 +36,7 @@
import Dropdown from '~/components/Dropdown'
import find from 'lodash/find'
import orderBy from 'lodash/orderBy'
import { mapMutations } from 'vuex'
export default {
components: {
@ -65,10 +66,11 @@ export default {
},
},
methods: {
...mapMutations({ setPlaceholderText: 'editor/SET_PLACEHOLDER_TEXT' }),
changeLanguage(locale, toggleMenu) {
this.$i18n.set(locale)
toggleMenu()
this.$root.$emit('changeLanguage')
this.setPlaceholderText(this.$t('editor.placeholder'))
},
matcher(locale) {
return locale === this.$i18n.locale()

View File

@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Modal from './Modal.vue'
import DeleteModal from './Modal/DeleteModal.vue'
import ConfirmModal from './Modal/ConfirmModal.vue'
import DisableModal from './Modal/DisableModal.vue'
import ReportModal from './Modal/ReportModal.vue'
import Vuex from 'vuex'
@ -60,7 +60,7 @@ describe('Modal.vue', () => {
it('initially empty', () => {
wrapper = Wrapper()
expect(wrapper.contains(DeleteModal)).toBe(false)
expect(wrapper.contains(ConfirmModal)).toBe(false)
expect(wrapper.contains(DisableModal)).toBe(false)
expect(wrapper.contains(ReportModal)).toBe(false)
})
@ -75,10 +75,6 @@ describe('Modal.vue', () => {
id: 'c456',
title: 'some title',
},
callbacks: {
confirm: null,
cancel: null,
},
},
}
wrapper = Wrapper()
@ -93,10 +89,6 @@ describe('Modal.vue', () => {
type: 'contribution',
name: 'some title',
id: 'c456',
callbacks: {
confirm: null,
cancel: null,
},
})
})
@ -117,20 +109,12 @@ describe('Modal.vue', () => {
name: 'Author name',
},
},
callbacks: {
confirm: null,
cancel: null,
},
}
wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({
type: 'comment',
name: 'Author name',
id: 'c456',
callbacks: {
confirm: null,
cancel: null,
},
})
})
@ -140,20 +124,12 @@ describe('Modal.vue', () => {
resource: {
id: 'c456',
},
callbacks: {
confirm: null,
cancel: null,
},
}
wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({
type: 'comment',
name: '',
id: 'c456',
callbacks: {
confirm: null,
cancel: null,
},
})
})
})

View File

@ -6,7 +6,6 @@
:id="data.resource.id"
:type="data.type"
:name="name"
:callbacks="data.callbacks"
@close="close"
/>
<release-modal
@ -21,22 +20,21 @@
:id="data.resource.id"
:type="data.type"
:name="name"
:callbacks="data.callbacks"
@close="close"
/>
<delete-modal
<confirm-modal
v-if="open === 'delete'"
:id="data.resource.id"
:type="data.type"
:name="name"
:callbacks="data.callbacks"
:modalData="data.modalsData.delete"
@close="close"
/>
</div>
</template>
<script>
import DeleteModal from '~/components/Modal/DeleteModal'
import ConfirmModal from '~/components/Modal/ConfirmModal'
import DisableModal from '~/components/Modal/DisableModal'
import ReleaseModal from '~/components/ReleaseModal/ReleaseModal.vue'
import ReportModal from '~/components/Modal/ReportModal'
@ -48,7 +46,7 @@ export default {
DisableModal,
ReleaseModal,
ReportModal,
DeleteModal,
ConfirmModal,
},
computed: {
...mapGetters({
@ -63,7 +61,7 @@ export default {
switch (this.data.type) {
case 'user':
return name
case 'contribution':
case 'contribution': // REFACTORING: In ConfirmModal Already replaced "title" by "this.menuModalsData.delete.messageParams".
return title
case 'comment':
return author && author.name

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