diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js
index eeb3a49d3..1a9891a7a 100644
--- a/cypress/integration/common/steps.js
+++ b/cypress/integration/common/steps.js
@@ -5,7 +5,7 @@ import { getLangByName } from '../../support/helpers'
let lastPost = {}
-const loginCredentials = {
+let loginCredentials = {
email: 'peterpan@example.org',
password: '1234'
}
@@ -244,3 +244,48 @@ Then(
cy.get('.error').should('contain', message)
}
)
+
+Given('my user account has the following login credentials:', table => {
+ loginCredentials = table.hashes()[0]
+ cy.debug()
+ cy.factory().create('User', loginCredentials)
+})
+
+When('I fill the password form with:', table => {
+ table = table.rowsHash()
+ cy.get('input[id=oldPassword]')
+ .type(table['Your old password'])
+ .get('input[id=newPassword]')
+ .type(table['Your new passsword'])
+ .get('input[id=confirmPassword]')
+ .type(table['Confirm new password'])
+})
+
+When('submit the form', () => {
+ cy.get('form').submit()
+})
+
+Then('I cannot login anymore with password {string}', password => {
+ cy.reload()
+ const { email } = loginCredentials
+ cy.visit(`/login`)
+ cy.get('input[name=email]')
+ .trigger('focus')
+ .type(email)
+ cy.get('input[name=password]')
+ .trigger('focus')
+ .type(password)
+ cy.get('button[name=submit]')
+ .as('submitButton')
+ .click()
+ cy.get('.iziToast-wrapper').should('contain', 'Incorrect email address or password.')
+})
+
+Then('I can login successfully with password {string}', password => {
+ cy.reload()
+ cy.login({
+ ...loginCredentials,
+ ...{password}
+ })
+ cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
+})
diff --git a/cypress/integration/settings/ChangePassword.feature b/cypress/integration/settings/ChangePassword.feature
new file mode 100644
index 000000000..44e4e5483
--- /dev/null
+++ b/cypress/integration/settings/ChangePassword.feature
@@ -0,0 +1,31 @@
+Feature: Change password
+ As a user
+ I want to change my password in my settings
+ For security, e.g. if I exposed my password by accident
+
+ Login via email and password is a well-known authentication procedure and you
+ can assure to the server that you are who you claim to be. Either if you
+ exposed your password by acccident and you want to invalidate the exposed
+ password or just out of an good habit, you want to change your password.
+
+ Background:
+ Given my user account has the following login credentials:
+ | email | password |
+ | user@example.org | exposed |
+ And I am logged in
+
+ Scenario: Change my password
+ Given I am on the "settings" page
+ And I click on "Security"
+ When I fill the password form with:
+ | Your old password | exposed |
+ | Your new passsword | secure |
+ | Confirm new password | secure |
+ And submit the form
+ And I see a success message:
+ """
+ Password successfully changed!
+ """
+ And I log out through the menu in the top right corner
+ Then I cannot login anymore with password "exposed"
+ But I can login successfully with password "secure"
diff --git a/webapp/components/ChangePassword.spec.js b/webapp/components/ChangePassword.spec.js
new file mode 100644
index 000000000..98a66da72
--- /dev/null
+++ b/webapp/components/ChangePassword.spec.js
@@ -0,0 +1,154 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import ChangePassword from './ChangePassword.vue'
+import Vue from 'vue'
+import Styleguide from '@human-connection/styleguide'
+
+const localVue = createLocalVue()
+
+localVue.use(Styleguide)
+
+describe('ChangePassword.vue', () => {
+ let mocks
+ let wrapper
+
+ beforeEach(() => {
+ mocks = {
+ validate: jest.fn(),
+ $toast: {
+ error: jest.fn(),
+ success: jest.fn()
+ },
+ $t: jest.fn(),
+ $store: {
+ commit: jest.fn()
+ },
+ $apollo: {
+ mutate: jest
+ .fn()
+ .mockRejectedValue({ message: 'Ouch!' })
+ .mockResolvedValueOnce({ data: { changePassword: 'NEWTOKEN' } })
+ }
+ }
+ })
+
+ describe('mount', () => {
+ let wrapper
+ const Wrapper = () => {
+ return mount(ChangePassword, { mocks, localVue })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ it('renders three input fields', () => {
+ expect(wrapper.findAll('input')).toHaveLength(3)
+ })
+
+ describe('validations', () => {
+ it('invalid', () => {
+ expect(wrapper.vm.disabled).toBe(true)
+ })
+
+ describe('old password and new password', () => {
+ describe('match', () => {
+ beforeEach(() => {
+ wrapper.find('input#oldPassword').setValue('some secret')
+ wrapper.find('input#newPassword').setValue('some secret')
+ })
+
+ it('invalid', () => {
+ expect(wrapper.vm.disabled).toBe(true)
+ })
+
+ it.skip('displays a warning', () => {
+ const calls = mocks.validate.mock.calls
+ const expected = [
+ ['change-password.validations.old-and-new-password-match']
+ ]
+ expect(calls).toEqual(expect.arrayContaining(expected))
+ })
+ })
+ })
+
+ describe('new password and confirmation', () => {
+ describe('mismatch', () => {
+ it.todo('invalid')
+ it.todo('displays a warning')
+ })
+
+ describe('match', () => {
+ describe('and old password mismatch', () => {
+ it.todo('valid')
+ })
+
+ describe('clicked', () => {
+ it.todo('sets loading')
+ })
+ })
+ })
+ })
+
+ describe('given valid input', () => {
+ beforeEach(() => {
+ wrapper.find('input#oldPassword').setValue('supersecret')
+ wrapper.find('input#newPassword').setValue('superdupersecret')
+ wrapper.find('input#confirmPassword').setValue('superdupersecret')
+ })
+
+ describe('submit form', () => {
+ beforeEach(() => {
+ wrapper.find('form').trigger('submit')
+ })
+
+ it('calls changePassword mutation', () => {
+ expect(mocks.$apollo.mutate).toHaveBeenCalled()
+ })
+
+ it('passes form data as variables', () => {
+ expect(mocks.$apollo.mutate.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ variables: {
+ oldPassword: 'supersecret',
+ newPassword: 'superdupersecret',
+ confirmPassword: 'superdupersecret'
+ }
+ })
+ )
+ })
+
+ describe('mutation resolves', () => {
+ beforeEach(() => {
+ mocks.$apollo.mutate = jest.fn().mockResolvedValue()
+ wrapper = Wrapper()
+ })
+
+ it('calls auth/SET_TOKEN with response', () => {
+ expect(mocks.$store.commit).toHaveBeenCalledWith(
+ 'auth/SET_TOKEN',
+ 'NEWTOKEN'
+ )
+ })
+
+ it('displays success message', () => {
+ expect(mocks.$t).toHaveBeenCalledWith(
+ 'settings.security.change-password.success'
+ )
+ expect(mocks.$toast.success).toHaveBeenCalled()
+ })
+ })
+
+ describe('mutation rejects', () => {
+ beforeEach(() => {
+ // second call will reject
+ wrapper.find('form').trigger('submit')
+ })
+
+ it('displays error message', () => {
+ expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!')
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/webapp/components/ChangePassword.vue b/webapp/components/ChangePassword.vue
new file mode 100644
index 000000000..dc8cc09da
--- /dev/null
+++ b/webapp/components/ChangePassword.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+ {{ $t('settings.security.change-password.button') }}
+
+
+
+
+
+
+
diff --git a/webapp/locales/de.json b/webapp/locales/de.json
index 2ec3bac9f..6e47d7122 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -31,7 +31,11 @@
"labelBio": "Über dich"
},
"security": {
- "name": "Sicherheit"
+ "name": "Sicherheit",
+ "change-password": {
+ "button": "Passwort ändern",
+ "success": "Passwort erfolgreich geändert!"
+ }
},
"invites": {
"name": "Einladungen"
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index fe92f901a..62c8f3e19 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -31,7 +31,11 @@
"labelBio": "About You"
},
"security": {
- "name": "Security"
+ "name": "Security",
+ "change-password": {
+ "button": "Change password",
+ "success": "Password successfully changed!"
+ }
},
"invites": {
"name": "Invites"
diff --git a/webapp/pages/settings/security.vue b/webapp/pages/settings/security.vue
index 937aac9dd..376f104e5 100644
--- a/webapp/pages/settings/security.vue
+++ b/webapp/pages/settings/security.vue
@@ -1,18 +1,16 @@
-
+