From 8e2356aca0647ba549cf5bea74b084a354d57d3c Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Sun, 29 Dec 2019 22:39:35 +0100 Subject: [PATCH] Add user dialog model --- js/src/user.js | 2 +- js/src/userdialogmodel.js | 172 ++++++++++++++++++++ js/test/userdialogmodel.test.js | 280 ++++++++++++++++++++++++++++++++ 3 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 js/src/userdialogmodel.js create mode 100644 js/test/userdialogmodel.test.js diff --git a/js/src/user.js b/js/src/user.js index 5329b44..18a6c9b 100644 --- a/js/src/user.js +++ b/js/src/user.js @@ -102,7 +102,7 @@ export default class uUser extends uListItem { /** * @param {string} password - * @param {string} oldPassword + * @param {string=} oldPassword Needed when changing own password * @return {Promise} */ setPassword(password, oldPassword) { diff --git a/js/src/userdialogmodel.js b/js/src/userdialogmodel.js new file mode 100644 index 0000000..5fda980 --- /dev/null +++ b/js/src/userdialogmodel.js @@ -0,0 +1,172 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import { lang as $, auth, config } from './initializer.js'; +import ViewModel from './viewmodel.js'; +import uDialog from './dialog.js'; +import uUser from './user.js'; +import uUtils from './utils.js'; + +export default class UserDialogModel extends ViewModel { + + /** + * @param {UserViewModel} viewModel + * @param {string} type + */ + constructor(viewModel, type) { + super({ + onUserDelete: null, + onUserUpdate: null, + onUserAdd: null, + onCancel: null, + login: null, + password: null, + password2: null, + oldPassword: null + }); + this.user = viewModel.state.currentUser; + this.type = type; + this.userVM = viewModel; + this.model.onUserDelete = () => this.onUserDelete(); + this.model.onUserUpdate = () => this.onUserUpdate(); + this.model.onUserAdd = () => this.onUserAdd(); + this.model.onCancel = () => this.onCancel(); + } + + init() { + const html = this.getHtml(); + this.dialog = new uDialog(html); + this.dialog.show(); + this.bindAll(this.dialog.element); + } + + onUserDelete() { + if (uDialog.isConfirmed($._('userdelwarn', uUtils.htmlEncode(this.user.login)))) { + this.user.delete().then(() => { + this.userVM.onUserDeleted(); + this.dialog.destroy(); + }).catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); + } + } + + onUserUpdate() { + if (this.validate()) { + const user = this.type === 'pass' ? auth.user : this.user; + user.setPassword(this.model.password, this.model.oldPassword) + .then(() => this.dialog.destroy()) + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); + } + } + + onUserAdd() { + if (this.validate()) { + uUser.add(this.model.login, this.model.password).then((user) => { + this.userVM.onUserAdded(user); + this.dialog.destroy(); + }).catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); + } + } + + onCancel() { + this.dialog.destroy(); + } + + /** + * Validate form + * @return {boolean} True if valid + */ + validate() { + if (this.type === 'add') { + if (!this.model.login) { + alert($._('allrequired')); + return false; + } + } else if (this.type === 'pass') { + if (!this.model.oldPassword) { + alert($._('allrequired')); + return false; + } + } + if (!this.model.password || !this.model.password2) { + alert($._('allrequired')); + return false; + } + if (this.model.password !== this.model.password2) { + alert($._('passnotmatch')); + return false; + } + if (!config.passRegex.test(this.model.password)) { + alert($._('passlenmin') + '\n' + $._('passrules')); + return false; + } + return true; + } + + /** + * @return {string} + */ + getHtml() { + let deleteButton = ''; + let header = ''; + let observer; + let fields; + switch (this.type) { + case 'add': + observer = 'onUserAdd'; + header = ` + `; + fields = ` + + + `; + break; + case 'edit': + observer = 'onUserUpdate'; + deleteButton = `
${$._('editinguser', `${uUtils.htmlEncode(this.user.login)}`)}
+ +
`; + fields = ` + + + `; + break; + case 'pass': + observer = 'onUserUpdate'; + fields = ` + + + + + `; + break; + default: + throw new Error(`Unknown dialog type: ${this.type}`); + } + return `${deleteButton} +
+ ${header} + ${fields} +
+ + +
+
`; + } + +} diff --git a/js/test/userdialogmodel.test.js b/js/test/userdialogmodel.test.js new file mode 100644 index 0000000..ff4cd0d --- /dev/null +++ b/js/test/userdialogmodel.test.js @@ -0,0 +1,280 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import { auth, config, lang } from '../src/initializer.js'; +import UserDialogModel from '../src/userdialogmodel.js'; +import uDialog from '../src/dialog.js'; +import uObserve from '../src/observe.js'; +import uState from '../src/state.js'; +import uUser from '../src/user.js'; + +describe('UserDialogModel tests', () => { + + let dm; + let mockVM; + let authUser; + let newUser; + let dialogType; + + beforeEach(() => { + config.reinitialize(); + config.interval = 10; + lang.init(config); + authUser = new uUser(3, 'authUser'); + newUser = new uUser(2, 'newUser'); + auth.user = authUser; + spyOn(lang, '_').and.returnValue('{placeholder}'); + mockVM = { state: new uState(), onUserDeleted: {}, onUserAdded: {} }; + dialogType = 'add'; + dm = new UserDialogModel(mockVM, dialogType); + dm.user = new uUser(1, 'testUser'); + spyOn(dm.user, 'delete').and.returnValue(Promise.resolve()); + spyOn(dm.user, 'setPassword').and.returnValue(Promise.resolve()); + spyOn(auth.user, 'setPassword').and.returnValue(Promise.resolve()); + spyOn(uUser, 'add').and.returnValue(Promise.resolve(newUser)); + spyOn(config.passRegex, 'test').and.returnValue(true); + spyOn(window, 'alert'); + }); + + afterEach(() => { + document.body.innerHTML = ''; + uObserve.unobserveAll(lang); + auth.user = null; + }); + + it('should create instance with parent view model as parameter', () => { + expect(dm).toBeDefined(); + expect(dm.userVM).toBe(mockVM); + expect(dm.type).toBe(dialogType); + }); + + it('should show add dialog for current user', () => { + // given + dm.type = 'add'; + // when + dm.init(); + // then + expect(document.querySelector('#modal')).toBeInstanceOf(HTMLDivElement); + expect(dm.dialog.element.querySelector("[data-bind='onUserAdd']")).toBeInstanceOf(HTMLButtonElement); + }); + + it('should show edit dialog for current user', () => { + // given + dm.type = 'edit'; + // when + dm.init(); + // then + expect(document.querySelector('#modal')).toBeInstanceOf(HTMLDivElement); + expect(dm.dialog.element.querySelector("[data-bind='onUserUpdate']")).toBeInstanceOf(HTMLButtonElement); + expect(dm.dialog.element.querySelector("[data-bind='onUserDelete']")).toBeInstanceOf(HTMLAnchorElement); + }); + + it('should show password change dialog for current user', () => { + // given + dm.type = 'pass'; + // when + dm.init(); + // then + expect(document.querySelector('#modal')).toBeInstanceOf(HTMLDivElement); + expect(dm.dialog.element.querySelector("[data-bind='onUserUpdate']")).toBeInstanceOf(HTMLButtonElement); + expect(dm.dialog.element.querySelector("[data-bind='onUserDelete']")).toBe(null); + }); + + it('should show confirmation dialog on user delete button click', (done) => { + // given + spyOn(uDialog, 'isConfirmed').and.returnValue(false); + dm.type = 'edit'; + dm.init(); + const button = dm.dialog.element.querySelector("[data-bind='onUserDelete']"); + // when + button.click(); + // then + setTimeout(() => { + expect(uDialog.isConfirmed).toHaveBeenCalledTimes(1); + done(); + }, 100); + }); + + it('should delete user and hide dialog on confirmation dialog accepted', (done) => { + // given + spyOn(uDialog, 'isConfirmed').and.returnValue(true); + spyOn(mockVM, 'onUserDeleted'); + dm.type = 'edit'; + dm.init(); + const button = dm.dialog.element.querySelector("[data-bind='onUserDelete']"); + // when + button.click(); + // then + setTimeout(() => { + expect(dm.user.delete).toHaveBeenCalledTimes(1); + expect(mockVM.onUserDeleted).toHaveBeenCalledTimes(1); + expect(document.querySelector('#modal')).toBe(null); + done(); + }, 100); + }); + + it('should update user password and hide edit dialog on positive button clicked', (done) => { + // given + spyOn(dm, 'validate').and.returnValue(true); + dm.type = 'edit'; + dm.init(); + const button = dm.dialog.element.querySelector("[data-bind='onUserUpdate']"); + const passEl = dm.dialog.element.querySelector("[data-bind='password']"); + const newPassword = 'newpass'; + // when + passEl.value = newPassword; + passEl.dispatchEvent(new Event('change')); + button.click(); + // then + setTimeout(() => { + expect(dm.user.setPassword).toHaveBeenCalledTimes(1); + expect(dm.user.setPassword).toHaveBeenCalledWith(newPassword, null); + expect(document.querySelector('#modal')).toBe(null); + done(); + }, 100); + }); + + it('should update other session user password and hide pass dialog on positive button clicked', (done) => { + // given + spyOn(dm, 'validate').and.returnValue(true); + dm.type = 'pass'; + dm.init(); + const button = dm.dialog.element.querySelector("[data-bind='onUserUpdate']"); + const passEl = dm.dialog.element.querySelector("[data-bind='password']"); + const passOldEl = dm.dialog.element.querySelector("[data-bind='oldPassword']"); + const newPassword = 'newpass'; + const oldPassword = 'oldpass'; + // when + passEl.value = newPassword; + passEl.dispatchEvent(new Event('change')); + passOldEl.value = oldPassword; + passOldEl.dispatchEvent(new Event('change')); + button.click(); + // then + setTimeout(() => { + expect(auth.user.setPassword).toHaveBeenCalledTimes(1); + expect(auth.user.setPassword).toHaveBeenCalledWith(newPassword, oldPassword); + expect(document.querySelector('#modal')).toBe(null); + done(); + }, 100); + }); + + it('should add user and hide edit dialog on positive button clicked', (done) => { + // given + spyOn(dm, 'validate').and.returnValue(true); + spyOn(mockVM, 'onUserAdded'); + dm.type = 'add'; + dm.init(); + const button = dm.dialog.element.querySelector("[data-bind='onUserAdd']"); + const loginEl = dm.dialog.element.querySelector("[data-bind='login']"); + const passEl = dm.dialog.element.querySelector("[data-bind='password']"); + const newPassword = 'newpass'; + // when + loginEl.value = newUser.login; + loginEl.dispatchEvent(new Event('change')); + passEl.value = newPassword; + passEl.dispatchEvent(new Event('change')); + button.click(); + // then + setTimeout(() => { + expect(uUser.add).toHaveBeenCalledTimes(1); + expect(uUser.add).toHaveBeenCalledWith(newUser.login, newPassword); + expect(mockVM.onUserAdded).toHaveBeenCalledWith(newUser); + expect(document.querySelector('#modal')).toBe(null); + done(); + }, 100); + }); + + it('should hide dialog on negative button clicked', (done) => { + // given + dm.init(); + const button = dm.dialog.element.querySelector("[data-bind='onCancel']"); + // when + button.click(); + // then + setTimeout(() => { + expect(document.querySelector('#modal')).toBe(null); + done(); + }, 100); + }); + + it('should return true on add user dialog validate', () => { + // given + dm.type = 'add'; + dm.model.login = 'test'; + dm.model.password = 'password'; + dm.model.password2 = 'password'; + // when + const result = dm.validate(); + // then + expect(result).toBe(true); + expect(window.alert).not.toHaveBeenCalled(); + }); + + it('should return false on add user dialog empty login', () => { + // given + dm.type = 'add'; + dm.model.login = ''; + dm.model.password = 'password'; + dm.model.password2 = 'password'; + // when + const result = dm.validate(); + // then + expect(result).toBe(false); + expect(window.alert).toHaveBeenCalledTimes(1); + }); + + it('should return false on password change dialog empty old password', () => { + // given + dm.type = 'pass'; + dm.model.login = 'test'; + dm.model.password = 'password'; + dm.model.password2 = 'password'; + dm.model.oldPassword = ''; + // when + const result = dm.validate(); + // then + expect(result).toBe(false); + expect(window.alert).toHaveBeenCalledTimes(1); + }); + + it('should return false on add user dialog passwords not match', () => { + // given + dm.model.login = 'test'; + dm.model.password = 'password'; + dm.model.password2 = 'password2'; + // when + const result = dm.validate(); + // then + expect(result).toBe(false); + expect(window.alert).toHaveBeenCalledTimes(1); + }); + + it('should test password regex on dialog validate', () => { + // given + const password = 'password'; + dm.model.login = 'test'; + dm.model.password = password; + dm.model.password2 = password; + // when + dm.validate(); + // then + expect(config.passRegex.test).toHaveBeenCalledWith(password); + }); +});