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 @@ + + + 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 @@