From 760f899379e074ddf84612fb34d5a3735d3ffa71 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 1 Jul 2019 16:18:14 +0200 Subject: [PATCH 01/18] import emotions --- .../migration/neo4j/emotions/delete.cql | 1 + .../migration/neo4j/emotions/emotions.cql | 57 +++++++++++++------ .../migration/neo4j/import.sh | 6 +- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql index e69de29bb..18fb6699f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql @@ -0,0 +1 @@ +MATCH (u:User)-[e:EMOTED]->(c:Post) DETACH DELETE e; \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql index 8aad9e923..06341f277 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql @@ -5,31 +5,54 @@ // [-] Omitted in Nitro // [?] Unclear / has work to be done for Nitro { -[ ] userId: { -[ ] type: String, -[ ] required: true, +[X] userId: { +[X] type: String, +[X] required: true, [-] index: true }, -[ ] contributionId: { -[ ] type: String, -[ ] required: true, +[X] contributionId: { +[X] type: String, +[X] required: true, [-] index: true }, -[ ] rated: { -[ ] type: String, +[?] rated: { +[X] type: String, [ ] required: true, -[ ] enum: ['funny', 'happy', 'surprised', 'cry', 'angry'] +[?] enum: ['funny', 'happy', 'surprised', 'cry', 'angry'] }, -[ ] createdAt: { -[ ] type: Date, -[ ] default: Date.now +[X] createdAt: { +[X] type: Date, +[X] default: Date.now }, -[ ] updatedAt: { -[ ] type: Date, -[ ] default: Date.now +[X] updatedAt: { +[X] type: Date, +[X] default: Date.now }, -[ ] wasSeeded: { type: Boolean } +[-] wasSeeded: { type: Boolean } } */ -CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion; +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion +MATCH (u:User {id: emotion.userId}), + (c:Post {id: emotion.contributionId}) +MERGE (u)-[e:EMOTED { + id: emotion._id["$oid"], + emotion: emotion.rated, + createdAt: datetime(emotion.createdAt.`$date`), + updatedAt: datetime(emotion.updatedAt.`$date`) + }]->(c) +RETURN e; +/* + // Queries + // user sets an emotion emotion: + // MERGE (u)-[e:EMOTED {id: ..., emotion: "funny", createdAt: ..., updatedAt: ...}]->(c) + // user removes emotion + // MATCH (u)-[e:EMOTED]->(c) DELETE e + // contribution distributions over every `emotion` property value for one post + // MATCH (u:User)-[e:EMOTED]->(c:Post {id: "5a70bbc8508f5b000b443b1a"}) RETURN e.emotion,COUNT(e.emotion) + // contribution distributions over every `emotion` property value for one user (advanced - "whats the primary emotion used by the user?") + // MATCH (u:User{id:"5a663b1ac64291000bf302a1"})-[e:EMOTED]->(c:Post) RETURN e.emotion,COUNT(e.emotion) + // contribution distributions over every `emotion` property value for all posts created by one user (advanced - "how do others react to my contributions?") + // MATCH (u:User)-[e:EMOTED]->(c:Post)<-[w:WROTE]-(a:User{id:"5a663b1ac64291000bf302a1"}) RETURN e.emotion,COUNT(e.emotion) + // if we can filter the above an a variable timescale that would be great (should be possible on createdAt and updatedAt fields) +*/ \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh index 8eef68c92..cef2846a7 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh @@ -60,8 +60,8 @@ delete_collection "contributions" "contributions_post" delete_collection "contributions" "contributions_cando" delete_collection "shouts" "shouts" delete_collection "comments" "comments" +delete_collection "emotions" "emotions" -#delete_collection "emotions" #delete_collection "invites" #delete_collection "notifications" #delete_collection "organizations" @@ -82,12 +82,12 @@ 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_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" "emotions/emotions.cql" -# import_collection "emotions" # import_collection "invites" # import_collection "notifications" # import_collection "organizations" From 44205e72687a48752bbf055689f32f2d25f94e23 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 1 Jul 2019 16:18:49 +0200 Subject: [PATCH 02/18] delete follows correctly --- .../maintenance-worker/migration/neo4j/follows/delete.cql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql index 3624448c3..3de01f8ea 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql @@ -1 +1 @@ -// this is just a relation between users(?) - no need to delete \ No newline at end of file +MATCH (u1:User)-[f:FOLLOWS]->(u2:User) DETACH DELETE f; \ No newline at end of file From a264ff8eee2837d5d742bdfb6893e720779efc3a Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 1 Jul 2019 16:21:11 +0200 Subject: [PATCH 03/18] emotion data modelling --- backend/src/schema/types/type/EMOTED.gql | 10 ++++++++++ backend/src/schema/types/type/Post.gql | 2 ++ backend/src/schema/types/type/User.gql | 2 ++ 3 files changed, 14 insertions(+) create mode 100644 backend/src/schema/types/type/EMOTED.gql diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql new file mode 100644 index 000000000..4c79d88e3 --- /dev/null +++ b/backend/src/schema/types/type/EMOTED.gql @@ -0,0 +1,10 @@ +type EMOTED @relation(name: "EMOTED") { + from: User + to: Post + + emotion: String + #createdAt: DateTime + #updatedAt: DateTime + createdAt: String + updatedAt: String +} \ No newline at end of file diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 271d92750..4b5d67572 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -48,4 +48,6 @@ type Post { RETURN COUNT(u) >= 1 """ ) + + emotions: [EMOTED] } diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 6836f16fe..63151db8d 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -77,4 +77,6 @@ type User { badges: [Badge]! @relation(name: "REWARDED", direction: "IN") badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + + emotions: [EMOTED] } From d76923c47171202eb2b961bb8bda709685265970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Jul 2019 16:56:06 +0200 Subject: [PATCH 04/18] Copy all relevant files changed from branch `signup` --- .../Registration/CreateUserAccount.spec.js | 125 +++++++++++++ .../Registration/CreateUserAccount.vue | 169 ++++++++++++++++++ webapp/components/Registration/Signup.spec.js | 145 +++++++++++++++ webapp/components/Registration/Signup.vue | 141 +++++++++++++++ webapp/locales/de.json | 26 ++- webapp/locales/en.json | 27 ++- webapp/nuxt.config.js | 8 +- webapp/pages/admin.vue | 4 + webapp/pages/admin/invite.vue | 23 +++ webapp/pages/login.vue | 2 +- .../registration/create-user-account.vue | 27 +++ 11 files changed, 690 insertions(+), 7 deletions(-) create mode 100644 webapp/components/Registration/CreateUserAccount.spec.js create mode 100644 webapp/components/Registration/CreateUserAccount.vue create mode 100644 webapp/components/Registration/Signup.spec.js create mode 100644 webapp/components/Registration/Signup.vue create mode 100644 webapp/pages/admin/invite.vue create mode 100644 webapp/pages/registration/create-user-account.vue diff --git a/webapp/components/Registration/CreateUserAccount.spec.js b/webapp/components/Registration/CreateUserAccount.spec.js new file mode 100644 index 000000000..f8364ca0d --- /dev/null +++ b/webapp/components/Registration/CreateUserAccount.spec.js @@ -0,0 +1,125 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import CreateUserAccount, { SignupVerificationMutation } from './CreateUserAccount' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('CreateUserAccount', () => { + let wrapper + let Wrapper + let mocks + let propsData + + beforeEach(() => { + mocks = { + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $t: jest.fn(), + $apollo: { + loading: false, + mutate: jest.fn(), + }, + } + propsData = {} + }) + + describe('mount', () => { + Wrapper = () => { + return mount(CreateUserAccount, { + mocks, + propsData, + localVue, + }) + } + + describe('given email and nonce', () => { + beforeEach(() => { + propsData.nonce = '666777' + propsData.email = 'sixseven@example.org' + }) + + it('renders a form to create a new user', () => { + wrapper = Wrapper() + expect(wrapper.find('.create-user-account').exists()).toBe(true) + }) + + describe('submit', () => { + let action + beforeEach(() => { + action = async () => { + wrapper = Wrapper() + wrapper.find('input#name').setValue('John Doe') + wrapper.find('input#password').setValue('hellopassword') + wrapper.find('input#confirmPassword').setValue('hellopassword') + await wrapper.find('form').trigger('submit') + } + }) + + it('calls CreateUserAccount graphql mutation', async () => { + await action() + const expected = expect.objectContaining({ mutation: SignupVerificationMutation }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + it('delivers data to backend', async () => { + await action() + const expected = expect.objectContaining({ + variables: { + about: '', + name: 'John Doe', + email: 'sixseven@example.org', + nonce: '666777', + password: 'hellopassword', + }, + }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + describe('in case mutation resolves', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest.fn().mockResolvedValue({ + data: { + SignupVerification: { + id: 'u1', + name: 'John Doe', + slug: 'john-doe', + }, + }, + }) + }) + + it('displays success', async () => { + await action() + expect(mocks.$t).toHaveBeenCalledWith('registration.create-user-account.success') + }) + + describe('after timeout', () => { + beforeEach(jest.useFakeTimers) + + it('emits `userCreated` with user', async () => { + await action() + jest.runAllTimers() + expect(wrapper.emitted('userCreated')).toBeTruthy() + }) + }) + }) + + describe('in case mutation rejects', () => { + beforeEach(() => { + mocks.$apollo.mutate.mockRejectedValue(new Error('Invalid nonce')) + }) + + it('displays form errors', async () => { + await action() + jest.runAllTimers() + expect(wrapper.find('.errors').text()).toContain('Invalid nonce') + }) + }) + }) + }) + }) +}) diff --git a/webapp/components/Registration/CreateUserAccount.vue b/webapp/components/Registration/CreateUserAccount.vue new file mode 100644 index 000000000..f38ebe509 --- /dev/null +++ b/webapp/components/Registration/CreateUserAccount.vue @@ -0,0 +1,169 @@ + + + diff --git a/webapp/components/Registration/Signup.spec.js b/webapp/components/Registration/Signup.spec.js new file mode 100644 index 000000000..eb0d65d42 --- /dev/null +++ b/webapp/components/Registration/Signup.spec.js @@ -0,0 +1,145 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import Signup, { SignupMutation, SignupByInvitationMutation } from './Signup' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('Signup', () => { + let wrapper + let Wrapper + let mocks + let propsData + + beforeEach(() => { + mocks = { + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $t: jest.fn(), + $apollo: { + loading: false, + mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }), + }, + } + propsData = {} + }) + + describe('mount', () => { + beforeEach(jest.useFakeTimers) + + Wrapper = () => { + return mount(Signup, { + mocks, + propsData, + localVue, + }) + } + + describe('without invitation code', () => { + it('renders signup form', () => { + wrapper = Wrapper() + expect(wrapper.find('.signup').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 Signup graphql mutation', () => { + const expected = expect.objectContaining({ mutation: SignupMutation }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + it('delivers email to backend', () => { + const expected = expect.objectContaining({ + variables: { email: 'mail@example.org', token: null }, + }) + 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 mail for email verification was sent', () => { + const expected = ['registration.signup.form.success', { 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' }]]) + }) + }) + }) + }) + + describe('with invitation code', () => { + let action + beforeEach(() => { + propsData.token = '666777' + action = async () => { + wrapper = Wrapper() + wrapper.find('input#email').setValue('mail@example.org') + await wrapper.find('form').trigger('submit') + } + }) + + describe('submit', () => { + it('calls SignupByInvitation graphql mutation', async () => { + await action() + const expected = expect.objectContaining({ mutation: SignupByInvitationMutation }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + it('delivers invitation token to backend', async () => { + await action() + const expected = expect.objectContaining({ + variables: { email: 'mail@example.org', token: '666777' }, + }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + describe('in case a user account with the email already exists', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest + .fn() + .mockRejectedValue( + new Error('UserInputError: User account with this email already exists.'), + ) + }) + + it.skip('explains the error', async () => { + await action() + expect(mocks.$t).toHaveBeenCalledWith('registration.signup.form.errors.email-exists') + }) + }) + + describe('in case the invitation code was incorrect', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest + .fn() + .mockRejectedValue( + new Error('UserInputError: Invitation code already used or does not exist.'), + ) + }) + + it.skip('explains the error', async () => { + await action() + expect(mocks.$t).toHaveBeenCalledWith( + 'registration.signup.form.errors.invalid-invitation-token', + ) + }) + }) + }) + }) + }) +}) diff --git a/webapp/components/Registration/Signup.vue b/webapp/components/Registration/Signup.vue new file mode 100644 index 000000000..3324b51c5 --- /dev/null +++ b/webapp/components/Registration/Signup.vue @@ -0,0 +1,141 @@ + + + diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 1df329fe1..4a6b0955e 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -14,7 +14,8 @@ "moreInfo": "Was ist Human Connection?", "moreInfoURL": "https://human-connection.org", "moreInfoHint": "zur Präsentationsseite", - "hello": "Hallo" + "hello": "Hallo", + "success": "Du bist eingeloggt!" }, "password-reset": { "title": "Passwort zurücksetzen", @@ -24,6 +25,24 @@ "submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an {email}" } }, + "registration": { + "signup": { + "title": "Mach mit bei Human Connection!", + "form": { + "description": "Um loszulegen, gib deine E-Mail Adresse ein:", + "errors": { + "email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!", + "invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden." + }, + "submit": "Konto erstellen", + "success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an {email} geschickt" + } + }, + "create-user-account": { + "title": "Benutzerkonto anlegen", + "success": "Dein Benutzerkonto wurde erstellt!" + } + }, "verify-code": { "form": { "code": "Code eingeben", @@ -174,6 +193,11 @@ }, "settings": { "name": "Einstellungen" + }, + "invites": { + "name": "Benutzer einladen", + "title": "Benutzer als Admin anmelden", + "description": "Dieses Anmeldeformular ist zu sehen sobald die Anmeldung öffentlich zugänglich ist." } }, "post": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index bdf6f85fb..2919a38ca 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -14,7 +14,8 @@ "moreInfo": "What is Human Connection?", "moreInfoURL": "https://human-connection.org/en/", "moreInfoHint": "to the presentation page", - "hello": "Hello" + "hello": "Hello", + "success": "You are logged in!" }, "password-reset": { "title": "Reset your password", @@ -24,6 +25,25 @@ "submitted": "A mail with further instruction has been sent to {email}" } }, + "registration": { + "signup": { + "title": "Join Human Connection!", + "form": { + "description": "To get started, enter your email address:", + "invitation-code": "Your invitation code is: {code}", + "errors": { + "email-exists": "There is already a user account with this email address!", + "invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once." + }, + "submit": "Create an account", + "success": "A mail with a link to complete your registration has been sent to {email}" + } + }, + "create-user-account": { + "title": "Create user account", + "success": "Your account has been created!" + } + }, "verify-code": { "form": { "code": "Enter your code", @@ -174,6 +194,11 @@ }, "settings": { "name": "Settings" + }, + "invites": { + "name": "Invite users", + "title": "Signup users as admin", + "description": "This registration form will be visible as soon as the registration is open to the public." } }, "post": { diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 7383f408a..cb3a3c643 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -31,10 +31,10 @@ module.exports = { 'password-reset-request', 'password-reset-verify-code', 'password-reset-change-password', - 'register', - 'signup', - 'reset', - 'reset-token', + // 'registration-signup', TODO: uncomment to open public registration + 'registration-signup-by-invitation-code', + 'registration-verify-code', + 'registration-create-user-account', 'pages-slug', ], // pages to keep alive diff --git a/webapp/pages/admin.vue b/webapp/pages/admin.vue index 0cbee05d5..1cf503808 100644 --- a/webapp/pages/admin.vue +++ b/webapp/pages/admin.vue @@ -54,6 +54,10 @@ export default { name: this.$t('admin.tags.name'), path: `/admin/tags`, }, + { + name: this.$t('admin.invites.name'), + path: `/admin/invite`, + }, // TODO implement /* { name: this.$t('admin.settings.name'), diff --git a/webapp/pages/admin/invite.vue b/webapp/pages/admin/invite.vue new file mode 100644 index 000000000..36e679112 --- /dev/null +++ b/webapp/pages/admin/invite.vue @@ -0,0 +1,23 @@ + + + diff --git a/webapp/pages/login.vue b/webapp/pages/login.vue index 2679b2e99..e659d8448 100644 --- a/webapp/pages/login.vue +++ b/webapp/pages/login.vue @@ -115,7 +115,7 @@ export default { async onSubmit() { try { await this.$store.dispatch('auth/login', { ...this.form }) - this.$toast.success('You are logged in!') + this.$toast.success(this.$t('login.success')) this.$router.replace(this.$route.query.path || '/') } catch (err) { this.$toast.error(err.message) diff --git a/webapp/pages/registration/create-user-account.vue b/webapp/pages/registration/create-user-account.vue new file mode 100644 index 000000000..812e4ec60 --- /dev/null +++ b/webapp/pages/registration/create-user-account.vue @@ -0,0 +1,27 @@ + + + From 29bbeaa0fae4cd4d85dc0c64cdc8b847c6a192eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Jul 2019 17:42:53 +0200 Subject: [PATCH 05/18] Fix tests by calling `wrapper.html()` once more I really don't understand why, but apparently `wrapper.html()` does some re-rendering which in our cases fixes the tests, because we reach the new sub component in the DOM tree. --- .../Registration/CreateUserAccount.spec.js | 15 +++++++++++---- .../components/Registration/CreateUserAccount.vue | 5 +++-- webapp/components/Registration/Signup.spec.js | 5 +++-- webapp/pages/registration/create-user-account.vue | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/webapp/components/Registration/CreateUserAccount.spec.js b/webapp/components/Registration/CreateUserAccount.spec.js index f8364ca0d..05a11bc33 100644 --- a/webapp/components/Registration/CreateUserAccount.spec.js +++ b/webapp/components/Registration/CreateUserAccount.spec.js @@ -56,6 +56,7 @@ describe('CreateUserAccount', () => { wrapper.find('input#password').setValue('hellopassword') wrapper.find('input#confirmPassword').setValue('hellopassword') await wrapper.find('form').trigger('submit') + await wrapper.html() } }) @@ -100,22 +101,28 @@ describe('CreateUserAccount', () => { describe('after timeout', () => { beforeEach(jest.useFakeTimers) - it('emits `userCreated` with user', async () => { + it('emits `userCreated` with { password, email }', async () => { await action() jest.runAllTimers() - expect(wrapper.emitted('userCreated')).toBeTruthy() + expect(wrapper.emitted('userCreated')).toEqual([ + [ + { + email: 'sixseven@example.org', + password: 'hellopassword', + }, + ], + ]) }) }) }) describe('in case mutation rejects', () => { beforeEach(() => { - mocks.$apollo.mutate.mockRejectedValue(new Error('Invalid nonce')) + mocks.$apollo.mutate = jest.fn().mockRejectedValue(new Error('Invalid nonce')) }) it('displays form errors', async () => { await action() - jest.runAllTimers() expect(wrapper.find('.errors').text()).toContain('Invalid nonce') }) }) diff --git a/webapp/components/Registration/CreateUserAccount.vue b/webapp/components/Registration/CreateUserAccount.vue index f38ebe509..4b91ab216 100644 --- a/webapp/components/Registration/CreateUserAccount.vue +++ b/webapp/components/Registration/CreateUserAccount.vue @@ -143,12 +143,13 @@ export default { try { await this.$apollo.mutate({ mutation: SignupVerificationMutation, - variables: { name, password, about, email, nonce } + variables: { name, password, about, email, nonce }, }) this.success = true setTimeout(() => { this.$emit('userCreated', { - email, password + email, + password, }) }, 3000) } catch (err) { diff --git a/webapp/components/Registration/Signup.spec.js b/webapp/components/Registration/Signup.spec.js index eb0d65d42..e66b39bb7 100644 --- a/webapp/components/Registration/Signup.spec.js +++ b/webapp/components/Registration/Signup.spec.js @@ -90,6 +90,7 @@ describe('Signup', () => { wrapper = Wrapper() wrapper.find('input#email').setValue('mail@example.org') await wrapper.find('form').trigger('submit') + await wrapper.html() } }) @@ -117,7 +118,7 @@ describe('Signup', () => { ) }) - it.skip('explains the error', async () => { + it('explains the error', async () => { await action() expect(mocks.$t).toHaveBeenCalledWith('registration.signup.form.errors.email-exists') }) @@ -132,7 +133,7 @@ describe('Signup', () => { ) }) - it.skip('explains the error', async () => { + it('explains the error', async () => { await action() expect(mocks.$t).toHaveBeenCalledWith( 'registration.signup.form.errors.invalid-invitation-token', diff --git a/webapp/pages/registration/create-user-account.vue b/webapp/pages/registration/create-user-account.vue index 812e4ec60..677aea5eb 100644 --- a/webapp/pages/registration/create-user-account.vue +++ b/webapp/pages/registration/create-user-account.vue @@ -13,7 +13,7 @@ export default { CreateUserAccount, }, methods: { - async handleUserCreated({email, password}) { + async handleUserCreated({ email, password }) { try { await this.$store.dispatch('auth/login', { email, password }) this.$toast.success('You are logged in!') From 813a6b5c50e9b3ea39d6816d9026c809990bd72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Jul 2019 17:55:50 +0200 Subject: [PATCH 06/18] Subpages of registration/ must be unauthenticated --- webapp/pages/registration.vue | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 webapp/pages/registration.vue diff --git a/webapp/pages/registration.vue b/webapp/pages/registration.vue new file mode 100644 index 000000000..e75ec03e1 --- /dev/null +++ b/webapp/pages/registration.vue @@ -0,0 +1,14 @@ + + + From 90aa1822b5ed9757b7047a86a518782c8c51c78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Jul 2019 18:13:24 +0200 Subject: [PATCH 07/18] Disable the form button with a slot scope --- webapp/components/Password/Change.vue | 12 +- .../PasswordReset/ChangePassword.vue | 62 +++++----- .../Registration/CreateUserAccount.vue | 108 +++++++++--------- 3 files changed, 80 insertions(+), 102 deletions(-) diff --git a/webapp/components/Password/Change.vue b/webapp/components/Password/Change.vue index 63c797157..ab9a89cbc 100644 --- a/webapp/components/Password/Change.vue +++ b/webapp/components/Password/Change.vue @@ -3,10 +3,8 @@ v-model="formData" :schema="formSchema" @submit="handleSubmit" - @input="handleInput" - @input-valid="handleInputValid" > -