diff --git a/.travis.yml b/.travis.yml index 4d9a4c733..6ba9d7f12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: script: # Backend - docker-compose exec backend yarn run lint - - docker-compose exec backend yarn run test:jest --ci --verbose=false + - docker-compose exec backend yarn run test:jest --ci --verbose=false --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 @@ -30,7 +30,7 @@ script: - docker-compose exec backend yarn run db:seed # Frontend - docker-compose exec webapp yarn run lint - - docker-compose exec webapp yarn run test --ci --verbose=false + - docker-compose exec webapp yarn run test --ci --verbose=false --coverage - docker-compose exec -d backend yarn run test:before:seeder # Fullstack - CYPRESS_RETRIES=1 yarn run cypress:run diff --git a/backend/Dockerfile b/backend/Dockerfile index 750d284dc..3b1581b1e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,6 +7,9 @@ ENV BUILD_COMMIT=$BUILD_COMMIT ARG WORKDIR=/nitro-backend RUN mkdir -p $WORKDIR WORKDIR $WORKDIR + +RUN apk --no-cache add git + COPY package.json yarn.lock ./ COPY .env.template .env CMD ["yarn", "run", "start"] diff --git a/backend/package.json b/backend/package.json index b1739a4cb..9f2bbe7ab 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,6 @@ "license": "MIT", "jest": { "verbose": true, - "collectCoverage": true, "collectCoverageFrom": [ "**/*.js", "!**/node_modules/**", @@ -56,7 +55,7 @@ "date-fns": "2.0.0-alpha.27", "debug": "~4.1.1", "dotenv": "~8.0.0", - "express": "~4.17.0", + "express": "~4.17.1", "faker": "~4.1.0", "graphql": "~14.3.1", "graphql-custom-directives": "~0.2.14", @@ -71,7 +70,7 @@ "lodash": "~4.17.11", "ms": "~2.1.1", "neo4j-driver": "~1.7.4", - "neo4j-graphql-js": "~2.6.1", + "neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes", "node-fetch": "~2.6.0", "npm-run-all": "~4.1.5", "request": "~2.88.0", @@ -97,7 +96,7 @@ "eslint": "~5.16.0", "eslint-config-standard": "~12.0.0", "eslint-config-prettier": "~4.3.0", - "eslint-plugin-import": "~2.17.2", + "eslint-plugin-import": "~2.17.3", "eslint-plugin-jest": "~22.6.4", "eslint-plugin-node": "~9.1.0", "eslint-plugin-prettier": "~3.1.0", @@ -105,8 +104,8 @@ "eslint-plugin-standard": "~4.0.0", "graphql-request": "~1.8.2", "jest": "~24.8.0", - "nodemon": "~1.19.0", + "nodemon": "~1.19.1", "prettier": "~1.14.3", "supertest": "~4.0.2" } -} \ No newline at end of file +} diff --git a/backend/public/uploads/.gitkeep b/backend/public/uploads/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js index 0942b2381..a846541e7 100644 --- a/backend/src/graphql-schema.js +++ b/backend/src/graphql-schema.js @@ -12,6 +12,7 @@ import rewards from './resolvers/rewards.js' import socialMedia from './resolvers/socialMedia.js' import notifications from './resolvers/notifications' import comments from './resolvers/comments' +import users from './resolvers/users' export const typeDefs = fs .readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')) @@ -35,5 +36,6 @@ export const resolvers = { ...socialMedia.Mutation, ...notifications.Mutation, ...comments.Mutation, + ...users.Mutation, }, } diff --git a/backend/src/middleware/notifications/extractMentions.js b/backend/src/middleware/notifications/extractIds/index.js similarity index 94% rename from backend/src/middleware/notifications/extractMentions.js rename to backend/src/middleware/notifications/extractIds/index.js index d6fa8ac3a..c2fcf169c 100644 --- a/backend/src/middleware/notifications/extractMentions.js +++ b/backend/src/middleware/notifications/extractIds/index.js @@ -2,6 +2,7 @@ import cheerio from 'cheerio' const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g export default function(content) { + if (!content) return [] const $ = cheerio.load(content) const urls = $('.mention') .map((_, el) => { diff --git a/backend/src/middleware/notifications/extractMentions.spec.js b/backend/src/middleware/notifications/extractIds/spec.js similarity index 93% rename from backend/src/middleware/notifications/extractMentions.spec.js rename to backend/src/middleware/notifications/extractIds/spec.js index d55c492ce..341c39cec 100644 --- a/backend/src/middleware/notifications/extractMentions.spec.js +++ b/backend/src/middleware/notifications/extractIds/spec.js @@ -1,6 +1,12 @@ -import extractIds from './extractMentions' +import extractIds from '.' + +describe('extractIds', () => { + describe('content undefined', () => { + it('returns empty array', () => { + expect(extractIds()).toEqual([]) + }) + }) -describe('extract', () => { describe('searches through links', () => { it('ignores links without .mention class', () => { const content = diff --git a/backend/src/middleware/notifications/index.js b/backend/src/middleware/notifications/index.js index 866a4376a..ca460a512 100644 --- a/backend/src/middleware/notifications/index.js +++ b/backend/src/middleware/notifications/index.js @@ -1,4 +1,4 @@ -import extractIds from './extractMentions' +import extractIds from './extractIds' const notify = async (resolve, root, args, context, resolveInfo) => { // extract user ids before xss-middleware removes link classes diff --git a/backend/src/middleware/validation/index.js b/backend/src/middleware/validation/index.js index 8e9be59ef..cfc852dcb 100644 --- a/backend/src/middleware/validation/index.js +++ b/backend/src/middleware/validation/index.js @@ -3,7 +3,7 @@ import { UserInputError } from 'apollo-server' const USERNAME_MIN_LENGTH = 3 const validateUsername = async (resolve, root, args, context, info) => { - if (args.name && args.name.length >= USERNAME_MIN_LENGTH) { + if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) { /* eslint-disable-next-line no-return-await */ return await resolve(root, args, context, info) } else { diff --git a/backend/src/resolvers/fileUpload/index.js b/backend/src/resolvers/fileUpload/index.js new file mode 100644 index 000000000..c37d87e39 --- /dev/null +++ b/backend/src/resolvers/fileUpload/index.js @@ -0,0 +1,27 @@ +import { createWriteStream } from 'fs' +import path from 'path' +import slug from 'slug' + +const storeUpload = ({ createReadStream, fileLocation }) => + new Promise((resolve, reject) => + createReadStream() + .pipe(createWriteStream(`public${fileLocation}`)) + .on('finish', resolve) + .on('error', reject), + ) + +export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) { + const upload = params[file] + + if (upload) { + const { createReadStream, filename } = await upload + const { name } = path.parse(filename) + const fileLocation = `/uploads/${Date.now()}-${slug(name)}` + await uploadCallback({ createReadStream, fileLocation }) + delete params[file] + + params[url] = fileLocation + } + + return params +} diff --git a/backend/src/resolvers/fileUpload/spec.js b/backend/src/resolvers/fileUpload/spec.js new file mode 100644 index 000000000..5767d6457 --- /dev/null +++ b/backend/src/resolvers/fileUpload/spec.js @@ -0,0 +1,65 @@ +import fileUpload from '.' + +describe('fileUpload', () => { + let params + let uploadCallback + + beforeEach(() => { + params = { + uploadAttribute: { + filename: 'avatar.jpg', + mimetype: 'image/jpeg', + encoding: '7bit', + createReadStream: jest.fn(), + }, + } + uploadCallback = jest.fn() + }) + + it('calls uploadCallback', async () => { + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(uploadCallback).toHaveBeenCalled() + }) + + describe('file name', () => { + it('saves the upload url in params[url]', async () => { + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/) + }) + + it('uses the name without file ending', async () => { + params.uploadAttribute.filename = 'somePng.png' + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/) + }) + + it('creates a url safe name', async () => { + params.uploadAttribute.filename = + '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar' + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/) + }) + + describe('in case of duplicates', () => { + it('creates unique names to avoid overwriting existing files', async () => { + const { attribute: first } = await fileUpload( + { + ...params, + }, + { file: 'uploadAttribute', url: 'attribute' }, + uploadCallback, + ) + + await new Promise(resolve => setTimeout(resolve, 1000)) + const { attribute: second } = await fileUpload( + { + ...params, + }, + { file: 'uploadAttribute', url: 'attribute' }, + uploadCallback, + ) + expect(first).not.toEqual(second) + }) + }) + }) +}) diff --git a/backend/src/resolvers/posts.js b/backend/src/resolvers/posts.js index e4d0d6876..ea962a662 100644 --- a/backend/src/resolvers/posts.js +++ b/backend/src/resolvers/posts.js @@ -1,8 +1,15 @@ import { neo4jgraphql } from 'neo4j-graphql-js' +import fileUpload from './fileUpload' export default { Mutation: { + UpdatePost: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreatePost: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) const result = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() diff --git a/backend/src/resolvers/users.js b/backend/src/resolvers/users.js new file mode 100644 index 000000000..53bf0967e --- /dev/null +++ b/backend/src/resolvers/users.js @@ -0,0 +1,15 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' +import fileUpload from './fileUpload' + +export default { + Mutation: { + UpdateUser: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreateUser: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + }, +} diff --git a/backend/src/resolvers/users.spec.js b/backend/src/resolvers/users.spec.js index bf55784fd..22096b6c8 100644 --- a/backend/src/resolvers/users.spec.js +++ b/backend/src/resolvers/users.spec.js @@ -67,6 +67,7 @@ describe('users', () => { 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) diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 902a7abf9..a581d287c 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -1,3 +1,5 @@ +scalar Upload + type Query { isLoggedIn: Boolean! # Get the currently logged in User based on the given JWT Token @@ -18,6 +20,7 @@ type Query { ) CommentByPost(postId: ID!): [Comment]! } + type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! @@ -99,6 +102,7 @@ type User { slug: String password: String! avatar: String + avatarUpload: Upload deleted: Boolean disabled: Boolean disabledBy: User @relation(name: "DISABLED", direction: "IN") @@ -175,6 +179,7 @@ type Post { content: String! contentExcerpt: String image: String + imageUpload: Upload visibility: VisibilityEnum deleted: Boolean disabled: Boolean diff --git a/backend/yarn.lock b/backend/yarn.lock index c486f174d..176af3893 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3043,10 +3043,10 @@ eslint-plugin-es@^1.4.0: eslint-utils "^1.3.0" regexpp "^2.0.1" -eslint-plugin-import@~2.17.2: - version "2.17.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz#d227d5c6dc67eca71eb590d2bb62fb38d86e9fcb" - integrity sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g== +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== dependencies: array-includes "^3.0.3" contains-path "^0.1.0" @@ -3058,7 +3058,7 @@ eslint-plugin-import@~2.17.2: lodash "^4.17.11" minimatch "^3.0.4" read-pkg-up "^2.0.0" - resolve "^1.10.0" + resolve "^1.11.0" eslint-plugin-jest@~22.6.4: version "22.6.4" @@ -3286,10 +3286,10 @@ expect@^24.8.0: jest-message-util "^24.8.0" jest-regex-util "^24.3.0" -express@^4.0.0, express@^4.16.3, express@~4.17.0: - version "4.17.0" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.0.tgz#288af62228a73f4c8ea2990ba3b791bb87cd4438" - integrity sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ== +express@^4.0.0, express@^4.16.3, express@~4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== dependencies: accepts "~1.3.7" array-flatten "1.1.1" @@ -5599,10 +5599,9 @@ neo4j-driver@^1.7.3, neo4j-driver@~1.7.4: text-encoding "^0.6.4" uri-js "^4.2.1" -neo4j-graphql-js@~2.6.1: +"neo4j-graphql-js@git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes": version "2.6.1" - resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.6.1.tgz#fc25d44d875a73114b6df08259985b555704b9bb" - integrity sha512-bPqzKumlCoHtS2qfPoTcZXrTkdXky210Kdu6Ubh5GhT+84wf+mo4Dzj4nxgDP2UgB77uA/caesIUImrSTLM8yQ== + resolved "git+https://github.com/Human-Connection/neo4j-graphql-js.git#84d529b9ecbc5c284cce4f86238c6d19b192cf0f" dependencies: graphql "^14.2.1" graphql-auth-directives "^2.1.0" @@ -5697,10 +5696,10 @@ node-releases@^1.1.19: dependencies: semver "^5.3.0" -nodemon@~1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.0.tgz#358e005549a1e9e1148cb2b9b8b28957dc4e4527" - integrity sha512-NHKpb/Je0Urmwi3QPDHlYuFY9m1vaVfTsRZG5X73rY46xPj0JpNe8WhUGQdkDXQDOxrBNIU3JrcflE9Y44EcuA== +nodemon@~1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071" + integrity sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg== dependencies: chokidar "^2.1.5" debug "^3.1.0" @@ -6704,10 +6703,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.1.tgz#664842ac960795bbe758221cdccda61fb64b5f18" - integrity sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA== +resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" + integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw== dependencies: path-parse "^1.0.6" diff --git a/cypress/fixtures/onourjourney.png b/cypress/fixtures/onourjourney.png new file mode 100644 index 000000000..8e606fabd Binary files /dev/null and b/cypress/fixtures/onourjourney.png differ diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js new file mode 100644 index 000000000..cb5689f63 --- /dev/null +++ b/cypress/integration/common/profile.js @@ -0,0 +1,34 @@ +import { When, Then } from 'cypress-cucumber-preprocessor/steps' + +/* global cy */ + +When('I visit my profile page', () => { + cy.openPage('profile/peter-pan') +}) + +Then('I should be able to change my profile picture', () => { + const avatarUpload = 'onourjourney.png' + + cy.fixture(avatarUpload, 'base64').then(fileContent => { + cy.get('#customdropzone').upload( + { fileContent, fileName: avatarUpload, mimeType: 'image/png' }, + { subjectType: 'drag-n-drop' }, + ) + }) + cy.get('#customdropzone') + .should('have.attr', 'style') + .and('contains', 'onourjourney') + cy.contains('.iziToast-message', 'Upload successful') + .should('have.length', 1) +}) + +When("I visit another user's profile page", () => { + cy.openPage('profile/peter-pan') +}) + +Then('I cannot upload a picture', () => { + cy.get('.ds-card-content') + .children() + .should('not.have.id', 'customdropzone') + .should('have.class', 'ds-avatar') +}) \ No newline at end of file diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index b6621ec87..664ffcff8 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -45,6 +45,7 @@ When('people visit my profile page', url => { cy.openPage('/profile/peter-pan') }) + When('they can see the text in the info box below my avatar', () => { cy.contains(aboutMeText) }) diff --git a/cypress/integration/user_profile/UploadUserProfileImage.feature b/cypress/integration/user_profile/UploadUserProfileImage.feature new file mode 100644 index 000000000..b46a31de8 --- /dev/null +++ b/cypress/integration/user_profile/UploadUserProfileImage.feature @@ -0,0 +1,18 @@ +Feature: Upload UserProfile Image + As a user + I would like to be able to add an avatar/profile pic to my profile + So that I can personalize my profile + + + Background: + Given I have a user account + + Scenario: Change my UserProfile Image + Given I am logged in + And I visit my profile page + Then I should be able to change my profile picture + + Scenario: Unable to change another user's avatar + Given I am logged in with a "user" role + And I visit another user's profile page + Then I cannot upload a picture \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a7cb76a27..f6253af20 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -13,7 +13,7 @@ // Cypress.Commands.add('login', (email, password) => { ... }) /* globals Cypress cy */ - +import 'cypress-file-upload' import { getLangByName } from './helpers' import users from '../fixtures/users.json' diff --git a/deployment/human-connection/README.md b/deployment/human-connection/README.md index 887b2300c..cd49fef33 100644 --- a/deployment/human-connection/README.md +++ b/deployment/human-connection/README.md @@ -6,7 +6,7 @@ just apply our provided configuration files to your cluster. ## Configuration -Copy our provided templates: +Change into the `./deployment` directory and copy our provided templates: ```bash # in folder deployment/human-connection/ @@ -14,7 +14,7 @@ $ cp templates/secrets.template.yaml ./secrets.yaml $ cp templates/configmap.template.yaml ./configmap.yaml ``` -Change the `configmap.yaml` as needed, all variables will be available as +Change the `configmap.yaml` in the `./deployment/human-connection` directory as needed, all variables will be available as environment variables in your deployed kubernetes pods. Probably you want to change this environment variable to your actual domain: @@ -28,7 +28,7 @@ If you want to edit secrets, you have to `base64` encode them. See [kubernetes d ```bash # example how to base64 a string: -$ echo -n 'admin' | base64 --wrap 0 +$ echo -n 'admin' | base64 YWRtaW4= ``` diff --git a/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db b/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db index 233798527..6ffdf8e3f 100755 --- a/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db +++ b/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db @@ -8,5 +8,5 @@ do fi done -/migration/mongo/import.sh +/migration/mongo/export.sh /migration/neo4j/import.sh diff --git a/deployment/legacy-migration/maintenance-worker/migration/mongo/.env b/deployment/legacy-migration/maintenance-worker/migration/mongo/.env new file mode 100644 index 000000000..602f51fc4 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/mongo/.env @@ -0,0 +1,16 @@ +# SSH Access +# SSH_USERNAME='username' +# SSH_HOST='example.org' + +# Mongo DB on Remote Maschine +# MONGODB_USERNAME='mongouser' +# MONGODB_PASSWORD='mongopassword' +# MONGODB_DATABASE='mongodatabase' +# MONGODB_AUTH_DB='admin' + +# Export Settings +# On Windows this resolves to C:\Users\dornhoeschen\AppData\Local\Temp\mongo-export (MinGW) +EXPORT_PATH='/tmp/mongo-export/' +EXPORT_MONGOEXPORT_BIN='mongoexport' +# On Windows use something like this +# EXPORT_MONGOEXPORT_BIN='C:\Program Files\MongoDB\Server\3.6\bin\mongoexport.exe' \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh new file mode 100755 index 000000000..257bc4c61 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -e + +# import .env config +set -o allexport +source $(dirname "$0")/.env +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" + mkdir -p ${EXPORT_PATH}splits/$1/ + split -l 1000 -a 3 ${EXPORT_PATH}$1.json ${EXPORT_PATH}splits/$1/ +} + +# Delete old export & ensure directory +rm -rf ${EXPORT_PATH}* +mkdir -p ${EXPORT_PATH} + +# Open SSH Tunnel +ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST} + +# Export all Data from the Alpha to json and split them up +export_collection "badges" +export_collection "categories" +export_collection "comments" +export_collection "contributions" +export_collection "emotions" +export_collection "follows" +export_collection "invites" +export_collection "notifications" +export_collection "organizations" +export_collection "pages" +export_collection "projects" +export_collection "settings" +export_collection "shouts" +export_collection "status" +export_collection "systemnotifications" +export_collection "users" +export_collection "userscandos" +export_collection "usersettings" + +# Close SSH Tunnel +ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST} +ssh -S my-ctrl-socket -O exit -l ${SSH_USERNAME} ${SSH_HOST} diff --git a/deployment/legacy-migration/maintenance-worker/migration/mongo/import.sh b/deployment/legacy-migration/maintenance-worker/migration/mongo/import.sh deleted file mode 100755 index d68a8c2a8..000000000 --- a/deployment/legacy-migration/maintenance-worker/migration/mongo/import.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "SSH_USERNAME ${SSH_USERNAME}" -echo "SSH_HOST ${SSH_HOST}" -echo "MONGODB_USERNAME ${MONGODB_USERNAME}" -echo "MONGODB_PASSWORD ${MONGODB_PASSWORD}" -echo "MONGODB_DATABASE ${MONGODB_DATABASE}" -echo "MONGODB_AUTH_DB ${MONGODB_AUTH_DB}" -echo "-------------------------------------------------" - - -rm -rf /tmp/mongo-export/* -mkdir -p /tmp/mongo-export/ - -ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST} - -for collection in "categories" "badges" "users" "contributions" "comments" "follows" "shouts" -do - mongoexport --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $collection --collection $collection --out "/tmp/mongo-export/$collection.json" - mkdir -p /tmp/mongo-export/splits/$collection/ - split -l 1000 -a 3 /tmp/mongo-export/$collection.json /tmp/mongo-export/splits/$collection/ -done - -ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST} -ssh -S my-ctrl-socket -O exit -l ${SSH_USERNAME} ${SSH_HOST} diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env b/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env new file mode 100644 index 000000000..7691306ac --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env @@ -0,0 +1,16 @@ +# Neo4J Settings +# NEO4J_USERNAME='neo4j' +# NEO4J_PASSWORD='letmein' + +# Import Settings +# On Windows this resolves to C:\Users\dornhoeschen\AppData\Local\Temp\mongo-export (MinGW) +IMPORT_PATH='/tmp/mongo-export/' +IMPORT_CHUNK_PATH='/tmp/mongo-export/splits/current-chunk.json' + +IMPORT_CHUNK_PATH_CQL='/tmp/mongo-export/splits/current-chunk.json' +# On Windows this path needs to be windows style since the cypher-shell runs native - note the forward slash +# IMPORT_CHUNK_PATH_CQL='C:/Users/dornhoeschen/AppData/Local/Temp/mongo-export/splits/current-chunk.json' + +IMPORT_CYPHERSHELL_BIN='cypher-shell' +# On Windows use something like this +# IMPORT_CYPHERSHELL_BIN='C:\Program Files\neo4j-community\bin\cypher-shell.bat' \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql index 6b6a09592..62eddf124 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as badge +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as badge MERGE(b:Badge {id: badge._id["$oid"]}) ON CREATE SET b.key = badge.key, diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql index 776811bec..81e73c1b9 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as category +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as category MERGE(c:Category {id: category._id["$oid"]}) ON CREATE SET c.name = category.title, diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql index 234d29d26..1c8eb9397 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as json +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as json MERGE (comment:Comment {id: json._id["$oid"]}) ON CREATE SET diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql index 01647f7fb..e4e148af3 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as post +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as post MERGE (p:Post {id: post._id["$oid"]}) ON CREATE SET p.title = post.title, @@ -20,6 +20,6 @@ MATCH (c:Category {id: categoryId}) MERGE (p)-[:CATEGORIZED]->(c) WITH p, post.tags AS tags UNWIND tags AS tag -MERGE (t:Tag {id: apoc.create.uuid(), name: tag}) +MERGE (t:Tag {id: tag, name: tag}) MERGE (p)-[:TAGGED]->(t) ; diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql index 0cd6d9cfc..bf3837b04 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as follow +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as follow MATCH (u1:User {id: follow.userId}), (u2:User {id: follow.foreignId}) MERGE (u1)-[:FOLLOWS]->(u2) ; diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh index b7de74782..12cc7ce67 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh @@ -1,17 +1,48 @@ #!/usr/bin/env bash set -e -SECONDS=0 -SCRIPT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# import .env config +set -o allexport +source $(dirname "$0")/.env +set +o allexport -echo "MATCH (n) DETACH DELETE n;" | cypher-shell - -for collection in "badges" "categories" "users" "follows" "contributions" "shouts" "comments" -do - for chunk in /tmp/mongo-export/splits/$collection/* +# Import collection function defintion +function import_collection () { + for chunk in ${IMPORT_PATH}splits/$1/* do - mv $chunk /tmp/mongo-export/splits/current-chunk.json - echo "Import ${chunk}" && cypher-shell < $SCRIPT_DIRECTORY/$collection.cql + mv $chunk ${IMPORT_CHUNK_PATH} + NEO4J_COMMAND="$(envsubst '${IMPORT_CHUNK_PATH_CQL}' < $(dirname "$0")/$1.cql)" + echo "Import ${chunk}" + echo "${NEO4J_COMMAND}" | "${IMPORT_CYPHERSHELL_BIN}" -u ${NEO4J_USERNAME} -p ${NEO4J_PASSWORD} done -done +} + +# Time variable +SECONDS=0 + +# Delete all Neo4J Database content +echo "Deleting Database Contents" +echo "MATCH (n) DETACH DELETE n;" | "${IMPORT_CYPHERSHELL_BIN}" -u ${NEO4J_USERNAME} -p ${NEO4J_PASSWORD} + +# 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 "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" + echo "Time elapsed: $SECONDS seconds" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql index 5019cdc32..a82a7a33d 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as shout +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as shout MATCH (u:User {id: shout.userId}), (p:Post {id: shout.foreignId}) MERGE (u)-[:SHOUTED]->(p) ; diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql index c877f8377..693fd75b6 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql @@ -1,4 +1,4 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as user +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL}") YIELD value as user MERGE(u:User {id: user._id["$oid"]}) ON CREATE SET u.name = user.name, diff --git a/package.json b/package.json index 0c2e47271..dd7454c54 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,12 @@ "cross-env": "^5.2.0", "cypress": "^3.3.1", "cypress-cucumber-preprocessor": "^1.11.2", + "cypress-file-upload": "^3.1.2", "cypress-plugin-retries": "^1.2.2", "dotenv": "^8.0.0", "faker": "^4.1.0", "graphql-request": "^1.8.2", - "neo4j-driver": "^1.7.4", + "neo4j-driver": "^1.7.5", "npm-run-all": "^4.1.5" } -} +} \ No newline at end of file diff --git a/webapp/components/Avatar/Avatar.spec.js b/webapp/components/Avatar/Avatar.spec.js new file mode 100644 index 000000000..ae91fecfe --- /dev/null +++ b/webapp/components/Avatar/Avatar.spec.js @@ -0,0 +1,69 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import Styleguide from '@human-connection/styleguide' +import Avatar from './Avatar.vue' + +const localVue = createLocalVue() +localVue.use(Styleguide) + +describe('Avatar.vue', () => { + let propsData = {} + + const Wrapper = () => { + return mount(Avatar, { propsData, localVue }) + } + + it('renders no image', () => { + expect( + Wrapper() + .find('img') + .exists(), + ).toBe(false) + }) + + it('renders an icon', () => { + expect( + Wrapper() + .find('.ds-icon') + .exists(), + ).toBe(true) + }) + + describe('given a user', () => { + describe('with a relative avatar url', () => { + beforeEach(() => { + propsData = { + user: { + avatar: '/avatar.jpg', + }, + } + }) + + it('adds a prefix to load the image from the uploads service', () => { + expect( + Wrapper() + .find('img') + .attributes('src'), + ).toBe('/api/avatar.jpg') + }) + }) + + describe('with an absolute avatar url', () => { + beforeEach(() => { + propsData = { + user: { + avatar: 'http://lorempixel.com/640/480/animals', + }, + } + }) + + it('keeps the avatar URL as is', () => { + // e.g. our seeds have absolute image URLs + expect( + Wrapper() + .find('img') + .attributes('src'), + ).toBe('http://lorempixel.com/640/480/animals') + }) + }) + }) +}) diff --git a/webapp/components/Avatar/Avatar.vue b/webapp/components/Avatar/Avatar.vue new file mode 100644 index 000000000..2c5cf1ddc --- /dev/null +++ b/webapp/components/Avatar/Avatar.vue @@ -0,0 +1,33 @@ + + + diff --git a/webapp/components/Editor/index.vue b/webapp/components/Editor/index.vue index ac1391a42..ca6ee4779 100644 --- a/webapp/components/Editor/index.vue +++ b/webapp/components/Editor/index.vue @@ -327,10 +327,18 @@ export default { }, }, }, + mounted() { + this.$root.$on('changeLanguage', () => { + this.changePlaceHolderText() + }) + }, 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/LocaleSwitch.vue b/webapp/components/LocaleSwitch.vue index a00f38109..840990c19 100644 --- a/webapp/components/LocaleSwitch.vue +++ b/webapp/components/LocaleSwitch.vue @@ -14,7 +14,8 @@ {{ current.code.toUpperCase() }} + /> + {{ current.code.toUpperCase() }} +
+ +
+ + + diff --git a/webapp/components/Upload/spec.js b/webapp/components/Upload/spec.js new file mode 100644 index 000000000..85215ea59 --- /dev/null +++ b/webapp/components/Upload/spec.js @@ -0,0 +1,71 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import Upload from '.' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) + +describe('Upload', () => { + let wrapper + + const mocks = { + $apollo: { + mutate: jest + .fn() + .mockResolvedValueOnce({ + data: { UpdateUser: { id: 'upload1', avatar: '/upload/avatar.jpg' } }, + }) + .mockRejectedValue({ + message: 'File upload unsuccessful! Whatcha gonna do?', + }), + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + } + + const propsData = { + user: { + avatar: '/api/generic.jpg', + }, + } + + const file = { + filename: 'avatar.jpg', + previewElement: { + classList: { + remove: jest.fn(), + add: jest.fn(), + }, + querySelectorAll: jest.fn().mockReturnValue([ + { + alt: '', + style: { + 'background-image': '/api/generic.jpg', + }, + }, + ]), + }, + } + + const dataUrl = 'avatar.jpg' + + beforeEach(() => { + jest.useFakeTimers() + wrapper = shallowMount(Upload, { localVue, propsData, mocks }) + }) + + it('sends a the UpdateUser mutation when vddrop is called', () => { + wrapper.vm.vddrop([{ filename: 'avatar.jpg' }]) + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + }) + + it('thumbnail', () => { + wrapper.vm.thumbnail(file, dataUrl) + expect(file.previewElement.classList.add).toHaveBeenCalledTimes(1) + }) +}) diff --git a/webapp/components/User/index.vue b/webapp/components/User/index.vue index 82a08c5e1..fd435d0c5 100644 --- a/webapp/components/User/index.vue +++ b/webapp/components/User/index.vue @@ -3,10 +3,7 @@
- +
-
@@ -143,6 +137,7 @@ import { mapGetters } from 'vuex' import HcRelativeDateTime from '~/components/RelativeDateTime' import HcFollowButton from '~/components/FollowButton' import HcBadges from '~/components/Badges' +import HcAvatar from '~/components/Avatar/Avatar.vue' import Dropdown from '~/components/Dropdown' export default { @@ -150,6 +145,7 @@ export default { components: { HcRelativeDateTime, HcFollowButton, + HcAvatar, HcBadges, Dropdown, }, @@ -183,12 +179,6 @@ export default {