mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 552-update_comment
This commit is contained in:
commit
5e9f46405e
@ -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
|
||||
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -9,4 +9,4 @@
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.autoFixOnSave": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 })
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
72
backend/src/schema/resolvers/passwordReset.js
Normal file
72
backend/src/schema/resolvers/passwordReset.js
Normal 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
|
||||
},
|
||||
},
|
||||
}
|
||||
180
backend/src/schema/resolvers/passwordReset.spec.js
Normal file
180
backend/src/schema/resolvers/passwordReset.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
85
backend/src/schema/resolvers/passwordReset/emailTemplates.js
Normal file
85
backend/src/schema/resolvers/passwordReset/emailTemplates.js
Normal 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
|
||||
`,
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
"""
|
||||
)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' }),
|
||||
])
|
||||
|
||||
|
||||
@ -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":
|
||||
"""
|
||||
|
||||
@ -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":
|
||||
"""
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
18
deployment/human-connection/mailserver/README.md
Normal file
18
deployment/human-connection/mailserver/README.md
Normal 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.
|
||||
@ -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: {}
|
||||
@ -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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
# SSH Access
|
||||
# SSH_USERNAME='username'
|
||||
# SSH_HOST='example.org'
|
||||
|
||||
# UPLOADS_DIRECTORY=/var/www/api/uploads
|
||||
OUTPUT_DIRECTORY='/uploads/'
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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`
|
||||
@ -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})
|
||||
@ -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"
|
||||
|
||||
@ -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),
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -25,6 +25,9 @@ describe('Comment.vue', () => {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$filters: {
|
||||
truncate: a => a,
|
||||
},
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue(),
|
||||
},
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
213
webapp/components/ContributionForm/ContributionForm.spec.js
Normal file
213
webapp/components/ContributionForm/ContributionForm.spec.js
Normal 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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
183
webapp/components/DeleteData/DeleteData.spec.js
Normal file
183
webapp/components/DeleteData/DeleteData.spec.js
Normal 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!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
225
webapp/components/DeleteData/DeleteData.vue
Normal file
225
webapp/components/DeleteData/DeleteData.vue
Normal 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>
|
||||
@ -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() {
|
||||
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
68
webapp/components/LocaleSwitch/LocaleSwitch.spec.js
Normal file
68
webapp/components/LocaleSwitch/LocaleSwitch.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user