mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-05 09:05:33 +00:00
refactor(webapp): extract password form into its own component (#9469)
This commit is contained in:
parent
da95664285
commit
8849db6cbf
@ -1,166 +0,0 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import ChangePassword from './Change.vue'
|
|
||||||
|
|
||||||
const localVue = global.localVue
|
|
||||||
|
|
||||||
describe('ChangePassword.vue', () => {
|
|
||||||
let mocks
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mocks = {
|
|
||||||
$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', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// $t must return strings so async-validator produces proper error messages
|
|
||||||
// (jest.fn() returns undefined which causes Error(undefined) → DsInputError type warning)
|
|
||||||
mocks.$t = jest.fn((key) => key)
|
|
||||||
wrapper = Wrapper()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('new password and confirmation', () => {
|
|
||||||
describe('mismatch', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await wrapper.find('input#oldPassword').setValue('oldsecret')
|
|
||||||
await wrapper.find('input#password').setValue('superdupersecret')
|
|
||||||
await wrapper.find('input#passwordConfirmation').setValue('different')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not submit the form', async () => {
|
|
||||||
mocks.$apollo.mutate.mockReset()
|
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays a validation error', async () => {
|
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
expect(wrapper.vm.formErrors).toHaveProperty('passwordConfirmation')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('match', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await wrapper.find('input#oldPassword').setValue('oldsecret')
|
|
||||||
await wrapper.find('input#password').setValue('superdupersecret')
|
|
||||||
await wrapper.find('input#passwordConfirmation').setValue('superdupersecret')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('passes validation and submits', async () => {
|
|
||||||
mocks.$apollo.mutate.mockReset()
|
|
||||||
mocks.$apollo.mutate.mockResolvedValue({ data: { changePassword: 'TOKEN' } })
|
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('while mutation is pending', () => {
|
|
||||||
it('sets loading while mutation is pending', async () => {
|
|
||||||
mocks.$apollo.mutate.mockReset()
|
|
||||||
let resolvePromise
|
|
||||||
mocks.$apollo.mutate.mockReturnValue(
|
|
||||||
new Promise((resolve) => {
|
|
||||||
resolvePromise = resolve
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const promise = wrapper.vm.handleSubmit()
|
|
||||||
expect(wrapper.vm.loading).toBe(true)
|
|
||||||
|
|
||||||
resolvePromise({ data: { changePassword: 'TOKEN' } })
|
|
||||||
await promise
|
|
||||||
expect(wrapper.vm.loading).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('given valid input', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper.find('input#oldPassword').setValue('supersecret')
|
|
||||||
wrapper.find('input#password').setValue('superdupersecret')
|
|
||||||
wrapper.find('input#passwordConfirmation').setValue('superdupersecret')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('submit form', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await 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',
|
|
||||||
password: 'superdupersecret',
|
|
||||||
passwordConfirmation: 'superdupersecret',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('mutation resolves', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper.find('form').trigger('submit')
|
|
||||||
})
|
|
||||||
|
|
||||||
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(async () => {
|
|
||||||
await wrapper.find('input#oldPassword').setValue('supersecret')
|
|
||||||
await wrapper.find('input#password').setValue('supersecret')
|
|
||||||
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays error message', async () => {
|
|
||||||
expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
183
webapp/components/Password/PasswordForm.spec.js
Normal file
183
webapp/components/Password/PasswordForm.spec.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import PasswordForm from './PasswordForm.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('PasswordForm.vue', () => {
|
||||||
|
let mocks
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn((key) => key),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Wrapper = (props = {}) => {
|
||||||
|
return mount(PasswordForm, { mocks, localVue, propsData: props })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('without requireOldPassword', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders two input fields (new password, confirm)', () => {
|
||||||
|
expect(wrapper.findAll('input')).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render old password field', () => {
|
||||||
|
expect(wrapper.find('input#oldPassword').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders new password field', () => {
|
||||||
|
expect(wrapper.find('input#password').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders password confirmation field', () => {
|
||||||
|
expect(wrapper.find('input#passwordConfirmation').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders submit button', () => {
|
||||||
|
expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders password strength indicator', () => {
|
||||||
|
expect(wrapper.findComponent({ name: 'PasswordMeter' }).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit with empty fields', () => {
|
||||||
|
it('does not emit submit', async () => {
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.emitted('submit')).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit with mismatched passwords', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('different')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not emit submit', async () => {
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.emitted('submit')).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit with valid input', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits submit with form data', async () => {
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.emitted('submit')).toBeTruthy()
|
||||||
|
expect(wrapper.emitted('submit')[0][0]).toEqual({
|
||||||
|
password: 'supersecret',
|
||||||
|
passwordConfirmation: 'supersecret',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets loading state on submit', async () => {
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.vm.loading).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('done()', () => {
|
||||||
|
it('resets loading and form fields', async () => {
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
wrapper.vm.done()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
expect(wrapper.vm.formData.password).toBe('')
|
||||||
|
expect(wrapper.vm.formData.passwordConfirmation).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fail()', () => {
|
||||||
|
it('resets loading but keeps form data', async () => {
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
wrapper.vm.fail()
|
||||||
|
expect(wrapper.vm.loading).toBe(false)
|
||||||
|
expect(wrapper.vm.formData.password).toBe('supersecret')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with requireOldPassword', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper({ requireOldPassword: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders three input fields', () => {
|
||||||
|
expect(wrapper.findAll('input')).toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders old password field', () => {
|
||||||
|
expect(wrapper.find('input#oldPassword').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit without old password', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not emit submit', async () => {
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.emitted('submit')).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit with all fields valid', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('input#oldPassword').setValue('oldsecret')
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits submit with all form data including oldPassword', async () => {
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.emitted('submit')[0][0]).toEqual({
|
||||||
|
oldPassword: 'oldsecret',
|
||||||
|
password: 'supersecret',
|
||||||
|
passwordConfirmation: 'supersecret',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('done()', () => {
|
||||||
|
it('also resets oldPassword field', async () => {
|
||||||
|
await wrapper.find('input#oldPassword').setValue('oldsecret')
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
wrapper.vm.done()
|
||||||
|
expect(wrapper.vm.formData.oldPassword).toBe('')
|
||||||
|
expect(wrapper.vm.formData.password).toBe('')
|
||||||
|
expect(wrapper.vm.formData.passwordConfirmation).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="onSubmit" novalidate>
|
<form @submit.prevent="onSubmit" novalidate>
|
||||||
<ocelot-input
|
<ocelot-input
|
||||||
|
v-if="requireOldPassword"
|
||||||
id="oldPassword"
|
id="oldPassword"
|
||||||
model="oldPassword"
|
model="oldPassword"
|
||||||
type="password"
|
type="password"
|
||||||
@ -42,14 +43,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||||
import { iconRegistry } from '~/utils/iconRegistry'
|
import { iconRegistry } from '~/utils/iconRegistry'
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import PasswordStrength from './Strength'
|
import PasswordStrength from './Strength'
|
||||||
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||||
import formValidation from '~/mixins/formValidation'
|
import formValidation from '~/mixins/formValidation'
|
||||||
import OcelotInput from '~/components/OcelotInput/OcelotInput.vue'
|
import OcelotInput from '~/components/OcelotInput/OcelotInput.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChangePassword',
|
name: 'PasswordForm',
|
||||||
mixins: [formValidation],
|
mixins: [formValidation],
|
||||||
components: {
|
components: {
|
||||||
OsButton,
|
OsButton,
|
||||||
@ -57,24 +57,31 @@ export default {
|
|||||||
PasswordStrength,
|
PasswordStrength,
|
||||||
OcelotInput,
|
OcelotInput,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
/** Require old password field (for authenticated password change) */
|
||||||
|
requireOldPassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.icons = iconRegistry
|
this.icons = iconRegistry
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const passwordForm = PasswordForm({ translate: this.$t })
|
const passwordForm = PasswordForm({ translate: this.$t })
|
||||||
|
const formData = { ...passwordForm.formData }
|
||||||
|
const formSchema = { ...passwordForm.formSchema }
|
||||||
|
if (this.requireOldPassword) {
|
||||||
|
formData.oldPassword = ''
|
||||||
|
formSchema.oldPassword = {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: this.$t('settings.security.change-password.message-old-password-required'),
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
formData: {
|
formData,
|
||||||
oldPassword: '',
|
formSchema,
|
||||||
...passwordForm.formData,
|
|
||||||
},
|
|
||||||
formSchema: {
|
|
||||||
oldPassword: {
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
message: this.$t('settings.security.change-password.message-old-password-required'),
|
|
||||||
},
|
|
||||||
...passwordForm.formSchema,
|
|
||||||
},
|
|
||||||
loading: false,
|
loading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -82,29 +89,20 @@ export default {
|
|||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.formSubmit(this.handleSubmit)
|
this.formSubmit(this.handleSubmit)
|
||||||
},
|
},
|
||||||
async handleSubmit(data) {
|
handleSubmit() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const mutation = gql`
|
this.$emit('submit', { ...this.formData })
|
||||||
mutation ($oldPassword: String!, $password: String!) {
|
},
|
||||||
changePassword(oldPassword: $oldPassword, newPassword: $password)
|
/** Called by parent after mutation completes */
|
||||||
}
|
done() {
|
||||||
`
|
this.loading = false
|
||||||
const variables = this.formData
|
const resetData = { password: '', passwordConfirmation: '' }
|
||||||
|
if (this.requireOldPassword) resetData.oldPassword = ''
|
||||||
try {
|
this.formData = resetData
|
||||||
const { data } = await this.$apollo.mutate({ mutation, variables })
|
},
|
||||||
this.$store.commit('auth/SET_TOKEN', data.changePassword)
|
/** Called by parent on mutation error */
|
||||||
this.$toast.success(this.$t('settings.security.change-password.success'))
|
fail() {
|
||||||
this.formData = {
|
this.loading = false
|
||||||
oldPassword: '',
|
|
||||||
password: '',
|
|
||||||
passwordConfirmation: '',
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.$toast.error(err.message)
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1,103 +0,0 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import ChangePassword from './ChangePassword'
|
|
||||||
|
|
||||||
const localVue = global.localVue
|
|
||||||
|
|
||||||
const stubs = {
|
|
||||||
'sweetalert-icon': true,
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ChangePassword ', () => {
|
|
||||||
let wrapper
|
|
||||||
let Wrapper
|
|
||||||
let mocks
|
|
||||||
let propsData
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
propsData = {}
|
|
||||||
mocks = {
|
|
||||||
$toast: {
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
},
|
|
||||||
$t: jest.fn(),
|
|
||||||
$apollo: {
|
|
||||||
loading: false,
|
|
||||||
mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('mount', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.useFakeTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
Wrapper = () => {
|
|
||||||
return mount(ChangePassword, {
|
|
||||||
mocks,
|
|
||||||
propsData,
|
|
||||||
localVue,
|
|
||||||
stubs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('given email and nonce', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
propsData.email = 'mail@example.org'
|
|
||||||
propsData.nonce = '12345'
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('submitting new password', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = Wrapper()
|
|
||||||
wrapper.find('input#password').setValue('supersecret')
|
|
||||||
wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
|
||||||
wrapper.find('form').trigger('submit')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls resetPassword graphql mutation', () => {
|
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('delivers new password to backend', () => {
|
|
||||||
const expected = expect.objectContaining({
|
|
||||||
variables: { nonce: '12345', email: 'mail@example.org', password: 'supersecret' },
|
|
||||||
})
|
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('password reset successful', () => {
|
|
||||||
it('displays success message', () => {
|
|
||||||
const expected = 'components.password-reset.change-password.success'
|
|
||||||
expect(mocks.$t).toHaveBeenCalledWith(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('after animation', () => {
|
|
||||||
beforeEach(jest.runAllTimers)
|
|
||||||
|
|
||||||
it('emits `change-password-sucess`', () => {
|
|
||||||
expect(wrapper.emitted('passwordResetResponse')).toEqual([['success']])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('password reset not successful', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mocks.$apollo.mutate = jest.fn().mockRejectedValue({
|
|
||||||
message: 'Ouch!',
|
|
||||||
})
|
|
||||||
wrapper = Wrapper()
|
|
||||||
wrapper.find('input#password').setValue('supersecret')
|
|
||||||
wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
|
||||||
wrapper.find('form').trigger('submit')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('display a toast error', () => {
|
|
||||||
expect(mocks.$toast.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="ds-mt-base ds-mb-xxx-small">
|
|
||||||
<form
|
|
||||||
v-if="!changePasswordResult"
|
|
||||||
@submit.prevent="onSubmit"
|
|
||||||
class="change-password"
|
|
||||||
novalidate
|
|
||||||
>
|
|
||||||
<ocelot-input
|
|
||||||
id="password"
|
|
||||||
model="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="off"
|
|
||||||
:label="$t('settings.security.change-password.label-new-password')"
|
|
||||||
/>
|
|
||||||
<ocelot-input
|
|
||||||
id="passwordConfirmation"
|
|
||||||
model="passwordConfirmation"
|
|
||||||
type="password"
|
|
||||||
autocomplete="off"
|
|
||||||
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
|
||||||
/>
|
|
||||||
<password-strength :password="formData.password" />
|
|
||||||
<div class="ds-mt-base ds-mb-xxx-small">
|
|
||||||
<os-button
|
|
||||||
variant="primary"
|
|
||||||
appearance="filled"
|
|
||||||
:loading="$apollo.loading"
|
|
||||||
:disabled="!!formErrors"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<os-icon :icon="icons.lock" />
|
|
||||||
</template>
|
|
||||||
{{ $t('settings.security.change-password.button') }}
|
|
||||||
</os-button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div v-else class="ds-mb-large">
|
|
||||||
<template v-if="changePasswordResult === 'success'">
|
|
||||||
<transition name="ds-transition-fade">
|
|
||||||
<sweetalert-icon icon="success" />
|
|
||||||
</transition>
|
|
||||||
<p class="ds-text">
|
|
||||||
{{ $t('components.password-reset.change-password.success') }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<transition name="ds-transition-fade">
|
|
||||||
<sweetalert-icon icon="error" />
|
|
||||||
</transition>
|
|
||||||
<p class="ds-text">
|
|
||||||
{{ $t(`components.password-reset.change-password.error`) }}
|
|
||||||
</p>
|
|
||||||
<p class="ds-text">
|
|
||||||
{{ $t('components.password-reset.change-password.help') }}
|
|
||||||
</p>
|
|
||||||
<p class="ds-text">
|
|
||||||
<a :href="'mailto:' + supportEmail">{{ supportEmail }}</a>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
|
||||||
import { iconRegistry } from '~/utils/iconRegistry'
|
|
||||||
import emails from '../../constants/emails.js'
|
|
||||||
import PasswordStrength from '../Password/Strength'
|
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
|
||||||
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
|
||||||
import formValidation from '~/mixins/formValidation'
|
|
||||||
import OcelotInput from '~/components/OcelotInput/OcelotInput.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [formValidation],
|
|
||||||
components: {
|
|
||||||
OsButton,
|
|
||||||
OsIcon,
|
|
||||||
SweetalertIcon,
|
|
||||||
PasswordStrength,
|
|
||||||
OcelotInput,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
email: { type: String, required: true },
|
|
||||||
nonce: { type: String, required: true },
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.icons = iconRegistry
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
const passwordForm = PasswordForm({ translate: this.$t })
|
|
||||||
return {
|
|
||||||
supportEmail: emails.SUPPORT_EMAIL,
|
|
||||||
formData: {
|
|
||||||
...passwordForm.formData,
|
|
||||||
},
|
|
||||||
formSchema: {
|
|
||||||
...passwordForm.formSchema,
|
|
||||||
},
|
|
||||||
disabled: true,
|
|
||||||
changePasswordResult: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onSubmit() {
|
|
||||||
this.formSubmit(this.handleSubmitPassword)
|
|
||||||
},
|
|
||||||
async handleSubmitPassword() {
|
|
||||||
const mutation = gql`
|
|
||||||
mutation ($nonce: String!, $email: String!, $password: String!) {
|
|
||||||
resetPassword(nonce: $nonce, email: $email, newPassword: $password)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const { password } = this.formData
|
|
||||||
const { email, nonce } = this
|
|
||||||
const variables = { password, email, nonce }
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: { resetPassword },
|
|
||||||
} = await this.$apollo.mutate({ mutation, variables })
|
|
||||||
this.changePasswordResult = resetPassword ? 'success' : 'error'
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$emit('passwordResetResponse', this.changePasswordResult)
|
|
||||||
}, 3000)
|
|
||||||
this.formData = {
|
|
||||||
password: '',
|
|
||||||
passwordConfirmation: '',
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.$toast.error(err.message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
20
webapp/composables/useChangePassword.js
Normal file
20
webapp/composables/useChangePassword.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { changePasswordMutation } from '~/graphql/Password'
|
||||||
|
|
||||||
|
export function useChangePassword({ apollo, store, toast, t }) {
|
||||||
|
async function changePassword({ oldPassword, password }) {
|
||||||
|
try {
|
||||||
|
const { data } = await apollo.mutate({
|
||||||
|
mutation: changePasswordMutation,
|
||||||
|
variables: { oldPassword, password },
|
||||||
|
})
|
||||||
|
store.commit('auth/SET_TOKEN', data.changePassword)
|
||||||
|
toast.success(t('settings.security.change-password.success'))
|
||||||
|
return { success: true }
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message)
|
||||||
|
return { success: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { changePassword }
|
||||||
|
}
|
||||||
58
webapp/composables/useChangePassword.spec.js
Normal file
58
webapp/composables/useChangePassword.spec.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useChangePassword } from './useChangePassword'
|
||||||
|
|
||||||
|
describe('useChangePassword', () => {
|
||||||
|
let apollo, store, toast, t, changePassword
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
apollo = { mutate: jest.fn().mockResolvedValue({ data: { changePassword: 'NEWTOKEN' } }) }
|
||||||
|
store = { commit: jest.fn() }
|
||||||
|
toast = { success: jest.fn(), error: jest.fn() }
|
||||||
|
t = jest.fn((key) => key)
|
||||||
|
;({ changePassword } = useChangePassword({ apollo, store, toast, t }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls apollo mutate with oldPassword and password', async () => {
|
||||||
|
await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(apollo.mutate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: { oldPassword: 'old', password: 'new' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('commits new token to store on success', async () => {
|
||||||
|
await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(store.commit).toHaveBeenCalledWith('auth/SET_TOKEN', 'NEWTOKEN')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows success toast', async () => {
|
||||||
|
await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(toast.success).toHaveBeenCalledWith('settings.security.change-password.success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns success true', async () => {
|
||||||
|
const result = await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(result).toEqual({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('on error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apollo.mutate.mockRejectedValue({ message: 'Ouch!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error toast', async () => {
|
||||||
|
await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(toast.error).toHaveBeenCalledWith('Ouch!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns success false', async () => {
|
||||||
|
const result = await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(result).toEqual({ success: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not commit to store', async () => {
|
||||||
|
await changePassword({ oldPassword: 'old', password: 'new' })
|
||||||
|
expect(store.commit).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
19
webapp/composables/useResetPassword.js
Normal file
19
webapp/composables/useResetPassword.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { resetPasswordMutation } from '~/graphql/Password'
|
||||||
|
|
||||||
|
export function useResetPassword({ apollo, toast }) {
|
||||||
|
async function resetPassword({ password, email, nonce }) {
|
||||||
|
try {
|
||||||
|
const { data } = await apollo.mutate({
|
||||||
|
mutation: resetPasswordMutation,
|
||||||
|
variables: { password, email, nonce },
|
||||||
|
})
|
||||||
|
const success = !!data.resetPassword
|
||||||
|
return { success, result: success ? 'success' : 'error' }
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message)
|
||||||
|
return { success: false, result: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { resetPassword }
|
||||||
|
}
|
||||||
52
webapp/composables/useResetPassword.spec.js
Normal file
52
webapp/composables/useResetPassword.spec.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useResetPassword } from './useResetPassword'
|
||||||
|
|
||||||
|
describe('useResetPassword', () => {
|
||||||
|
let apollo, toast, resetPassword
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
apollo = { mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }) }
|
||||||
|
toast = { error: jest.fn() }
|
||||||
|
;({ resetPassword } = useResetPassword({ apollo, toast }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls apollo mutate with password, email and nonce', async () => {
|
||||||
|
await resetPassword({ password: 'secret', email: 'a@b.c', nonce: '123' })
|
||||||
|
expect(apollo.mutate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: { password: 'secret', email: 'a@b.c', nonce: '123' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns success and result on successful reset', async () => {
|
||||||
|
const res = await resetPassword({ password: 'secret', email: 'a@b.c', nonce: '123' })
|
||||||
|
expect(res).toEqual({ success: true, result: 'success' })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when backend returns false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apollo.mutate.mockResolvedValue({ data: { resetPassword: false } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns error result', async () => {
|
||||||
|
const res = await resetPassword({ password: 'secret', email: 'a@b.c', nonce: '123' })
|
||||||
|
expect(res).toEqual({ success: false, result: 'error' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('on error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apollo.mutate.mockRejectedValue({ message: 'Ouch!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error toast', async () => {
|
||||||
|
await resetPassword({ password: 'secret', email: 'a@b.c', nonce: '123' })
|
||||||
|
expect(toast.error).toHaveBeenCalledWith('Ouch!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns failure', async () => {
|
||||||
|
const res = await resetPassword({ password: 'secret', email: 'a@b.c', nonce: '123' })
|
||||||
|
expect(res).toEqual({ success: false, result: null })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
13
webapp/graphql/Password.js
Normal file
13
webapp/graphql/Password.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const changePasswordMutation = gql`
|
||||||
|
mutation ($oldPassword: String!, $password: String!) {
|
||||||
|
changePassword(oldPassword: $oldPassword, newPassword: $password)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const resetPasswordMutation = gql`
|
||||||
|
mutation ($nonce: String!, $email: String!, $password: String!) {
|
||||||
|
resetPassword(nonce: $nonce, email: $email, newPassword: $password)
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -3,33 +3,103 @@ import changePassword from './change-password'
|
|||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
'sweetalert-icon': true,
|
||||||
|
'nuxt-link': true,
|
||||||
|
}
|
||||||
|
|
||||||
describe('change-password', () => {
|
describe('change-password', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
let mocks
|
let mocks
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks = {
|
mocks = {
|
||||||
$t: jest.fn(),
|
$toast: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
$t: jest.fn((key) => key),
|
||||||
$route: {
|
$route: {
|
||||||
query: jest.fn().mockResolvedValue({ email: 'peter@lustig.de', nonce: '12345' }),
|
query: { email: 'mail@example.org', nonce: '12345' },
|
||||||
|
},
|
||||||
|
$router: {
|
||||||
|
push: jest.fn(),
|
||||||
},
|
},
|
||||||
$apollo: {
|
$apollo: {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
const Wrapper = () => {
|
|
||||||
return mount(changePassword, { mocks, localVue })
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = Wrapper()
|
jest.useFakeTimers()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(changePassword, {
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
stubs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
expect(wrapper.findAll('form')).toHaveLength(1)
|
expect(wrapper.findAll('form')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('submitting new password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#password').setValue('supersecret')
|
||||||
|
wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls resetPassword graphql mutation', () => {
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivers new password to backend', () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
variables: { nonce: '12345', email: 'mail@example.org', password: 'supersecret' },
|
||||||
|
})
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('password reset successful', () => {
|
||||||
|
it('displays success message', () => {
|
||||||
|
const expected = 'components.password-reset.change-password.success'
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after animation', () => {
|
||||||
|
beforeEach(jest.runAllTimers)
|
||||||
|
|
||||||
|
it('redirects to login', () => {
|
||||||
|
expect(mocks.$router.push).toHaveBeenCalledWith('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('password reset not successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.$apollo.mutate = jest.fn().mockRejectedValue({
|
||||||
|
message: 'Ouch!',
|
||||||
|
})
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#password').setValue('supersecret')
|
||||||
|
wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('display a toast error', () => {
|
||||||
|
expect(mocks.$toast.error).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,30 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<change-password
|
<div class="ds-mt-base ds-mb-xxx-small">
|
||||||
:email="email"
|
<password-form v-if="!changePasswordResult" ref="form" @submit="handleSubmit" />
|
||||||
:nonce="nonce"
|
<div v-else class="ds-mb-large">
|
||||||
@passwordResetResponse="handlePasswordResetResponse"
|
<template v-if="changePasswordResult === 'success'">
|
||||||
>
|
<transition name="ds-transition-fade">
|
||||||
<div class="ds-mb-large ds-space-centered">
|
<sweetalert-icon icon="success" />
|
||||||
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
|
</transition>
|
||||||
|
<p class="ds-text">
|
||||||
|
{{ $t('components.password-reset.change-password.success') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<transition name="ds-transition-fade">
|
||||||
|
<sweetalert-icon icon="error" />
|
||||||
|
</transition>
|
||||||
|
<p class="ds-text">
|
||||||
|
{{ $t(`components.password-reset.change-password.error`) }}
|
||||||
|
</p>
|
||||||
|
<p class="ds-text">
|
||||||
|
{{ $t('components.password-reset.change-password.help') }}
|
||||||
|
</p>
|
||||||
|
<p class="ds-text">
|
||||||
|
<a :href="'mailto:' + supportEmail">{{ supportEmail }}</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<div class="ds-mb-large ds-space-centered">
|
||||||
|
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</change-password>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ChangePassword from '~/components/PasswordReset/ChangePassword'
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
import emails from '~/constants/emails.js'
|
||||||
|
import { useResetPassword } from '~/composables/useResetPassword'
|
||||||
|
import PasswordForm from '~/components/Password/PasswordForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
SweetalertIcon,
|
||||||
|
PasswordForm,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
const { email = '', nonce = '' } = this.$route.query
|
const { email = '', nonce = '' } = this.$route.query
|
||||||
return { email, nonce }
|
return {
|
||||||
|
email,
|
||||||
|
nonce,
|
||||||
|
supportEmail: emails.SUPPORT_EMAIL,
|
||||||
|
changePasswordResult: null,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
components: {
|
created() {
|
||||||
ChangePassword,
|
const { resetPassword } = useResetPassword({
|
||||||
|
apollo: this.$apollo,
|
||||||
|
toast: this.$toast,
|
||||||
|
})
|
||||||
|
this._resetPassword = resetPassword
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handlePasswordResetResponse(response) {
|
async handleSubmit(formData) {
|
||||||
if (response === 'success') {
|
const { password } = formData
|
||||||
this.$router.push('/login')
|
const { email, nonce } = this
|
||||||
|
const { success, result } = await this._resetPassword({ password, email, nonce })
|
||||||
|
this.changePasswordResult = result
|
||||||
|
if (success) {
|
||||||
|
this.$refs.form.done()
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.changePasswordResult === 'success') {
|
||||||
|
this.$router.push('/login')
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
} else {
|
||||||
|
this.$refs.form.fail()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,21 +4,31 @@ import Security from './security.vue'
|
|||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
describe('security.vue', () => {
|
describe('security.vue', () => {
|
||||||
let wrapper
|
|
||||||
let mocks
|
let mocks
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks = {
|
mocks = {
|
||||||
$t: jest.fn(),
|
$toast: {
|
||||||
|
error: jest.fn(),
|
||||||
|
success: jest.fn(),
|
||||||
|
},
|
||||||
|
$t: jest.fn((key) => key),
|
||||||
|
$store: {
|
||||||
|
commit: jest.fn(),
|
||||||
|
},
|
||||||
|
$apollo: {
|
||||||
|
mutate: jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue({ message: 'Ouch!' })
|
||||||
|
.mockResolvedValueOnce({ data: { changePassword: 'NEWTOKEN' } }),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
|
let wrapper
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(Security, {
|
return mount(Security, { mocks, localVue })
|
||||||
mocks,
|
|
||||||
localVue,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -28,5 +38,83 @@ describe('security.vue', () => {
|
|||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
expect(wrapper.classes('os-card')).toBe(true)
|
expect(wrapper.classes('os-card')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders three input fields (old, new, confirm)', () => {
|
||||||
|
expect(wrapper.findAll('input')).toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validations', () => {
|
||||||
|
describe('new password and confirmation mismatch', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('input#oldPassword').setValue('oldsecret')
|
||||||
|
await wrapper.find('input#password').setValue('superdupersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('different')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not submit the form', async () => {
|
||||||
|
mocks.$apollo.mutate.mockReset()
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given valid input', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.find('input#oldPassword').setValue('supersecret')
|
||||||
|
wrapper.find('input#password').setValue('superdupersecret')
|
||||||
|
wrapper.find('input#passwordConfirmation').setValue('superdupersecret')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit form', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await 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',
|
||||||
|
password: 'superdupersecret',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mutation resolves', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
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(async () => {
|
||||||
|
await wrapper.find('input#oldPassword').setValue('supersecret')
|
||||||
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays error message', async () => {
|
||||||
|
expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,20 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<os-card>
|
<os-card>
|
||||||
<h2 class="title">{{ $t('settings.security.name') }}</h2>
|
<h2 class="title">{{ $t('settings.security.name') }}</h2>
|
||||||
<change-password />
|
<password-form ref="form" require-old-password @submit="handleSubmit" />
|
||||||
</os-card>
|
</os-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { OsCard } from '@ocelot-social/ui'
|
import { OsCard } from '@ocelot-social/ui'
|
||||||
import ChangePassword from '~/components/Password/Change'
|
import { useChangePassword } from '~/composables/useChangePassword'
|
||||||
|
import PasswordForm from '~/components/Password/PasswordForm'
|
||||||
import scrollToContent from './scroll-to-content.js'
|
import scrollToContent from './scroll-to-content.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [scrollToContent],
|
mixins: [scrollToContent],
|
||||||
components: {
|
components: {
|
||||||
OsCard,
|
OsCard,
|
||||||
ChangePassword,
|
PasswordForm,
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const { changePassword } = useChangePassword({
|
||||||
|
apollo: this.$apollo,
|
||||||
|
store: this.$store,
|
||||||
|
toast: this.$toast,
|
||||||
|
t: this.$t,
|
||||||
|
})
|
||||||
|
this._changePassword = changePassword
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleSubmit(formData) {
|
||||||
|
const { success } = await this._changePassword(formData)
|
||||||
|
if (success) {
|
||||||
|
this.$refs.form.done()
|
||||||
|
} else {
|
||||||
|
this.$refs.form.fail()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user