diff --git a/.travis.yml b/.travis.yml index 4ca819f67..f48b0bb36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 908252f41..e2a727871 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,4 +9,4 @@ ], "editor.formatOnSave": true, "eslint.autoFixOnSave": true -} \ No newline at end of file +} diff --git a/SUMMARY.md b/SUMMARY.md index 701eac2d0..c281e2fae 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -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) diff --git a/backend/.env.template b/backend/.env.template index e905d1eb6..0c80529a1 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -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" diff --git a/backend/README.md b/backend/README.md index 3cce123ac..cd56e231f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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. diff --git a/backend/package.json b/backend/package.json index f6cb0de6b..276daed2f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/config/index.js b/backend/src/config/index.js index aed6f7c1c..320b636e9 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -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, diff --git a/backend/src/middleware/fixImageUrlsMiddleware.js b/backend/src/middleware/fixImageUrlsMiddleware.js deleted file mode 100644 index 3bfa8537a..000000000 --- a/backend/src/middleware/fixImageUrlsMiddleware.js +++ /dev/null @@ -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) - }, -} diff --git a/backend/src/middleware/fixImageUrlsMiddleware.spec.js b/backend/src/middleware/fixImageUrlsMiddleware.spec.js deleted file mode 100644 index 0da66811a..000000000 --- a/backend/src/middleware/fixImageUrlsMiddleware.spec.js +++ /dev/null @@ -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) - }) -}) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 75314abc0..9b85bd340 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -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', diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 83f5cd29b..b9234c99b 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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 diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 79bba0a5d..4e060dc90 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -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 }) }) diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 8bca3bb98..be257d912 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -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) diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js new file mode 100644 index 000000000..13789662b --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset.js @@ -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 + }, + }, +} diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js new file mode 100644 index 000000000..545945f51 --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -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() + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/passwordReset/emailTemplates.js b/backend/src/schema/resolvers/passwordReset/emailTemplates.js new file mode 100644 index 000000000..8508adccc --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset/emailTemplates.js @@ -0,0 +1,85 @@ +import CONFIG from '../../../config' + +export const from = '"Human Connection" ' + +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 + `, + } +} diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 9e2ec70a2..3bff53ddb 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -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), + ) + }) + }) }) }) diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index eb07a07b3..e33314f7e 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -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, diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index cf648a6bd..463c5ea6d 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -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') diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 53bf0967e..c5c3701b5 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -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) + }, }, } diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index a5c50f4f9..352d38eaa 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -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, + ) + }) + }) + }) }) }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index ab8b25399..1ef83bac3 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -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") } - diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index c402a1233..271d92750 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -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 - """ - ) + """ + ) } diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js index ea92f7d9f..b8e30ee2e 100644 --- a/backend/src/seed/factories/posts.js +++ b/backend/src/seed/factories/posts.js @@ -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 diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 27af1106a..27c07868d 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -214,21 +214,21 @@ import Factory from './factories' 'Hey @jenny-rostock, 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' }), ]) diff --git a/backend/test/features/activity-follow.feature b/backend/test/features/activity-follow.feature index 3cfe73340..7aa0c447d 100644 --- a/backend/test/features/activity-follow.feature +++ b/backend/test/features/activity-follow.feature @@ -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": """ diff --git a/backend/test/features/activity-like.feature b/backend/test/features/activity-like.feature index ec8c99110..26ef9c857 100644 --- a/backend/test/features/activity-like.feature +++ b/backend/test/features/activity-like.feature @@ -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": """ diff --git a/backend/yarn.lock b/backend/yarn.lock index e92070fe9..eb6a714ea 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -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" diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 8f5bcc8ea..73313d331 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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"); +}); diff --git a/cypress/integration/post/WritePost.feature b/cypress/integration/post/WritePost.feature index fed1bbf2f..06ac4a175 100644 --- a/cypress/integration/post/WritePost.feature +++ b/cypress/integration/post/WritePost.feature @@ -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 diff --git a/deployment/digital-ocean/https/templates/ingress.template.yaml b/deployment/digital-ocean/https/templates/ingress.template.yaml index ba4681bc8..9d0068e08 100644 --- a/deployment/digital-ocean/https/templates/ingress.template.yaml +++ b/deployment/digital-ocean/https/templates/ingress.template.yaml @@ -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 diff --git a/deployment/human-connection/mailserver/README.md b/deployment/human-connection/mailserver/README.md new file mode 100644 index 000000000..9a224a3b9 --- /dev/null +++ b/deployment/human-connection/mailserver/README.md @@ -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. diff --git a/deployment/human-connection/mailserver/deployment-mailserver.yaml b/deployment/human-connection/mailserver/deployment-mailserver.yaml new file mode 100644 index 000000000..d97a66bc9 --- /dev/null +++ b/deployment/human-connection/mailserver/deployment-mailserver.yaml @@ -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: {} diff --git a/deployment/human-connection/mailserver/service-mailserver.yaml b/deployment/human-connection/mailserver/service-mailserver.yaml new file mode 100644 index 000000000..655488b89 --- /dev/null +++ b/deployment/human-connection/mailserver/service-mailserver.yaml @@ -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 diff --git a/deployment/human-connection/templates/configmap.template.yaml b/deployment/human-connection/templates/configmap.template.yaml index 87b51a7d3..762901ae8 100644 --- a/deployment/human-connection/templates/configmap.template.yaml +++ b/deployment/human-connection/templates/configmap.template.yaml @@ -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" diff --git a/deployment/human-connection/templates/secrets.template.yaml b/deployment/human-connection/templates/secrets.template.yaml index 8f18dbf46..9f59b948a 100644 --- a/deployment/human-connection/templates/secrets.template.yaml +++ b/deployment/human-connection/templates/secrets.template.yaml @@ -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 diff --git a/deployment/legacy-migration/maintenance-worker/binaries/.env b/deployment/legacy-migration/maintenance-worker/binaries/.env new file mode 100644 index 000000000..773918095 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/binaries/.env @@ -0,0 +1,6 @@ +# SSH Access +# SSH_USERNAME='username' +# SSH_HOST='example.org' + +# UPLOADS_DIRECTORY=/var/www/api/uploads +OUTPUT_DIRECTORY='/uploads/' \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_uploads b/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_uploads index 11fd81623..5c0b67d74 100755 --- a/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_uploads +++ b/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_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} diff --git a/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh index abed9b0f5..8d16f42fa 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh @@ -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" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql similarity index 94% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql index 62cd4a2cc..027cea019 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql @@ -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` diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/badges_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/categories.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/categories.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/categories_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/comments.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/comments.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/comments_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql similarity index 90% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql index 98d8f24e9..a0f1418aa 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql @@ -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}) diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/_delete_all.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/delete_all.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/_delete_all.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/delete_all.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/follows_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/follows.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/follows.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh index ac256e3f0..8eef68c92 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh @@ -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" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/invites_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/invites.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/invites.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/invites.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/notifications.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/notifications.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/organizations.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/organizations.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/pages_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/pages.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/pages.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/pages.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/projects_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/projects.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/projects.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/projects.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/settings_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/settings.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/settings.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/settings.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/shouts.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/shouts.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/status_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/status_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/status/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/status.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/status.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/status.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/status/status.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/systemnotifications.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/systemnotifications.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/users_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/users/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql similarity index 92% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql index aec5499fc..4d7c9aa9f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql @@ -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), diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/userscandos.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/userscandos.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/delete.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings_delete.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/delete.cql diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/usersettings.cql similarity index 100% rename from deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings.cql rename to deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/usersettings.cql diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a71418229..016984d3b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -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 diff --git a/package.json b/package.json index dd7454c54..38c5c9a39 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index db967e973..560249b4a 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -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; diff --git a/webapp/components/Avatar/Avatar.spec.js b/webapp/components/Avatar/Avatar.spec.js index ae91fecfe..626e584c9 100644 --- a/webapp/components/Avatar/Avatar.spec.js +++ b/webapp/components/Avatar/Avatar.spec.js @@ -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') }) }) }) diff --git a/webapp/components/Avatar/Avatar.vue b/webapp/components/Avatar/Avatar.vue index 0d997c745..ec2f9b28b 100644 --- a/webapp/components/Avatar/Avatar.vue +++ b/webapp/components/Avatar/Avatar.vue @@ -1,5 +1,10 @@ + diff --git a/webapp/components/Editor/index.vue b/webapp/components/Editor/index.vue index 59aa73cb2..84649f436 100644 --- a/webapp/components/Editor/index.vue +++ b/webapp/components/Editor/index.vue @@ -12,9 +12,7 @@ @{{ user.slug }} -
- No users found -
+
No users found
@@ -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() { diff --git a/webapp/components/Editor/spec.js b/webapp/components/Editor/spec.js index 249192b57..b982d941d 100644 --- a/webapp/components/Editor/spec.js +++ b/webapp/components/Editor/spec.js @@ -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', + ) + }) + }) }) }) diff --git a/webapp/components/FilterMenu/FilterMenu.spec.js b/webapp/components/FilterMenu/FilterMenu.spec.js index 030ad20da..e345531d0 100644 --- a/webapp/components/FilterMenu/FilterMenu.spec.js +++ b/webapp/components/FilterMenu/FilterMenu.spec.js @@ -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 diff --git a/webapp/components/FilterMenu/FilterMenu.vue b/webapp/components/FilterMenu/FilterMenu.vue index 70dd3c236..16009b6eb 100644 --- a/webapp/components/FilterMenu/FilterMenu.vue +++ b/webapp/components/FilterMenu/FilterMenu.vue @@ -2,13 +2,16 @@ - - {{ $t('filter-menu.title') }} - + {{ $t('filter-menu.title') }}
- - - - diff --git a/webapp/components/Image/spec.js b/webapp/components/Image/spec.js deleted file mode 100644 index be568964a..000000000 --- a/webapp/components/Image/spec.js +++ /dev/null @@ -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') - }) - }) -}) diff --git a/webapp/components/LocaleSwitch/LocaleSwitch.spec.js b/webapp/components/LocaleSwitch/LocaleSwitch.spec.js new file mode 100644 index 000000000..ae81881d6 --- /dev/null +++ b/webapp/components/LocaleSwitch/LocaleSwitch.spec.js @@ -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) + }) + }) +}) diff --git a/webapp/components/LocaleSwitch.vue b/webapp/components/LocaleSwitch/LocaleSwitch.vue similarity index 92% rename from webapp/components/LocaleSwitch.vue rename to webapp/components/LocaleSwitch/LocaleSwitch.vue index f6f1a9727..aeee580b5 100644 --- a/webapp/components/LocaleSwitch.vue +++ b/webapp/components/LocaleSwitch/LocaleSwitch.vue @@ -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() diff --git a/webapp/components/Modal.spec.js b/webapp/components/Modal.spec.js index 0e2158e96..35ece0bee 100644 --- a/webapp/components/Modal.spec.js +++ b/webapp/components/Modal.spec.js @@ -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, - }, }) }) }) diff --git a/webapp/components/Modal.vue b/webapp/components/Modal.vue index 317b5007a..3c83a0922 100644 --- a/webapp/components/Modal.vue +++ b/webapp/components/Modal.vue @@ -6,7 +6,6 @@ :id="data.resource.id" :type="data.type" :name="name" - :callbacks="data.callbacks" @close="close" /> -
diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js new file mode 100644 index 000000000..c9128a70e --- /dev/null +++ b/webapp/components/PasswordReset/Request.spec.js @@ -0,0 +1,77 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import Request from './Request' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('Request', () => { + let wrapper + let Wrapper + let mocks + + beforeEach(() => { + mocks = { + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $t: jest.fn(), + $apollo: { + loading: false, + mutate: jest.fn().mockResolvedValue({ data: { reqestPasswordReset: true } }), + }, + } + }) + + describe('mount', () => { + beforeEach(jest.useFakeTimers) + + Wrapper = () => { + return mount(Request, { + mocks, + localVue, + }) + } + + it('renders a password reset form', () => { + wrapper = Wrapper() + expect(wrapper.find('.password-reset').exists()).toBe(true) + }) + + describe('submit', () => { + beforeEach(async () => { + wrapper = Wrapper() + wrapper.find('input#email').setValue('mail@example.org') + await wrapper.find('form').trigger('submit') + }) + + it('calls requestPasswordReset graphql mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it('delivers email to backend', () => { + const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + it('hides form to avoid re-submission', () => { + expect(wrapper.find('form').exists()).not.toBeTruthy() + }) + + it('displays a message that a password email was requested', () => { + const expected = ['password-reset.form.submitted', { email: 'mail@example.org' }] + expect(mocks.$t).toHaveBeenCalledWith(...expected) + }) + + describe('after animation', () => { + beforeEach(jest.runAllTimers) + + it('emits `handleSubmitted`', () => { + expect(wrapper.emitted('handleSubmitted')).toEqual([[{ email: 'mail@example.org' }]]) + }) + }) + }) + }) +}) diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue new file mode 100644 index 000000000..8ca2da89b --- /dev/null +++ b/webapp/components/PasswordReset/Request.vue @@ -0,0 +1,107 @@ + + + diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js new file mode 100644 index 000000000..22cdfd885 --- /dev/null +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -0,0 +1,53 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import VerifyCode from './VerifyCode' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('VerifyCode ', () => { + let wrapper + let Wrapper + let mocks + let propsData + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + propsData = { + email: 'mail@example.org', + } + }) + + describe('mount', () => { + beforeEach(jest.useFakeTimers) + + Wrapper = () => { + return mount(VerifyCode, { + mocks, + localVue, + propsData, + }) + } + + it('renders a verify code form', () => { + wrapper = Wrapper() + expect(wrapper.find('.verify-code').exists()).toBe(true) + }) + + describe('after verification code given', () => { + beforeEach(() => { + wrapper = Wrapper() + wrapper.find('input#code').setValue('123456') + wrapper.find('form').trigger('submit') + }) + + it('emits `verifyCode`', () => { + const expected = [[{ code: '123456', email: 'mail@example.org' }]] + expect(wrapper.emitted('verification')).toEqual(expected) + }) + }) + }) +}) diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue new file mode 100644 index 000000000..de1495e36 --- /dev/null +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -0,0 +1,67 @@ + + + diff --git a/webapp/components/PostCard/index.spec.js b/webapp/components/PostCard/index.spec.js new file mode 100644 index 000000000..390396383 --- /dev/null +++ b/webapp/components/PostCard/index.spec.js @@ -0,0 +1,130 @@ +import { config, shallowMount, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils' +import Styleguide from '@human-connection/styleguide' +import Vuex from 'vuex' +import Filters from '~/plugins/vue-filters' +import PostCard from '.' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) +localVue.use(Filters) + +config.stubs['no-ssr'] = '' +config.stubs['v-popover'] = '' + +describe('PostCard', () => { + let store + let stubs + let mocks + let propsData + let getters + let Wrapper + let wrapper + + beforeEach(() => { + propsData = { + post: { + id: 'p23', + name: 'It is a post', + author: { + id: 'u1', + }, + disabled: false, + }, + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return {} + }, + }, + }) + stubs = { + NuxtLink: RouterLinkStub, + } + mocks = { + $t: jest.fn(), + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $apollo: { + mutate: jest.fn().mockResolvedValue(), + }, + } + getters = { + 'auth/user': () => { + return {} + }, + } + }) + + describe('shallowMount', () => { + Wrapper = () => { + return shallowMount(PostCard, { + store, + propsData, + mocks, + localVue, + }) + } + + beforeEach(jest.useFakeTimers) + + describe('test Post callbacks', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + describe('deletion of Post from Page by invoking "deletePostCallback()"', () => { + beforeEach(() => { + wrapper.vm.deletePostCallback() + }) + + describe('after timeout', () => { + beforeEach(jest.runAllTimers) + + it('does call mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + }) + + it('mutation is successful', () => { + expect(mocks.$toast.success).toHaveBeenCalledTimes(1) + }) + + it('emits "removePostFromList"', () => { + expect(wrapper.emitted().removePostFromList).toHaveLength(1) + }) + }) + }) + }) + }) + + describe('mount', () => { + Wrapper = () => { + const store = new Vuex.Store({ + getters, + }) + return mount(PostCard, { + stubs, + mocks, + propsData, + store, + localVue, + }) + } + + describe('given a post', () => { + beforeEach(() => { + propsData.post = { + title: "It's a title", + } + }) + + it('renders title', () => { + expect(Wrapper().text()).toContain("It's a title") + }) + }) + }) +}) diff --git a/webapp/components/PostCard/index.vue b/webapp/components/PostCard/index.vue index d8172cb77..ade2d9175 100644 --- a/webapp/components/PostCard/index.vue +++ b/webapp/components/PostCard/index.vue @@ -1,6 +1,9 @@