From c129c3fbae291b51d42d331213a23e3d29331c6e Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 5 Sep 2019 17:22:04 +0200 Subject: [PATCH 01/27] Admin page handles loading and error state --- webapp/graphql/admin/Statistics.js | 15 ++ webapp/locales/de.json | 1 + webapp/locales/en.json | 1 + webapp/pages/admin/index.vue | 227 ++++++++++++++++------------- 4 files changed, 142 insertions(+), 102 deletions(-) create mode 100644 webapp/graphql/admin/Statistics.js diff --git a/webapp/graphql/admin/Statistics.js b/webapp/graphql/admin/Statistics.js new file mode 100644 index 000000000..94c3f91f0 --- /dev/null +++ b/webapp/graphql/admin/Statistics.js @@ -0,0 +1,15 @@ +import gql from 'graphql-tag' + +export const Statistics = gql` + query { + statistics { + countUsers + countPosts + countComments + countNotifications + countInvites + countFollows + countShouts + } + } +` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 506cefde2..0d283f8af 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -22,6 +22,7 @@ }, "site": { "thanks": "Danke!", + "error-occurred": "Ein Fehler ist aufgetreten.", "made": "Mit ❤ gemacht", "imprint": "Impressum", "data-privacy": "Datenschutz", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 05d8ac631..b1283c9b2 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -22,6 +22,7 @@ }, "site": { "thanks": "Thanks!", + "error-occurred": "An error occurred.", "made": "Made with ❤", "imprint": "Imprint", "termsAndConditions": "Terms and conditions", diff --git a/webapp/pages/admin/index.vue b/webapp/pages/admin/index.vue index 17d756fbd..b966fd416 100644 --- a/webapp/pages/admin/index.vue +++ b/webapp/pages/admin/index.vue @@ -1,90 +1,134 @@ From c61b03483e51a9047d525462fe8e15ef1ae8bf2d Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 5 Sep 2019 18:14:24 +0200 Subject: [PATCH 02/27] Failed to write a test for admin/index.vue I am not able to properly mock the data for ApolloQuery. However I find it valuable to push this particular piece of dead code. Maybe people find it helpful. --- webapp/pages/admin/index.spec.js | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 webapp/pages/admin/index.spec.js diff --git a/webapp/pages/admin/index.spec.js b/webapp/pages/admin/index.spec.js new file mode 100644 index 000000000..68256aa9d --- /dev/null +++ b/webapp/pages/admin/index.spec.js @@ -0,0 +1,53 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import AdminIndexPage from './index.vue' +import Styleguide from '@human-connection/styleguide' +import VueApollo from 'vue-apollo' + +const localVue = createLocalVue() + +localVue.use(Styleguide) +localVue.use(VueApollo) + +describe('admin/index.vue', () => { + let Wrapper + let store + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + Wrapper = () => { + return mount(AdminIndexPage, { + store, + mocks, + localVue, + }) + } + + describe('in loading state', () => { + beforeEach(() => { + mocks = { ...mocks, $apolloData: { loading: true } } + }) + + it.skip('shows a loading spinner', () => { + // I don't know how to mock the data that gets passed to + // ApolloQuery component + // What I found: + // https://github.com/Akryum/vue-apollo/issues/656 + // https://github.com/Akryum/vue-apollo/issues/609 + Wrapper() + const calls = mocks.$t.mock.calls + const expected = [['site.error-occurred']] + expect(calls).toEqual(expected) + }) + }) + + describe('in error state', () => { + it.todo('displays an error message') + }) + }) +}) From c963dee8d9c9df9d3ba0ac5bae7667786130f877 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2019 04:22:27 +0000 Subject: [PATCH 03/27] Bump eslint-plugin-jest from 22.16.0 to 22.17.0 in /backend Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 22.16.0 to 22.17.0. - [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases) - [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v22.16.0...v22.17.0) Signed-off-by: dependabot-preview[bot] --- backend/package.json | 2 +- backend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/package.json b/backend/package.json index 454ab16d9..9c8f2403c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -120,7 +120,7 @@ "eslint-config-prettier": "~6.2.0", "eslint-config-standard": "~14.1.0", "eslint-plugin-import": "~2.18.2", - "eslint-plugin-jest": "~22.16.0", + "eslint-plugin-jest": "~22.17.0", "eslint-plugin-node": "~10.0.0", "eslint-plugin-prettier": "~3.1.0", "eslint-plugin-promise": "~4.2.1", diff --git a/backend/yarn.lock b/backend/yarn.lock index 2bf269e86..b6f9d363a 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3336,10 +3336,10 @@ eslint-plugin-import@~2.18.2: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~22.16.0: - version "22.16.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05" - integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg== +eslint-plugin-jest@~22.17.0: + version "22.17.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.17.0.tgz#dc170ec8369cd1bff9c5dd8589344e3f73c88cf6" + integrity sha512-WT4DP4RoGBhIQjv+5D0FM20fAdAUstfYAf/mkufLNTojsfgzc5/IYW22cIg/Q4QBavAZsROQlqppiWDpFZDS8Q== dependencies: "@typescript-eslint/experimental-utils" "^1.13.0" From 8362e4c94ff6867d4ba342ae7eae1f5c30f36e22 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2019 04:22:36 +0000 Subject: [PATCH 04/27] Bump eslint-plugin-jest from 22.16.0 to 22.17.0 in /webapp Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 22.16.0 to 22.17.0. - [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases) - [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v22.16.0...v22.17.0) Signed-off-by: dependabot-preview[bot] --- webapp/package.json | 2 +- webapp/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index fed1f8472..605e0f63b 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -107,7 +107,7 @@ "eslint-config-standard": "~12.0.0", "eslint-loader": "~3.0.0", "eslint-plugin-import": "~2.18.2", - "eslint-plugin-jest": "~22.16.0", + "eslint-plugin-jest": "~22.17.0", "eslint-plugin-node": "~10.0.0", "eslint-plugin-prettier": "~3.1.0", "eslint-plugin-promise": "~4.2.1", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index ce5d4e46a..3b28d5fb0 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -6392,10 +6392,10 @@ eslint-plugin-import@~2.18.2: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~22.16.0: - version "22.16.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05" - integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg== +eslint-plugin-jest@~22.17.0: + version "22.17.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.17.0.tgz#dc170ec8369cd1bff9c5dd8589344e3f73c88cf6" + integrity sha512-WT4DP4RoGBhIQjv+5D0FM20fAdAUstfYAf/mkufLNTojsfgzc5/IYW22cIg/Q4QBavAZsROQlqppiWDpFZDS8Q== dependencies: "@typescript-eslint/experimental-utils" "^1.13.0" From 0568f049bddf6099e72303ce37fbd6c9d3dfc09c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2019 04:23:18 +0000 Subject: [PATCH 05/27] Bump neo4j from 3.5.8 to 3.5.9 in /neo4j Bumps neo4j from 3.5.8 to 3.5.9. Signed-off-by: dependabot-preview[bot] --- neo4j/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index 1cfa04507..fcd53fa04 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,4 +1,4 @@ -FROM neo4j:3.5.8 +FROM neo4j:3.5.9 LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" ARG BUILD_COMMIT From 8d4874effb8ce3502b8eb217eb396817f510e0d7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2019 04:25:14 +0000 Subject: [PATCH 06/27] Bump node from 12.9-alpine to 12.10.0-alpine in /backend Bumps node from 12.9-alpine to 12.10.0-alpine. Signed-off-by: dependabot-preview[bot] --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 72b9973e7..250ee845b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.9-alpine as base +FROM node:12.10.0-alpine as base LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" EXPOSE 4000 From a646b0d19e103d6b544a3d8c9dc0a6d0c3d54426 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2019 04:25:23 +0000 Subject: [PATCH 07/27] Bump node from 12.9-alpine to 12.10.0-alpine in /webapp Bumps node from 12.9-alpine to 12.10.0-alpine. Signed-off-by: dependabot-preview[bot] --- webapp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/Dockerfile b/webapp/Dockerfile index 4b219d2fe..cf9c4c698 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.9-alpine as base +FROM node:12.10.0-alpine as base LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" EXPOSE 3000 From 0b464bf596f72113ee41b58c55c7772d6b12896b Mon Sep 17 00:00:00 2001 From: roschaefer Date: Fri, 6 Sep 2019 10:22:11 +0200 Subject: [PATCH 08/27] Fix leftover of #1479 --- webapp/pages/profile/_id/_slug.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index edefe6be0..ae1d6c0e4 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -388,7 +388,7 @@ export default { resetPostList() { this.offset = 0 this.posts = [] - this.hasMore = false + this.hasMore = true }, async block(user) { await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } }) From f58c1c7cf090d163d99d612d0822243b11756578 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Fri, 6 Sep 2019 11:37:08 +0200 Subject: [PATCH 09/27] Refactor registration.spec.js Only 7 specs left that still use GraphQLClient --- .../src/schema/resolvers/registration.spec.js | 496 ++++++++++-------- 1 file changed, 268 insertions(+), 228 deletions(-) diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 8e33bf314..803684cbe 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -1,18 +1,33 @@ -import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' -import { neode } from '../../bootstrap/neo4j' +import { gql } from '../../jest/helpers' +import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' +import createServer from '../../server' +import { createTestClient } from 'apollo-server-testing' -let factory -let client +const factory = Factory() +const neode = getNeode() + +let mutate +let authenticatedUser +let user let variables -let action -let userParams -const instance = neode() +const driver = getDriver() beforeEach(async () => { variables = {} - factory = Factory() +}) + +beforeAll(() => { + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + mutate = createTestClient(server).mutate }) afterEach(async () => { @@ -20,83 +35,77 @@ afterEach(async () => { }) describe('CreateInvitationCode', () => { - const mutation = `mutation { CreateInvitationCode { token } }` + const mutation = gql` + mutation { + CreateInvitationCode { + token + } + } + ` - it('throws Authorization error', async () => { - const client = new GraphQLClient(host) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised!') + describe('unauthenticated', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws Authorization error', async () => { + await expect(mutate({ mutation })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) }) describe('authenticated', () => { beforeEach(async () => { - userParams = { + user = await factory.create('User', { id: 'i123', name: 'Inviter', email: 'inviter@example.org', password: '1234', termsAndConditionsAgreedVersion: '0.0.1', - } - action = async () => { - const factory = Factory() - await factory.create('User', userParams) - const headers = await login(userParams) - client = new GraphQLClient(host, { headers }) - return client.request(mutation) - } + }) + authenticatedUser = await user.toJson() }) it('resolves', async () => { - await expect(action()).resolves.toEqual({ - CreateInvitationCode: { token: expect.any(String) }, + await expect(mutate({ mutation })).resolves.toMatchObject({ + data: { CreateInvitationCode: { token: expect.any(String) } }, }) }) it('creates an InvitationCode with a `createdAt` attribute', async () => { - await action() - const codes = await instance.all('InvitationCode') + await mutate({ mutation }) + const codes = await neode.all('InvitationCode') const invitation = await codes.first().toJson() expect(invitation.createdAt).toBeTruthy() expect(Date.parse(invitation.createdAt)).toEqual(expect.any(Number)) }) it('relates inviting User to InvitationCode', async () => { - await action() - const result = await instance.cypher( + await mutate({ mutation }) + const result = await neode.cypher( 'MATCH(code:InvitationCode)<-[:GENERATED]-(user:User) RETURN user', ) - const inviter = instance.hydrateFirst(result, 'user', instance.model('User')) + const inviter = neode.hydrateFirst(result, 'user', neode.model('User')) await expect(inviter.toJson()).resolves.toEqual(expect.objectContaining({ name: 'Inviter' })) }) describe('who has invited a lot of users already', () => { - beforeEach(() => { - action = async () => { - const factory = Factory() - await factory.create('User', userParams) - const headers = await login(userParams) - client = new GraphQLClient(host, { headers }) - await Promise.all( - [1, 2, 3].map(() => { - return client.request(mutation) - }), - ) - return client.request(mutation, variables) - } + beforeEach(async () => { + await Promise.all([mutate({ mutation }), mutate({ mutation }), mutate({ mutation })]) }) describe('as ordinary `user`', () => { it('throws `Not Authorised` because of maximum number of invitations', async () => { - await expect(action()).rejects.toThrow('Not Authorised') + await expect(mutate({ mutation })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) }) - it('creates no additional invitation codes', async done => { - try { - await action() - } catch (e) { - const invitationCodes = await instance.all('InvitationCode') - await expect(invitationCodes.toJson()).resolves.toHaveLength(3) - done() - } + it('creates no additional invitation codes', async () => { + await mutate({ mutation }) + const invitationCodes = await neode.all('InvitationCode') + await expect(invitationCodes.toJson()).resolves.toHaveLength(3) }) }) @@ -118,132 +127,145 @@ describe('CreateInvitationCode', () => { }) describe('SignupByInvitation', () => { - const mutation = `mutation($email: String!, $token: String!) { - SignupByInvitation(email: $email, token: $token) { email } - }` - - beforeEach(() => { - client = new GraphQLClient(host) - action = async () => { - return client.request(mutation, variables) + const mutation = gql` + mutation($email: String!, $token: String!) { + SignupByInvitation(email: $email, token: $token) { + email + } } - }) + ` describe('with valid email but invalid InvitationCode', () => { beforeEach(() => { - variables.email = 'any-email@example.org' - variables.token = 'wut?' + variables = { + ...variables, + email: 'any-email@example.org', + token: 'wut?', + } }) it('throws UserInputError', async () => { - await expect(action()).rejects.toThrow('Invitation code already used or does not exist.') - }) - }) - - describe('with valid InvitationCode', () => { - beforeEach(async () => { - const inviterParams = { - name: 'Inviter', - email: 'inviter@example.org', - password: '1234', - } - const factory = Factory() - await factory.create('User', inviterParams) - const headersOfInviter = await login(inviterParams) - const anotherClient = new GraphQLClient(host, { headers: headersOfInviter }) - const invitationMutation = `mutation { CreateInvitationCode { token } }` - const { - CreateInvitationCode: { token }, - } = await anotherClient.request(invitationMutation) - variables.token = token + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'UserInputError: Invitation code already used or does not exist.' }], + }) }) - describe('given an invalid email', () => { - beforeEach(() => { - variables.email = 'someuser' - }) - - it('throws `email is not a valid email`', async () => { - await expect(action()).rejects.toThrow('"email" must be a valid email') - }) - - it('creates no additional EmailAddress node', async done => { - try { - await action() - } catch (e) { - let emailAddresses = await instance.all('EmailAddress') - emailAddresses = await emailAddresses.toJson - expect(emailAddresses).toHaveLength(0) - done() + describe('with valid InvitationCode', () => { + beforeEach(async () => { + const inviter = await factory.create('User', { + name: 'Inviter', + email: 'inviter@example.org', + password: '1234', + }) + authenticatedUser = await inviter.toJson() + const invitationMutation = gql` + mutation { + CreateInvitationCode { + token + } + } + ` + const { + data: { + CreateInvitationCode: { token }, + }, + } = await mutate({ mutation: invitationMutation }) + authenticatedUser = null + variables = { + ...variables, + token, } }) - }) - describe('given a valid email', () => { - beforeEach(() => { - variables.email = 'someUser@example.org' - }) + describe('given an invalid email', () => { + beforeEach(() => { + variables = { ...variables, email: 'someuser' } + }) - it('resolves', async () => { - await expect(action()).resolves.toEqual({ - SignupByInvitation: { email: 'someuser@example.org' }, + it('throws `email is not a valid email`', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: expect.stringContaining('"email" must be a valid email') }], + }) + }) + + it('creates no additional EmailAddress node', async () => { + let emailAddresses = await neode.all('EmailAddress') + emailAddresses = await emailAddresses.toJson() + expect(emailAddresses).toHaveLength(1) + await mutate({ mutation, variables }) + emailAddresses = await neode.all('EmailAddress') + emailAddresses = await emailAddresses.toJson() + expect(emailAddresses).toHaveLength(1) }) }) - describe('creates a EmailAddress node', () => { - it('with a `createdAt` attribute', async () => { - await action() - let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.createdAt).toBeTruthy() - expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) + describe('given a valid email', () => { + beforeEach(() => { + variables = { ...variables, email: 'someUser@example.org' } }) - it('with a cryptographic `nonce`', async () => { - await action() - let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) - }) - - it('connects inviter through invitation code', async () => { - await action() - const result = await instance.cypher( - 'MATCH(inviter:User)-[:GENERATED]->(:InvitationCode)-[:ACTIVATED]->(email:EmailAddress {email: {email}}) RETURN inviter', - { email: 'someuser@example.org' }, - ) - const inviter = instance.hydrateFirst(result, 'inviter', instance.model('User')) - await expect(inviter.toJson()).resolves.toEqual( - expect.objectContaining({ name: 'Inviter' }), - ) - }) - - describe('using the same InvitationCode twice', () => { - it('rejects because codes can be used only once', async done => { - await action() - try { - variables.email = 'yetanotheremail@example.org' - await action() - } catch (e) { - expect(e.message).toMatch(/Invitation code already used/) - done() - } + it('resolves', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { SignupByInvitation: { email: 'someuser@example.org' } }, }) }) - describe('if a user account with the given email already exists', () => { - beforeEach(async () => { - await factory.create('User', { email: 'someuser@example.org' }) + describe('creates a EmailAddress node', () => { + it('with a `createdAt` attribute', async () => { + await mutate({ mutation, variables }) + let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.toJson() + expect(emailAddress.createdAt).toBeTruthy() + expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) }) - it('throws unique violation error', async () => { - await expect(action()).rejects.toThrow('User account with this email already exists.') + it('with a cryptographic `nonce`', async () => { + await mutate({ mutation, variables }) + let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.toJson() + expect(emailAddress.nonce).toEqual(expect.any(String)) }) - }) - describe('if the EmailAddress already exists but without user account', () => { - // shall we re-send the registration email? - it.todo('decide what to do') + it('connects inviter through invitation code', async () => { + await mutate({ mutation, variables }) + const result = await neode.cypher( + 'MATCH(inviter:User)-[:GENERATED]->(:InvitationCode)-[:ACTIVATED]->(email:EmailAddress {email: {email}}) RETURN inviter', + { email: 'someuser@example.org' }, + ) + const inviter = neode.hydrateFirst(result, 'inviter', neode.model('User')) + await expect(inviter.toJson()).resolves.toEqual( + expect.objectContaining({ name: 'Inviter' }), + ) + }) + + describe('using the same InvitationCode twice', () => { + it('rejects because codes can be used only once', async () => { + await mutate({ mutation, variables }) + variables = { ...variables, email: 'yetanotheremail@example.org' } + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [ + { message: 'UserInputError: Invitation code already used or does not exist.' }, + ], + }) + }) + }) + + describe('if a user account with the given email already exists', () => { + beforeEach(async () => { + await factory.create('User', { email: 'someuser@example.org' }) + }) + + it('throws unique violation error', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'User account with this email already exists.' }], + }) + }) + }) + + describe('if the EmailAddress already exists but without user account', () => { + // shall we re-send the registration email? + it.todo('decide what to do') + }) }) }) }) @@ -251,61 +273,79 @@ describe('SignupByInvitation', () => { }) describe('Signup', () => { - const mutation = `mutation($email: String!) { - Signup(email: $email) { email } - }` - - it('throws AuthorizationError', async () => { - client = new GraphQLClient(host) - await expect( - client.request(mutation, { email: 'get-me-a-user-account@example.org' }), - ).rejects.toThrow('Not Authorised') + const mutation = gql` + mutation($email: String!) { + Signup(email: $email) { + email + } + } + ` + beforeEach(() => { + variables = { ...variables, email: 'someuser@example.org' } }) - describe('as admin', () => { - beforeEach(async () => { - userParams = { - role: 'admin', - email: 'admin@example.org', - password: '1234', - } - variables.email = 'someuser@example.org' - const factory = Factory() - await factory.create('User', userParams) - const headers = await login(userParams) - client = new GraphQLClient(host, { headers }) - action = async () => { - return client.request(mutation, variables) - } + describe('unauthenticated', () => { + beforeEach(() => { + authenticatedUser = null }) - it('is allowed to signup users by email', async () => { - await expect(action()).resolves.toEqual({ Signup: { email: 'someuser@example.org' } }) + it('throws AuthorizationError', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) }) - it('creates a Signup with a cryptographic `nonce`', async () => { - await action() - let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) + describe('as admin', () => { + beforeEach(async () => { + const admin = await factory.create('User', { + role: 'admin', + email: 'admin@example.org', + password: '1234', + }) + authenticatedUser = await admin.toJson() + }) + + it('is allowed to signup users by email', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { Signup: { email: 'someuser@example.org' } }, + }) + }) + + it('creates a Signup with a cryptographic `nonce`', async () => { + await mutate({ mutation, variables }) + let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.toJson() + expect(emailAddress.nonce).toEqual(expect.any(String)) + }) }) }) }) describe('SignupVerification', () => { - const mutation = ` - mutation($name: String!, $password: String!, $email: String!, $nonce: String!, $termsAndConditionsAgreedVersion: String!) { - SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) { - id - termsAndConditionsAgreedVersion - } + const mutation = gql` + mutation( + $name: String! + $password: String! + $email: String! + $nonce: String! + $termsAndConditionsAgreedVersion: String! + ) { + SignupVerification( + name: $name + password: $password + email: $email + nonce: $nonce + termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion + ) { + id + termsAndConditionsAgreedVersion } - ` + } + ` describe('given valid password and email', () => { - let variables - beforeEach(async () => { variables = { + ...variables, nonce: '123456', name: 'John Doe', password: '123', @@ -316,15 +356,15 @@ describe('SignupVerification', () => { describe('unauthenticated', () => { beforeEach(async () => { - client = new GraphQLClient(host) + authenticatedUser = null }) describe('EmailAddress exists, but is already related to a user account', () => { beforeEach(async () => { const { email, nonce } = variables const [emailAddress, user] = await Promise.all([ - instance.model('EmailAddress').create({ email, nonce }), - instance + neode.model('EmailAddress').create({ email, nonce }), + neode .model('User') .create({ name: 'Somebody', password: '1234', email: 'john@example.org' }), ]) @@ -333,13 +373,13 @@ describe('SignupVerification', () => { describe('sending a valid nonce', () => { beforeEach(() => { - variables.nonce = '123456' + variables = { ...variables, nonce: '123456' } }) it('rejects', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow( - 'Invalid email or nonce', - ) + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Invalid email or nonce' }], + }) }) }) }) @@ -350,22 +390,23 @@ describe('SignupVerification', () => { email: 'john@example.org', nonce: '123456', } - await instance.model('EmailAddress').create(args) + await neode.model('EmailAddress').create(args) }) describe('sending a valid nonce', () => { it('creates a user account', async () => { - const expected = { - SignupVerification: expect.objectContaining({ - id: expect.any(String), - }), - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { + SignupVerification: expect.objectContaining({ + id: expect.any(String), + }), + }, + }) }) it('sets `verifiedAt` attribute of EmailAddress', async () => { - await client.request(mutation, variables) - const email = await instance.first('EmailAddress', { email: 'john@example.org' }) + await mutate({ mutation, variables }) + const email = await neode.first('EmailAddress', { email: 'john@example.org' }) await expect(email.toJson()).resolves.toEqual( expect.objectContaining({ verifiedAt: expect.any(String), @@ -378,8 +419,8 @@ describe('SignupVerification', () => { MATCH(email:EmailAddress)-[:BELONGS_TO]->(u:User {name: {name}}) RETURN email ` - await client.request(mutation, variables) - const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' }) + await mutate({ mutation, variables }) + const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' }) expect(emails).toHaveLength(1) }) @@ -388,39 +429,38 @@ describe('SignupVerification', () => { MATCH(email:EmailAddress)<-[:PRIMARY_EMAIL]-(u:User {name: {name}}) RETURN email ` - await client.request(mutation, variables) - const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' }) + await mutate({ mutation, variables }) + const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' }) expect(emails).toHaveLength(1) }) it('is version of terms and conditions saved correctly', async () => { - const expected = { - SignupVerification: expect.objectContaining({ - termsAndConditionsAgreedVersion: '0.0.1', - }), - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { + SignupVerification: expect.objectContaining({ + termsAndConditionsAgreedVersion: '0.0.1', + }), + }, + }) }) it('rejects if version of terms and conditions has wrong format', async () => { - await expect( - client.request(mutation, { - ...variables, - termsAndConditionsAgreedVersion: 'invalid version format', - }), - ).rejects.toThrow('Invalid version format!') + variables = { ...variables, termsAndConditionsAgreedVersion: 'invalid version format' } + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Invalid version format!' }], + }) }) }) describe('sending invalid nonce', () => { beforeEach(() => { - variables.nonce = 'wut2' + variables = { ...variables, nonce: 'wut2' } }) it('rejects', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow( - 'Invalid email or nonce', - ) + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Invalid email or nonce' }], + }) }) }) }) From 4005d2fb01ff783c308f120f920ceb372e1f7bc1 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Fri, 6 Sep 2019 12:10:41 +0200 Subject: [PATCH 10/27] Fix #1505 remove html --- webapp/pages/moderation/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/pages/moderation/index.vue b/webapp/pages/moderation/index.vue index e6b8532fc..59755316b 100644 --- a/webapp/pages/moderation/index.vue +++ b/webapp/pages/moderation/index.vue @@ -26,7 +26,7 @@ params: { id: scope.row.comment.post.id, slug: scope.row.comment.post.slug }, }" > - {{ scope.row.comment.contentExcerpt | truncate(50) }} + {{ scope.row.comment.contentExcerpt | removeHtml | truncate(50) }}
From d3ad06a5f01131fdfe6cd899d53f7beeede039f7 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Fri, 6 Sep 2019 12:17:09 +0200 Subject: [PATCH 11/27] Fix preview image craziness - depending on the size of the image, the preview looked really blurry Also, the preview image differed from the image on the PostCard --- webapp/components/TeaserImage/TeaserImage.vue | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/webapp/components/TeaserImage/TeaserImage.vue b/webapp/components/TeaserImage/TeaserImage.vue index cb657fe9a..6a3adff7d 100644 --- a/webapp/components/TeaserImage/TeaserImage.vue +++ b/webapp/components/TeaserImage/TeaserImage.vue @@ -75,18 +75,21 @@ export default { return '' }, thumbnail: (file, dataUrl) => { - let thumbnailElement, contributionImage, uploadArea + let thumbnailElement, contributionImage, uploadArea, thumbnailPreview, image if (file.previewElement) { thumbnailElement = document.querySelectorAll('#postdropzone')[0] contributionImage = document.querySelectorAll('.contribution-image')[0] + thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0] if (contributionImage) { uploadArea = document.querySelectorAll('.hc-attachments-upload-area-update-post')[0] uploadArea.removeChild(contributionImage) uploadArea.classList.remove('hc-attachments-upload-area-update-post') } - thumbnailElement.classList.add('image-preview') - thumbnailElement.alt = file.name - thumbnailElement.style.backgroundImage = 'url("' + dataUrl + '")' + image = new Image() + image.src = URL.createObjectURL(file) + image.classList.add('thumbnail-preview') + if (thumbnailPreview) return thumbnailElement.replaceChild(image, thumbnailPreview) + thumbnailElement.appendChild(image) } }, }, @@ -99,24 +102,23 @@ export default { background-color: $background-color-softest; } -#postdropzone.image-preview { - background-repeat: no-repeat; - background-size: cover; - background-position: center; - height: auto; - transition: all 0.2s ease-out; - +#postdropzone img { width: 100%; + max-height: 300px; + -o-object-fit: cover; + object-fit: cover; + -o-object-position: center; + object-position: center; } @media only screen and (max-width: 400px) { - #postdropzone.image-preview { + #postdropzone.thumbnail-preview { height: 200px; } } @media only screen and (min-width: 401px) and (max-width: 960px) { - #postdropzone.image-preview { + #postdropzone.thumbnail-preview { height: 300px; } } From daf1ae864c9135bffe96fe899146e4b62f69a2d8 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Fri, 6 Sep 2019 13:08:28 +0200 Subject: [PATCH 12/27] Reuse css from ds-card-image, consistent width - the width of the contribution form is now the same as the post card --- .../ContributionForm/ContributionForm.vue | 14 +++++----- webapp/components/TeaserImage/TeaserImage.vue | 27 +++++-------------- webapp/pages/post/create.vue | 2 +- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 43f9f3d8c..9ff43d236 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -1,14 +1,14 @@