From ba97a4285e65d904bb47b2fb209f10795fc4a39e Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 29 Apr 2024 07:30:31 +0200 Subject: [PATCH 01/67] PB-33179 - Reuse testing pgpkeys assets served by styleguide and remove browser extension duplicate Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 ++-- package.json | 2 +- test/fixture/pgpKeys/keys.js | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b99ed509e..324089321 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.7.0", + "version": "4.8.0-alpha.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.7.0", + "version": "4.8.0-alpha.0", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index f8369ca76..6d6694b54 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.7.0", + "version": "4.8.0-alpha.0", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/test/fixture/pgpKeys/keys.js b/test/fixture/pgpKeys/keys.js index 2e6b00822..1d0bb3c8c 100644 --- a/test/fixture/pgpKeys/keys.js +++ b/test/fixture/pgpKeys/keys.js @@ -34,6 +34,7 @@ exports.pgpKeys = { revoked: false }, admin: { + userId: "f642271d-bbb1-401e-bbd1-7ec370f8e19b", public: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nFYNCFtV1fw==\n=amwR\n-----END PGP PUBLIC KEY BLOCK-----", private: "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\n=/G+C\n-----END PGP PRIVATE KEY BLOCK-----", private_decrypted: "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\n=hJM/\n-----END PGP PRIVATE KEY BLOCK-----", @@ -54,6 +55,10 @@ exports.pgpKeys = { public: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=KJsT\n-----END PGP PUBLIC KEY BLOCK-----", key_id: "d34374d5", fingerprint: "A754860C3ADE5AB04599025ED3F1FE4BE61D7009", + user_ids: [{ + email: "betty@passbolt.com", + name: "Betty Holberton", + }], }, account_recovery_organization: { fingerprint: "28FBD1034880416B2B8CA75A289BCE03F3C0893F", From 20838e0b8b8ef436a287f8a0df2502ee5bb57552 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 29 Apr 2024 15:15:04 +0200 Subject: [PATCH 02/67] PB-33188 - Reuse testing account recovery assets served by styleguide and remove browser extension duplicate Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 +- package.json | 2 +- ...countRecoveryPrivateKeyEntity.test.data.js | 2 +- ...overyPrivateKeyPasswordEntity.test.data.js | 75 +++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyPasswordEntity.test.data.js diff --git a/package-lock.json b/package-lock.json index 324089321..76afcf420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.0", + "version": "4.8.0-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.0", + "version": "4.8.0-alpha.1", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 6d6694b54..d44d3a6a1 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.0", + "version": "4.8.0-alpha.1", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyEntity.test.data.js b/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyEntity.test.data.js index 28069315f..13917865c 100644 --- a/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyEntity.test.data.js +++ b/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyEntity.test.data.js @@ -16,7 +16,7 @@ import {v4 as uuidv4} from "uuid"; import { bettyAccountRecoveryPrivateKeyPasswordDto, defaultAccountRecoveryPrivateKeyPasswordDto -} from "../../../../../../passbolt-browser-extension/src/all/background_page/model/entity/accountRecovery/accountRecoveryPrivateKeyPasswordEntity.test.data"; +} from "./accountRecoveryPrivateKeyPasswordEntity.test.data"; import {pgpKeys} from "../../../../../test/fixture/pgpKeys/keys"; export const defaultAccountRecoveryPrivateKeyDto = (data = {}) => { diff --git a/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyPasswordEntity.test.data.js b/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyPasswordEntity.test.data.js new file mode 100644 index 000000000..b0474de12 --- /dev/null +++ b/src/shared/models/entity/accountRecovery/accountRecoveryPrivateKeyPasswordEntity.test.data.js @@ -0,0 +1,75 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 3.6.0 + */ + +import {v4 as uuidv4} from "uuid"; +import {pgpKeys} from "../../../../../test/fixture/pgpKeys/keys"; + +/** + * The Test Account Recovery Organization gpg key is used to encrypt the private key password. + * The Ada gpg key is used to sign the encrypted message. + * The private key password belong to the private key of Ada. + * Clear text password: {type":"account-recovery-private-key-password-decrypted-data","version":"v1","domain":"https://passbolt.local","private_key_user_id":"f848277c-5398-58f8-a82a-72397af2d450","private_key_fingerprint":"03F60E958F4CB29723ACDF761353B5B15D9B054F","private_key_secret":"f7cf1fa06f973a9ecbb5f0e2bc6d1830532e53ad50da231036bd6c8c00dd7c7dc6c07b04004615cd6808bea2cb6a4ce4c46f7f36b8865292c0f7a28cd6f56112","created":"2022-04-21T14:45:15.205Z"} + */ +export const createAccountRecoveryPrivateKeyPasswordDto = (data = {}) => { + const defaultData = { + private_key_id: uuidv4(), + recipient_foreign_model: "AccountRecoveryOrganizationKey", + recipient_foreign_key: uuidv4(), + recipient_fingerprint: pgpKeys.account_recovery_organization.fingerprint, + data: "-----BEGIN PGP MESSAGE-----\n\n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\ngOsGFf0DYcZc/TtQY1i/BVg=\n=Ar7O\n-----END PGP MESSAGE-----" + }; + + return Object.assign(defaultData, data); +}; + +export const defaultAccountRecoveryPrivateKeyPasswordDto = (data = {}) => { + const defaultData = { + id: uuidv4(), + }; + return createAccountRecoveryPrivateKeyPasswordDto({ + ...defaultData, + ...data + }); +}; + +/** + * The Test Account Recovery Organization gpg key is used to encrypt the private key password. + * The Betty gpg key is used to sign the encrypted message. + * The private key password belong to the private key of Betty. + * Clear text password: {"type":"account-recovery-private-key-password-decrypted-data","version":"v1","domain":"https://passbolt.local","private_key_user_id":"e97b14ba-8957-57c9-a357-f78a6e1e1a46","private_key_fingerprint":"A754860C3ADE5AB04599025ED3F1FE4BE61D7009","private_key_secret":"96946dd4d62e1ef5815c25ec8c5152a3abf5d80795f112719b92b0eb86d5d1102e68baab3486025886769bc05177ae7825e484420e58bf5a4692f30021425b85","created":"2022-04-24T13:16:48.469Z"} + */ +export const bettyAccountRecoveryPrivateKeyPasswordDto = (data = {}) => { + const defaultData = { + data: "-----BEGIN PGP MESSAGE-----\n\n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\nLbiX0dVls2TmnrqaDwjdkS8=\n=DKmX\n-----END PGP MESSAGE-----", + }; + return defaultAccountRecoveryPrivateKeyPasswordDto({ + ...defaultData, + ...data + }); +}; + +/** + * The Test Account Recovery Organization gpg key is used to encrypt the private key password. + * Clear text password: {"type":"not-a-valid-decrypted-data-entity-type","version":"v1","domain":"https://passbolt.local","private_key_user_id":"f848277c-5398-58f8-a82a-72397af2d450","private_key_fingerprint":"03F60E958F4CB29723ACDF761353B5B15D9B054F","private_key_secret":"f7cf1fa06f973a9ecbb5f0e2bc6d1830532e53ad50da231036bd6c8c00dd7c7dc6c07b04004615cd6808bea2cb6a4ce4c46f7f36b8865292c0f7a28cd6f56112","created":"2022-04-24T20:22:54.324Z"} + */ +export const secretSubstitutionAttackAccountRecoveryPrivateKeyPasswordDto = (data = {}) => { + const defaultData = { + data: "-----BEGIN PGP MESSAGE-----\n\nwcFMA9FTFjWeSbtEAQ/+NFBdfWsHpLPFl7NrysmueomBu4JWiy21vfDrlbBX\nIE1RDXSi57nTVc48dUJUSff6mRZEjaTvlAf3JjsVhc47rbbFO5DRvY8xq2l0\n00YL/90ZLi881EjsV07DfCE3kzhOZZBIrExx68pF/jOxJsRh8fBbq80ElrvY\nZDBNPemIPp6CnZnRGiqNxgECylF928qj3QVYSH/JlvdV3GZOH0MOMw+6iom5\nGMIuHop8vOowbWUyOTHA3SPMLZb7rXzNmDwYsgJF6VGfFDNVOkcXJydDLVLw\nC8LwM1KLVCxKBVR112N0LAsCGrPcB/ZR+/6YHlozxmwGLWQvSnpDFLrgKnLb\ns44YOs2tN5n2OwiXGCxkIaW7pF1oWnVB1ygIA0iGWxfLE2b66e5GUwSlAvkU\np1NlZlg5CcIsBKmr5RRG6LzuM1RjNmvw9x7BtV8hOCzLknrOCxGclLmZHsX4\nTDJJpmRxtPmIwHRtEsG5f4Yf5MEmasYWj4SvNcE92sGbJR1dFeTanSrt4ULJ\nt36f6Hmgb+8pFpJZOWlnHf0kjCfvmnp4KkUPHfwWQ/Rh+u8yQn9YmOk3QlKY\nSiU5C6ae26HGWmjpSxY5TxIlK2Tu6g1eXODJUHi4rPA+yHXPQN96IaqmcQhp\na+Lyvyr4awy7QDS6NnmoHrfIeJYBMK6NwuUfv/7CzJvSwRMBH3+QpUSybq5g\nhSbN+uZhoGXP8HNXK1rOkzUgZKmQt/geuFoX+wWFtXCBGTID8DcVSMStrOoj\n5vC+kD3xuKOCjYbZTAYwZXLuDEL0RP14PbxL7pmSNzVWTHk5+5V9rehBAP0m\nl4BQWan84DjN5+Bc7QC0AZqJFL39jKIKG1kdWuvZbVYLJGivwTnYUvUYEYgw\nnu8rg9fOttcdMMdo8gCQq46yaItAJOU0CMDFBqzw10gLHswi+qgt9Fjx9ovY\nosYiE9mKCoDigUDknylq8twqQCsGj9BF/QctwW0ExhXCc8ryetV+f1jCrM3z\najrVVuN/JgBr5wXCxeYPIY4dmCf6VEf3XlEdjggHBfzMwGtoxv9S8skCLJVp\nuukqSNv0EcuBQnidrYk5gLvTeuwrffHZKP+cKil9YIJlBFt0CNwIZ+r+t7Wg\nAGSwYRowfT1YGoxofXoKpLA4/aOlCKU1nr/1Drn5YbSVGcdP1wGhgvHbzyBG\nneibYnDajT31fPU2IY0P0DoyY9xgqDn762GLr9B9mhE3kkomG3swMzYA6BuK\nCA794XE/QY11RWTdLqncwcTayqBFYTOW8TyMHIeDRA9kxnQG1ozC6teyZEkm\nMR/BkOEbgA==\n=hQcu\n-----END PGP MESSAGE-----" + }; + + return defaultAccountRecoveryPrivateKeyPasswordDto({ + ...defaultData, + ...data + }); +}; From 04d4ca5b5f9a7a809e5a6de915770503aab1c6a5 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 30 Apr 2024 10:24:13 +0200 Subject: [PATCH 03/67] PB-33171 Simulate user event on autofill --- .../Autofill/Autofill.js | 60 ++++------------ .../lib/InForm/InFormCallToActionField.js | 24 ------- .../lib/InForm/InFormManager.js | 15 ++-- .../lib/User/UserEventsService.js | 45 ++++++++++++ .../lib/User/UserEventsService.test.js | 71 +++++++++++++++++++ 5 files changed, 139 insertions(+), 76 deletions(-) create mode 100644 src/react-web-integration/lib/User/UserEventsService.js create mode 100644 src/react-web-integration/lib/User/UserEventsService.test.js diff --git a/src/react-web-integration/Autofill/Autofill.js b/src/react-web-integration/Autofill/Autofill.js index b74a4dd04..e6549e18d 100644 --- a/src/react-web-integration/Autofill/Autofill.js +++ b/src/react-web-integration/Autofill/Autofill.js @@ -11,9 +11,8 @@ * @link https://www.passbolt.com Passbolt(tm) * @since 3.3.0 */ - -const PASSWORD_INPUT_SELECTOR = "input[type='password']:not([hidden]):not([disabled]), input[type='Password']:not([hidden]):not([disabled]), input[type='PASSWORD']:not([hidden]):not([disabled])"; -const USERNAME_INPUT_SELECTOR = "input[type='text']:not([hidden]):not([disabled]), input[type='Text']:not([hidden]):not([disabled]), input[type='TEXT']:not([hidden]):not([disabled]), input[type='email']:not([hidden]):not([disabled]), input[type='Email']:not([hidden]):not([disabled]), input[type='EMAIL']:not([hidden]):not([disabled]), input:not([type]):not([hidden]):not([disabled])"; +import UserEventsService from "../lib/User/UserEventsService"; +import InFormFieldSelector from "../lib/InForm/InFormFieldSelector"; /** * Fill the login form. @@ -44,10 +43,10 @@ const fillForm = function(formData) { usernameElement = getUsernameElementBasedOnPasswordElement(formData, passwordElement.parentElement); // If username element exists, fill username if (usernameElement !== null) { - fillInputField(usernameElement, formData.username); + UserEventsService.autofill(usernameElement, formData.username); } // Fill password - fillInputField(passwordElement, formData.secret); + UserEventsService.autofill(passwordElement, formData.secret); } else { /* * When no password element found on the page @@ -56,7 +55,7 @@ const fillForm = function(formData) { usernameElement = getUsernameElement(formData, document); // If username element exists, fill username if (usernameElement !== null) { - fillInputField(usernameElement, formData.username); + UserEventsService.autofill(usernameElement, formData.username); } else { throw new Error('Unable to find the username element on this page.'); } @@ -121,39 +120,6 @@ const validateData = function(formData) { } }; -/** - * Fill form field. - * @param {DomElement} element The element to fill - * @param {string} value The value to use - */ -const fillInputField = function(element, value) { - /* - * In order to ensure a high level of compatibility with most forms (even ones - * controlled by javascript), the process needs to simulate how a user will - * interact with the form: - * 1. Focus the element by clicking on it; - * 2. Once focused, trigger an input event to change the value of the field. - */ - - if (element || '') { - const keydownEvent = new KeyboardEvent("keydown", {bubbles: true}); - const keypressEvent = new KeyboardEvent("keypress", {bubbles: true}); - const inputEvent = new InputEvent("input", {inputType: "insertText", data: value, bubbles: true}); - const keyupEvent = new KeyboardEvent("keyup", {bubbles: true}); - const changeEvent = new Event("change", {bubbles: true}); - - element.value = value; - - // Dispatch events, they happen in this order: down, press, input, up, change, ↑, ↑, ↓, ↓, ←, →, ←, →, B, A - element.focus(); - element.dispatchEvent(keydownEvent); - element.dispatchEvent(keypressEvent); - element.dispatchEvent(inputEvent); - element.dispatchEvent(keyupEvent); - element.dispatchEvent(changeEvent); - } -}; - /** * Get input elements from an iframe * @param {string} type - either `password` or `username` to find elements @@ -211,13 +177,13 @@ const getAccessedIframeContentDocument = function(iframe) { const findInputElementInIframe = function(type, iframeDocument) { let inputElement = null; if (type === 'password') { - inputElement = iframeDocument.querySelectorAll(PASSWORD_INPUT_SELECTOR); + inputElement = iframeDocument.querySelectorAll(InFormFieldSelector.PASSWORD_FIELD_SELECTOR); // Password element has been found. if (inputElement.length) { return inputElement[0]; } } else if (type === 'username') { - inputElement = iframeDocument.querySelectorAll(USERNAME_INPUT_SELECTOR); + inputElement = iframeDocument.querySelectorAll(InFormFieldSelector.USERNAME_FIELD_SELECTOR); if (inputElement.length) { // When username element found, extract it from an array of dom elements. inputElement = extractUsernameElementWithFallback(inputElement); @@ -232,10 +198,10 @@ const findInputElementInIframe = function(type, iframeDocument) { /** * Find the password element on the page. - * @return {DomElement/null} + * @return {HTMLInputElement/null} */ const getPasswordElement = function(formData) { - const passwordElements = document.querySelectorAll(PASSWORD_INPUT_SELECTOR); + const passwordElements = document.querySelectorAll(InFormFieldSelector.PASSWORD_FIELD_SELECTOR); // A password element has been found. if (passwordElements.length) { @@ -259,7 +225,7 @@ const getPasswordElement = function(formData) { /** * Find the username element on the page based on password's parent as reference element. * @param {DomElement} referenceElement The element reference to start the search. - * @return {DomElement/null} + * @return {HTMLInputElement/null} */ const getUsernameElementBasedOnPasswordElement = function(formData, referenceElement) { // No parent element found. @@ -273,7 +239,7 @@ const getUsernameElementBasedOnPasswordElement = function(formData, referenceEle let usernameElement = null; // The username field can be an input field of type email or text. - const elements = referenceElement.querySelectorAll(USERNAME_INPUT_SELECTOR); + const elements = referenceElement.querySelectorAll(InFormFieldSelector.USERNAME_FIELD_SELECTOR); /* * No input fields found in the reference element. @@ -302,13 +268,13 @@ const getUsernameElementBasedOnPasswordElement = function(formData, referenceEle /** * Find the username element on the page. * @param {DomElement} fallbackUsernameElement The element reference to start the search. - * @return {DomElement/null} + * @return {HTMLInputElement/null} */ const getUsernameElement = function(formData, fallbackUsernameElement) { let usernameElement = null; // The username field can be an input field of type email or text. - const elements = fallbackUsernameElement.querySelectorAll(USERNAME_INPUT_SELECTOR); + const elements = fallbackUsernameElement.querySelectorAll(InFormFieldSelector.USERNAME_FIELD_SELECTOR); // When username element found, extract it from an array of dom elements. if (elements.length) { diff --git a/src/react-web-integration/lib/InForm/InFormCallToActionField.js b/src/react-web-integration/lib/InForm/InFormCallToActionField.js index 422bfc261..e0d991767 100644 --- a/src/react-web-integration/lib/InForm/InFormCallToActionField.js +++ b/src/react-web-integration/lib/InForm/InFormCallToActionField.js @@ -263,30 +263,6 @@ class InFormCallToActionField { this.scrollableFieldParent.addEventListener('scroll', this.removeCallToActionIframe); } - - /** AUTOFILL **/ - - /** - * Autofill a field - * @param text the text to fill in the field - */ - autofill(text) { - const keydownEvent = new KeyboardEvent("keydown", {bubbles: true}); - const keypressEvent = new KeyboardEvent("keypress", {bubbles: true}); - const inputEvent = new InputEvent("input", {inputType: "insertText", data: text, bubbles: true}); - const keyupEvent = new KeyboardEvent("keyup", {bubbles: true}); - const changeEvent = new Event("change", {bubbles: true}); - - this.field.value = text; - - // Dispatch events, they happen in this order: down, press, input, up, change, ↑, ↑, ↓, ↓, ←, →, ←, →, B, A - this.field.dispatchEvent(keydownEvent); - this.field.dispatchEvent(keypressEvent); - this.field.dispatchEvent(inputEvent); - this.field.dispatchEvent(keyupEvent); - this.field.dispatchEvent(changeEvent); - } - /** DESTROY */ /** diff --git a/src/react-web-integration/lib/InForm/InFormManager.js b/src/react-web-integration/lib/InForm/InFormManager.js index c040155e2..1ef4d2eac 100644 --- a/src/react-web-integration/lib/InForm/InFormManager.js +++ b/src/react-web-integration/lib/InForm/InFormManager.js @@ -18,6 +18,7 @@ import InFormMenuField from "./InformMenuField"; import InFormCredentialsFormField from "./InFormCredentialsFormField"; import DomUtils from "../Dom/DomUtils"; import debounce from "debounce-promise"; +import UserEventsService from "../User/UserEventsService"; /** * Manages the in-form web integration including call-to-action and menu @@ -259,22 +260,26 @@ class InFormManager { const isUsernameType = currentFieldType === 'username'; const isPasswordType = currentFieldType === 'password'; if (!isUsernameType) { - this.lastCallToActionFieldClicked.autofill(password); + // Simulate a user to autofill the password field + UserEventsService.autofill(this.lastCallToActionFieldClicked.field, password); // Get username fields and find the one with the lowest common ancestor const usernameFields = this.callToActionFields .filter(callToActionField => callToActionField.fieldType === 'username'); const usernameField = DomUtils.getFieldWithLowestCommonAncestor(this.lastCallToActionFieldClicked.field, usernameFields); if (usernameField) { - usernameField.autofill(username); + // Simulate a user to autofill the username field + UserEventsService.autofill(usernameField.field, username); } } else if (!isPasswordType) { - this.lastCallToActionFieldClicked.autofill(username); + // Simulate a user to autofill the username field + UserEventsService.autofill(this.lastCallToActionFieldClicked.field, username); // Get password fields and find the one with the lowest common ancestor const passwordFields = this.callToActionFields .filter(callToActionField => callToActionField.fieldType === 'password'); const passwordField = DomUtils.getFieldWithLowestCommonAncestor(this.lastCallToActionFieldClicked.field, passwordFields); if (passwordField) { - passwordField.autofill(password); + // Simulate a user to autofill the password field + UserEventsService.autofill(passwordField.field, password); } } }); @@ -288,7 +293,7 @@ class InFormManager { const passwordFields = this.callToActionFields .filter(callToActionField => callToActionField.fieldType === 'password'); // Autofill only empty passwords field - passwordFields.forEach(callToActionField => !callToActionField.field.value && callToActionField.autofill(password)); + passwordFields.forEach(callToActionField => !callToActionField.field.value && UserEventsService.autofill(callToActionField.field, password)); this.menuField.removeMenuIframe(); // Listen the auto-save on the appropriate form field const formField = this.credentialsFormFields.find(formField => formField.field.contains(this.lastCallToActionFieldClicked.field)); diff --git a/src/react-web-integration/lib/User/UserEventsService.js b/src/react-web-integration/lib/User/UserEventsService.js new file mode 100644 index 000000000..47946367d --- /dev/null +++ b/src/react-web-integration/lib/User/UserEventsService.js @@ -0,0 +1,45 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ + +/** + * The user events service + */ +class UserEventsService { + /** + * Autofill a field with the value and simulate all user events + * @param {HTMLInputElement} field the field to autofill + * @param {string} value the value to fill in the field + */ + static autofill(field, value) { + // Check if field is not null + if (field) { + const keydownEvent = new KeyboardEvent("keydown", {bubbles: true}); + const inputEvent = new InputEvent("input", {inputType: "insertText", data: value, bubbles: true}); + const keyupEvent = new KeyboardEvent("keyup", {bubbles: true}); + const changeEvent = new Event("change", {bubbles: true}); + + // Click on the field + field.click(); + // Set the value + field.value = value; + // Dispatch events, they happen in this order: down, input, up, change, ↑, ↑, ↓, ↓, ←, →, ←, →, B, A + field.dispatchEvent(keydownEvent); + field.dispatchEvent(inputEvent); + field.dispatchEvent(keyupEvent); + field.dispatchEvent(changeEvent); + } + } +} + +export default UserEventsService; diff --git a/src/react-web-integration/lib/User/UserEventsService.test.js b/src/react-web-integration/lib/User/UserEventsService.test.js new file mode 100644 index 000000000..41f1a402e --- /dev/null +++ b/src/react-web-integration/lib/User/UserEventsService.test.js @@ -0,0 +1,71 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) 2021 Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) 2021 Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 3.3.0 + */ + +/** + * Unit tests on UserEventsService in regard of specifications + */ + +import UserEventsService from "./UserEventsService"; + +beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); +}); + +describe("UserEventsService", () => { + it("As LU I should autofill field with all events", async() => { + expect.assertions(6); + const field = { + click: jest.fn(), + value: "", + dispatchEvent: jest.fn() + }; + UserEventsService.autofill(field, "test"); + expect(field.click).toHaveBeenCalledTimes(1); + expect(field.value).toStrictEqual("test"); + expect(field.dispatchEvent).toHaveBeenCalledWith(new KeyboardEvent("keydown", {bubbles: true})); + expect(field.dispatchEvent).toHaveBeenCalledWith(new InputEvent("input", {inputType: "insertText", data: "test", bubbles: true})); + expect(field.dispatchEvent).toHaveBeenCalledWith(new KeyboardEvent("keyup", {bubbles: true})); + expect(field.dispatchEvent).toHaveBeenCalledWith(new Event("change", {bubbles: true})); + }); + + it("As LU I should autofill field with null value", async() => { + expect.assertions(6); + const field = { + click: jest.fn(), + value: "", + dispatchEvent: jest.fn() + }; + UserEventsService.autofill(field, null); + expect(field.click).toHaveBeenCalledTimes(1); + expect(field.value).toStrictEqual(null); + expect(field.dispatchEvent).toHaveBeenCalledWith(new KeyboardEvent("keydown", {bubbles: true})); + expect(field.dispatchEvent).toHaveBeenCalledWith(new InputEvent("input", {inputType: "insertText", data: null, bubbles: true})); + expect(field.dispatchEvent).toHaveBeenCalledWith(new KeyboardEvent("keyup", {bubbles: true})); + expect(field.dispatchEvent).toHaveBeenCalledWith(new Event("change", {bubbles: true})); + }); + + it("As LU I should not autofill null field", async() => { + expect.assertions(1); + const field = null; + try { + UserEventsService.autofill(field, "test"); + expect(true).toBeTruthy(); + } catch (error) { + // Should not catch error + expect(error).toBeNull(); + } + }); +}); + From 875bfa07b56c7ba5ca879aa2f1f7760078f80870 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 30 Apr 2024 10:59:55 +0200 Subject: [PATCH 04/67] PB-33191 - Cover GroupUser entity with test and ensure non regression on validation changes Signed-off-by: Cedric Alfonsi --- .../context/Rbac/RbacContext.test.data.js | 4 +- .../groupUserEntity.test.data.js | 37 ++++++++++--------- .../entity/user/userEntity.test.data.js | 4 +- ...owIfGroupManagerInOneGroupFunction.test.js | 2 +- 4 files changed, 25 insertions(+), 22 deletions(-) rename src/shared/models/entity/{user => groupUser}/groupUserEntity.test.data.js (62%) diff --git a/src/shared/context/Rbac/RbacContext.test.data.js b/src/shared/context/Rbac/RbacContext.test.data.js index 678810e31..d579b4956 100644 --- a/src/shared/context/Rbac/RbacContext.test.data.js +++ b/src/shared/context/Rbac/RbacContext.test.data.js @@ -12,7 +12,7 @@ * @since 4.1.0 */ -import {defaultGroupsUser} from "../../models/entity/user/groupUserEntity.test.data"; +import {defaultGroupUser} from "../../models/entity/groupUser/groupUserEntity.test.data"; import {defaultUserDto} from "../../models/entity/user/userEntity.test.data"; /** @@ -54,7 +54,7 @@ export function denyRbacContext(data = {}) { export function defaultLoggedInUser(data = {}) { const user = defaultUserDto({ groups_users: [ - defaultGroupsUser({ + defaultGroupUser({ is_admin: true }) ] diff --git a/src/shared/models/entity/user/groupUserEntity.test.data.js b/src/shared/models/entity/groupUser/groupUserEntity.test.data.js similarity index 62% rename from src/shared/models/entity/user/groupUserEntity.test.data.js rename to src/shared/models/entity/groupUser/groupUserEntity.test.data.js index 416eaa1f5..ed885fe6a 100644 --- a/src/shared/models/entity/user/groupUserEntity.test.data.js +++ b/src/shared/models/entity/groupUser/groupUserEntity.test.data.js @@ -12,26 +12,29 @@ * @since 4.5.0 */ import {v4 as uuidv4} from "uuid"; -import {defaultUserDto} from "./userEntity.test.data"; +import {defaultUserDto} from "../user/userEntity.test.data"; -export const createGroupUser = (data = {}) => { - const defaultData = { - user_id: uuidv4(), - group_id: uuidv4(), - is_admin: false - }; +export const minimumGroupUserDto = (data = {}) => ({ + user_id: uuidv4(), + is_admin: false, + ...data +}); - return Object.assign(defaultData, data); -}; +export const createGroupUser = (data = {}) => ({ + user_id: uuidv4(), + group_id: uuidv4(), + is_admin: false, + ...data +}); -export const defaultGroupsUser = (data = {}) => { - const defaultData = createGroupUser({ - id: uuidv4(), - user_id: uuidv4(), - created: "2022-01-13T13:19:04.661Z", - }); - return Object.assign(defaultData, data); -}; +export const defaultGroupUser = (data = {}) => ({ + id: uuidv4(), + user_id: uuidv4(), + group_id: uuidv4(), + is_admin: false, + created: "2022-01-13T13:19:04.661Z", + ...data +}); export function groupsWithoutOwnership(data = {}) { const groupsUsers = [ diff --git a/src/shared/models/entity/user/userEntity.test.data.js b/src/shared/models/entity/user/userEntity.test.data.js index df8adba3d..985b41fe0 100644 --- a/src/shared/models/entity/user/userEntity.test.data.js +++ b/src/shared/models/entity/user/userEntity.test.data.js @@ -15,7 +15,7 @@ import {v4 as uuid} from "uuid"; import {defaultProfileDto} from "../profile/ProfileEntity.test.data"; import {adminRoleDto, TEST_ROLE_USER_ID, userRoleDto} from "../role/role.test.data"; -import {defaultGroupsUser} from "./groupUserEntity.test.data"; +import {defaultGroupUser} from "../groupUser/groupUserEntity.test.data"; import {defaultGpgkeyDto} from "../gpgkey/gpgkeyEntity.test.data"; import { createAcceptedAccountRecoveryUserSettingDto @@ -58,7 +58,7 @@ export const defaultUserDto = (data = {}, options = {}) => { defaultData.profile = profile; if (!data.groups_users && options?.withGroupsUsers) { - defaultData.groups_users = [defaultGroupsUser({user_id: defaultData.id})]; + defaultData.groups_users = [defaultGroupUser({user_id: defaultData.id})]; } if (!data.gpgkey && options?.withGpgkey) { diff --git a/src/shared/services/rbacs/controlFunctions/allowIfGroupManagerInOneGroupFunction.test.js b/src/shared/services/rbacs/controlFunctions/allowIfGroupManagerInOneGroupFunction.test.js index 6b910402a..6d31fea8a 100644 --- a/src/shared/services/rbacs/controlFunctions/allowIfGroupManagerInOneGroupFunction.test.js +++ b/src/shared/services/rbacs/controlFunctions/allowIfGroupManagerInOneGroupFunction.test.js @@ -13,7 +13,7 @@ */ import {defaultLoggedInUser} from "../../../context/Rbac/RbacContext.test.data"; -import {groupsWithoutOwnership} from "../../../models/entity/user/groupUserEntity.test.data"; +import {groupsWithoutOwnership} from "../../../models/entity/groupUser/groupUserEntity.test.data"; import AllowIfGroupManagerInOneGroupFunction from "./allowIfGroupManagerInOneGroupFunction"; describe("AllowIfGroupManagerInOneGroupFunction", () => { From 106901e806156dab166bf0cc1cbd993429c0b544 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 30 Apr 2024 11:01:51 +0200 Subject: [PATCH 05/67] PB-32981 Follow-up: Use a callback to destroy content script from a port with invalid context --- src/public-website-sign-in/lib/SignIn/SignInManager.js | 10 +++++++--- src/react-extension/test/mock/MockPort.js | 5 +++++ src/react-web-integration/lib/InForm/InFormManager.js | 10 +++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/public-website-sign-in/lib/SignIn/SignInManager.js b/src/public-website-sign-in/lib/SignIn/SignInManager.js index e2ad4c45b..968d8fde2 100644 --- a/src/public-website-sign-in/lib/SignIn/SignInManager.js +++ b/src/public-website-sign-in/lib/SignIn/SignInManager.js @@ -78,12 +78,16 @@ class SignInManager { */ handlePortDestroyEvent() { /* - * This is extremely important, when an extension update has been done - * and if the port has not been destroyed correctly, - * The port cannot reconnect due to an invalid context, + * This is extremely important, when an extension is available * so the port receive the message 'passbolt.port.destroy' to clean all data and listeners */ port.on('passbolt.content-script.destroy', this.destroy); + /* + * If the port has not been destroyed correctly, + * The port cannot reconnect due to an invalid context in case of a manual update of the extension, + * So to prevent error, a callback destroy listeners is assigned + */ + port.onConnectError(this.destroy); } } diff --git a/src/react-extension/test/mock/MockPort.js b/src/react-extension/test/mock/MockPort.js index 98933141e..29179d940 100644 --- a/src/react-extension/test/mock/MockPort.js +++ b/src/react-extension/test/mock/MockPort.js @@ -10,6 +10,7 @@ class MockPort { this.onListeners = {}; this.requestListeners = {}; this.emitListener = {}; + this.onConnectErrorHandler = {}; } async emit(name, eventObject) { @@ -64,6 +65,10 @@ class MockPort { addRequestListener(name, callback) { this.requestListeners[name] = callback; } + + onConnectError(callback) { + this.onConnectErrorHandler.callback = callback; + } } export default MockPort; diff --git a/src/react-web-integration/lib/InForm/InFormManager.js b/src/react-web-integration/lib/InForm/InFormManager.js index c040155e2..f01998096 100644 --- a/src/react-web-integration/lib/InForm/InFormManager.js +++ b/src/react-web-integration/lib/InForm/InFormManager.js @@ -312,12 +312,16 @@ class InFormManager { */ handlePortDestroyEvent() { /* - * This is extremely important, when an extension update has been done - * and if the port has not been destroyed correctly, - * The port cannot reconnect due to an invalid context, + * This is extremely important, when an extension is available * so the port receive the message 'passbolt.port.destroy' to clean all data and listeners */ port.on('passbolt.content-script.destroy', this.destroy); + /* + * If the port has not been destroyed correctly, + * The port cannot reconnect due to an invalid context in case of a manual update of the extension, + * So to prevent error, a callback destroy listeners is assigned + */ + port.onConnectError(this.destroy); } } From adcf43d30fbaeef64a6c155fb8c66d283e0871ae Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 30 Apr 2024 12:33:38 +0200 Subject: [PATCH 06/67] PB-33191 - styleguide version bump Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76afcf420..adbe5ab4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.1", + "version": "4.8.0-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.1", + "version": "4.8.0-alpha.2", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index d44d3a6a1..25c75d72f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.1", + "version": "4.8.0-alpha.2", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", From 0ae606e75f855a0c9df251e463ecc758cde98187 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Thu, 2 May 2024 08:46:04 +0200 Subject: [PATCH 07/67] PB-33216 - Add optional ignoreInvalid parameter to user entity in order to ignore associated groups users which could be invalid --- package-lock.json | 4 +- package.json | 2 +- test/assert/assertEntityProperty.js | 280 +++++++++++++++------------- 3 files changed, 150 insertions(+), 136 deletions(-) diff --git a/package-lock.json b/package-lock.json index adbe5ab4e..a0bb7486b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.2", + "version": "4.8.0-alpha.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.2", + "version": "4.8.0-alpha.3", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 25c75d72f..dbf68a365 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.2", + "version": "4.8.0-alpha.3", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/test/assert/assertEntityProperty.js b/test/assert/assertEntityProperty.js index eea20d278..2de06e073 100644 --- a/test/assert/assertEntityProperty.js +++ b/test/assert/assertEntityProperty.js @@ -13,167 +13,181 @@ */ import {v4 as uuidv4} from "uuid"; -const string = (EntityClass, propertyName) => { - // Valid scenarios - [ - {scenario: "a string", value: "valid-string"}, - ].forEach((test) => { +export const SCENARIO_EMPTY = {scenario: "empty", value: ""}; +export const SCENARIO_STRING = {scenario: "a string", value: "valid-string"}; +export const SCENARIO_INTEGER = {scenario: "a number", value: 42}; +export const SCENARIO_OBJECT = {scenario: "an object", value: {str: "string"}}; +export const SCENARIO_ARRAY = {scenario: "an array", value: ["string"]}; +export const SCENARIO_UUID = {scenario: "a uuid", value: uuidv4()}; +export const SCENARIO_NULL = {scenario: "null", value: null}; +export const SCENARIO_YEAR = {scenario: "year", value: "2018"}; +export const SCENARIO_YEAR_MONTH = {scenario: "year and month", value: "2018-10"}; +export const SCENARIO_YEAR_MONTH_DAY = {scenario: "year, month and day", value: "2018-10-18"}; +export const SCENARIO_YEAR_MONTH_DAY_TIME = {scenario: "year, month, day and time", value: "2021-11-17T13:19:48+00:00"}; +export const SCENARIO_TRUE = {scenario: "true", value: true}; +export const SCENARIO_FALSE = {scenario: "false", value: false}; + +export const SUCCESS_STRING_SCENARIOS = [SCENARIO_EMPTY, SCENARIO_STRING]; +export const FAIL_STRING_SCENARIOS = [SCENARIO_INTEGER, SCENARIO_TRUE, SCENARIO_FALSE, SCENARIO_OBJECT, SCENARIO_ARRAY]; + +export const assert = (EntityClass, propertyName, successScenarios, failScenarios, rule) => { + successScenarios.forEach(test => { const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "type", dto); + expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, rule, dto); }); - - // Not valid scenarios - [ - {scenario: "a number", value: 42}, - {scenario: "a boolean", value: true}, - {scenario: "an object", value: {str: "string"}}, - {scenario: "an array", value: ["string"]}, - ].forEach((test) => { + failScenarios.forEach(test => { const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "type", dto); + expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, rule, dto); }); }; -const uuid = (EntityClass, propertyName) => { - // Valid scenarios - [ - {scenario: "a uuid", value: uuidv4()}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "format", dto); - }); +export const string = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_STRING_SCENARIOS, FAIL_STRING_SCENARIOS, "type"); +}; - // Not valid scenarios - [ - {scenario: "not a uuid", value: "invalid-id"}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "format", dto); - }); +export const SUCCESS_UUID_SCENARIOS = [SCENARIO_UUID]; +export const FAIL_UUID_SCENARIOS = [SCENARIO_STRING]; +export const uuid = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_UUID_SCENARIOS, FAIL_UUID_SCENARIOS, "format"); }; -const required = (EntityClass, propertyName) => { +export const required = (EntityClass, propertyName) => { const dto = {}; expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "required", dto); }; -const notRequired = (EntityClass, propertyName) => { +export const notRequired = (EntityClass, propertyName) => { const dto = {}; expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "required", dto); }; -const nullable = (EntityClass, propertyName) => { - // Valid scenarios - [ - {scenario: "null", value: null}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "type", dto); - }); +export const SUCCESS_NULL_SCENARIO = [SCENARIO_NULL]; +export const nullable = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_NULL_SCENARIO, [], "type"); }; -const notNullable = (EntityClass, propertyName) => { - // Not valid scenarios - [ - {scenario: "null", value: null}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "type", dto); - }); +export const notNullable = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, [], SUCCESS_NULL_SCENARIO, "type"); }; -const minLength = (EntityClass, propertyName, minLength) => { - // Valid scenarios - [ - {scenario: "valid length", value: "a".repeat(minLength)}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "minLength", dto); - }); - - // Not valid scenarios - if (minLength !== 0) { - [ - {scenario: "too short", value: "a".repeat(minLength - 1)}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "minLength", dto); - }); - } +const SUCCESS_MIN_LENGTH_SCENARIO = minLength => ([{scenario: "valid length", value: "a".repeat(minLength)}]); +const FAIL_MIN_LENGTH_SCENARIO = minLength => (!minLength ? [] : [{scenario: "too short", value: "a".repeat(minLength - 1)}]); +export const minLength = (EntityClass, propertyName, minLength) => { + assert(EntityClass, propertyName, SUCCESS_MIN_LENGTH_SCENARIO(minLength), FAIL_MIN_LENGTH_SCENARIO(minLength), "minLength"); }; -const maxLength = (EntityClass, propertyName, maxLength) => { - // Valid scenarios - [ - {scenario: "valid length", value: "a".repeat(maxLength)}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "maxLength", dto); - }); - - // Not valid scenarios - [ - {scenario: "too long", value: "a".repeat(maxLength + 1)}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "maxLength", dto); - }); +const SUCCESS_MAX_LENGTH_SCENARIO = maxLength => ([{scenario: "valid length", value: "a".repeat(maxLength)}]); +const FAIL_MAX_LENGTH_SCENARIO = maxLength => ([{scenario: "too long", value: "a".repeat(maxLength + 1)}]); +export const maxLength = (EntityClass, propertyName, maxLength) => { + assert(EntityClass, propertyName, SUCCESS_MAX_LENGTH_SCENARIO(maxLength), FAIL_MAX_LENGTH_SCENARIO(maxLength), "maxLength"); }; -const dateTime = (EntityClass, propertyName) => { - // Valid scenarios - [ - {scenario: "year", value: "2018"}, - {scenario: "year and month", value: "2018-10"}, - {scenario: "year, month and day", value: "2018-10-18"}, - {scenario: "year, month, day and time", value: "2021-11-17T13:19:48+00:00"}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "format", dto); - }); - - // Not valid scenarios - [ - {scenario: "empty", value: ""}, - {scenario: "not a date", value: "not-a-date"}, - {scenario: "year, month, day, time and zulu", value: "2018-10-18T08:04:30+00:00Z"}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "format", dto); - }); +export const SUCCESS_DATETIME_SCENARIO = [ + SCENARIO_YEAR, + SCENARIO_YEAR_MONTH, + SCENARIO_YEAR_MONTH_DAY, + SCENARIO_YEAR_MONTH_DAY_TIME, +]; +export const FAIL_DATETIME_SCENARIO = [ + SCENARIO_EMPTY, + {scenario: "not a date", value: "not-a-date"}, + {scenario: "year, month, day, time and zulu", value: "2018-10-18T08:04:30+00:00Z"}, +]; +export const dateTime = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_DATETIME_SCENARIO, FAIL_DATETIME_SCENARIO, "format"); }; -const boolean = (EntityClass, propertyName) => { - // Valid scenarios - [ - {scenario: true, value: false}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).not.toThrowEntityValidationError(propertyName, "type", dto); - }); +export const SUCCESS_BOOLEAN_SCENARIO = [SCENARIO_TRUE, SCENARIO_FALSE]; +export const FAIL_BOOLEAN_SCENARIO = [SCENARIO_EMPTY, SCENARIO_STRING, SCENARIO_INTEGER, SCENARIO_OBJECT, SCENARIO_ARRAY]; +export const boolean = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_BOOLEAN_SCENARIO, FAIL_BOOLEAN_SCENARIO, "type"); +}; - // Not valid scenarios - [ - {scenario: "a string", value: "string"}, - {scenario: "a number", value: 42}, - {scenario: "an object", value: {}}, - {scenario: "an array", value: []}, - {scenario: "null", value: null}, - ].forEach((test) => { - const dto = {[propertyName]: test.value}; - expect(() => new EntityClass(dto)).toThrowEntityValidationError(propertyName, "type", dto); - }); +export const SUCCESS_EMAIL_SCENARIO = [ + {scenario: "abc.efg@domain.com", value: "abc.efg@domain.com"}, + {scenario: "efg@domain.com", value: "efg@domain.com"}, + {scenario: "abc-efg@domain.com", value: "abc-efg@domain.com"}, + {scenario: "abc_efg@domain.com", value: "abc_efg@domain.com"}, + {scenario: "raw@test.ra.ru", value: "raw@test.ra.ru"}, + {scenario: "abc-efg@domain-hyphened.com", value: "abc-efg@domain-hyphened.com"}, + {scenario: "p.o'malley@domain.com", value: "p.o'malley@domain.com"}, + {scenario: "abc+efg@domain.com", value: "abc+efg@domain.com"}, + {scenario: "abc&efg@domain.com", value: "abc&efg@domain.com"}, + {scenario: "abc.efg@12345.com", value: "abc.efg@12345.com"}, + {scenario: "abc.efg@12345.co.jp", value: "abc.efg@12345.co.jp"}, + {scenario: "abc@g.cn", value: "abc@g.cn"}, + {scenario: "abc@x.com", value: "abc@x.com"}, + {scenario: "henrik@sbcglobal.net", value: "henrik@sbcglobal.net"}, + {scenario: "sani@sbcglobal.net", value: "sani@sbcglobal.net"}, + // all ICANN TLDs + {scenario: "abc@example.aero", value: "abc@example.aero"}, + {scenario: "abc@example.asia", value: "abc@example.asia"}, + {scenario: "abc@example.biz", value: "abc@example.biz"}, + {scenario: "abc@example.cat", value: "abc@example.cat"}, + {scenario: "abc@example.com", value: "abc@example.com"}, + {scenario: "abc@example.coop", value: "abc@example.coop"}, + {scenario: "abc@example.edu", value: "abc@example.edu"}, + {scenario: "abc@example.gov", value: "abc@example.gov"}, + {scenario: "abc@example.info", value: "abc@example.info"}, + {scenario: "abc@example.int", value: "abc@example.int"}, + {scenario: "abc@example.jobs", value: "abc@example.jobs"}, + {scenario: "abc@example.mil", value: "abc@example.mil"}, + {scenario: "abc@example.mobi", value: "abc@example.mobi"}, + {scenario: "abc@example.museum", value: "abc@example.museum"}, + {scenario: "abc@example.name", value: "abc@example.name"}, + {scenario: "abc@example.net", value: "abc@example.net"}, + {scenario: "abc@example.org", value: "abc@example.org"}, + {scenario: "abc@example.pro", value: "abc@example.pro"}, + {scenario: "abc@example.tel", value: "abc@example.tel"}, + {scenario: "abc@example.travel", value: "abc@example.travel"}, + {scenario: "someone@st.t-com.hr", value: "someone@st.t-com.hr"}, + // gTLD's + {scenario: "example@host.local", value: "example@host.local"}, + {scenario: "example@x.org", value: "example@x.org"}, + {scenario: "example@host.xxx", value: "example@host.xxx"}, + // strange, but technically valid email addresses + {scenario: "S=postmaster/OU=rz/P=uni-frankfurt/A=d400/C=de@gateway.d400.de", value: "S=postmaster/OU=rz/P=uni-frankfurt/A=d400/C=de@gateway.d400.de"}, + {scenario: "customer/department=shipping@example.com", value: "customer/department=shipping@example.com"}, + {scenario: "$A12345@example.com", value: "$A12345@example.com"}, + {scenario: "!def!xyz%abc@example.com", value: "!def!xyz%abc@example.com"}, + {scenario: "_somename@example.com", value: "_somename@example.com"}, + // Unicode + {scenario: "some@eräume.foo", value: "some@eräume.foo"}, + {scenario: "äu@öe.eräume.foo", value: "äu@öe.eräume.foo"}, + {scenario: "Nyrée.surname@example.com", value: "Nyrée.surname@example.com"}, +]; +export const FAIL_EMAIL_SCENARIO = [ + {scenario: "abc@example", value: "abc@example"}, + {scenario: "abc@example.c", value: "abc@example.c"}, + {scenario: "abc@example.com.", value: "abc@example.com."}, + {scenario: "abc.@example.com", value: "abc.@example.com"}, + {scenario: "abc@example..com", value: "abc@example..com"}, + {scenario: "abc@example.com.a", value: "abc@example.com.a"}, + {scenario: "abc;@example.com", value: "abc;@example.com"}, + {scenario: "abc@example.com;", value: "abc@example.com;"}, + {scenario: "abc@efg@example.com", value: "abc@efg@example.com"}, + {scenario: "abc@@example.com", value: "abc@@example.com"}, + {scenario: "abc efg@example.com", value: "abc efg@example.com"}, + {scenario: "abc,efg@example.com", value: "abc,efg@example.com"}, + {scenario: "abc@sub,example.com", value: "abc@sub,example.com"}, + {scenario: "abc@sub'example.com", value: "abc@sub'example.com"}, + {scenario: "abc@sub/example.com", value: "abc@sub/example.com"}, + {scenario: "abc@yahoo!.com", value: "abc@yahoo!.com"}, + {scenario: "abc@example_underscored.com", value: "abc@example_underscored.com"}, + {scenario: "raw@test.ra.ru....com", value: "raw@test.ra.ru....com"}, +]; +export const email = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_EMAIL_SCENARIO, FAIL_EMAIL_SCENARIO, "custom"); }; -export default { - boolean, - dateTime, - minLength, - maxLength, - notNullable, - notRequired, - nullable, - required, - string, - uuid, +export const SUCCESS_LOCALE_SCENARIO = [ + {scenario: "en-UK", value: "en-UK"}, + {scenario: "fr-FR", value: "fr-FR"}, +]; +export const FAIL_LOCALE_SCENARIO = [ + {scenario: "Wrong caps", value: "EN-UK"}, + {scenario: "Incomplete", value: "fr"}, +]; +export const locale = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_LOCALE_SCENARIO, FAIL_LOCALE_SCENARIO, "format"); }; From b7a5876e7880231897467d54d1fc2935694b4ef6 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Sat, 4 May 2024 09:44:53 +0200 Subject: [PATCH 08/67] PB-33230 - Ensure performance creating groups collection with large dataset remains effective Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 +-- package.json | 2 +- .../entity/abstract/entityCollection.js | 23 ++++++++----- .../entity/abstract/entityCollection.test.js | 11 ++++++ .../entity/abstract/entityV2Collection.js | 21 ++++++++---- .../abstract/entityV2Collection.test.js | 34 +++++++++++++++++-- 6 files changed, 75 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0bb7486b..aacfa088e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.3", + "version": "4.8.0-alpha.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.3", + "version": "4.8.0-alpha.4", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index dbf68a365..633beca66 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.3", + "version": "4.8.0-alpha.4", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/abstract/entityCollection.js b/src/shared/models/entity/abstract/entityCollection.js index b885b3a6c..a7a109d82 100644 --- a/src/shared/models/entity/abstract/entityCollection.js +++ b/src/shared/models/entity/abstract/entityCollection.js @@ -257,19 +257,24 @@ class EntityCollection { * Assert that no item in the collection already has the given value for the given property. * @param {string} propName The property name for checking value uniqueness. * @param {string|boolean|number} propValue The property value for checking value uniqueness. - * @param {string} [message] The error message. If none given, it will fallback on a default one. + * @param {object} [options] Options. + * @param {string} [options.message] The error message. If none given, it will fallback on a default one. + * @param {Set} [options.haystackSet] A haystack set to reuse if given. Used as cache to improve performance. * @throw {EntityValidationError} If another item already has the given value for the given property. */ - assertNotExist(propName, propValue, message) { - const propValues = this.extract(propName); - // Set is the preferred approach for performance reasons, it does a deduplicate in 0n. - const uniqueElements = new Set(propValues); - const sizeBefore = uniqueElements.size; - uniqueElements.add(propValue); + assertNotExist(propName, propValue, options = {}) { + let haystackSet = options?.haystackSet; + + // If not given initialize the haystack set with the values of the items properties. + if (!haystackSet) { + const propValues = this.extract(propName); + haystackSet = new Set(propValues); + } - if (sizeBefore === uniqueElements.size) { + if (haystackSet.has(propValue)) { const error = new EntityValidationError(); - message = message || `The collection already includes an element that has a property (${propName}) with an identical value.`; + const message = options?.message + || `The collection already includes an element that has a property (${propName}) with an identical value.`; error.addError(propName, 'unique', message); throw error; } diff --git a/src/shared/models/entity/abstract/entityCollection.test.js b/src/shared/models/entity/abstract/entityCollection.test.js index a49727513..b39f82985 100644 --- a/src/shared/models/entity/abstract/entityCollection.test.js +++ b/src/shared/models/entity/abstract/entityCollection.test.js @@ -361,5 +361,16 @@ describe("EntityCollection", () => { expect.assertions(1); expect(() => collection.assertNotExist('name', 'zero')).not.toThrow(); }); + + it("should use a given haystack if given", () => { + const collection = new EntityCollection(); + collection.push(new TestEntity({name: 'first'})); + collection.push(new TestEntity({name: 'second'})); + collection.push(new TestEntity({name: 'second'})); + + expect.assertions(2); + expect(() => collection.assertNotExist('name', 'first', {haystackSet: new Set()})).not.toThrow(); + expect(() => collection.assertNotExist('name', 'first', {haystackSet: new Set(collection.extract("name"))})).toThrow(); + }); }); }); diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 929999fae..1f678bd13 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -33,9 +33,10 @@ class EntityV2Collection extends EntityCollection { /** * Validate the collection build rules. * @param {Entity} item The entity to validate the build rules for. + * @param {object} [options] Options. */ // eslint-disable-next-line no-unused-vars - validateBuildRules(item) { + validateBuildRules(item, options = {}) { // Override this method to add entity validation build rules. } @@ -52,8 +53,11 @@ class EntityV2Collection extends EntityCollection { * @param {object} [entityOptions] Options for constructing the entity, identical to those accepted by the Entity * constructor that will be utilized for its creation. * @throws {EntityValidationError} If the item doesn't validate. + * @param {object} [options] Options. + * @param {object} [options.validateBuildRules] Options to pass to validate build rules function @see EntityV2Collection::validateBuildRules + * @param {function} [options.onItemPushed] Callback to execute after the item has been pushed to the collection. */ - push(data, entityOptions = {}) { + push(data, entityOptions = {}, options = {}) { if (!data || typeof data !== 'object') { throw new TypeError(`Collection push expects "data" to be an object.`); } @@ -63,8 +67,9 @@ class EntityV2Collection extends EntityCollection { } const entity = new this.entityClass(data, entityOptions); - this.validateBuildRules(entity); + this.validateBuildRules(entity, options?.validateBuildRules); this._items.push(entity); + options?.onItemPushed?.(entity); } /** @@ -72,17 +77,21 @@ class EntityV2Collection extends EntityCollection { * @param {object|Entity|array} data The item(s) to add to the collection should be in the form of a DTO, an entity, * or an array comprising any of the aforementioned. * @param {object} [entityOptions] Options for constructing the entity, identical to those accepted by the Entity - * constructor that will be utilized for its creation. + * constructor that will be utilized for its creation. Note, this entity options will be passed to the associated + * collections and entities. + * @param {object} [options] Options. + * @param {object} [options.validateBuildRules] Options to pass to validate build rules function @see EntityV2Collection::validateBuildRules + * @param {function} [options.onItemPushed] Callback to execute after the item has been pushed to the collection. * @throws {CollectionValidationError} If one item doesn't validate. */ - pushMany(data, entityOptions = {}) { + pushMany(data, entityOptions = {}, options = {}) { if (!Array.isArray(data)) { throw new TypeError(`${this.constructor.name} pushMany expects "data" to be an array.`); } data.forEach((itemDto, index) => { try { - this.push(itemDto, entityOptions); + this.push(itemDto, entityOptions, options); } catch (error) { if (error instanceof EntityValidationError || error instanceof CollectionValidationError || error instanceof EntityCollectionError) { if (!entityOptions?.ignoreInvalidEntity) { diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index be54e3c50..8fcb65e40 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -145,8 +145,26 @@ describe("EntityV2Collection", () => { expect(collection.items[0]._props.name).toEqual(entity.name); }); - // @todo do we want this capability on this function? - it.todo("should ignore invalid content"); + it("should call the onItemPushed callback when an item is added to the collection", () => { + const collection = new TestEntityV2Collection([]); + const entity = new TestEntity(defaultTestEntityDto()); + const onItemPushed = jest.fn(); + + expect.assertions(1); + collection.push(entity, {}, {onItemPushed}); + expect(onItemPushed).toHaveBeenLastCalledWith(expect.anything(TestEntity)); + }); + + it("should pass along validateBuildRules options", () => { + const collection = new TestEntityV2Collection([]); + const entity = new TestEntity(defaultTestEntityDto()); + const validateBuildRules = {opt1: "value1"}; + jest.spyOn(collection, "validateBuildRules"); + + expect.assertions(1); + collection.push(entity, {}, {validateBuildRules}); + expect(collection.validateBuildRules).toHaveBeenLastCalledWith(expect.anything(TestEntity), validateBuildRules); + }); }); describe("GroupsCollection:pushMany", () => { @@ -277,5 +295,17 @@ describe("EntityV2Collection", () => { expect(collection.items[1]).toBeInstanceOf(TestEntity); expect(collection.items[1].id).toEqual(entity3.id); }); + + it("should pass along entities options and local options to push function", () => { + const collection = new TestEntityV2Collection([]); + const entity = defaultTestEntityDto(); + const entitiesOptions = {ignoreInvalidEntity: true}; + const options = {opt1: "value1"}; + jest.spyOn(collection, "push"); + + expect.assertions(1); + collection.pushMany([entity], entitiesOptions, options); + expect(collection.push).toHaveBeenLastCalledWith(expect.anything(Object), entitiesOptions, options); + }); }); }); From 8f598d6d63cc07c751d4fdc42856a4b92c210453 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 6 May 2024 18:57:33 +0200 Subject: [PATCH 09/67] PB-33236 - Ensure performance creating users collection with large dataset remains effective Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 +-- package.json | 2 +- .../entity/user/userEntity.test.data.js | 7 +++- .../entity/user/usersCollection.test.data.js | 35 +++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 src/shared/models/entity/user/usersCollection.test.data.js diff --git a/package-lock.json b/package-lock.json index aacfa088e..5dd4e70f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.4", + "version": "4.8.0-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.4", + "version": "4.8.0-alpha.5", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 633beca66..26bd6b8fa 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.4", + "version": "4.8.0-alpha.5", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/user/userEntity.test.data.js b/src/shared/models/entity/user/userEntity.test.data.js index 985b41fe0..7002d57a6 100644 --- a/src/shared/models/entity/user/userEntity.test.data.js +++ b/src/shared/models/entity/user/userEntity.test.data.js @@ -58,7 +58,12 @@ export const defaultUserDto = (data = {}, options = {}) => { defaultData.profile = profile; if (!data.groups_users && options?.withGroupsUsers) { - defaultData.groups_users = [defaultGroupUser({user_id: defaultData.id})]; + const groupsUsersCount = typeof options?.withGroupsUsers === "number" ? options?.withGroupsUsers : 1; + defaultData.groups_users = []; + for (let i = 0; i < groupsUsersCount; i++) { + const groupUserDto = defaultGroupUser({user_id: defaultData.id}); + defaultData.groups_users.push(groupUserDto); + } } if (!data.gpgkey && options?.withGpgkey) { diff --git a/src/shared/models/entity/user/usersCollection.test.data.js b/src/shared/models/entity/user/usersCollection.test.data.js new file mode 100644 index 000000000..04f0a0f77 --- /dev/null +++ b/src/shared/models/entity/user/usersCollection.test.data.js @@ -0,0 +1,35 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ + +import {defaultUserDto} from "./userEntity.test.data"; + +/** + * Build groups dtos. + * @param {number} [groupsCount=10] The number of groups. + * @param {Object} [options] + * @param {Object} [options.withRole=false] Add role default dto. + * @param {Object} [options.withGpgkey=false] Add gpg key default dto. + * @param {Object} [options.withAccountRecoveryUserSetting=false] Add account recover user settings default dto. + * @param {Object} [options.withPendingAccountRecoveryUserRequest=false] Add pending account recover user request default dto. + * @param {Object} [options.withGroupsUsers=0] Add groups users default dto. + * @returns {object} + */ +export const defaultUsersDtos = (groupsCount = 10, options = {}) => { + const dtos = []; + for (let i = 0; i < groupsCount; i++) { + const dto = defaultUserDto({username: `user${i}@domain.test`}, options); + dtos.push(dto); + } + return dtos; +}; From 6f808c3cf8cda24763f917a4291d19a8d882aefa Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 6 May 2024 22:57:32 +0200 Subject: [PATCH 10/67] PB-33267 - Validate PermissionEntity schema Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 +- package.json | 2 +- .../contexts/ExtAppContext.test.data.js | 4 +- .../context/Rbac/RbacContext.test.data.js | 2 +- .../entity/group/groupEntity.test.data.js | 69 +++++++++++++++++++ .../permission/permissionEntity.test.data.js | 48 +++++++++++-- .../entity/user/userEntity.test.data.js | 14 ++-- test/assert/assertEntityProperty.js | 15 +++- 8 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 src/shared/models/entity/group/groupEntity.test.data.js diff --git a/package-lock.json b/package-lock.json index 5dd4e70f5..19586e626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.5", + "version": "4.8.0-alpha.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.5", + "version": "4.8.0-alpha.6", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 26bd6b8fa..04f0f9ab0 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.5", + "version": "4.8.0-alpha.6", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/react-extension/contexts/ExtAppContext.test.data.js b/src/react-extension/contexts/ExtAppContext.test.data.js index 3cf2c415e..e56b8387c 100644 --- a/src/react-extension/contexts/ExtAppContext.test.data.js +++ b/src/react-extension/contexts/ExtAppContext.test.data.js @@ -74,7 +74,7 @@ export const defaultUserAppContext = (data = {}) => { resourceTypesSettings: new ResourceTypesSettings(siteSettings, resourceTypesCollectionDto()), port: new MockPort(), storage: new MockStorage(), - loggedInUser: defaultUserDto(), + loggedInUser: defaultUserDto({}, {withRole: true}), users: [], roles: [userRoleDto(), adminRoleDto()], resources: [], @@ -90,6 +90,6 @@ export const defaultUserAppContext = (data = {}) => { * @returns {object} */ export const defaultAdministratorAppContext = (data = {}) => defaultUserAppContext({ - loggedInUser: defaultAdminUserDto(), + loggedInUser: defaultAdminUserDto({}, {withRole: true}), ...data }); diff --git a/src/shared/context/Rbac/RbacContext.test.data.js b/src/shared/context/Rbac/RbacContext.test.data.js index d579b4956..8c2e7a4c2 100644 --- a/src/shared/context/Rbac/RbacContext.test.data.js +++ b/src/shared/context/Rbac/RbacContext.test.data.js @@ -58,6 +58,6 @@ export function defaultLoggedInUser(data = {}) { is_admin: true }) ] - }); + }, {withRole: true}); return Object.assign(user, data); } diff --git a/src/shared/models/entity/group/groupEntity.test.data.js b/src/shared/models/entity/group/groupEntity.test.data.js new file mode 100644 index 000000000..491e23499 --- /dev/null +++ b/src/shared/models/entity/group/groupEntity.test.data.js @@ -0,0 +1,69 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 3.6.0 + */ +import {v4 as uuidv4} from "uuid"; +import { + defaultGroupUser +} from "../groupUser/groupUserEntity.test.data"; +import {defaultUserDto} from "../user/userEntity.test.data"; + +export const minimumGroupUserDto = (data = {}) => ({ + name: "Current group", + ...data +}); + +/** + * Build default group dto as the API would return. + * @param {object} data The data to override the default dto. + * @param {Object} [options] + * @param {boolean} [options.withModifier=false] Add modifier default dto. + * @param {boolean} [options.withCreator=false] Add creator default dto. + * @param {boolean} [options.withMyGroupUser=false] Add my group user default dto. + * @param {boolean|integer} [options.withGroupsUsers=0] Add groups users default dto. + * @returns {object} + */ +export const defaultGroupDto = (data = {}, options = {}) => { + const groupId = data.id || uuidv4(); + const defaultData = { + id: groupId, + name: "Current group", + created: "2022-01-13T13:19:04.661Z", + modified: "2022-01-13T13:19:04.661Z", + created_by: uuidv4(), + modified_by: uuidv4(), + ...data + }; + + if (!data.my_group_user && options?.withMyGroupUser) { + defaultData.my_group_user = defaultGroupUser({group_id: groupId, is_admin: true}); + } + + if (!data.creator && options?.withCreator) { + defaultData.creator = defaultUserDto(); + } + + if (!data.modifier && options?.withModifier) { + defaultData.modifier = defaultUserDto(); + } + + if (!data.groupsUsers && options?.withGroupsUsers) { + const groupsUsersCount = typeof options?.withGroupsUsers === "number" ? options?.withGroupsUsers : 1; + defaultData.groups_users = []; + for (let i = 0; i < groupsUsersCount; i++) { + const groupUserDto = defaultGroupUser({user_id: uuidv4(), group_id: groupId, is_admin: true}); + defaultData.groups_users.push(groupUserDto); + } + } + + return defaultData; +}; diff --git a/src/shared/models/entity/permission/permissionEntity.test.data.js b/src/shared/models/entity/permission/permissionEntity.test.data.js index 13f92142c..09df09564 100644 --- a/src/shared/models/entity/permission/permissionEntity.test.data.js +++ b/src/shared/models/entity/permission/permissionEntity.test.data.js @@ -12,25 +12,61 @@ * @since 4.1.0 */ import {v4 as uuidv4} from "uuid"; +import {defaultUserDto} from "../user/userEntity.test.data"; +import {defaultGroupDto} from "../group/groupEntity.test.data"; -export const ownerPermissionDto = (data = {}) => ({ - id: uuidv4(), +export const minimumPermissionDto = (data = {}) => ({ aco: "Resource", aco_foreign_key: uuidv4(), aro: "User", aro_foreign_key: uuidv4(), - created: "2022-03-04T13:59:11+00:00", - modified: "2022-03-04T13:59:11+00:00", type: 15, ...data }); -export const updatePermissionDto = (data = {}) => ownerPermissionDto({ +/** + * Build default permissiondto. + * @param {object} data The data to override the default dto. + * @param {Object} [options] + * @param {Object} [options.withUser=false] Add user default dto. + * @param {Object} [options.withGroup=false] Add group default dto. + * @returns {object} + */ +export const defaultPermissionDto = (data = {}, options = {}) => { + const defaultData = { + id: uuidv4(), + aco: "Resource", + aco_foreign_key: uuidv4(), + aro: "User", + aro_foreign_key: uuidv4(), + created: "2022-03-04T13:59:11+00:00", + modified: "2022-03-04T13:59:11+00:00", + type: 15, + ...data + }; + + if (!data.user && options?.withUser) { + defaultData.user = defaultUserDto({id: defaultData.aro_foreign_key}); + } + + if (!data.group && options?.withGroup) { + defaultData.group = defaultGroupDto({id: defaultData.aro_foreign_key}); + } + + return defaultData; +}; + +export const ownerPermissionDto = (data = {}) => defaultPermissionDto({ + type: 15, + ...data +}); + +export const updatePermissionDto = (data = {}) => defaultPermissionDto({ type: 7, ...data }); -export const readPermissionDto = (data = {}) => ownerPermissionDto({ +export const readPermissionDto = (data = {}) => defaultPermissionDto({ type: 1, ...data }); diff --git a/src/shared/models/entity/user/userEntity.test.data.js b/src/shared/models/entity/user/userEntity.test.data.js index 7002d57a6..b86b3e645 100644 --- a/src/shared/models/entity/user/userEntity.test.data.js +++ b/src/shared/models/entity/user/userEntity.test.data.js @@ -26,11 +26,11 @@ import {pendingAccountRecoveryRequestDto} from "../accountRecovery/pendingAccoun * Default user dto. * @param {Object} data The data to override * @param {Object} [options] - * @param {Object} [options.withGroupsUsers=false] Add groups users default dto. - * @param {Object} [options.withRole=false] Add role default dto. - * @param {Object} [options.withGpgkey=false] Add gpg key default dto. - * @param {Object} [options.withAccountRecoveryUserSetting=false] Add account recover user settings default dto. - * @param {Object} [options.withPendingAccountRecoveryUserRequest=false] Add pending account recover user request default dto. + * @param {boolean|integer} [options.withGroupsUsers=false] Add groups users default dto. + * @param {boolean} [options.withRole=false] Add role default dto. + * @param {boolean} [options.withGpgkey=false] Add gpg key default dto. + * @param {boolean} [options.withAccountRecoveryUserSetting=false] Add account recover user settings default dto. + * @param {boolean} [options.withPendingAccountRecoveryUserRequest=false] Add pending account recover user request default dto. * @returns {object} */ export const defaultUserDto = (data = {}, options = {}) => { @@ -47,8 +47,8 @@ export const defaultUserDto = (data = {}, options = {}) => { ...data }; - if (!data.role) { - defaultData.role = userRoleDto(); + if (!data.role && options?.withRole) { + defaultData.role = userRoleDto({id: defaultData.role_id}); } const profile = data?.profile || defaultProfileDto({ diff --git a/test/assert/assertEntityProperty.js b/test/assert/assertEntityProperty.js index 2de06e073..d702dbdec 100644 --- a/test/assert/assertEntityProperty.js +++ b/test/assert/assertEntityProperty.js @@ -15,7 +15,8 @@ import {v4 as uuidv4} from "uuid"; export const SCENARIO_EMPTY = {scenario: "empty", value: ""}; export const SCENARIO_STRING = {scenario: "a string", value: "valid-string"}; -export const SCENARIO_INTEGER = {scenario: "a number", value: 42}; +export const SCENARIO_INTEGER = {scenario: "an integer", value: 42}; +export const SCENARIO_FLOAT = {scenario: "a float", value: 42.2}; export const SCENARIO_OBJECT = {scenario: "an object", value: {str: "string"}}; export const SCENARIO_ARRAY = {scenario: "an array", value: ["string"]}; export const SCENARIO_UUID = {scenario: "a uuid", value: uuidv4()}; @@ -191,3 +192,15 @@ export const FAIL_LOCALE_SCENARIO = [ export const locale = (EntityClass, propertyName) => { assert(EntityClass, propertyName, SUCCESS_LOCALE_SCENARIO, FAIL_LOCALE_SCENARIO, "format"); }; + +export const enumeration = (EntityClass, propertyName, successValues, failValues = []) => { + const successScenario = successValues.map(successValue => ({scenario: successValue, value: successValue})); + const failScenario = failValues.map(failValue => ({scenario: failValue, value: failValue})); + assert(EntityClass, propertyName, successScenario, failScenario, "enum"); +}; + +export const SUCCESS_INTEGER_SCENARIO = [SCENARIO_INTEGER]; +export const FAIL_INTEGER_SCENARIO = [SCENARIO_EMPTY, SCENARIO_STRING, SCENARIO_FLOAT, SCENARIO_OBJECT, SCENARIO_ARRAY]; +export const integer = (EntityClass, propertyName) => { + assert(EntityClass, propertyName, SUCCESS_INTEGER_SCENARIO, FAIL_INTEGER_SCENARIO, "type"); +}; From edd43144f7cc72e93799952dd0f9e2f3a8c6939b Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 7 May 2024 11:10:48 +0200 Subject: [PATCH 11/67] PB-33267 - Apply review feedback Signed-off-by: Cedric Alfonsi --- .../models/entity/abstract/entityV2Collection.test.js | 6 +++--- src/shared/models/entity/group/groupEntity.test.data.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index 8fcb65e40..1edff3cba 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -152,7 +152,7 @@ describe("EntityV2Collection", () => { expect.assertions(1); collection.push(entity, {}, {onItemPushed}); - expect(onItemPushed).toHaveBeenLastCalledWith(expect.anything(TestEntity)); + expect(onItemPushed).toHaveBeenLastCalledWith(entity); }); it("should pass along validateBuildRules options", () => { @@ -163,7 +163,7 @@ describe("EntityV2Collection", () => { expect.assertions(1); collection.push(entity, {}, {validateBuildRules}); - expect(collection.validateBuildRules).toHaveBeenLastCalledWith(expect.anything(TestEntity), validateBuildRules); + expect(collection.validateBuildRules).toHaveBeenLastCalledWith(entity, validateBuildRules); }); }); @@ -305,7 +305,7 @@ describe("EntityV2Collection", () => { expect.assertions(1); collection.pushMany([entity], entitiesOptions, options); - expect(collection.push).toHaveBeenLastCalledWith(expect.anything(Object), entitiesOptions, options); + expect(collection.push).toHaveBeenLastCalledWith(entity, entitiesOptions, options); }); }); }); diff --git a/src/shared/models/entity/group/groupEntity.test.data.js b/src/shared/models/entity/group/groupEntity.test.data.js index 491e23499..18b4b54dc 100644 --- a/src/shared/models/entity/group/groupEntity.test.data.js +++ b/src/shared/models/entity/group/groupEntity.test.data.js @@ -1,12 +1,12 @@ /** * Passbolt ~ Open source password manager for teams - * Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * Copyright (c) Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 3.6.0 From fbfb4d2bfcbdf4d6ce7674ee5fc95da7e594c3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Fri, 10 May 2024 09:56:18 +0200 Subject: [PATCH 12/67] PB-33264: add coverage on ApplicationEntity for healthcheck --- .../associations/applicationEntity.test.js | 94 +++++++++++++++++++ src/shared/utils/userUtils.js | 2 +- test/assert/assertEntityProperty.js | 2 +- test/jest.setup.js | 3 + test/matchers/extendExpect.js | 25 +++++ .../toThrowCollectionValidationError.js | 49 ++++++++++ test/matchers/toThrowEntityValidationError.js | 58 ++++++++++++ ...oThrowEntityValidationErrorOnProperties.js | 44 +++++++++ 8 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 src/shared/models/entity/healthcheck/associations/applicationEntity.test.js create mode 100644 test/matchers/extendExpect.js create mode 100644 test/matchers/toThrowCollectionValidationError.js create mode 100644 test/matchers/toThrowEntityValidationError.js create mode 100644 test/matchers/toThrowEntityValidationErrorOnProperties.js diff --git a/src/shared/models/entity/healthcheck/associations/applicationEntity.test.js b/src/shared/models/entity/healthcheck/associations/applicationEntity.test.js new file mode 100644 index 000000000..13b912195 --- /dev/null +++ b/src/shared/models/entity/healthcheck/associations/applicationEntity.test.js @@ -0,0 +1,94 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ +import EntitySchema from "../../abstract/entitySchema"; +import ApplicationEntity from "./applicationEntity"; +import * as assertEntityProperty from "../../../../../../test/assert/assertEntityProperty"; + +describe("ApplicationEntity", () => { + describe("ApplicationEntity::getSchema", () => { + it("schema must validate", () => { + EntitySchema.validateSchema(ApplicationEntity.ENTITY_NAME, ApplicationEntity.getSchema()); + }); + + it("validates info property", () => { + const successScenarios = [ + assertEntityProperty.SCENARIO_OBJECT, + ]; + /* + * @todo: //add failing scenarios when nested object will be checked + */ + const failingScenarios = []; + assertEntityProperty.assert(ApplicationEntity, "info", successScenarios, failingScenarios, "type"); + assertEntityProperty.required(ApplicationEntity, "info"); + }); + + it("validates latestVersion property", () => { + assertEntityProperty.boolean(ApplicationEntity, "latestVersion"); + assertEntityProperty.nullable(ApplicationEntity, "latestVersion"); + assertEntityProperty.required(ApplicationEntity, "latestVersion"); + }); + + it("validates schema property", () => { + assertEntityProperty.boolean(ApplicationEntity, "schema"); + assertEntityProperty.required(ApplicationEntity, "schema"); + }); + + it("validates robotsIndexDisabled property", () => { + assertEntityProperty.boolean(ApplicationEntity, "robotsIndexDisabled"); + assertEntityProperty.required(ApplicationEntity, "robotsIndexDisabled"); + }); + + it("validates sslForce property", () => { + assertEntityProperty.boolean(ApplicationEntity, "sslForce"); + assertEntityProperty.required(ApplicationEntity, "sslForce"); + }); + + it("validates sslFullBaseUrl property", () => { + assertEntityProperty.boolean(ApplicationEntity, "sslFullBaseUrl"); + assertEntityProperty.required(ApplicationEntity, "sslFullBaseUrl"); + }); + + it("validates seleniumDisabled property", () => { + assertEntityProperty.boolean(ApplicationEntity, "seleniumDisabled"); + assertEntityProperty.required(ApplicationEntity, "seleniumDisabled"); + }); + + it("validates registrationClosed property", () => { + const successScenarios = [ + assertEntityProperty.SCENARIO_OBJECT, + ]; + /* + * @todo: //add failing scenarios when nested object will be checked + */ + const failingScenarios = []; + assertEntityProperty.assert(ApplicationEntity, "registrationClosed", successScenarios, failingScenarios, "type"); + assertEntityProperty.required(ApplicationEntity, "registrationClosed"); + }); + + it("validates hostAvailabilityCheckEnabled property", () => { + assertEntityProperty.boolean(ApplicationEntity, "hostAvailabilityCheckEnabled"); + assertEntityProperty.required(ApplicationEntity, "hostAvailabilityCheckEnabled"); + }); + + it("validates jsProd property", () => { + assertEntityProperty.boolean(ApplicationEntity, "jsProd"); + assertEntityProperty.required(ApplicationEntity, "jsProd"); + }); + + it("validates emailNotificationEnabled property", () => { + assertEntityProperty.boolean(ApplicationEntity, "emailNotificationEnabled"); + assertEntityProperty.required(ApplicationEntity, "emailNotificationEnabled"); + }); + }); +}); diff --git a/src/shared/utils/userUtils.js b/src/shared/utils/userUtils.js index ee5cc50fd..7acd8e54d 100644 --- a/src/shared/utils/userUtils.js +++ b/src/shared/utils/userUtils.js @@ -16,7 +16,7 @@ * An enum to gather the different possible user statuses. * These are use in the UI directly and needs to be translated. * - * Hack the translation mecanism: + * Hack the translation mechanism: * this.props.t('active') * this.props.t('suspended') * this.props.t('deleted') diff --git a/test/assert/assertEntityProperty.js b/test/assert/assertEntityProperty.js index d702dbdec..8ef2750ee 100644 --- a/test/assert/assertEntityProperty.js +++ b/test/assert/assertEntityProperty.js @@ -190,7 +190,7 @@ export const FAIL_LOCALE_SCENARIO = [ {scenario: "Incomplete", value: "fr"}, ]; export const locale = (EntityClass, propertyName) => { - assert(EntityClass, propertyName, SUCCESS_LOCALE_SCENARIO, FAIL_LOCALE_SCENARIO, "format"); + assert(EntityClass, propertyName, SUCCESS_LOCALE_SCENARIO, FAIL_LOCALE_SCENARIO, "type"); }; export const enumeration = (EntityClass, propertyName, successValues, failValues = []) => { diff --git a/test/jest.setup.js b/test/jest.setup.js index 719d5f655..eead11dff 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -1,5 +1,8 @@ +import "./matchers/extendExpect"; + // Disable console debug, warning and error while executing the tests. // Keep console log as it can be useful for testing. + global.console = { ...console, debug: jest.fn(), diff --git a/test/matchers/extendExpect.js b/test/matchers/extendExpect.js new file mode 100644 index 000000000..f09dbdb03 --- /dev/null +++ b/test/matchers/extendExpect.js @@ -0,0 +1,25 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ + +import {toThrowEntityValidationErrorOnProperties} from "./toThrowEntityValidationErrorOnProperties"; +import {toThrowCollectionValidationError} from "./toThrowCollectionValidationError"; +import {toThrowEntityValidationError} from "./toThrowEntityValidationError"; + +const extensions = { + toThrowEntityValidationErrorOnProperties, + toThrowCollectionValidationError, + toThrowEntityValidationError +}; + +expect.extend(extensions); diff --git a/test/matchers/toThrowCollectionValidationError.js b/test/matchers/toThrowCollectionValidationError.js new file mode 100644 index 000000000..78530dc5f --- /dev/null +++ b/test/matchers/toThrowCollectionValidationError.js @@ -0,0 +1,49 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ + +const getNestedPropertyValue = (obj, path) => + path.split('.').reduce( + (accumulator, currentValue) => accumulator && accumulator[currentValue] ? accumulator[currentValue] : undefined + , obj + ); + +exports.toThrowCollectionValidationError = function(callback, expectedErrorPath) { + const {printExpected, printReceived, matcherHint} = this.utils; + + const passMessage = errorDetails => + `${matcherHint('.not.toThrowCollectionValidationError')}\n\n` + + `Expected collection validation to not fail on item property:\n` + + ` ${printExpected(expectedErrorPath)}\n` + + `Received:\n` + + ` ${printReceived(errorDetails)}`; + + const failMessage = errorDetails => + `${matcherHint('.toThrowCollectionValidationError')}\n\n` + + `Expected collection validation to fail on item property:\n` + + ` ${printExpected(expectedErrorPath)}\n` + + `Received:\n` + + ` ${printReceived(errorDetails)}`; + + let pass = false; + let errorDetails; + try { + callback(); + } catch (error) { + errorDetails = error?.details; + const errorPropertyValue = getNestedPropertyValue(errorDetails, expectedErrorPath); + pass = typeof errorPropertyValue !== "undefined"; + } + + return {pass: pass, message: () => (pass ? passMessage(errorDetails) : failMessage(errorDetails))}; +}; diff --git a/test/matchers/toThrowEntityValidationError.js b/test/matchers/toThrowEntityValidationError.js new file mode 100644 index 000000000..0823c0036 --- /dev/null +++ b/test/matchers/toThrowEntityValidationError.js @@ -0,0 +1,58 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ + +exports.toThrowEntityValidationError = function(received, propertyName, validationRule, dto) { + const {printExpected, printReceived, matcherHint} = this.utils; + let errorDetails; + + try { + received(); + } catch (error) { + errorDetails = error.details; + } + + let expectedPropertyMessage = propertyName; + if (validationRule) { + expectedPropertyMessage += `:${validationRule}`; + } + + let passMessage = + `${matcherHint('.not.toThrowEntityValidationErrorOnProperty') + }\n\n` + + `Expected validation to not fail on property & optionally validation rule: ` + + ` ${printExpected(expectedPropertyMessage)}\n` + + `Received:\n` + + ` ${printReceived(errorDetails)}`; + + let failMessage = + `${matcherHint('.toThrowEntityValidationErrorOnProperty') + }\n\n` + + `Expected validation to fail on properties:\n` + + ` ${printExpected(expectedPropertyMessage)}\n` + + `Received:\n` + + ` ${printReceived(errorDetails)}`; + + if (dto) { + passMessage += `\nDTO:\n` + + ` ${printReceived(dto)}\n`; + failMessage += `\nDTO:\n` + + ` ${printReceived(dto)}\n`; + } + + const pass = Boolean(errorDetails) + && propertyName in errorDetails + && (!validationRule || validationRule in errorDetails[propertyName]); + + return {pass: pass, message: () => (pass ? passMessage : failMessage)}; +}; diff --git a/test/matchers/toThrowEntityValidationErrorOnProperties.js b/test/matchers/toThrowEntityValidationErrorOnProperties.js new file mode 100644 index 000000000..32d721c16 --- /dev/null +++ b/test/matchers/toThrowEntityValidationErrorOnProperties.js @@ -0,0 +1,44 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) 2022 Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 3.6.0 + */ + +export const contains = (equals, list, value) => list.findIndex(item => equals(item, value)) > -1; + +/** + * @deprecated To remove or adapt, works on promise only. + */ +exports.toThrowEntityValidationErrorOnProperties = function(error, expected) { + const {printExpected, printReceived, matcherHint} = this.utils; + const actual = error.details || []; + + const passMessage = + `${matcherHint('.not.toThrowEntityValidationErrorOnProperties') + }\n\n` + + `Expected validation to not fail on properties:\n` + + ` ${printExpected(expected)}\n` + + `Received:\n` + + ` ${printReceived(Object.keys(actual))}`; + + const failMessage = + `${matcherHint('.toThrowEntityValidationErrorOnProperties') + }\n\n` + + `Expected validation to fail on properties:\n` + + ` ${printExpected(expected)}\n` + + `Received:\n` + + ` ${printReceived(Object.keys(actual))}`; + + const objectKeys = Object.keys(actual); + const pass = objectKeys.length === expected.length && expected.every(key => contains(this.equals, objectKeys, key)); + + return {pass: pass, message: () => (pass ? passMessage : failMessage)}; +}; From 144bd035f649a9cb2db6f20e61b6e9cb1add8a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Fri, 10 May 2024 11:22:43 +0200 Subject: [PATCH 13/67] PB-33264: add coverage for PasswordExpiryProSettingsEntity --- .../passwordExpiryProSettingsEntity.test.js | 124 +++++++++--------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js b/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js index d604bca2e..e0ed66851 100644 --- a/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js +++ b/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js @@ -11,16 +11,69 @@ * @link https://www.passbolt.com Passbolt(tm) * @since 4.5.0 */ - -import each from "jest-each"; -import EntitySchema from "passbolt-styleguide/src/shared/models/entity/abstract/entitySchema"; -import EntityValidationError from "passbolt-styleguide/src/shared/models/entity/abstract/entityValidationError"; +import EntitySchema from "passbolt-styleguide/src/shared/models/entity/abstract/entitySchema";; import PasswordExpiryProSettingsEntity from "./passwordExpiryProSettingsEntity"; import {defaultPasswordExpiryProSettingsDto} from "../passwordExpiry/passwordExpirySettingsEntity.test.data"; +import * as assertEntityProperty from "../../../../../test/assert/assertEntityProperty"; describe("passwordExpiryProSettings entity", () => { - it("schema must validate", () => { - EntitySchema.validateSchema(PasswordExpiryProSettingsEntity.ENTITY_NAME, PasswordExpiryProSettingsEntity.getSchema()); + describe("PasswordExpiryProSettingsEntity::getSchema", () => { + it("schema must validate", () => { + EntitySchema.validateSchema(PasswordExpiryProSettingsEntity.ENTITY_NAME, PasswordExpiryProSettingsEntity.getSchema()); + }); + + it("validates id property", () => { + assertEntityProperty.uuid(PasswordExpiryProSettingsEntity, "id"); + assertEntityProperty.notRequired(PasswordExpiryProSettingsEntity, "id"); + }); + + it("validates default_expiry_period property", () => { + assertEntityProperty.integer(PasswordExpiryProSettingsEntity, "default_expiry_period"); + assertEntityProperty.nullable(PasswordExpiryProSettingsEntity, "default_expiry_period", 1); + /* + * @todo: add min and max validation where the schema will be reviewed + * assertEntityProperty.min(PasswordExpiryProSettingsEntity, "default_expiry_period", 1); + * assertEntityProperty.max(PasswordExpiryProSettingsEntity, "default_expiry_period", 999); + */ + assertEntityProperty.notRequired(PasswordExpiryProSettingsEntity, "default_expiry_period"); + }); + + it("validates policy_override property", () => { + assertEntityProperty.boolean(PasswordExpiryProSettingsEntity, "policy_override"); + assertEntityProperty.required(PasswordExpiryProSettingsEntity, "policy_override"); + }); + + it("validates automatic_expiry property", () => { + assertEntityProperty.boolean(PasswordExpiryProSettingsEntity, "automatic_expiry"); + assertEntityProperty.required(PasswordExpiryProSettingsEntity, "automatic_expiry"); + }); + + it("validates automatic_update property", () => { + assertEntityProperty.boolean(PasswordExpiryProSettingsEntity, "automatic_update"); + assertEntityProperty.required(PasswordExpiryProSettingsEntity, "automatic_update"); + }); + + it("validates created property", () => { + assertEntityProperty.string(PasswordExpiryProSettingsEntity, "created"); + assertEntityProperty.dateTime(PasswordExpiryProSettingsEntity, "created"); + assertEntityProperty.notRequired(PasswordExpiryProSettingsEntity, "created"); + }); + + it("validates modified property", () => { + assertEntityProperty.string(PasswordExpiryProSettingsEntity, "modified"); + assertEntityProperty.dateTime(PasswordExpiryProSettingsEntity, "modified"); + assertEntityProperty.notRequired(PasswordExpiryProSettingsEntity, "modified"); + }); + + it("validates created_by property", () => { + assertEntityProperty.uuid(PasswordExpiryProSettingsEntity, "created_by"); + assertEntityProperty.notRequired(PasswordExpiryProSettingsEntity, "created_by"); + }); + + it("validates modified_by property", () => { + assertEntityProperty.uuid(PasswordExpiryProSettingsEntity, "modified_by"); + assertEntityProperty.notRequired(PasswordExpiryProSettingsEntity, "modified_by"); + }); }); it("should accept a mininal valid DTO", () => { @@ -54,63 +107,4 @@ describe("passwordExpiryProSettings entity", () => { const entity = PasswordExpiryProSettingsEntity.createFromDefault(expectedDto); expect(entity.toDto()).toStrictEqual(expectedDto); }); - - it("should throw an exception if required fields are not present", () => { - const requiredFieldNames = PasswordExpiryProSettingsEntity.getSchema().required; - const requiredFieldCount = 3; - expect.assertions(requiredFieldCount * 2 + 1); - - expect(requiredFieldNames.length).toStrictEqual(requiredFieldCount); - - for (let i = 0; i < requiredFieldNames.length; i++) { - const fieldName = requiredFieldNames[i]; - const dto = defaultPasswordExpiryProSettingsDto(); - delete dto[fieldName]; - try { - new PasswordExpiryProSettingsEntity(dto); - } catch (e) { - expect(e).toBeInstanceOf(EntityValidationError); - expect(e.hasError(fieldName, "required")).toStrictEqual(true); - } - } - }); - - each([ - {dto: {id: "string but not uuid"}, errorType: "format"}, - {dto: {id: -1}, errorType: "type"}, - - {dto: {default_expiry_period: true}, errorType: "type"}, - {dto: {default_expiry_period: "50"}, errorType: "type"}, - {dto: {default_expiry_period: -1}, errorType: "type"}, - - {dto: {policy_override: 0}, errorType: "type"}, - - {dto: {automatic_update: 0}, errorType: "type"}, - {dto: {automatic_expiry: 0}, errorType: "type"}, - - {dto: {created: "string but not a date"}, errorType: "format"}, - {dto: {created: -1}, errorType: "type"}, - - {dto: {created_by: "string but not uuid"}, errorType: "format"}, - {dto: {created_by: -1}, errorType: "type"}, - - {dto: {modified: "string but not a date"}, errorType: "format"}, - {dto: {modified: -1}, errorType: "type"}, - - {dto: {modified_by: "string but not uuid"}, errorType: "format"}, - {dto: {modified_by: -1}, errorType: "type"}, - ]).describe("should throw an exception if DTO contains invalid values", scenario => { - it(`scenario: ${JSON.stringify(scenario)}`, () => { - expect.assertions(2); - const fieldName = Object.keys(scenario.dto)[0]; - const erroneousDto = defaultPasswordExpiryProSettingsDto(scenario.dto); - - try { - new PasswordExpiryProSettingsEntity(erroneousDto); - } catch (e) { - expect(e).toBeInstanceOf(EntityValidationError); - expect(e.hasError(fieldName, scenario.errorType)).toStrictEqual(true); - } - }); - }); }); From 48394b9854436abd411717e9abb754c37e9b86e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Fri, 10 May 2024 11:51:09 +0200 Subject: [PATCH 14/67] PB-33264: add coverage on SsoSettingsEntity --- .../passwordExpiryProSettingsEntity.test.js | 2 +- .../ssoSettings/SsoSettingsEntity.test.js | 108 ++++++++++++++---- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js b/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js index e0ed66851..02e5425ac 100644 --- a/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js +++ b/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.test.js @@ -11,7 +11,7 @@ * @link https://www.passbolt.com Passbolt(tm) * @since 4.5.0 */ -import EntitySchema from "passbolt-styleguide/src/shared/models/entity/abstract/entitySchema";; +import EntitySchema from "passbolt-styleguide/src/shared/models/entity/abstract/entitySchema"; import PasswordExpiryProSettingsEntity from "./passwordExpiryProSettingsEntity"; import {defaultPasswordExpiryProSettingsDto} from "../passwordExpiry/passwordExpirySettingsEntity.test.data"; import * as assertEntityProperty from "../../../../../test/assert/assertEntityProperty"; diff --git a/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js b/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js index d1ecd5aff..6e37339d7 100644 --- a/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js +++ b/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js @@ -28,13 +28,98 @@ import { } from "./SsoSettingsEntity.test.data"; import {v4 as uuid} from "uuid"; import AdfsSsoSettingsEntity from "./AdfsSsoSettingsEntity"; +import * as assertEntityProperty from "../../../../../test/assert/assertEntityProperty"; describe("SsoSettingsEntity", () => { - describe("SsoSettingsEntity:constructor", () => { + describe("SsoSettingsEntity::getSchema", () => { it("schema must validate", () => { EntitySchema.validateSchema(SsoSettingsEntity.ENTITY_NAME, SsoSettingsEntity.getSchema()); }); + it("validates id property", () => { + assertEntityProperty.uuid(SsoSettingsEntity, "id"); + assertEntityProperty.notRequired(SsoSettingsEntity, "id"); + }); + + it("validates providers property", () => { + const successScenarios = [ + assertEntityProperty.SCENARIO_ARRAY + ]; + const failingScenarios = [ + assertEntityProperty.SCENARIO_INTEGER, + assertEntityProperty.SCENARIO_STRING, + assertEntityProperty.SCENARIO_NULL + ]; + assertEntityProperty.assert(SsoSettingsEntity, "providers", successScenarios, failingScenarios, "type"); + assertEntityProperty.notRequired(SsoSettingsEntity, "providers"); + }); + + it("validates provider property", () => { + const successValues = [ + "azure", + "adfs", + "google", + "oauth2", + ]; + + const failingValues = [ + "test", + "other", + "unknown" + ]; + + const successScenarios = successValues.map(value => ({scenario: `with value "${value}}"`, value: value})); + const failingScenarios = failingValues.map(value => ({scenario: `with value "${value}}"`, value: value})); + + assertEntityProperty.assert(SsoSettingsEntity, "provider", successScenarios, failingScenarios, "type"); + assertEntityProperty.nullable(SsoSettingsEntity, "provider"); + assertEntityProperty.notRequired(SsoSettingsEntity, "provider"); + }); + + it("validates data property", () => { + const successScenarios = [ + assertEntityProperty.SCENARIO_OBJECT, + ]; + + /* + * @todo: add object failing scenarios when the schema will handled such checks + * const failingScenarios = [ + * assertEntityProperty.SCENARIO_INTEGER, + * assertEntityProperty.SCENARIO_NULL, + * assertEntityProperty.SCENARIO_STRING, + * assertEntityProperty.SCENARIO_ARRAY + * ]; + */ + + const failingScenarios = []; + assertEntityProperty.assert(SsoSettingsEntity, "data", successScenarios, failingScenarios, "type"); + assertEntityProperty.notRequired(SsoSettingsEntity, "data"); + }); + + it("validates created property", () => { + assertEntityProperty.string(SsoSettingsEntity, "created"); + assertEntityProperty.dateTime(SsoSettingsEntity, "created"); + assertEntityProperty.notRequired(SsoSettingsEntity, "created"); + }); + + it("validates modified property", () => { + assertEntityProperty.string(SsoSettingsEntity, "modified"); + assertEntityProperty.dateTime(SsoSettingsEntity, "modified"); + assertEntityProperty.notRequired(SsoSettingsEntity, "modified"); + }); + + it("validates created_by property", () => { + assertEntityProperty.uuid(SsoSettingsEntity, "created_by"); + assertEntityProperty.notRequired(SsoSettingsEntity, "created_by"); + }); + + it("validates modified_by property", () => { + assertEntityProperty.uuid(SsoSettingsEntity, "modified_by"); + assertEntityProperty.notRequired(SsoSettingsEntity, "modified_by"); + }); + }); + + describe("SsoSettingsEntity:constructor", () => { each([ {provider: null, dto: defaultSsoSettings()}, {provider: AzureSsoSettingsEntity.PROVIDER_ID, dto: defaultSsoSettingsWithAzure({id: uuid()})}, @@ -53,27 +138,6 @@ describe("SsoSettingsEntity", () => { }); }); - each([ - {dto: {id: "string but not uuid"}, errorType: "format"}, - {dto: {id: -1}, errorType: "type"}, - - {dto: {providers: -1}, errorType: "type"}, - - {dto: {data: 15}, errorType: "type"}, - ]).describe("should throw an exception if DTO contains invalid values", scenario => { - it(`scenario: ${JSON.stringify(scenario)}`, () => { - expect.assertions(2); - const fieldName = Object.keys(scenario.dto)[0]; - const erroneousDto = defaultSsoSettingsWithAzure(scenario.dto); - try { - new SsoSettingsEntity(erroneousDto); - } catch (e) { - expect(e).toBeInstanceOf(EntityValidationError); - expect(e.hasError(fieldName, scenario.errorType)).toStrictEqual(true); - } - }); - }); - it(`should throw an exception if OAuth2 specific validation fails`, () => { expect.assertions(2); const erroneousDto = defaultSsoSettingsWithOAuth2({ From f64f056441c98d8c76e553f5d2722c79639f8e7c Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Fri, 10 May 2024 09:53:09 +0000 Subject: [PATCH 15/67] PB-33306 - Switch ResourcesSecretsCollection to EntityV2Collection --- .../models/entity/abstract/entityCollection.js | 6 ++++++ .../models/entity/abstract/entityCollection.test.js | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/shared/models/entity/abstract/entityCollection.js b/src/shared/models/entity/abstract/entityCollection.js index a7a109d82..a6668ea7d 100644 --- a/src/shared/models/entity/abstract/entityCollection.js +++ b/src/shared/models/entity/abstract/entityCollection.js @@ -255,6 +255,8 @@ class EntityCollection { /** * Assert that no item in the collection already has the given value for the given property. + * Note: The assertion ignore undefined prop value, it is the schema responsibility to ensure properties are defined. + * * @param {string} propName The property name for checking value uniqueness. * @param {string|boolean|number} propValue The property value for checking value uniqueness. * @param {object} [options] Options. @@ -263,6 +265,10 @@ class EntityCollection { * @throw {EntityValidationError} If another item already has the given value for the given property. */ assertNotExist(propName, propValue, options = {}) { + if (typeof propValue === "undefined") { + return; + } + let haystackSet = options?.haystackSet; // If not given initialize the haystack set with the values of the items properties. diff --git a/src/shared/models/entity/abstract/entityCollection.test.js b/src/shared/models/entity/abstract/entityCollection.test.js index b39f82985..a0f7ee1d4 100644 --- a/src/shared/models/entity/abstract/entityCollection.test.js +++ b/src/shared/models/entity/abstract/entityCollection.test.js @@ -338,6 +338,18 @@ describe("EntityCollection", () => { expect(() => collection.assertNotExist('name', 'zero')).not.toThrow(); }); + it("should not throw if the given property value is not defined", () => { + const collection = new EntityCollection(); + collection.push(new TestEntity({name: 'first'})); + collection.push(new TestEntity({name: 'second'})); + collection.push(new TestEntity({name: 'third'})); + collection.push(new TestEntity({})); + collection.push(new TestEntity({name: null})); + + expect.assertions(1); + expect(() => collection.assertNotExist('name', undefined)).not.toThrow(); + }); + it("should throw if an item exists for the given property and value", () => { const collection = new EntityCollection(); collection.push(new TestEntity({name: 'first'})); From e60241a9290df83d515d59ed5f57cd89cc641b9e Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Thu, 9 May 2024 15:42:28 +0200 Subject: [PATCH 16/67] PB-33320 Cover PermissionCollection --- package-lock.json | 4 +-- package.json | 2 +- .../permissionCollection.test.data.js | 32 +++++++++++++++++++ .../permission/permissionEntity.test.data.js | 25 ++++++++++++--- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 src/shared/models/entity/permission/permissionCollection.test.data.js diff --git a/package-lock.json b/package-lock.json index 19586e626..8f4ac735b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.6", + "version": "4.8.0-alpha.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.6", + "version": "4.8.0-alpha.7", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 04f0f9ab0..d45954604 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.6", + "version": "4.8.0-alpha.7", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/permission/permissionCollection.test.data.js b/src/shared/models/entity/permission/permissionCollection.test.data.js new file mode 100644 index 000000000..be6975a0d --- /dev/null +++ b/src/shared/models/entity/permission/permissionCollection.test.data.js @@ -0,0 +1,32 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ + + +import {defaultPermissionDto} from "./permissionEntity.test.data"; + +/** + * Build dtos. + * @param {number} [count=10] The number of dtos. + * @returns {object} + */ +export const defaultPermissionsDtos = (count = 10) => { + const dtos = []; + const acoForeignKey = crypto.randomUUID(); + for (let i = 0; i < count; i++) { + const groupDto = defaultPermissionDto({aco_foreign_key: acoForeignKey}); + dtos.push(groupDto); + } + return dtos; +}; + diff --git a/src/shared/models/entity/permission/permissionEntity.test.data.js b/src/shared/models/entity/permission/permissionEntity.test.data.js index 09df09564..29bb944d4 100644 --- a/src/shared/models/entity/permission/permissionEntity.test.data.js +++ b/src/shared/models/entity/permission/permissionEntity.test.data.js @@ -24,12 +24,29 @@ export const minimumPermissionDto = (data = {}) => ({ ...data }); +export const ownerMinimalFolderPermissionDto = (data = {}) => minimumPermissionDto({ + aco: "Folder", + ...data +}); + +export const updateMinimalFolderPermissionDto = (data = {}) => minimumPermissionDto({ + aco: "Folder", + type: 7, + ...data +}); + +export const readMinimalFolderPermissionDto = (data = {}) => minimumPermissionDto({ + aco: "Folder", + type: 1, + ...data +}); + /** * Build default permissiondto. * @param {object} data The data to override the default dto. * @param {Object} [options] - * @param {Object} [options.withUser=false] Add user default dto. - * @param {Object} [options.withGroup=false] Add group default dto. + * @param {Object|boolean} [options.withUser=false] Add user default dto. Can be used to pass parameter to the user factory. + * @param {Object|boolean} [options.withGroup=false] Add group default dto. Can be used to pass parameter to the group factory. * @returns {object} */ export const defaultPermissionDto = (data = {}, options = {}) => { @@ -46,11 +63,11 @@ export const defaultPermissionDto = (data = {}, options = {}) => { }; if (!data.user && options?.withUser) { - defaultData.user = defaultUserDto({id: defaultData.aro_foreign_key}); + defaultData.user = defaultUserDto({id: defaultData.aro_foreign_key}, options.withUser); } if (!data.group && options?.withGroup) { - defaultData.group = defaultGroupDto({id: defaultData.aro_foreign_key}); + defaultData.group = defaultGroupDto({id: defaultData.aro_foreign_key}, options.withGroup); } return defaultData; From 1aef3920dde20f6aec12b24d9fa922523788d52e Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Sat, 11 May 2024 19:36:12 +0200 Subject: [PATCH 17/67] PB-33320 - Switch PermissionsCollection to EntityV2Collection Signed-off-by: Cedric Alfonsi --- .../abstract/collectionValidationError.js | 23 +++++- .../entity/abstract/entity.test.data.js | 7 ++ .../entity/abstract/entityV2Collection.js | 78 ++++++++++++------- .../abstract/entityV2Collection.test.js | 36 ++++++++- test/jest.setup.js | 22 +++++- test/mocks/mockCrypto.js | 22 ++++++ 6 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 test/mocks/mockCrypto.js diff --git a/src/shared/models/entity/abstract/collectionValidationError.js b/src/shared/models/entity/abstract/collectionValidationError.js index c756d9353..7a28f54f3 100644 --- a/src/shared/models/entity/abstract/collectionValidationError.js +++ b/src/shared/models/entity/abstract/collectionValidationError.js @@ -22,7 +22,7 @@ class CollectionValidationError extends Error { constructor(message = 'Collection validation error.') { super(message); this.name = 'CollectionValidationError'; - this.items = []; + this.errors = []; } /** @@ -38,7 +38,16 @@ class CollectionValidationError extends Error { if (!(validationError instanceof EntityValidationError) && !(validationError instanceof CollectionValidationError)) { throw new TypeError('CollectionValidationError addEntityValidationError expects "entityValidationError" to be an instance of EntityValidationError or CollectionValidationError.'); } - this.items[position] = validationError; + this.errors[position] = validationError; + } + + /** + * Add a collection validation error. + * @param {string} rule The collection rule. + * @param {string} message The error message. + */ + addCollectionValidationError(rule, message) { + this.errors[rule] = message; } /** @@ -47,8 +56,14 @@ class CollectionValidationError extends Error { */ get details() { const details = []; - for (const itemPosition in this.items) { - details[itemPosition] = this.items[itemPosition].details; + for (const key in this.errors) { + if (this.errors[key] instanceof EntityValidationError) { + details[key] = this.errors[key].details; + } else if (this.errors[key] instanceof CollectionValidationError) { + details[key] = this.errors[key].details; + } else { + details[key] = this.errors[key]; + } } return details; } diff --git a/src/shared/models/entity/abstract/entity.test.data.js b/src/shared/models/entity/abstract/entity.test.data.js index 8e5507b75..9560ccb85 100644 --- a/src/shared/models/entity/abstract/entity.test.data.js +++ b/src/shared/models/entity/abstract/entity.test.data.js @@ -53,10 +53,17 @@ export class TestEntity extends Entity { get id() { return this._props.id; } + set id(id) { + this._props.id = id; + } get name() { return this._props.name; } + + set name(name) { + this._props.name = name; + } } export class TestAssociatedEntity extends Entity { diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 1f678bd13..119ed918f 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -25,13 +25,34 @@ class EntityV2Collection extends EntityCollection { throw new Error("The collection class should declare the entity class that is handled."); } + /** + * Build or clone entity. + * @param {object|Entity} data The data of the item to push + * @param {object} [entityOptions] Options for constructing the entity, identical to those accepted by the Entity + * constructor that will be utilized for its creation. + * @throws {EntityValidationError} If the item doesn't validate. + * @returns {this.entityClass} + */ + buildOrCloneEntity(data, entityOptions = {}) { + if (!data || typeof data !== 'object') { + throw new TypeError(`${this.entityClass.name}::buildOrCloneEntity expects "data" to be an object.`); + } + + if (data instanceof this.entityClass) { + data = data.toDto(this.entityClass?.ALL_CONTAIN_OPTIONS); // deep clone + } + + return new this.entityClass(data, entityOptions); + } + /* * ================================================== * Validation * ================================================== */ + /** - * Validate the collection build rules. + * Validate the item build rules. It is used to verify the integrity of the collection before adding an item to it. * @param {Entity} item The entity to validate the build rules for. * @param {object} [options] Options. */ @@ -58,15 +79,7 @@ class EntityV2Collection extends EntityCollection { * @param {function} [options.onItemPushed] Callback to execute after the item has been pushed to the collection. */ push(data, entityOptions = {}, options = {}) { - if (!data || typeof data !== 'object') { - throw new TypeError(`Collection push expects "data" to be an object.`); - } - - if (data instanceof this.entityClass) { - data = data.toDto(this.entityClass?.ALL_CONTAIN_OPTIONS); // deep clone - } - - const entity = new this.entityClass(data, entityOptions); + const entity = this.buildOrCloneEntity(data, entityOptions); this.validateBuildRules(entity, options?.validateBuildRules); this._items.push(entity); options?.onItemPushed?.(entity); @@ -93,25 +106,38 @@ class EntityV2Collection extends EntityCollection { try { this.push(itemDto, entityOptions, options); } catch (error) { - if (error instanceof EntityValidationError || error instanceof CollectionValidationError || error instanceof EntityCollectionError) { - if (!entityOptions?.ignoreInvalidEntity) { - /* - * The validation process for checking entity associations in the collection is functional. However, the error - * details provided is not fully detailed. While it identifies the correct data item that fails validation in - * the collection, it fails to clearly indicate which specific property of the parent entity is problematic. - */ - const collectionValidationError = new CollectionValidationError(); - collectionValidationError.addEntityValidationError(index, error); - throw collectionValidationError; - } else { - console.debug(`${this.entityClass.name}::pushMany ignore item (${index}) due to validation error ${JSON.stringify(error?.details)}`); - } - } else { - throw error; - } + this.handlePushItemError(index, error, entityOptions); } }); } + + /** + * Handle error occurring while adding an item to the collection. + * @param {number} index The index the error occurred on. + * @param {Error} error The error. + * @param {object} [entityOptions] Options for constructing the entity, identical to those accepted by the Entity + * constructor that will be utilized for its creation. Note, this entity options will be passed to the associated + * collections and entities. + * @protected + */ + handlePushItemError(index, error, entityOptions) { + if (error instanceof EntityValidationError || error instanceof CollectionValidationError || error instanceof EntityCollectionError) { + if (!entityOptions?.ignoreInvalidEntity) { + /* + * The validation process for checking entity associations in the collection is functional. However, the error + * details provided is not fully detailed. While it identifies the correct data item that fails validation in + * the collection, it fails to clearly indicate which specific property of the parent entity is problematic. + */ + const collectionValidationError = new CollectionValidationError(); + collectionValidationError.addEntityValidationError(index, error); + throw collectionValidationError; + } else { + console.debug(`${this.entityClass.name}::pushMany ignore item (${index}) due to validation error ${JSON.stringify(error?.details)}`); + } + } else { + throw error; + } + } } export default EntityV2Collection; diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index 1edff3cba..8db86dbb5 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -18,6 +18,40 @@ import {TestEntityV2Collection} from "./entityV2Collection.test.data"; import {defaultAssociatedTestEntityDto, defaultTestEntityDto, TestEntity} from "./entity.test.data"; describe("EntityV2Collection", () => { + describe("EntityV2Collection:buildOrCloneEntity", () => { + it("should throw an exception if the data parameter is not an object.", () => { + const collection = new TestEntityV2Collection([]); + expect.assertions(1); + expect(() => collection.buildOrCloneEntity(42)).toThrow(TypeError); + }); + + it("should create entity from dto.", () => { + const collection = new TestEntityV2Collection([]); + const entityDto1 = defaultTestEntityDto(); + + expect.assertions(3); + const entity = collection.buildOrCloneEntity(entityDto1); + expect(entity).toBeInstanceOf(TestEntity); + expect(entity.id).toEqual(entityDto1.id); + expect(entity.name).toEqual(entityDto1.name); + }); + + it("should clone entity.", () => { + const collection = new TestEntityV2Collection([]); + const entity1 = new TestEntity(defaultTestEntityDto()); + + expect.assertions(5); + const entity2 = collection.buildOrCloneEntity(entity1); + expect(entity2).toBeInstanceOf(TestEntity); + expect(entity2.id).toEqual(entity1.id); + expect(entity2.name).toEqual(entity1.name); + entity1.id = crypto.randomUUID(); + entity1.name = "updated name"; + expect(entity2.id).not.toEqual(entity1.id); + expect(entity2.name).not.toEqual(entity1.name); + }); + }); + describe("EntityV2Collection:push", () => { it("should throw an exception if the data parameter is not an object.", () => { const collection = new TestEntityV2Collection([]); @@ -27,7 +61,7 @@ describe("EntityV2Collection", () => { it("should accept dto as data parameter", () => { const collection = new TestEntityV2Collection([]); - const entityDto1 = defaultTestEntityDto(); + const entityDto1 = defaultTestEntityDto(); const entityDto2 = defaultTestEntityDto(); const entityDto3 = defaultTestEntityDto(); diff --git a/test/jest.setup.js b/test/jest.setup.js index eead11dff..2c5ad5f46 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -1,8 +1,24 @@ -import "./matchers/extendExpect"; +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 3.0.0 + */ +import "./mocks/mockCrypto"; -// Disable console debug, warning and error while executing the tests. -// Keep console log as it can be useful for testing. +import "./matchers/extendExpect"; +/* + * Disable console debug, warning and error while executing the tests. + * Keep console log as it can be useful for testing. + */ global.console = { ...console, debug: jest.fn(), diff --git a/test/mocks/mockCrypto.js b/test/mocks/mockCrypto.js new file mode 100644 index 000000000..d21751b15 --- /dev/null +++ b/test/mocks/mockCrypto.js @@ -0,0 +1,22 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.7.0 + */ + +import {v4 as uuid} from "uuid"; + +if (!global.crypto) { + global.crypto = {}; +} +if (!global.crypto.randomUUID) { + global.crypto.randomUUID = uuid; +} From d28c527dd4c90c6f83963660abee16b0fb3150ad Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 14 May 2024 08:00:23 +0200 Subject: [PATCH 18/67] PB-33447 Cover CollectionValidationError with tests Signed-off-by: Cedric Alfonsi --- .../abstract/collectionValidationError.js | 37 ++++-- .../collectionValidationError.test.js | 118 ++++++++++++++++++ .../entity/abstract/entityV2Collection.js | 2 +- 3 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 src/shared/models/entity/abstract/collectionValidationError.test.js diff --git a/src/shared/models/entity/abstract/collectionValidationError.js b/src/shared/models/entity/abstract/collectionValidationError.js index 7a28f54f3..6bada4d35 100644 --- a/src/shared/models/entity/abstract/collectionValidationError.js +++ b/src/shared/models/entity/abstract/collectionValidationError.js @@ -26,36 +26,49 @@ class CollectionValidationError extends Error { } /** - * Add a given an error for a given property and rule + * Add an error relative to an item and its position. + * Note: Collection validation error is supported as long as entity are not catching and associating them to the + * property which failed. * - * @param {number} position The index of the collection the error occurred. - * @param {EntityValidationError|CollectionValidationError} validationError The entity or collection validation error. + * @param {number} position The index of the item in the collection. + * @param {EntityValidationError|CollectionValidationError} validationError The validation error. + * @throws {TypeError} if the position is not an integer. + * @throws {TypeError} if the error is EntityValidationError or a CollectionValidationError. */ - addEntityValidationError(position, validationError) { + addItemValidationError(position, validationError) { if (!Number.isInteger(position)) { - throw new TypeError('CollectionValidationError addEntityValidationError expects "position" to be an integer.'); + throw new TypeError('CollectionValidationError::addEntityValidationError expects "position" to be an integer.'); } if (!(validationError instanceof EntityValidationError) && !(validationError instanceof CollectionValidationError)) { - throw new TypeError('CollectionValidationError addEntityValidationError expects "entityValidationError" to be an instance of EntityValidationError or CollectionValidationError.'); + throw new TypeError('CollectionValidationError::addEntityValidationError expects "entityValidationError" to be an instance of EntityValidationError or CollectionValidationError.'); } this.errors[position] = validationError; } /** - * Add a collection validation error. + * Add an error relative a collection rule. + * * @param {string} rule The collection rule. - * @param {string} message The error message. + * @param {error|string} error The error. + * @throws {TypeError} if the rule is not a string. + * @throws {TypeError} if the error is not a string. */ - addCollectionValidationError(rule, message) { - this.errors[rule] = message; + addCollectionValidationError(rule, error) { + if (typeof rule !== "string") { + throw new TypeError('CollectionValidationError::addCollectionValidationError expects "rule" to be a string.'); + } + if (typeof error !== "string") { + throw new TypeError('CollectionValidationError::addCollectionValidationError expects "error" to be a string.'); + } + this.errors[rule] = error; } /** * Return the error in the details expected format. - * @return {array} + * @return {object} */ get details() { - const details = []; + const details = {}; for (const key in this.errors) { if (this.errors[key] instanceof EntityValidationError) { details[key] = this.errors[key].details; diff --git a/src/shared/models/entity/abstract/collectionValidationError.test.js b/src/shared/models/entity/abstract/collectionValidationError.test.js new file mode 100644 index 000000000..2b6e49ace --- /dev/null +++ b/src/shared/models/entity/abstract/collectionValidationError.test.js @@ -0,0 +1,118 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.8.0 + */ +import EntityValidationError from "./entityValidationError"; +import CollectionValidationError from "./collectionValidationError"; + +describe("CollectionValidationError", () => { + describe("::addEntityValidationError", () => { + it("throws exception if position argument is not valid", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + const addedError = new EntityValidationError(); + addedError.addError("property_name", "rule_name", "error-message"); + expect(() => error.addItemValidationError("not-integer", addedError)).toThrow(TypeError); + }); + + it("throws exception if error argument is not valid", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + expect(() => error.addItemValidationError(42, "invalid-error")).toThrow(TypeError); + }); + + it("accepts EntityValidationError", () => { + const error = new CollectionValidationError(); + const addedError = new EntityValidationError(); + addedError.addError("property_name", "rule_name", "error-message"); + error.addItemValidationError(42, addedError); + expect(error.errors[42]).toEqual(addedError); + }); + + it("accepts CollectionValidationError", () => { + const error = new CollectionValidationError(); + const subError = new CollectionValidationError(); + const entityError = new EntityValidationError(); + entityError.addError("property_name", "rule_name", "error-message"); + subError.addItemValidationError(42, entityError); + error.addItemValidationError(1, subError); + expect(error.errors[1]).toEqual(subError); + }); + }); + + describe("::addCollectionValidationError", () => { + it("throws exception if rule argument is not valid", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + const addedError = new EntityValidationError(); + addedError.addError("property-name", "rule-name", "error-message"); + expect(() => error.addCollectionValidationError("not-integer", addedError)).toThrow(TypeError); + }); + + it("throws exception if error argument is not valid", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + expect(() => error.addCollectionValidationError("rule-name", 42)).toThrow(TypeError); + }); + + it("accepts error as string", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + error.addCollectionValidationError("rule-name", "The error message"); + expect(error.errors["rule-name"]).toEqual("The error message"); + }); + }); + + describe("::details", () => { + it("should return the details error if no errors were added.", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + expect(error.details).toEqual({}); + }); + + it("should return the details error if items or collection errors were added.", () => { + expect.assertions(1); + const error = new CollectionValidationError(); + const entityValidationError1 = new EntityValidationError(); + entityValidationError1.addError("property_name", "rule_name", "error-message"); + error.addItemValidationError(2, entityValidationError1); + const collectionValidationError1 = new CollectionValidationError(); + collectionValidationError1.addCollectionValidationError("rule_name_3", "error-message_3"); + error.addItemValidationError(15, collectionValidationError1); + const entityValidationError2 = new EntityValidationError(); + entityValidationError2.addError("property_name_2", "rule_name_2", "error-message_2"); + error.addItemValidationError(42, entityValidationError2); + const collectionValidationError2 = new CollectionValidationError(); + collectionValidationError2.addCollectionValidationError("rule_name_4", "error-message_4"); + error.addItemValidationError(57, collectionValidationError2); + const expectedDetails = { + "2": { + "property_name": { + "rule_name": "error-message" + } + }, + "15": { + "rule_name_3": "error-message_3" + }, + "42": { + "property_name_2": { + "rule_name_2": "error-message_2" + } + }, + "57": { + "rule_name_4": "error-message_4" + } + }; + expect(error.details).toEqual(expectedDetails); + }); + }); +}); diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 119ed918f..fa900b80f 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -129,7 +129,7 @@ class EntityV2Collection extends EntityCollection { * the collection, it fails to clearly indicate which specific property of the parent entity is problematic. */ const collectionValidationError = new CollectionValidationError(); - collectionValidationError.addEntityValidationError(index, error); + collectionValidationError.addItemValidationError(index, error); throw collectionValidationError; } else { console.debug(`${this.entityClass.name}::pushMany ignore item (${index}) due to validation error ${JSON.stringify(error?.details)}`); From c8bd83dca5a375dedec24a90f9a68a02b22d3d5e Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Sat, 11 May 2024 22:35:15 +0200 Subject: [PATCH 19/67] PB-33327 - Switch ResourcesCollection to EntityV2Collection Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 +- package.json | 2 +- .../permissionCollection.test.data.js | 8 +-- .../resource/resourceEntity.test.data.js | 53 ++++++++++++++----- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f4ac735b..2cbea6637 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.7", + "version": "4.8.0-alpha.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.7", + "version": "4.8.0-alpha.8", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index d45954604..e625cd156 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.7", + "version": "4.8.0-alpha.8", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/permission/permissionCollection.test.data.js b/src/shared/models/entity/permission/permissionCollection.test.data.js index be6975a0d..96888d441 100644 --- a/src/shared/models/entity/permission/permissionCollection.test.data.js +++ b/src/shared/models/entity/permission/permissionCollection.test.data.js @@ -18,14 +18,16 @@ import {defaultPermissionDto} from "./permissionEntity.test.data"; /** * Build dtos. * @param {number} [count=10] The number of dtos. + * @param {object} data The data to override the default dto. + * @param {object} options Options to pass to the permission factory. * @returns {object} */ -export const defaultPermissionsDtos = (count = 10) => { +export const defaultPermissionsDtos = (count = 10, data = {}, options = {}) => { const dtos = []; const acoForeignKey = crypto.randomUUID(); for (let i = 0; i < count; i++) { - const groupDto = defaultPermissionDto({aco_foreign_key: acoForeignKey}); - dtos.push(groupDto); + const dto = defaultPermissionDto({aco_foreign_key: acoForeignKey, ...data}, options); + dtos.push(dto); } return dtos; }; diff --git a/src/shared/models/entity/resource/resourceEntity.test.data.js b/src/shared/models/entity/resource/resourceEntity.test.data.js index 8ac4e8d86..19c679087 100644 --- a/src/shared/models/entity/resource/resourceEntity.test.data.js +++ b/src/shared/models/entity/resource/resourceEntity.test.data.js @@ -19,31 +19,58 @@ import { TEST_RESOURCE_TYPE_PASSWORD_DESCRIPTION_TOTP, TEST_RESOURCE_TYPE_TOTP } from "../resourceType/resourceTypeEntity.test.data"; +import {defaultUserDto} from "../user/userEntity.test.data"; +import {defaultPermissionsDtos} from "../permission/permissionCollection.test.data"; -export const defaultResourceDto = (data = {}) => { +/** + * Build default resource dto. + * @param {object} data The data to override the default dto. + * @param {Object} [options] + * @param {boolean} [options.withModifier=false] Add modifier default dto. + * @param {boolean} [options.withCreator=false] Add creator default dto. + * @param {boolean|integer} [options.withPermissions=0] Add permission default dtos. + * @param {boolean|integer} [options.withFavorite=false] Add favorite default dto. + * @returns {object} + */ +export const defaultResourceDto = (data = {}, options = {}) => { const id = data?.id || uuidv4(); - - return { + const defaultData = { id: id, + resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION, name: "Passbolt", - uri: "https://passbolt.com", username: "admin@passbolt.com", - folder_parent_id: null, + uri: "https://passbolt.com", + description: "", + expired: null, + deleted: false, created: "2022-03-04T13:59:11+00:00", - created_by: uuidv4(), modified: "2022-03-04T13:59:11+00:00", + created_by: uuidv4(), modified_by: uuidv4(), - expired: null, - deleted: false, - description: "", + folder_parent_id: null, personal: false, - resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION, - permission: ownerPermissionDto({aco_foreign_key: id}), - // permissions: [], // Permission are not retrieved by the process storing the information in the local storage. favorite: null, - // secrets: [], // Secrets are not retrieved by the process storing the information in the local storage. + permission: ownerPermissionDto({aco_foreign_key: id}), ...data }; + + if (!data.permissions && options.withPermissions) { + defaultData.permissions = defaultPermissionsDtos(options.withPermissions, {aco_foreign_key: id}); + } + + if (!data.creator && options?.withCreator) { + defaultData.creator = defaultUserDto(); + } + + if (!data.modifier && options?.withModifier) { + defaultData.modifier = defaultUserDto(); + } + + if (!data.favorite && options?.withFavorite) { + defaultData.favorite = defaultFavoriteDto({foreign_key: id}); + } + + return defaultData; }; export const resourceWithUpdatePermissionDto = (data = {}) => { From 498497d54dac943cf74cdb8f77105b9aa240b224 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 13 May 2024 16:01:01 +0200 Subject: [PATCH 20/67] PB-33447 - Ensure EntityV2Collection is treating items at the abstract constructor level --- package-lock.json | 4 ++-- package.json | 2 +- .../entity/abstract/entityV2Collection.js | 18 ++++++++++++++++++ .../abstract/entityV2Collection.test.data.js | 7 +++---- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2cbea6637..3606c550b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.8", + "version": "4.8.0-alpha.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.8", + "version": "4.8.0-alpha.9", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index e625cd156..12ec7cfa6 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.8", + "version": "4.8.0-alpha.9", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index fa900b80f..1376af8ee 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -20,11 +20,29 @@ class EntityV2Collection extends EntityCollection { /** * Retrieve the entity class this collection is handling * @return {Class} + * @abstract */ get entityClass() { throw new Error("The collection class should declare the entity class that is handled."); } + /** + * @inheritDoc + * The EntityV2 collection will push the dtos into the collection. + * @throws {EntityCollectionError} If a item does not validate its entity schema. + * @throws {EntityCollectionError} If a item does not validate the collection validation build rules. + */ + constructor(dtos = [], options = {}) { + super(dtos, options); + /* + * Push the items into the collection. + * Use the the _props property where EntityCollection V1 clone the dtos into. + * Delete it after usage. + */ + this.pushMany(this._props, {...options, clone: false}); + this._props = null; + } + /** * Build or clone entity. * @param {object|Entity} data The data of the item to push diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.data.js b/src/shared/models/entity/abstract/entityV2Collection.test.data.js index d3abeba32..d3454850b 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.data.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.data.js @@ -20,10 +20,9 @@ export class TestEntityV2Collection extends EntityV2Collection { return TestEntity; } - constructor(dto, options = {}) { - super(EntitySchema.validate(TestEntityV2Collection.name, dto, TestEntityV2Collection.getSchema()), options); - this.pushMany(this._props, {...options, clone: false}); - this._props = null; + constructor(dtos = [], options = {}) { + dtos = EntitySchema.validate(TestEntityV2Collection.name, dtos, TestEntityV2Collection.getSchema()); + super(dtos, options); } /** From c37c2e8a9a39063a33fd1b000bed8f574ce22684 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 14 May 2024 12:17:20 +0200 Subject: [PATCH 21/67] PB-33454 - Ensure collection v2 schema is validated at the abstract class level Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 +-- package.json | 2 +- .../entity/abstract/entityV2Collection.js | 21 ++++++++--- .../abstract/entityV2Collection.test.data.js | 6 ---- .../abstract/entityV2Collection.test.js | 36 +++++++++++++++++++ 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3606c550b..1ba6852c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.9", + "version": "4.8.0-alpha.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.9", + "version": "4.8.0-alpha.10", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 12ec7cfa6..55d7c29b7 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.9", + "version": "4.8.0-alpha.10", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 1376af8ee..9433f9c65 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -15,6 +15,7 @@ import EntityValidationError from "./entityValidationError"; import EntityCollection from "./entityCollection"; import CollectionValidationError from "./collectionValidationError"; import EntityCollectionError from "./entityCollectionError"; +import EntitySchema from "./entitySchema"; class EntityV2Collection extends EntityCollection { /** @@ -33,16 +34,26 @@ class EntityV2Collection extends EntityCollection { * @throws {EntityCollectionError} If a item does not validate the collection validation build rules. */ constructor(dtos = [], options = {}) { + // Note: EntityCollection V1 will clone the dtos into the instance _props property. Delete it after usage. super(dtos, options); - /* - * Push the items into the collection. - * Use the the _props property where EntityCollection V1 clone the dtos into. - * Delete it after usage. - */ + this._props = EntitySchema.validate( + this.constructor.name, + this._props, + this.constructor.getSchema() + ); this.pushMany(this._props, {...options, clone: false}); this._props = null; } + /** + * Return the schema representing this collection. + * @return {object} + * @abstract + */ + static getSchema() { + return {}; + } + /** * Build or clone entity. * @param {object|Entity} data The data of the item to push diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.data.js b/src/shared/models/entity/abstract/entityV2Collection.test.data.js index d3454850b..7bc16a525 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.data.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.data.js @@ -11,7 +11,6 @@ * @link https://www.passbolt.com Passbolt(tm) * @since 4.7.0 */ -import EntitySchema from "./entitySchema"; import EntityV2Collection from "./entityV2Collection"; import {TestEntity} from "./entity.test.data"; @@ -20,11 +19,6 @@ export class TestEntityV2Collection extends EntityV2Collection { return TestEntity; } - constructor(dtos = [], options = {}) { - dtos = EntitySchema.validate(TestEntityV2Collection.name, dtos, TestEntityV2Collection.getSchema()); - super(dtos, options); - } - /** * Get the collection schema * @returns {object} diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index 8db86dbb5..abcd0bfb2 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -52,6 +52,42 @@ describe("EntityV2Collection", () => { }); }); + describe("EntityV2Collection:constructor", () => { + it("should validate the collection schema.", () => { + expect.assertions(1); + expect(() => new TestEntityV2Collection({})).toThrowEntityValidationError("items"); + }); + + it("should push the dtos given as parameter into the collection.", () => { + expect.assertions(10); + const entityDto1 = defaultTestEntityDto(); + const entityDto2 = defaultTestEntityDto(); + const entityDto3 = defaultTestEntityDto(); + const dtos = [entityDto1, entityDto2, entityDto3]; + const collection = new TestEntityV2Collection(dtos); + expect(collection.items).toHaveLength(3); + expect(collection.items[0]).toBeInstanceOf(TestEntity); + expect(collection.items[0].id).toEqual(entityDto1.id); + expect(collection.items[0].name).toEqual(entityDto1.name); + expect(collection.items[1]).toBeInstanceOf(TestEntity); + expect(collection.items[1].id).toEqual(entityDto2.id); + expect(collection.items[1].name).toEqual(entityDto2.name); + expect(collection.items[2]).toBeInstanceOf(TestEntity); + expect(collection.items[2].id).toEqual(entityDto3.id); + expect(collection.items[2].name).toEqual(entityDto3.name); + }); + + it("should delete the _props property.", () => { + expect.assertions(1); + const entityDto1 = defaultTestEntityDto(); + const entityDto2 = defaultTestEntityDto(); + const entityDto3 = defaultTestEntityDto(); + const dtos = [entityDto1, entityDto2, entityDto3]; + const collection = new TestEntityV2Collection(dtos); + expect(collection._props).toBeNull(); + }); + }); + describe("EntityV2Collection:push", () => { it("should throw an exception if the data parameter is not an object.", () => { const collection = new TestEntityV2Collection([]); From 3d3a3c034c0e527fc085fd14c26705bb361a7bc4 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 14 May 2024 14:35:27 +0200 Subject: [PATCH 22/67] PB-33454 - Ensure collection v2 getSchema function throws an error if not redeclared and called Signed-off-by: Cedric Alfonsi --- .../models/entity/abstract/entityV2Collection.js | 2 +- .../entity/abstract/entityV2Collection.test.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 9433f9c65..93587365c 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -51,7 +51,7 @@ class EntityV2Collection extends EntityCollection { * @abstract */ static getSchema() { - return {}; + throw new Error("The collection class should declare its schema."); } /** diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index abcd0bfb2..ca7eb56f5 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -16,8 +16,17 @@ import CollectionValidationError from "./collectionValidationError"; import EntityValidationError from "./entityValidationError"; import {TestEntityV2Collection} from "./entityV2Collection.test.data"; import {defaultAssociatedTestEntityDto, defaultTestEntityDto, TestEntity} from "./entity.test.data"; +import EntityV2Collection from "./entityV2Collection"; describe("EntityV2Collection", () => { + describe("EntityV2Collection:entityClass", () => { + // It is expected to throw an error but does not for an unexpected reason. + it.failing("should throw an exception if called to mimic its abstract nature", () => { + expect.assertions(1); + expect(() => EntityV2Collection.entityClass).toThrow(); + }); + }); + describe("EntityV2Collection:buildOrCloneEntity", () => { it("should throw an exception if the data parameter is not an object.", () => { const collection = new TestEntityV2Collection([]); @@ -88,6 +97,13 @@ describe("EntityV2Collection", () => { }); }); + describe("EntityV2Collection:getSchema", () => { + it("should throw an exception if called to mimic its abstract nature", () => { + expect.assertions(1); + expect(() => EntityV2Collection.getSchema()).toThrow(); + }); + }); + describe("EntityV2Collection:push", () => { it("should throw an exception if the data parameter is not an object.", () => { const collection = new TestEntityV2Collection([]); From 0e479b3eab2022518a9600cbc0ffb031d001377a Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 14 May 2024 13:12:38 +0200 Subject: [PATCH 23/67] PB-33458 - Cache collection v2 schema into class static property and reduce memory footprint Signed-off-by: Cedric Alfonsi --- .../entity/abstract/entityV2Collection.js | 41 +++++++++++++++++-- .../abstract/entityV2Collection.test.js | 32 ++++++++++----- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 93587365c..6f77f0b61 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -18,6 +18,13 @@ import EntityCollectionError from "./entityCollectionError"; import EntitySchema from "./entitySchema"; class EntityV2Collection extends EntityCollection { + /** + * The collection cached schema. + * @type {object} + * @private + */ + static _cachedSchema; + /** * Retrieve the entity class this collection is handling * @return {Class} @@ -29,24 +36,50 @@ class EntityV2Collection extends EntityCollection { /** * @inheritDoc - * The EntityV2 collection will push the dtos into the collection. + * Additionally to the EntityCollection, the EntityV2 collection will: + * - Validate the collection schema. + * - Push the dtos into the collection. + * * @throws {EntityCollectionError} If a item does not validate its entity schema. * @throws {EntityCollectionError} If a item does not validate the collection validation build rules. */ constructor(dtos = [], options = {}) { // Note: EntityCollection V1 will clone the dtos into the instance _props property. Delete it after usage. super(dtos, options); + this.validateSchema(); + this.pushMany(this._props, {...options, clone: false}); + this._props = null; + } + + /** + * Validate the collection schema. + * Note: the collection schema will be created on first call and cached into a class static property. + * @private + */ + validateSchema() { this._props = EntitySchema.validate( this.constructor.name, this._props, - this.constructor.getSchema() + this.cachedSchema ); - this.pushMany(this._props, {...options, clone: false}); - this._props = null; + } + + /** + * Get the collection cached schema + * @returns {object} + * @private + */ + get cachedSchema() { + if (!this.constructor._cachedSchema) { + this.constructor._cachedSchema = this.constructor.getSchema(); + } + + return this.constructor._cachedSchema; } /** * Return the schema representing this collection. + * Override this method to define the collection schema. * @return {object} * @abstract */ diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index ca7eb56f5..dce596a3f 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -19,7 +19,7 @@ import {defaultAssociatedTestEntityDto, defaultTestEntityDto, TestEntity} from " import EntityV2Collection from "./entityV2Collection"; describe("EntityV2Collection", () => { - describe("EntityV2Collection:entityClass", () => { + describe("::entityClass", () => { // It is expected to throw an error but does not for an unexpected reason. it.failing("should throw an exception if called to mimic its abstract nature", () => { expect.assertions(1); @@ -27,7 +27,7 @@ describe("EntityV2Collection", () => { }); }); - describe("EntityV2Collection:buildOrCloneEntity", () => { + describe("::buildOrCloneEntity", () => { it("should throw an exception if the data parameter is not an object.", () => { const collection = new TestEntityV2Collection([]); expect.assertions(1); @@ -36,7 +36,7 @@ describe("EntityV2Collection", () => { it("should create entity from dto.", () => { const collection = new TestEntityV2Collection([]); - const entityDto1 = defaultTestEntityDto(); + const entityDto1 = defaultTestEntityDto(); expect.assertions(3); const entity = collection.buildOrCloneEntity(entityDto1); @@ -47,7 +47,7 @@ describe("EntityV2Collection", () => { it("should clone entity.", () => { const collection = new TestEntityV2Collection([]); - const entity1 = new TestEntity(defaultTestEntityDto()); + const entity1 = new TestEntity(defaultTestEntityDto()); expect.assertions(5); const entity2 = collection.buildOrCloneEntity(entity1); @@ -61,7 +61,7 @@ describe("EntityV2Collection", () => { }); }); - describe("EntityV2Collection:constructor", () => { + describe("::constructor", () => { it("should validate the collection schema.", () => { expect.assertions(1); expect(() => new TestEntityV2Collection({})).toThrowEntityValidationError("items"); @@ -69,7 +69,7 @@ describe("EntityV2Collection", () => { it("should push the dtos given as parameter into the collection.", () => { expect.assertions(10); - const entityDto1 = defaultTestEntityDto(); + const entityDto1 = defaultTestEntityDto(); const entityDto2 = defaultTestEntityDto(); const entityDto3 = defaultTestEntityDto(); const dtos = [entityDto1, entityDto2, entityDto3]; @@ -88,7 +88,7 @@ describe("EntityV2Collection", () => { it("should delete the _props property.", () => { expect.assertions(1); - const entityDto1 = defaultTestEntityDto(); + const entityDto1 = defaultTestEntityDto(); const entityDto2 = defaultTestEntityDto(); const entityDto3 = defaultTestEntityDto(); const dtos = [entityDto1, entityDto2, entityDto3]; @@ -97,14 +97,26 @@ describe("EntityV2Collection", () => { }); }); - describe("EntityV2Collection:getSchema", () => { + describe("::validateSchema", () => { + it("should retrieve the schema on first validation and cache for later usage.", () => { + expect.assertions(3); + jest.spyOn(TestEntityV2Collection, "getSchema"); + expect(TestEntityV2Collection._cachedSchema).toBeUndefined(); + new TestEntityV2Collection([]); + expect(TestEntityV2Collection._cachedSchema).toEqual(TestEntityV2Collection.getSchema()); + new TestEntityV2Collection([]); + expect(TestEntityV2Collection.getSchema).toHaveBeenCalledTimes(2); + }); + }); + + describe("::getSchema", () => { it("should throw an exception if called to mimic its abstract nature", () => { expect.assertions(1); expect(() => EntityV2Collection.getSchema()).toThrow(); }); }); - describe("EntityV2Collection:push", () => { + describe("::push", () => { it("should throw an exception if the data parameter is not an object.", () => { const collection = new TestEntityV2Collection([]); expect.assertions(1); @@ -113,7 +125,7 @@ describe("EntityV2Collection", () => { it("should accept dto as data parameter", () => { const collection = new TestEntityV2Collection([]); - const entityDto1 = defaultTestEntityDto(); + const entityDto1 = defaultTestEntityDto(); const entityDto2 = defaultTestEntityDto(); const entityDto3 = defaultTestEntityDto(); From 95a797630b02a002f3c371a455a472217ca1bfec Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Tue, 14 May 2024 15:08:45 +0200 Subject: [PATCH 24/67] PB-33458 - Cached schema collections could be multiple as inherited collection will reuse the same static property to store their cached schemas Signed-off-by: Cedric Alfonsi --- .../models/entity/abstract/entityV2Collection.js | 11 ++++++----- .../models/entity/abstract/entityV2Collection.test.js | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 6f77f0b61..2bd0ea676 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -19,11 +19,12 @@ import EntitySchema from "./entitySchema"; class EntityV2Collection extends EntityCollection { /** - * The collection cached schema. + * The collection cached schemas referenced by collection class name. + * The key will represent the collection class name while the value will be the schema definition object. * @type {object} * @private */ - static _cachedSchema; + static _cachedSchema = {}; /** * Retrieve the entity class this collection is handling @@ -70,11 +71,11 @@ class EntityV2Collection extends EntityCollection { * @private */ get cachedSchema() { - if (!this.constructor._cachedSchema) { - this.constructor._cachedSchema = this.constructor.getSchema(); + if (!this.constructor._cachedSchema[this.constructor.name]) { + this.constructor._cachedSchema[this.constructor.name] = this.constructor.getSchema(); } - return this.constructor._cachedSchema; + return this.constructor._cachedSchema[this.constructor.name]; } /** diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index dce596a3f..5d45ea3ac 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -18,6 +18,10 @@ import {TestEntityV2Collection} from "./entityV2Collection.test.data"; import {defaultAssociatedTestEntityDto, defaultTestEntityDto, TestEntity} from "./entity.test.data"; import EntityV2Collection from "./entityV2Collection"; +beforeEach(() => { + TestEntityV2Collection._cachedSchema = {}; +}); + describe("EntityV2Collection", () => { describe("::entityClass", () => { // It is expected to throw an error but does not for an unexpected reason. @@ -101,9 +105,9 @@ describe("EntityV2Collection", () => { it("should retrieve the schema on first validation and cache for later usage.", () => { expect.assertions(3); jest.spyOn(TestEntityV2Collection, "getSchema"); - expect(TestEntityV2Collection._cachedSchema).toBeUndefined(); + expect(TestEntityV2Collection._cachedSchema.TestEntityV2Collection).toBeUndefined(); new TestEntityV2Collection([]); - expect(TestEntityV2Collection._cachedSchema).toEqual(TestEntityV2Collection.getSchema()); + expect(TestEntityV2Collection._cachedSchema.TestEntityV2Collection).toEqual(TestEntityV2Collection.getSchema()); new TestEntityV2Collection([]); expect(TestEntityV2Collection.getSchema).toHaveBeenCalledTimes(2); }); From f8de722662f7c592abcb629c2c6c5e5dcf000773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Tue, 14 May 2024 13:11:40 +0000 Subject: [PATCH 25/67] Feature/pb 32891 entities validating null in any of should use nullable schema property --- package-lock.json | 4 +-- package.json | 2 +- .../models/entity/abstract/entitySchema.js | 12 ++++++++- .../associations/applicationEntity.js | 25 ++++++++----------- .../passwordExpiryProSettingsEntity.js | 11 +++----- .../entity/ssoSettings/SsoSettingsEntity.js | 9 +++---- .../ssoSettings/SsoSettingsEntity.test.js | 2 +- .../PasswordExpirySettingsViewModel.test.js | 2 +- test/assert/assertEntityProperty.js | 2 +- 9 files changed, 35 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ba6852c7..1b56f8521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.10", + "version": "4.8.0-alpha.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.10", + "version": "4.8.0-alpha.11", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 55d7c29b7..5d6fb7060 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.10", + "version": "4.8.0-alpha.11", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/abstract/entitySchema.js b/src/shared/models/entity/abstract/entitySchema.js index eb98ea4c7..d83eebe71 100644 --- a/src/shared/models/entity/abstract/entitySchema.js +++ b/src/shared/models/entity/abstract/entitySchema.js @@ -119,11 +119,21 @@ class EntitySchema { continue; } + // check if propery is null + if (dto?.[propName] === null) { + // the prop is explicitly null, is it explicitly nullable? + if ((schemaProps[propName]?.nullable) === true) { + result[propName] = null; + continue; + } + // @todo: else => props is not nullable and null we could set an error and then continue but it requires all schema to migrate to explicit "nullable": true + } + // Check if property is required if (requiredProps.includes(propName)) { if (!Object.prototype.hasOwnProperty.call(dto, propName)) { validationError = EntitySchema.getOrInitEntityValidationError(name, validationError); - validationError.addError(propName, 'required', `The ${propName} is required.`, validationError); + validationError.addError(propName, 'required', `The ${propName} is required.`); continue; } } else { diff --git a/src/shared/models/entity/healthcheck/associations/applicationEntity.js b/src/shared/models/entity/healthcheck/associations/applicationEntity.js index 1da92d536..f8605e0d7 100644 --- a/src/shared/models/entity/healthcheck/associations/applicationEntity.js +++ b/src/shared/models/entity/healthcheck/associations/applicationEntity.js @@ -41,21 +41,19 @@ class ApplicationEntity extends Entity { "type": "object", "required": ["remoteVersion", "currentVersion"], "properties": { - "remoteVersion": {"anyOf": [{ - "type": "string" - }, { - "type": "null" - }]}, + "remoteVersion": { + "type": "string", + "nullable": true, + }, "currentVersion": { "type": "string" } } }, - "latestVersion": {"anyOf": [{ + "latestVersion": { "type": "boolean", - }, { - "type": "null" - }]}, + "nullable": true, + }, "schema": { "type": "boolean" }, @@ -81,11 +79,10 @@ class ApplicationEntity extends Entity { "isSelfRegistrationPluginEnabled": { "type": "boolean" }, - "selfRegistrationProvider": {"anyOf": [{ - "type": "string" - }, { - "type": "null" - }]}, + "selfRegistrationProvider": { + "type": "string", + "nullable": true, + }, "isRegistrationPublicRemovedFromPassbolt": { "type": "boolean" } diff --git a/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.js b/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.js index fcd236e09..c6a68542e 100644 --- a/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.js +++ b/src/shared/models/entity/passwordExpiryPro/passwordExpiryProSettingsEntity.js @@ -47,13 +47,10 @@ class PasswordExpiryProSettingsEntity extends Entity { "format": "uuid", }, "default_expiry_period": { - "anyOf": [{ - "type": "integer", - "gte": 1, - "lte": 999 - }, { - "type": "null" - }] + "type": "integer", + "gte": 1, + "lte": 999, + "nullable": true, }, "policy_override": { "type": "boolean", diff --git a/src/shared/models/entity/ssoSettings/SsoSettingsEntity.js b/src/shared/models/entity/ssoSettings/SsoSettingsEntity.js index 31b6e9fca..fc2dca7e3 100644 --- a/src/shared/models/entity/ssoSettings/SsoSettingsEntity.js +++ b/src/shared/models/entity/ssoSettings/SsoSettingsEntity.js @@ -61,12 +61,9 @@ class SsoSettingsEntity extends Entity { }, }, "provider": { - "anyOf": [{ - "type": "string", - "enum": SsoSettingsEntity.AVAILABLE_PROVIDERS, - }, { - "type": "null" - }], + "type": "string", + "enum": SsoSettingsEntity.AVAILABLE_PROVIDERS, + "nullable": true, }, "data": { "type": "object", diff --git a/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js b/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js index 6e37339d7..dc1ec03cb 100644 --- a/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js +++ b/src/shared/models/entity/ssoSettings/SsoSettingsEntity.test.js @@ -71,7 +71,7 @@ describe("SsoSettingsEntity", () => { const successScenarios = successValues.map(value => ({scenario: `with value "${value}}"`, value: value})); const failingScenarios = failingValues.map(value => ({scenario: `with value "${value}}"`, value: value})); - assertEntityProperty.assert(SsoSettingsEntity, "provider", successScenarios, failingScenarios, "type"); + assertEntityProperty.assert(SsoSettingsEntity, "provider", successScenarios, failingScenarios, "enum"); assertEntityProperty.nullable(SsoSettingsEntity, "provider"); assertEntityProperty.notRequired(SsoSettingsEntity, "provider"); }); diff --git a/src/shared/models/passwordExpirySettings/PasswordExpirySettingsViewModel.test.js b/src/shared/models/passwordExpirySettings/PasswordExpirySettingsViewModel.test.js index 003cb7010..01dcfde81 100644 --- a/src/shared/models/passwordExpirySettings/PasswordExpirySettingsViewModel.test.js +++ b/src/shared/models/passwordExpirySettings/PasswordExpirySettingsViewModel.test.js @@ -203,7 +203,7 @@ describe("PasswordExpirySettingsViewModel", () => { }, { dto: {default_expiry_period: -1}, - expectedErrors: {default_expiry_period: {type: "The default_expiry_period does not match any of the supported types."}}, + expectedErrors: {default_expiry_period: {type: "The default_expiry_period is not a valid integer."}}, }, ]).describe("should validate the current data set with PasswordExpiryProSettingsEntity", scenario => { it(`for: ${JSON.stringify(scenario.dto)}`, () => { diff --git a/test/assert/assertEntityProperty.js b/test/assert/assertEntityProperty.js index 8ef2750ee..7010f7470 100644 --- a/test/assert/assertEntityProperty.js +++ b/test/assert/assertEntityProperty.js @@ -190,7 +190,7 @@ export const FAIL_LOCALE_SCENARIO = [ {scenario: "Incomplete", value: "fr"}, ]; export const locale = (EntityClass, propertyName) => { - assert(EntityClass, propertyName, SUCCESS_LOCALE_SCENARIO, FAIL_LOCALE_SCENARIO, "type"); + assert(EntityClass, propertyName, SUCCESS_LOCALE_SCENARIO, FAIL_LOCALE_SCENARIO, "pattern"); }; export const enumeration = (EntityClass, propertyName, successValues, failValues = []) => { From 67f9b9b2c89a8b60ba009d5e6f5e848b25b08e6a Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Wed, 15 May 2024 08:51:30 +0200 Subject: [PATCH 26/67] PB-33459 - Ensure entity v2 schema is validated at an abstract class level Signed-off-by: Cedric Alfonsi --- src/shared/models/entity/abstract/entityV2.js | 107 ++++++++++++++++ .../entity/abstract/entityV2.test.data.js | 120 ++++++++++++++++++ .../models/entity/abstract/entityV2.test.js | 93 ++++++++++++++ .../entity/abstract/entityV2Collection.js | 1 + 4 files changed, 321 insertions(+) create mode 100644 src/shared/models/entity/abstract/entityV2.js create mode 100644 src/shared/models/entity/abstract/entityV2.test.data.js create mode 100644 src/shared/models/entity/abstract/entityV2.test.js diff --git a/src/shared/models/entity/abstract/entityV2.js b/src/shared/models/entity/abstract/entityV2.js new file mode 100644 index 000000000..a17760156 --- /dev/null +++ b/src/shared/models/entity/abstract/entityV2.js @@ -0,0 +1,107 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import EntitySchema from "./entitySchema"; +import Entity from "./entity"; + +class EntityV2 extends Entity { + /** + * The entity cached schemas referenced by entity class name. + * The key will represent the entity class name while the value will be the schema definition object. + * @type {object} + * @private + */ + static _cachedSchema = {}; + + /** + * @inheritDoc + * Additionally to the Entity, the EntityV2 will: + * - Validate the entity schema. + * - Validate the entity build rules. + * + * @throws {EntityValidationError} If the dto does not validate the entity schema. + * @throws {EntityValidationError} If the dto does not validate the entity build rules. + */ + constructor(dtos = {}, options = {}) { + // Note: Entity V1 will clone the dtos into the instance _props property. + super(dtos, options); + this.marshall(); + this.validateSchema(); + this.validateBuildRules(); + } + + /** + * Marshall the entity props. + * Caution, the marshalling happens before the validation. + * @private + */ + marshall() { + // Override this method to marshall the entity props prior to validation. + } + + /* + * ================================================== + * Validation + * ================================================== + */ + + /** + * Validate the entity schema. + * Note: the entity schema will be created on first call and cached into a class static property. + * @private + */ + validateSchema() { + this._props = EntitySchema.validate( + this.constructor.name, + this._props, + this.cachedSchema + ); + } + + /** + * Get the entity cached schema + * Note: The getter can only be accessed only from an instance context as it uses the instance scope. + * @returns {object} + * @private + */ + get cachedSchema() { + if (!this.constructor._cachedSchema[this.constructor.name]) { + this.constructor._cachedSchema[this.constructor.name] = this.constructor.getSchema(); + } + + return this.constructor._cachedSchema[this.constructor.name]; + } + + /** + * Return the schema representing this entity. + * Override this method to define the entity schema. + * @return {object} + * @abstract + */ + static getSchema() { + throw new Error("The entity class should declare its schema."); + } + + /** + * Validate the item build rules. + * It is used to validate other rules that are not covered by the schema definition, by instance to check if + * a password and its confirmation are identical. + * @param {object} [options] Options. + */ + // eslint-disable-next-line no-unused-vars + validateBuildRules(options = {}) { + // Override this method to add entity validation build rules. + } +} + +export default EntityV2; diff --git a/src/shared/models/entity/abstract/entityV2.test.data.js b/src/shared/models/entity/abstract/entityV2.test.data.js new file mode 100644 index 000000000..7572401bd --- /dev/null +++ b/src/shared/models/entity/abstract/entityV2.test.data.js @@ -0,0 +1,120 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import {v4 as uuid} from "uuid"; +import EntityV2 from "./entityV2"; +import EntityValidationError from "./entityValidationError"; + +export class TestEntityV2 extends EntityV2 { + constructor(dto, options) { + super(dto, options); + if (this._props.associated_entity) { + this._associatedEntity = new TestAssociatedEntityV2(this._props.associated_entity); + delete this._props.associated_entity; + } + } + + static getSchema() { + return { + "type": "object", + "required": [], + "properties": { + "id": { + "anyOf": [{ + "type": "string", + "format": "uuid" + }, { + "type": "null" + }], + }, + "name": { + "anyOf": [{ + "type": "string" + }, { + "type": "null" + }], + }, + "associated_entity": TestAssociatedEntityV2.getSchema() + } + }; + } + marshall() { + if (this._props?.name === "K4r3n") { + this._props.name = "Karen"; + } + } + + // eslint-disable-next-line no-unused-vars + validateBuildRules(options = {}) { + if (this.name === "Karen") { + const error = new EntityValidationError(); + error.addError("name", "karen", "I want to see the manager"); + throw error; + } + } + + get id() { + return this._props.id || null; + } + set id(id) { + this._props.id = id; + } + + get name() { + return this._props.name; + } + + set name(name) { + this._props.name = name; + } + + get associatedEntity() { + return this._associatedEntity; + } +} + +export class TestAssociatedEntityV2 extends TestEntityV2 { + static getSchema() { + return { + "type": "object", + "required": [], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + } + }; + } + + get id() { + return this._props.id || null; + } +} + +export const minimalTestEntityV2Dto = data => ({ + name: "test name", + ...data +}); + +export const defaultTestEntityV2Dto = data => ({ + id: uuid(), + name: "test name", + associated_entity: defaultAssociatedTestEntityV2Dto(), + ...data +}); + +export const defaultAssociatedTestEntityV2Dto = data => ({ + id: uuid(), + ...data +}); diff --git a/src/shared/models/entity/abstract/entityV2.test.js b/src/shared/models/entity/abstract/entityV2.test.js new file mode 100644 index 000000000..ace5426fd --- /dev/null +++ b/src/shared/models/entity/abstract/entityV2.test.js @@ -0,0 +1,93 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ + +import { + defaultTestEntityV2Dto, + minimalTestEntityV2Dto, + TestAssociatedEntityV2, + TestEntityV2 +} from "./entityV2.test.data"; + +beforeEach(() => { + TestEntityV2._cachedSchema = {}; + TestAssociatedEntityV2._cachedSchema = {}; +}); + +describe("EntityV2", () => { + describe("::constructor", () => { + it("should accept minimal dto.", () => { + expect.assertions(2); + const dto = minimalTestEntityV2Dto(); + const entity = new TestEntityV2(dto); + expect(entity.id).toBeNull(); + expect(entity.name).toEqual(dto.name); + }); + + it("should accept complete dto including asscoiated entities.", () => { + expect.assertions(4); + const dto = defaultTestEntityV2Dto(); + const entity = new TestEntityV2(dto); + expect(entity.id).toEqual(dto.id); + expect(entity.name).toEqual(dto.name); + expect(entity.associatedEntity).toBeInstanceOf(TestAssociatedEntityV2); + expect(entity.associatedEntity.id).toEqual(dto.associated_entity.id); + }); + + it("should throw if the dto does not validate against the entity schema.", () => { + expect.assertions(1); + const dto = minimalTestEntityV2Dto({name: 42}); + expect(() => new TestEntityV2(dto)).toThrowEntityValidationError("name", "type"); + }); + + it("should throw if a dto associated entity dto does not validate against the associated entity schema.", () => { + expect.assertions(2); + const associatedEntityDto = {id: 42}; + const dto = minimalTestEntityV2Dto({associated_entity: associatedEntityDto}); + // Ideally the thrown error should indicate the path of the error. + expect(() => new TestEntityV2(dto)) + .not.toThrowEntityValidationError("associated_entity", "id.type"); + expect(() => new TestEntityV2(dto)) + .toThrowEntityValidationError("id", "type"); + }); + + it("should throw if the dto does not validate against the entity build rules.", () => { + expect.assertions(1); + const dto = minimalTestEntityV2Dto({name: "Karen"}); + expect(() => new TestEntityV2(dto)).toThrowEntityValidationError("name", "karen"); + }); + + it("should marshall props prior to validate.", () => { + expect.assertions(1); + const dto = minimalTestEntityV2Dto({name: "K4r3n"}); + expect(() => new TestEntityV2(dto)).toThrowEntityValidationError("name", "karen"); + }); + }); + + describe("::validateSchema", () => { + it("should retrieve the schema on first validation and cache for later usage.", () => { + expect.assertions(6); + jest.spyOn(TestEntityV2, "getSchema"); + jest.spyOn(TestAssociatedEntityV2, "getSchema"); + expect(TestEntityV2._cachedSchema.TestEntityV2).toBeUndefined(); + expect(TestAssociatedEntityV2._cachedSchema.TestAssociatedEntityV2).toBeUndefined(); + new TestEntityV2(defaultTestEntityV2Dto()); + expect(TestEntityV2._cachedSchema.TestEntityV2).toEqual(TestEntityV2.getSchema()); + expect(TestAssociatedEntityV2._cachedSchema.TestAssociatedEntityV2).toEqual(TestAssociatedEntityV2.getSchema()); + new TestEntityV2(defaultTestEntityV2Dto()); + new TestEntityV2(defaultTestEntityV2Dto()); + expect(TestEntityV2.getSchema).toHaveBeenCalledTimes(2); + expect(TestAssociatedEntityV2.getSchema).toHaveBeenCalledTimes(4); + }); + }); +}); diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 2bd0ea676..2f5f2e750 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -67,6 +67,7 @@ class EntityV2Collection extends EntityCollection { /** * Get the collection cached schema + * Note: The getter can only be accessed only from an instance context as it uses the instance scope. * @returns {object} * @private */ From 82b32d0050192c373c8f242f8df2c82697ccf72d Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Wed, 15 May 2024 11:15:11 +0200 Subject: [PATCH 27/67] PB-33459 - Migrate RoleEntity to Entity v2 Signed-off-by: Cedric Alfonsi --- package-lock.json | 4 ++-- package.json | 2 +- src/shared/models/entity/role/roleEntity.js | 16 ++-------------- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b56f8521..5cedf2400 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.11", + "version": "4.8.0-alpha.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.11", + "version": "4.8.0-alpha.12", "license": "AGPL-3.0", "dependencies": { "@testing-library/dom": "^8.11.3", diff --git a/package.json b/package.json index 5d6fb7060..28e281db7 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.11", + "version": "4.8.0-alpha.12", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.", diff --git a/src/shared/models/entity/role/roleEntity.js b/src/shared/models/entity/role/roleEntity.js index b39477b8e..9e46eeef2 100644 --- a/src/shared/models/entity/role/roleEntity.js +++ b/src/shared/models/entity/role/roleEntity.js @@ -11,8 +11,7 @@ * @link https://www.passbolt.com Passbolt(tm) * @since 2.13.0 */ -import Entity from "../abstract/entity"; -import EntitySchema from "../abstract/entitySchema"; +import EntityV2 from "../abstract/entityV2"; const ENTITY_NAME = 'Role'; const ROLE_ADMIN = 'admin'; @@ -21,18 +20,7 @@ const ROLE_GUEST = 'guest'; const ROLE_ROOT = 'root'; const ROLE_NAME_MAX_LENGTH = 255; -class RoleEntity extends Entity { - /** - * @inheritDoc - */ - constructor(roleDto, options = {}) { - super(EntitySchema.validate( - RoleEntity.ENTITY_NAME, - roleDto, - RoleEntity.getSchema() - ), options); - } - +class RoleEntity extends EntityV2 { /** * Get role entity schema * @returns {Object} schema From fca1e476b463cc29fd38538358580c04f24faa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Tue, 21 May 2024 13:57:36 +0200 Subject: [PATCH 28/67] PB-33459: apply review feedback --- src/shared/models/entity/abstract/entityV2.js | 2 +- src/shared/models/entity/abstract/entityV2.test.data.js | 1 + src/shared/models/entity/abstract/entityV2.test.js | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/shared/models/entity/abstract/entityV2.js b/src/shared/models/entity/abstract/entityV2.js index a17760156..76db22874 100644 --- a/src/shared/models/entity/abstract/entityV2.js +++ b/src/shared/models/entity/abstract/entityV2.js @@ -43,7 +43,7 @@ class EntityV2 extends Entity { /** * Marshall the entity props. * Caution, the marshalling happens before the validation. - * @private + * @protected */ marshall() { // Override this method to marshall the entity props prior to validation. diff --git a/src/shared/models/entity/abstract/entityV2.test.data.js b/src/shared/models/entity/abstract/entityV2.test.data.js index 7572401bd..69cf286c2 100644 --- a/src/shared/models/entity/abstract/entityV2.test.data.js +++ b/src/shared/models/entity/abstract/entityV2.test.data.js @@ -52,6 +52,7 @@ export class TestEntityV2 extends EntityV2 { if (this._props?.name === "K4r3n") { this._props.name = "Karen"; } + super.marshall(); } // eslint-disable-next-line no-unused-vars diff --git a/src/shared/models/entity/abstract/entityV2.test.js b/src/shared/models/entity/abstract/entityV2.test.js index ace5426fd..d58164345 100644 --- a/src/shared/models/entity/abstract/entityV2.test.js +++ b/src/shared/models/entity/abstract/entityV2.test.js @@ -12,6 +12,7 @@ * @since 4.9.0 */ +import EntityV2 from "./entityV2"; import { defaultTestEntityV2Dto, minimalTestEntityV2Dto, @@ -72,6 +73,13 @@ describe("EntityV2", () => { const dto = minimalTestEntityV2Dto({name: "K4r3n"}); expect(() => new TestEntityV2(dto)).toThrowEntityValidationError("name", "karen"); }); + + it("should throw an error if getSchema is not overriden.", () => { + expect.assertions(1); + const expectedError = new Error("The entity class should declare its schema."); + const dto = minimalTestEntityV2Dto({name: "K4r3n"}); + expect(() => new EntityV2(dto)).toThrow(expectedError); + }); }); describe("::validateSchema", () => { From 6790acb2a965ca95d6a16022d3a73b0c82e2605c Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 21 May 2024 13:59:06 +0200 Subject: [PATCH 29/67] PB-32425 Add ConfirmCreateEdit coverage --- .../ConfirmCreateEdit/ConfirmCreateEdit.js | 4 +- .../ConfirmCreateEdit.test.data.js | 32 ++++ .../ConfirmCreateEdit.test.js | 143 ++++++++++++++++++ .../ConfirmCreateEdit.test.page.js | 124 +++++++++++++++ 4 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.data.js create mode 100644 src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.js create mode 100644 src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.page.js diff --git a/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.js b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.js index 6000247fe..727edcb58 100644 --- a/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.js +++ b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.js @@ -17,7 +17,6 @@ import DialogWrapper from "../../Common/Dialog/DialogWrapper/DialogWrapper"; import FormSubmitButton from "../../Common/Inputs/FormSubmitButton/FormSubmitButton"; import FormCancelButton from "../../Common/Inputs/FormSubmitButton/FormCancelButton"; import {Trans, withTranslation} from "react-i18next"; -import {withDialog} from "../../../contexts/DialogContext"; /** * The component display operation variations. @@ -69,6 +68,7 @@ class ConfirmCreateEdit extends Component { event.preventDefault(); if (!this.state.processing) { + this.setState({processing: true}); this.props.onConfirm(); this.props.onClose(); } @@ -175,4 +175,4 @@ ConfirmCreateEdit.propTypes = { t: PropTypes.func, // The translation function }; -export default withDialog(withTranslation('common')(ConfirmCreateEdit)); +export default withTranslation('common')(ConfirmCreateEdit); diff --git a/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.data.js b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.data.js new file mode 100644 index 000000000..adf611c00 --- /dev/null +++ b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.data.js @@ -0,0 +1,32 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ + +import {ConfirmEditCreateOperationVariations, ConfirmEditCreateRuleVariations} from "./ConfirmCreateEdit"; + +/** + * Default props + * @returns {object} + */ +export function defaultProps(data = {}) { + const defaultData = { + operation: ConfirmEditCreateOperationVariations.CREATE, + rule: ConfirmEditCreateRuleVariations.IN_DICTIONARY, + resourceName: "resourceName", + onClose: jest.fn(), + onConfirm: jest.fn(), + onReject: jest.fn(), + }; + + return Object.assign(defaultData, data); +} diff --git a/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.js b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.js new file mode 100644 index 000000000..a1c5a3343 --- /dev/null +++ b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.js @@ -0,0 +1,143 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ + +/** + * Unit tests on ConfirmCreateEdit in regard of specifications + */ +import {waitFor} from "@testing-library/react"; +import ConfirmCreateEditPage from "./ConfirmCreateEdit.test.page"; +import {defaultProps} from "./ConfirmCreateEdit.test.data"; +import {ConfirmEditCreateOperationVariations, ConfirmEditCreateRuleVariations} from "./ConfirmCreateEdit"; + +beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); +}); + +describe("ConfirmCreateEdit", () => { + describe('As LU I can confirm or cancel a created resource', () => { + it('As LU I can confirm a resource', async() => { + expect.assertions(6); + const props = defaultProps({operation: ConfirmEditCreateOperationVariations.CREATE, rule: ConfirmEditCreateRuleVariations.IN_DICTIONARY}); // The props to pass + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + + expect(page.exists).toBeTruthy(); + expect(page.title).toBe("Confirm resource creation"); + expect(page.rule).toBe("The password is part of an exposed data breach."); + expect(page.operation).toBe(`Are you sure you want to create the resource ${props.resourceName}?`); + + await page.save(); + + expect(props.onConfirm).toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('As LU I can cancel a resource', async() => { + expect.assertions(6); + const props = defaultProps({operation: ConfirmEditCreateOperationVariations.CREATE, rule: ConfirmEditCreateRuleVariations.MINIMUM_ENTROPY}); // The props to pass + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + + expect(page.exists).toBeTruthy(); + expect(page.title).toBe("Confirm resource creation"); + expect(page.rule).toBe("The password is very weak and might be part of an exposed data breach."); + expect(page.operation).toBe(`Are you sure you want to create the resource ${props.resourceName}?`); + + await page.cancel(); + + expect(props.onReject).toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('As LU I should see buttons disabled when the save is not finished', async() => { + expect.assertions(4); + const props = defaultProps(); + // Mock the request function to make it the expected result + let updateResolve; + const requestMockImpl = jest.fn(() => new Promise(resolve => { + updateResolve = resolve; + })); + jest.spyOn(props, "onConfirm").mockImplementation(requestMockImpl); + + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + + page.saveWithoutWaiting(); + + // API calls are made on submit, wait they are resolved. + await waitFor(() => { + expect(page.dialogClose.getAttribute("disabled")).not.toBeNull(); + expect(page.saveButton.hasAttribute('disabled')).toBeTruthy(); + expect(page.cancelButton.className).toBe("link cancel"); + expect(page.cancelButton.hasAttribute('disabled')).toBeTruthy(); + updateResolve(); + }); + }); + + it('As LU I can cancel by closing the dialog', async() => { + expect.assertions(1); + const props = defaultProps(); + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + await page.closeDialog(); + expect(props.onClose).toBeCalled(); + }); + + it('As LU I can cancel with the keyboard (escape)', async() => { + expect.assertions(1); + const props = defaultProps(); + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + await page.escapeKey(); + expect(props.onClose).toBeCalled(); + }); + }); + + describe('As LU I can confirm or cancel an edited resource', () => { + it('As LU I can confirm a resource', async() => { + expect.assertions(6); + const props = defaultProps({operation: ConfirmEditCreateOperationVariations.EDIT, rule: ConfirmEditCreateRuleVariations.IN_DICTIONARY}); // The props to pass + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + + expect(page.exists).toBeTruthy(); + expect(page.title).toBe("Confirm resource edition"); + expect(page.rule).toBe("The password is part of an exposed data breach."); + expect(page.operation).toBe(`Are you sure you want to edit the resource ${props.resourceName}?`); + + await page.save(); + + expect(props.onConfirm).toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('As LU I can cancel a resource', async() => { + expect.assertions(6); + const props = defaultProps({operation: ConfirmEditCreateOperationVariations.EDIT, rule: ConfirmEditCreateRuleVariations.MINIMUM_ENTROPY}); // The props to pass + const page = new ConfirmCreateEditPage(props); + await waitFor(() => {}); + + expect(page.exists).toBeTruthy(); + expect(page.title).toBe("Confirm resource edition"); + expect(page.rule).toBe("The password is very weak and might be part of an exposed data breach."); + expect(page.operation).toBe(`Are you sure you want to edit the resource ${props.resourceName}?`); + + await page.cancel(); + + expect(props.onReject).toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.page.js b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.page.js new file mode 100644 index 000000000..5d26c88ac --- /dev/null +++ b/src/react-extension/components/Resource/ConfirmCreateEdit/ConfirmCreateEdit.test.page.js @@ -0,0 +1,124 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import {fireEvent, render, waitFor} from "@testing-library/react"; +import React from "react"; +import ConfirmCreateEdit from "./ConfirmCreateEdit"; +import MockTranslationProvider from "../../../test/mock/components/Internationalisation/MockTranslationProvider"; +/** + * The ConfirmCreateEditPage component represented as a page + */ +export default class ConfirmCreateEditPage { + /** + * Default constructor + * @param props Props to attach + */ + constructor(props) { + this._page = render( + + + + ); + } + + /** + * Return the page object of the title header + */ + get title() { + return this._page.container.querySelector(".dialog-header-title").textContent; + } + + /** + * Returns the dialog element + */ + get dialog() { + return this._page.container.querySelector('.confirm-create-edit-password-dialog'); + } + + /** + * Returns the dialog close element + */ + get dialogClose() { + return this._page.container.querySelector('.dialog-close'); + } + + /** + * Returns the rule element + */ + get rule() { + return this._page.container.querySelectorAll('.form-content p')[0].textContent; + } + + /** + * Returns the operation element + */ + get operation() { + return this._page.container.querySelectorAll('.confirm-create-edit-password-dialog .form-content p')[1].textContent; + } + + /** + * Returns the save button element + */ + get saveButton() { + return this._page.container.querySelector('.submit-wrapper button[type=\"submit\"]'); + } + + /** + * Returns the cancel button element + */ + get cancelButton() { + return this._page.container.querySelector('.submit-wrapper .cancel'); + } + + /** + * Returns true if the page object exists in the container + */ + get exists() { + return this.dialog !== null; + } + + /** Click on the element */ + async click(element) { + const leftClick = {button: 0}; + fireEvent.click(element, leftClick); + await waitFor(() => {}); + } + + /** Click without wait for on the element */ + escapeKey() { + // Escape key down event + const escapeKeyDown = {keyCode: 27}; + fireEvent.keyDown(this.dialog, escapeKeyDown); + } + + /** Click on save button */ + async save() { + await this.click(this.saveButton); + } + + /** Click on save button */ + saveWithoutWaiting() { + const leftClick = {button: 0}; + fireEvent.click(this.saveButton, leftClick); + } + + /** Click on cancel button */ + async cancel() { + await this.click(this.cancelButton); + } + + /** Click on close dialog button */ + async closeDialog() { + await this.click(this.dialogClose); + } +} From b5237dc3a60257ee63085a4279d34b5bde20999b Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Wed, 15 May 2024 14:58:41 +0200 Subject: [PATCH 30/67] PB-33441 Performance search in preparation of search on folder metadata --- .../contexts/ResourceWorkspaceContext.js | 20 ++++++++++++-- .../ResourceWorkspaceContext.test.data.js | 26 +++++++++++++++++++ .../contexts/ResourceWorkspaceContext.test.js | 9 ++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.js b/src/react-extension/contexts/ResourceWorkspaceContext.js index 8f10687bd..f291d1fb0 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.js @@ -168,6 +168,7 @@ export class ResourceWorkspaceContextProvider extends React.Component { initializeProperties() { this.resources = null; // A cache of the last known list of resources from the App context this.folders = null; // A cache of the last known list of folders from the App context + this.foldersMapById = {}; // A cache of the last known list of folders map by ID from the App context } /** @@ -243,6 +244,10 @@ export class ResourceWorkspaceContextProvider extends React.Component { const hasFoldersChanged = this.props.context.folders !== this.folders; if (hasFoldersChanged) { this.folders = this.props.context.folders; + this.foldersMapById = this.folders.reduce((result, folder) => { + result[folder.id] = folder; + return result; + }, {}); await this.refreshSearchFilter(); await this.updateDetails(); } @@ -676,15 +681,26 @@ export class ResourceWorkspaceContextProvider extends React.Component { const text = filter.payload; const words = (text && text.split(/\s+/)) || ['']; const canUseTags = this.props.context.siteSettings.canIUse("tags"); + const foldersMatchCache = {}; // Test match of some escaped test words against the name / username / uri / description /tags resource properties const escapeWord = word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const wordToRegex = word => new RegExp(escapeWord(word), 'i'); const matchWord = (word, value) => wordToRegex(word).test(value); - const matchTagProperty = (word, resource) => resource.tags.some(tag => matchWord(word, tag.slug)); + const getFolderById = id => this.foldersMapById[id]; + const matchFolderNameProperty = (word, folder) => matchWord(word, folder?.name); + const matchFolder = (word, folder) => matchFolderNameProperty(word, folder) || (folder.folder_parent_id !== null && matchFolderCache(word, folder.folder_parent_id)); + const matchFolderCache = (word, id) => { + const key = word + id; + if (typeof foldersMatchCache[key] === "undefined") { + foldersMatchCache[key] = matchFolder(word, getFolderById(id)); + } + return foldersMatchCache[key]; + }; + const matchTagProperty = (word, resource) => resource.tags?.some(tag => matchWord(word, tag.slug)); const matchStringProperty = (word, resource) => ['name', 'username', 'uri', 'description'].some(key => matchWord(word, resource[key])); - const matchResource = (word, resource) => matchStringProperty(word, resource) || (canUseTags && matchTagProperty(word, resource)); + const matchResource = (word, resource) => matchStringProperty(word, resource) || (canUseTags && matchTagProperty(word, resource)) || (resource.folder_parent_id !== null && matchFolderCache(word, resource.folder_parent_id)); const matchText = resource => words.every(word => matchResource(word, resource)); const filteredResources = this.resources.filter(matchText); diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js index 9babcfba1..6eacd9fbf 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js @@ -647,6 +647,32 @@ const resources = [ "tags": [], "folder_parent_id": "907c3f61-f416-5834-86d2-e721501ee493", "personal": true + }, + { + "id": "97d75fef-d7ad-5a0d-8df0-0a0ffb7c44c9", + "name": "Inside Bank Password", + "username": "vector", + "uri": "https:\/\/private.org\/", + "description": "Bank password.", + "deleted": false, + "created": "2020-08-27T08:35:19+00:00", + "modified": "2020-08-27T08:35:19+00:00", + "created_by": "1ebc0060-9274-5451-aa12-ad0f31bc29dd", + "modified_by": "1ebc0060-9274-5451-aa12-ad0f31bc29dd", + "favorite": null, + "permission": { + "id": "9ea3efed-b358-541c-8379-7b7162a8f562", + "aco": "Resource", + "aco_foreign_key": "76d75fef-d7ed-5a0d-8df0-0a0ffb7c44c8", + "aro": "User", + "aro_foreign_key": "f848277c-5398-58f8-a82a-72397af2d450", + "type": 7, + "created": "2020-08-27T08:35:19+00:00", + "modified": "2020-08-27T08:35:19+00:00" + }, + "tags": [], + "folder_parent_id": "6592f71b-8874-5e91-bf6d-829b8ad188f5", + "personal": true } ]; diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.js index 5f51a1aff..99c71c384 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.js @@ -111,7 +111,7 @@ describe("Resource Workspace Context", () => { it.todo("AS LU I should have the most recent created resource when the filter is RECENTLY-MODIFIED"); it("AS LU I should have resources shared with me when the filter is SHARED-WITH-ME", async() => { - const expectedResourcesCount = 16; + const expectedResourcesCount = 17; await page.goToShareWithMe(); expect(page.filteredResources).toHaveLength(expectedResourcesCount); }); @@ -135,6 +135,13 @@ describe("Resource Workspace Context", () => { expect(page.filteredResources[0].name).toBe("Docker"); }); + it("AS LU I should have resources matching a text when the filter is TEXT in folder name", async() => { + const expectedResourcesCount = 1; + await page.goToText("Accounting"); + expect(page.filteredResources).toHaveLength(expectedResourcesCount); + expect(page.filteredResources[0].name).toBe("Inside Bank Password"); + }); + it("AS LU I should have resources belonged to a group when the filter is GROUP", async() => { const mockGroupResources = context.resources.slice(0, 3); const expectedResourcesCount = 3; From 631b3c53f10dd184a9e1f9950912b513842ac0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Fri, 24 May 2024 09:37:41 +0000 Subject: [PATCH 31/67] Feature/pb 33439 as a user i want to hide entropy on passphrases passwords --- src/less/abstractions/colors.less | 2 + .../abstractions/colors_custom.sample.less | 2 + src/less/abstractions/colors_migdar.less | 2 + .../abstractions/colors_solarized-dark.less | 2 + .../abstractions/colors_solarized-light.less | 2 + .../form/password-complexity-with-goal.less | 14 +- .../components/form/password-complexity.less | 42 ++++- .../GenerateOrganizationKey.test.js | 2 +- .../CreateGpgKey/CreateGpgKey.test.js | 10 +- .../CreateResource/CreateResource.test.js | 6 +- .../EditResource/EditResource.test.js | 6 +- .../EnterNewPassphrase.test.js | 12 +- .../GeneratePasswordPage.test.js | 2 +- .../PasswordComplexity/PasswordComplexity.js | 176 +++++++++++------- .../PasswordComplexityWithGoal.js | 61 ++++-- 15 files changed, 232 insertions(+), 109 deletions(-) diff --git a/src/less/abstractions/colors.less b/src/less/abstractions/colors.less index 37c72502e..4dc768ad2 100644 --- a/src/less/abstractions/colors.less +++ b/src/less/abstractions/colors.less @@ -14,6 +14,7 @@ @honeydew: #EDF7EB; @islamic-green: #0EAA00; @islamic-green-2: #009900; +@islamic-green-3: #7ac473; @deep-champagne: #FFDBA6; @blond: #FEF0BF; @@ -383,6 +384,7 @@ /* tables */ @table-background: @white; +@table-success-background: @islamic-green-3; @table-warning-background: @blond; @table-error-background: @light-salmon-pink; @table-border: @chinese-white; diff --git a/src/less/abstractions/colors_custom.sample.less b/src/less/abstractions/colors_custom.sample.less index 59671da01..d202948ad 100644 --- a/src/less/abstractions/colors_custom.sample.less +++ b/src/less/abstractions/colors_custom.sample.less @@ -15,6 +15,7 @@ @success-1: hsl(68,100%,30%); @success-2: hsl(45,100%,35%); +@success-3: hsl(68,50%,50%); @warning-1: hsl(18,80%,54%); @warning-2: hsl(18,80%,64%); @@ -368,6 +369,7 @@ /* tables */ @table-background: @main-color; +@table-success-background: @success-3; @table-warning-background: @warning-2; @table-error-background: @alert-3; @table-border: @main-color-gradient-21; diff --git a/src/less/abstractions/colors_migdar.less b/src/less/abstractions/colors_migdar.less index 68efebd67..1a293136a 100644 --- a/src/less/abstractions/colors_migdar.less +++ b/src/less/abstractions/colors_migdar.less @@ -12,6 +12,7 @@ @honeydew: #EDF7EB; @islamic-green: #0EAA00; @islamic-green-2: #009900; +@islamic-green-3: #7ac473; @gamboge-orange: #A46100; @@ -388,6 +389,7 @@ /* tables */ @table-background: @raisin-black; +@table-success-background: @islamic-green-3; @table-warning-background: @deep-champagne-2; @table-error-background: @light-salmon-pink; @table-border: @raisin-black; diff --git a/src/less/abstractions/colors_solarized-dark.less b/src/less/abstractions/colors_solarized-dark.less index 2cce5b116..17d823db2 100644 --- a/src/less/abstractions/colors_solarized-dark.less +++ b/src/less/abstractions/colors_solarized-dark.less @@ -12,6 +12,7 @@ @success-1: hsl(68,100%,30%); @success-2: hsl(68,100%,35%); +@success-3: hsl(68,50%,70%); @warning-1: hsl(18,80%,54%); @warning-2: hsl(18,80%,64%); @@ -395,6 +396,7 @@ /* tables */ @table-background: @main-color-gradient-8; +@table-success-background: @success-3; @table-warning-background: @warning-3; @table-error-background: @alert-3; @table-border: @main-color-gradient-8; diff --git a/src/less/abstractions/colors_solarized-light.less b/src/less/abstractions/colors_solarized-light.less index 3a151d5ec..f4868a983 100644 --- a/src/less/abstractions/colors_solarized-light.less +++ b/src/less/abstractions/colors_solarized-light.less @@ -15,6 +15,7 @@ @success-1: hsl(68,100%,30%); @success-2: hsl(45,100%,35%); +@success-3: hsl(68,50%,50%); @warning-1: hsl(18,80%,54%); @warning-2: hsl(18,80%,64%); @@ -396,6 +397,7 @@ /* tables */ @table-background: @main-color; +@table-success-background: @success-3; @table-warning-background: @warning-2; @table-error-background: @alert-3; @table-border: @main-color-gradient-21; diff --git a/src/less/components/form/password-complexity-with-goal.less b/src/less/components/form/password-complexity-with-goal.less index 2f40c7335..1c6c377c0 100644 --- a/src/less/components/form/password-complexity-with-goal.less +++ b/src/less/components/form/password-complexity-with-goal.less @@ -4,7 +4,7 @@ .complexity-text { display: flex; font-size: 1rem; - line-height: 1.4rem; + line-height: 1.362rem; color: @complexity-text-color; } @@ -14,6 +14,10 @@ position: relative; height: 1rem; width: 100%; + + .tooltip { + top: 0; + } } .progress-bar { @@ -38,7 +42,7 @@ &.required { background: @hint-error-color; } - + &.reached { background: @hint-success-color; } @@ -48,10 +52,14 @@ width: 0; background: @table-warning-background; z-index: 5; - + &.required { background: @table-error-background; } + + &.reached { + background: @table-success-background; + } } } diff --git a/src/less/components/form/password-complexity.less b/src/less/components/form/password-complexity.less index 209d56f36..5985d0284 100644 --- a/src/less/components/form/password-complexity.less +++ b/src/less/components/form/password-complexity.less @@ -3,20 +3,39 @@ .complexity-text { display: flex; font-size: 1rem; - line-height: 1.4rem; + line-height: 1.362rem; color: @complexity-text-color; + + .svg-icon { + display: inline; + vertical-align: middle; + svg { + width: 1rem; + height: 1rem; + stroke-width: .05rem; + --icon-color: @complexity-text-color; + --icon-stroke-width: .1rem; + } + } } .progress { width: 100%; box-sizing: border-box; display: block; + position: relative; + + .tooltip { + //putting a higher height than the entropy bar to ease the hovering for users + width: 100%; + height: .9rem; + position: absolute; + left: 0; + top: -.3rem; + z-index: 100; + } } .progress-bar { - // Variable to use in react component - --complexity-bar-background-default: @complexity-bar-default-background; - - background: @complexity-bar-gradient-background; border-radius: .1rem; width: 100%; height: .2rem; @@ -26,5 +45,18 @@ &.error { background: @complexity-bar-default-background; } + + &.background { + background: @complexity-bar-default-background; + } + + &.foreground { + transition: width .3s ease-in-out; + width: 0; + position: absolute; + left: 0; + top: 0; + margin: 0; + } } } diff --git a/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js b/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js index 2c4f71447..dff4836ed 100644 --- a/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js +++ b/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js @@ -312,7 +312,7 @@ describe('As AD I can generate an ORK', () => { await page.type("", page.passphraseField); - expect(page.passphraseStrength.textContent).toBe("Quality"); + expect(page.passphraseStrength.textContent).toBe("Quality Entropy: 0.0 / 80.0 bits"); expect(page.passphraseFieldError).toBeNull(); }); }); diff --git a/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js b/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js index ce26c0292..44995c420 100644 --- a/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js +++ b/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js @@ -262,12 +262,12 @@ describe("CreateGpgKey", () => { const page = new CreateGpgKeyPage(props); await page.fill("passphrase from breached data"); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); await page.generateKey(); - expect(page.passphraseComplexity.textContent).toContain("entropy: 0.0"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); await page.fill("passphrase from breached tada"); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); }); it(`As AN on the setup workflow I can submit the from after having changed the passphrase used from a data breach`, async() => { @@ -280,9 +280,9 @@ describe("CreateGpgKey", () => { const page = new CreateGpgKeyPage(props); await page.fill("passphrase from breached data"); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); await page.generateKey(); - expect(page.passphraseComplexity.textContent).toContain("entropy: 0.0"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); await page.fill(notBreachedPassphrase); await page.generateKey(); diff --git a/src/react-extension/components/Resource/CreateResource/CreateResource.test.js b/src/react-extension/components/Resource/CreateResource/CreateResource.test.js index b10fd80c6..592e82762 100644 --- a/src/react-extension/components/Resource/CreateResource/CreateResource.test.js +++ b/src/react-extension/components/Resource/CreateResource/CreateResource.test.js @@ -86,7 +86,7 @@ describe("See the Create Resource", () => { expect(passwordInputStyle.color).toBe(""); // Complexity label exists but is not yet defined. - expect(page.passwordCreate.complexityText.textContent).toBe("Quality"); + expect(page.passwordCreate.complexityText.textContent).toBe("Quality Entropy: 0.0 bits"); // Password view button exists. expect(page.passwordCreate.passwordViewButton).not.toBeNull(); @@ -110,7 +110,7 @@ describe("See the Create Resource", () => { expect.assertions(2); page.passwordCreate.focusInput(page.passwordCreate.password); await page.passwordCreate.click(page.passwordCreate.passwordGenerateButton); - expect(page.passwordCreate.complexityText.textContent).not.toBe("Quality"); + expect(page.passwordCreate.complexityText.textContent).not.toBe("Quality Entropy: 0.0 bits"); expect(page.passwordCreate.progressBar.classList.contains("error")).toBe(false); }); @@ -690,7 +690,7 @@ describe("See the Create Resource", () => { page.passwordCreate.fillInput(page.passwordCreate.password, ''); await page.passwordCreate.keyUpInput(page.passwordCreate.password); - expect(page.passwordCreate.complexityText.textContent).toBe("Quality"); + expect(page.passwordCreate.complexityText.textContent).toBe("Quality Entropy: 0.0 bits"); expect(page.passwordCreate.pwnedWarningMessage).toBeNull(); }); }); diff --git a/src/react-extension/components/Resource/EditResource/EditResource.test.js b/src/react-extension/components/Resource/EditResource/EditResource.test.js index cf9672166..366da0e89 100644 --- a/src/react-extension/components/Resource/EditResource/EditResource.test.js +++ b/src/react-extension/components/Resource/EditResource/EditResource.test.js @@ -88,7 +88,7 @@ describe("See the Edit Resource", () => { expect(passwordInputStyle.color).toBe(""); // Complexity label exists but is not yet defined. - expect(page.passwordEdit.complexityText.textContent).toBe("Quality"); + expect(page.passwordEdit.complexityText.textContent).toBe("Quality Entropy: 0.0 bits"); // Password view button exists. expect(page.passwordEdit.passwordViewButton).not.toBeNull(); @@ -240,7 +240,7 @@ describe("See the Edit Resource", () => { await page.passwordEdit.fillInputPassword(resourceMeta.password); await page.passwordEdit.blurInput(page.passwordEdit.password); - expect(page.passwordEdit.complexityText.textContent).not.toBe("Quality"); + expect(page.passwordEdit.complexityText.textContent).not.toBe("Quality Entropy: 0.0 bits"); expect(page.passwordEdit.progressBar.classList.contains("error")).toBe(false); page.passwordEdit.fillInput(page.passwordEdit.description, resourceMeta.description); @@ -652,7 +652,7 @@ describe("See the Edit Resource", () => { await page.passwordEdit.fillInputPassword(""); await waitFor(() => {}); expect(page.passwordEdit.pwnedWarningMessage).toBeNull(); - expect(page.passwordEdit.complexityText.textContent).toBe("Quality"); + expect(page.passwordEdit.complexityText.textContent).toBe("Quality Entropy: 0.0 bits"); }); }); }); diff --git a/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js b/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js index 276331996..ca99b0a57 100644 --- a/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js +++ b/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js @@ -204,11 +204,11 @@ describe("As LU I should see the user confirm passphrase page", () => { expect.assertions(3); jest.spyOn(PownedService.prototype, "evaluateSecret").mockImplementation(() => passphraseIsInDictionnary()); await page.insertPassphrase('passphrase from breached data'); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); await page.update(() => true); - expect(page.passphraseComplexity.textContent).toContain("entropy: 0.0"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); await page.insertPassphrase("passphrase from breached tada"); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); }); it(`As LU I can update my passphrase after having changed the passphrase used from a data breach`, async() => { @@ -224,10 +224,10 @@ describe("As LU I should see the user confirm passphrase page", () => { })); await page.insertPassphrase('passphrase from breached data'); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); await page.update(() => true); - expect(page.passphraseComplexity.textContent).toContain("entropy: 0.0"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); await page.insertPassphrase(notBreachedPassphrase); await page.update(() => true); @@ -249,7 +249,7 @@ describe("As LU I should see the user confirm passphrase page", () => { const spyOnPownedService = jest.spyOn(PownedService.prototype, "evaluateSecret").mockImplementation(() => passphraseIsInDictionnary()); await page.insertPassphrase('passphrase from breached data'); - expect(page.passphraseComplexity.textContent).toContain("entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); await page.update(() => true); await generateResolve(); expect(spyOnPownedService).not.toHaveBeenCalled(); diff --git a/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js b/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js index 8818fe2ad..80d1dce19 100644 --- a/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js +++ b/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js @@ -49,7 +49,7 @@ describe("Generate password", () => { jest.runAllTimers(); expect(page.title).toBe('Generate password'); - expect(page.complexityText).toBe('Fair (entropy: 111.1 bits)'); + expect(page.complexityText).toBe('Fair Entropy: 111.1 bits'); await page.applyGeneratePassword(); expect(props.history.goBack).toHaveBeenCalledTimes(1); }); diff --git a/src/shared/components/PasswordComplexity/PasswordComplexity.js b/src/shared/components/PasswordComplexity/PasswordComplexity.js index b2bce2486..b2256cd21 100644 --- a/src/shared/components/PasswordComplexity/PasswordComplexity.js +++ b/src/shared/components/PasswordComplexity/PasswordComplexity.js @@ -15,74 +15,77 @@ import React, {Component} from "react"; import PropTypes from "prop-types"; import {Trans, withTranslation} from "react-i18next"; import {SecretGeneratorComplexity} from "../../lib/SecretGenerator/SecretGeneratorComplexity"; +import Tooltip from "../../../react-extension/components/Common/Tooltip/Tooltip"; +import Icon from "../Icons/Icon"; + +const COLOR_GRADIENT = { + COLOR_1: hexToRgb("#A40000"), + COLOR_2: hexToRgb("#FFA724"), + COLOR_3: hexToRgb("#0EAA00"), +}; + +/** + * Hex color to rgb color object + * @param hex + * @returns {null|{red: number, green: number, blue: number}} + */ +function hexToRgb(hex) { + const hexRegex = new RegExp("^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$", "i"); + const result = hexRegex.exec(hex.trim()); + if (result) { + const red = parseInt(result[1], 16); + const green = parseInt(result[2], 16); + const blue = parseInt(result[3], 16); + return {red, green, blue}; + } + return null; +} /** * This component represents a password complexity with the strength, an entropy and a bar */ class PasswordComplexity extends Component { /** - * Get the rgb color at a specific position in percentage - * @param {number} fadeFraction The fade fraction - * @param {string} color1 The first color in hexadecimal - * @param {string} color2 The second color in hexadecimal - * @param {string} color3 The third color in hexadecimal - * @returns {string} the color in rgb(0,0,0) + * Get the entropy value formatted for display. + * @returns {number} */ - colorGradient(fadeFraction, color1, color2, color3) { - let rgbColor1, rgbColor2; - let fade = fadeFraction / 100 * 2; - - // Find which interval to use and adjust the fade percentage - if (fade >= 1) { - fade -= 1; - rgbColor1 = this.hexToRgb(color2); - rgbColor2 = this.hexToRgb(color3); - } else { - rgbColor1 = this.hexToRgb(color1); - rgbColor2 = this.hexToRgb(color2); - } - - const red = Math.floor(rgbColor1.red + (rgbColor2.red - rgbColor1.red) * fade); - const green = Math.floor(rgbColor1.green + (rgbColor2.green - rgbColor1.green) * fade); - const blue = Math.floor(rgbColor1.blue + (rgbColor2.blue - rgbColor1.blue) * fade); - - return `rgb(${red},${green},${blue})`; + get entropy() { + const entropy = this.props.entropy || 0.0; + return entropy.toFixed(1); } /** - * Hex color to rgb color object - * @param hex - * @returns {null|{red: number, green: number, blue: number}} + * Get the translated tooltip message. + * @returns {JSX} */ - hexToRgb(hex) { - const hexRegex = new RegExp("^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$", "i"); - const result = hexRegex.exec(hex.trim()); - if (result) { - const red = parseInt(result[1], 16); - const green = parseInt(result[2], 16); - const blue = parseInt(result[3], 16); - return {red, green, blue}; - } - return null; + get tooltipMessage() { + return (<> + Entropy: {this.entropy} bits + ); } /** - * Get the complexity bar style. - * @return {Object} + * Get the password strength label to display based on the actual entropy or error state. + * @returns {JSX}; */ - get complexityBarStyle() { - // Power curve with an asymptote at 100%. It will never reach 100% but will get infinitely closer. - const fade = (100 - (99 / (1 + Math.pow(this.props.entropy / 90, 10)))); - return {background: `linear-gradient(to right, ${this.colorGradient(fade, "#A40000", "#FFA724", "#0EAA00")} ${fade}%, var(--complexity-bar-background-default) ${fade}%`}; - } + get passwordStrengthLabel() { + const shouldDisplayEntropyLabel = this.hasEntropy() || this.hasError(); + if (!shouldDisplayEntropyLabel) { + return (Quality); + } - /** - * Get the entropy value to display. - * @returns {Number} - */ - get entropy() { - const entropy = this.props.entropy || 0.0; - return entropy.toFixed(1); + /* + * The parser can't find the translation for passwordStrength.label + * To fix that we can use it in comment + * this.translate("n/a") + * this.translate("Very weak") + * this.translate("Weak") + * this.translate("Fair") + * this.translate("Strong") + * this.translate("Very strong") + */ + const strength = SecretGeneratorComplexity.strength(this.props.entropy); + return (<>{strength.label}); } /** @@ -102,34 +105,65 @@ class PasswordComplexity extends Component { return this.props.error; } + /** + * Get the dynamic part style of the entropy progression bar. + * @returns {object} + */ + getProgresseBarStyle() { + const relativePositionForEntropy = this.getRelativeEntropyPosition(); + return {width: `${relativePositionForEntropy}%`, backgroundColor: this.colorGradient(relativePositionForEntropy)}; + } + + /** + * Get the rgb color at a specific position in percentage + * @param {number} fadeFraction The fade fraction + * @returns {string} the color in rgb(0,0,0) + */ + colorGradient(fadeFraction) { + let rgbColor1, rgbColor2; + let fade = fadeFraction / 100 * 2; + + // Find which interval to use and adjust the fade percentage + if (fade >= 1) { + fade -= 1; + rgbColor1 = COLOR_GRADIENT.COLOR_2; + rgbColor2 = COLOR_GRADIENT.COLOR_3; + } else { + rgbColor1 = COLOR_GRADIENT.COLOR_1; + rgbColor2 = COLOR_GRADIENT.COLOR_2; + } + + const red = Math.floor(rgbColor1.red + (rgbColor2.red - rgbColor1.red) * fade); + const green = Math.floor(rgbColor1.green + (rgbColor2.green - rgbColor1.green) * fade); + const blue = Math.floor(rgbColor1.blue + (rgbColor2.blue - rgbColor1.blue) * fade); + + return `rgb(${red},${green},${blue})`; + } + + /** + * Return a percentage value matching the position of the given entropy compared to the full value possible. + * @returns {number} + */ + getRelativeEntropyPosition() { + // Power curve with an asymptote at 100%. It will never reach 100% but will get infinitely closer. + return (100 - (99 / (1 + Math.pow(this.props.entropy / 90, 10)))); + } + /** * Render the component - * @return {JSX} + * @returns {JSX} */ render() { - /* - * The parser can't find the translation for passwordStrength.label - * To fix that we can use it in comment - * this.translate("n/a") - * this.translate("Very weak") - * this.translate("Weak") - * this.translate("Fair") - * this.translate("Strong") - * this.translate("Very strong") - */ - const strength = SecretGeneratorComplexity.strength(this.props.entropy); return (
- {(this.hasEntropy() || this.hasError()) && - <>{strength.label} (entropy: {this.entropy} bits) - } - {!this.hasEntropy() && !this.hasError() && - Quality - } + + {this.passwordStrengthLabel} + - + +
); diff --git a/src/shared/components/PasswordComplexityWithGoal/PasswordComplexityWithGoal.js b/src/shared/components/PasswordComplexityWithGoal/PasswordComplexityWithGoal.js index 5e28a8e22..a9d3a7822 100644 --- a/src/shared/components/PasswordComplexityWithGoal/PasswordComplexityWithGoal.js +++ b/src/shared/components/PasswordComplexityWithGoal/PasswordComplexityWithGoal.js @@ -16,6 +16,7 @@ import PropTypes from "prop-types"; import {Trans, withTranslation} from "react-i18next"; import {SecretGeneratorComplexity} from "../../lib/SecretGenerator/SecretGeneratorComplexity"; import Tooltip from "../../../react-extension/components/Common/Tooltip/Tooltip"; +import Icon from "../Icons/Icon"; /** * This component represents a password complexity with the strength and a goal, an entropy and a bar @@ -78,12 +79,55 @@ class PasswordComplexityWithGoal extends React.PureComponent { : "recommended"; } - get tooltipMessage() { + /** + * Get the translated message for the target tooltip + * @returns {string} + */ + get targetTooltipMessage() { return this.props.isMinimumEntropyRequired ? this.props.t("Minimal requirement") : this.props.t("Minimal recommendation"); } + /** + * Get the message for the current entropy tooltip + * @returns {JSX} + */ + get currentEntropyTooltipMessage() { + return (<> + Entropy: {this.formatEntropy(this.props.entropy)} / {this.formatEntropy(this.props.targetEntropy)} bits + ); + } + + /** + * Get the password strength label to display based on the actual entropy or error state. + * @returns {JSX}; + */ + get passwordStrengthLabel() { + const shouldDisplayEntropyLabel = this.hasEntropy() || this.hasError(); + if (!shouldDisplayEntropyLabel) { + return (Quality); + } + + /* + * The parser can't find the translation for passwordStrength.label + * To fix that we can use it in comment + * this.translate("n/a") + * this.translate("Very weak") + * this.translate("Weak") + * this.translate("Fair") + * this.translate("Strong") + * this.translate("Very strong") + */ + const strength = SecretGeneratorComplexity.strength(this.props.entropy); + return (<>{strength.label}); + } + + /** + * Get the dynamic part style of the entropy progression bar. + * @param {number} entropy + * @returns {object} + */ getProgresseBarStyle(entropy) { const relativePositionForEntropy = PasswordComplexityWithGoal.getRelativeEntropyPosition(entropy); return {width: `${relativePositionForEntropy}%`}; @@ -110,24 +154,19 @@ class PasswordComplexityWithGoal extends React.PureComponent { * @return {JSX} */ render() { - const shouldDisplayEntropyLabel = this.hasEntropy() || this.hasError(); - const strength = SecretGeneratorComplexity.strength(this.props.entropy); return (
- {shouldDisplayEntropyLabel && - <>{strength.label} (entropy: {this.formatEntropy(this.props.entropy)} / {this.formatEntropy(this.props.targetEntropy)} bits) - } - {!shouldDisplayEntropyLabel && - Quality - } + + {this.passwordStrengthLabel} + - + - +
From 1fcd4ba92b82b938a3860c98844a99439a8b516b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Mon, 27 May 2024 17:21:04 +0200 Subject: [PATCH 32/67] PB-33638: fix hiding entropy behing tooltip in the quickaccess --- src/less/themes/common/ext_quickaccess.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/less/themes/common/ext_quickaccess.less b/src/less/themes/common/ext_quickaccess.less index 078752174..9ed9469ee 100644 --- a/src/less/themes/common/ext_quickaccess.less +++ b/src/less/themes/common/ext_quickaccess.less @@ -19,4 +19,5 @@ @import "../../components/tabs.less"; @import "../../components/form/password.less"; @import "../../components/form/password-complexity.less"; -@import "../../components/sso-buttons.less"; \ No newline at end of file +@import "../../components/sso-buttons.less"; +@import "../../components/tooltips.less"; From 288424298be1586472f8e5a5a871cdb3e06524e4 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 4 Jun 2024 10:24:42 +0200 Subject: [PATCH 33/67] PB-33687 As a user navigating to a website with shadow-dom I can still autofill my credentials --- src/react-web-integration/lib/Dom/DomUtils.js | 25 +++++++++++++++++++ .../lib/InForm/InFormCallToActionField.js | 15 ++++++++++- .../lib/InForm/InformManager.test.js | 24 ++++++++++++++++++ .../lib/InForm/InformManager.test.page.js | 14 ++++++++--- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/react-web-integration/lib/Dom/DomUtils.js b/src/react-web-integration/lib/Dom/DomUtils.js index 2ec2c8f48..a10131306 100644 --- a/src/react-web-integration/lib/Dom/DomUtils.js +++ b/src/react-web-integration/lib/Dom/DomUtils.js @@ -38,6 +38,31 @@ class DomUtils { return iframeContentDocument; } + /** + * Returns accessible shadow dom documents in the page + * @return {Array} iframe document + */ + static getShadowDomDocuments() { + const filterByShadowRoot = element => element.shadowRoot ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP; + const treeWalker = document.createTreeWalker( + document.activeElement, + NodeFilter.SHOW_ELEMENT, + filterByShadowRoot + ); + const shadowDomDocuments = []; + // Check directly the next node to not have the main document + let currentNode = treeWalker?.nextNode(); + while (currentNode) { + const shadowDom = browser.dom?.openOrClosedShadowRoot(currentNode) || currentNode.shadowRoot; + if (shadowDom) { + shadowDomDocuments.push(shadowDom); + } + currentNode = treeWalker?.nextNode(); + } + return shadowDomDocuments; + } + /** * Check the requested document, top document and an iframe form is initiated from same domain. diff --git a/src/react-web-integration/lib/InForm/InFormCallToActionField.js b/src/react-web-integration/lib/InForm/InFormCallToActionField.js index e0d991767..e10f8ed1a 100644 --- a/src/react-web-integration/lib/InForm/InFormCallToActionField.js +++ b/src/react-web-integration/lib/InForm/InFormCallToActionField.js @@ -27,7 +27,8 @@ class InFormCallToActionField { static findAll(selector) { const domFields = Array.from(document.querySelectorAll(selector)); const iframesFields = InFormCallToActionField.findAllInIframes(selector); - return domFields.concat(iframesFields); + const shadowDomFields = InFormCallToActionField.findAllInShadowDom(selector); + return domFields.concat(iframesFields).concat(shadowDomFields); } /** @@ -42,6 +43,18 @@ class InFormCallToActionField { .flat(); } + /** + * Retrieve all the shadow dom elements which can be an in-form username or password fields + * @return {*} + */ + static findAllInShadowDom(selector) { + const shadowDomDocuments = DomUtils.getShadowDomDocuments(); + const queryMapper = shadowDom => Array.from(shadowDom.querySelectorAll(selector)); + return shadowDomDocuments + .map(queryMapper) + .flat(); + } + /** * Default constructor * @param field The DOM element diff --git a/src/react-web-integration/lib/InForm/InformManager.test.js b/src/react-web-integration/lib/InForm/InformManager.test.js index daa1768c2..5e8675a13 100644 --- a/src/react-web-integration/lib/InForm/InformManager.test.js +++ b/src/react-web-integration/lib/InForm/InformManager.test.js @@ -958,6 +958,30 @@ describe("InformManager", () => { expect(informManager.iframesLength).toBe(0); }); + it("As LU I should see the inform call to action on form with name attribute username in shadow dom", async() => { + const div = document.createElement("div"); + div.id = "shadow-root"; + document.body.appendChild(div); + const shadowRoot = div.attachShadow({mode: "open"}); + // Set up document shadowRoot + // eslint-disable-next-line no-unsanitized/property + shadowRoot.innerHTML = domElementLoginWithNameAttributeUsername; // The Dom + const informManager = new InformManagerPage(); + expect(informManager.iframesLength).toBe(0); + await informManager.focusOnUsername(); + expect(informManager.iframesLength).toBe(1); + await informManager.mouseOverOnPassword(); + expect(informManager.iframesLength).toBe(2); + await informManager.blurOnUsername(); + expect(informManager.iframesLength).toBe(1); + await informManager.blurOnPassword(); + expect(informManager.iframesLength).toBe(0); + await informManager.focusOnPassword(); + expect(informManager.iframesLength).toBe(1); + await informManager.mouseOverOnUsername(); + expect(informManager.iframesLength).toBe(2); + }); + it("As LU I should see the inform call to action in iframe", async() => { jest.spyOn(DomUtils, "isRequestInitiatedFromSameOrigin").mockImplementation(() => true); // Set up document body diff --git a/src/react-web-integration/lib/InForm/InformManager.test.page.js b/src/react-web-integration/lib/InForm/InformManager.test.page.js index 2c92174d1..300524e01 100644 --- a/src/react-web-integration/lib/InForm/InformManager.test.page.js +++ b/src/react-web-integration/lib/InForm/InformManager.test.page.js @@ -39,7 +39,8 @@ export default class InformManagerPage { * Returns the username element */ get username() { - return document.querySelector(InFormFieldSelector.USERNAME_FIELD_SELECTOR); + const username = InFormManager.callToActionFields.find(field => field.fieldType === "username"); + return username?.field; } /** @@ -54,14 +55,17 @@ export default class InformManagerPage { * Returns the usernames element */ get usernames() { - return document.querySelectorAll(InFormFieldSelector.USERNAME_FIELD_SELECTOR); + const isUsername = informCallToActionField => informCallToActionField.fieldType === "username"; + const getField = informCallToActionField => informCallToActionField.field; + return InFormManager.callToActionFields.filter(isUsername).map(getField); } /** * Returns the password element */ get password() { - return document.querySelector(InFormFieldSelector.PASSWORD_FIELD_SELECTOR); + const password = InFormManager.callToActionFields.find(field => field.fieldType === "password"); + return password?.field; } /** @@ -75,7 +79,9 @@ export default class InformManagerPage { * Returns the passwords element */ get passwords() { - return document.querySelectorAll(InFormFieldSelector.PASSWORD_FIELD_SELECTOR); + const isPassword = informCallToActionField => informCallToActionField.fieldType === "password"; + const getField = informCallToActionField => informCallToActionField.field; + return InFormManager.callToActionFields.filter(isPassword).map(getField); } /** From d6fe316449a7ac5816f992f8ab5bbdd137a515e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 5 Jun 2024 12:25:52 +0000 Subject: [PATCH 34/67] PB-33608: update entityV2 and collections to allow validation bypass --- src/shared/models/entity/abstract/entityV2.js | 9 +++++++-- .../models/entity/abstract/entityV2.test.data.js | 2 +- .../models/entity/abstract/entityV2.test.js | 16 ++++++++++++++++ .../models/entity/abstract/entityV2Collection.js | 7 ++++++- .../entity/abstract/entityV2Collection.test.js | 11 +++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/shared/models/entity/abstract/entityV2.js b/src/shared/models/entity/abstract/entityV2.js index 76db22874..42b530e80 100644 --- a/src/shared/models/entity/abstract/entityV2.js +++ b/src/shared/models/entity/abstract/entityV2.js @@ -25,6 +25,8 @@ class EntityV2 extends Entity { /** * @inheritDoc + * @param {boolean} [options.validate=true] validate the given props against the entity schema and the build rules. + * * Additionally to the Entity, the EntityV2 will: * - Validate the entity schema. * - Validate the entity build rules. @@ -36,8 +38,11 @@ class EntityV2 extends Entity { // Note: Entity V1 will clone the dtos into the instance _props property. super(dtos, options); this.marshall(); - this.validateSchema(); - this.validateBuildRules(); + const validate = options?.validate ?? true; + if (validate) { + this.validateSchema(); + this.validateBuildRules(options?.validateBuildRules); + } } /** diff --git a/src/shared/models/entity/abstract/entityV2.test.data.js b/src/shared/models/entity/abstract/entityV2.test.data.js index 69cf286c2..298c3a9df 100644 --- a/src/shared/models/entity/abstract/entityV2.test.data.js +++ b/src/shared/models/entity/abstract/entityV2.test.data.js @@ -19,7 +19,7 @@ export class TestEntityV2 extends EntityV2 { constructor(dto, options) { super(dto, options); if (this._props.associated_entity) { - this._associatedEntity = new TestAssociatedEntityV2(this._props.associated_entity); + this._associatedEntity = new TestAssociatedEntityV2(this._props.associated_entity, options); delete this._props.associated_entity; } } diff --git a/src/shared/models/entity/abstract/entityV2.test.js b/src/shared/models/entity/abstract/entityV2.test.js index d58164345..25427501a 100644 --- a/src/shared/models/entity/abstract/entityV2.test.js +++ b/src/shared/models/entity/abstract/entityV2.test.js @@ -97,5 +97,21 @@ describe("EntityV2", () => { expect(TestEntityV2.getSchema).toHaveBeenCalledTimes(2); expect(TestAssociatedEntityV2.getSchema).toHaveBeenCalledTimes(4); }); + + it("should not validate the schema if `validate: false` is passed as an option.", () => { + expect.assertions(4); + + jest.spyOn(TestEntityV2.prototype, "validateSchema"); + jest.spyOn(TestAssociatedEntityV2.prototype, "validateSchema"); + jest.spyOn(TestEntityV2.prototype, "validateBuildRules"); + jest.spyOn(TestAssociatedEntityV2.prototype, "validateBuildRules"); + + new TestEntityV2(defaultTestEntityV2Dto(), {validate: false}); + + expect(TestEntityV2.prototype.validateSchema).not.toHaveBeenCalled(); + expect(TestAssociatedEntityV2.prototype.validateSchema).not.toHaveBeenCalled(); + expect(TestEntityV2.prototype.validateBuildRules).not.toHaveBeenCalled(); + expect(TestAssociatedEntityV2.prototype.validateBuildRules).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/shared/models/entity/abstract/entityV2Collection.js b/src/shared/models/entity/abstract/entityV2Collection.js index 2f5f2e750..13f615656 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.js +++ b/src/shared/models/entity/abstract/entityV2Collection.js @@ -37,6 +37,8 @@ class EntityV2Collection extends EntityCollection { /** * @inheritDoc + * @param {boolean} [options.validate=true] validate the given props against the entity schema and the build rules. + * * Additionally to the EntityCollection, the EntityV2 collection will: * - Validate the collection schema. * - Push the dtos into the collection. @@ -47,7 +49,10 @@ class EntityV2Collection extends EntityCollection { constructor(dtos = [], options = {}) { // Note: EntityCollection V1 will clone the dtos into the instance _props property. Delete it after usage. super(dtos, options); - this.validateSchema(); + const validate = options?.validate ?? true; + if (validate) { + this.validateSchema(); + } this.pushMany(this._props, {...options, clone: false}); this._props = null; } diff --git a/src/shared/models/entity/abstract/entityV2Collection.test.js b/src/shared/models/entity/abstract/entityV2Collection.test.js index 5d45ea3ac..75e56d656 100644 --- a/src/shared/models/entity/abstract/entityV2Collection.test.js +++ b/src/shared/models/entity/abstract/entityV2Collection.test.js @@ -17,6 +17,7 @@ import EntityValidationError from "./entityValidationError"; import {TestEntityV2Collection} from "./entityV2Collection.test.data"; import {defaultAssociatedTestEntityDto, defaultTestEntityDto, TestEntity} from "./entity.test.data"; import EntityV2Collection from "./entityV2Collection"; +import {defaultTestEntityV2Dto} from "./entityV2.test.data"; beforeEach(() => { TestEntityV2Collection._cachedSchema = {}; @@ -111,6 +112,16 @@ describe("EntityV2Collection", () => { new TestEntityV2Collection([]); expect(TestEntityV2Collection.getSchema).toHaveBeenCalledTimes(2); }); + + it("should not validate the schema if `validate: false` is passed as an option.", () => { + expect.assertions(1); + + jest.spyOn(TestEntityV2Collection.prototype, "validateSchema"); + + new TestEntityV2Collection([defaultTestEntityV2Dto()], {validate: false}); + + expect(TestEntityV2Collection.prototype.validateSchema).not.toHaveBeenCalled(); + }); }); describe("::getSchema", () => { From 7128bae2b08ed080f120aef67c8e12943bb5107f Mon Sep 17 00:00:00 2001 From: Antony Bartolomucci Date: Wed, 12 Jun 2024 10:55:30 +0200 Subject: [PATCH 35/67] PB-33730 - Link admin page with troubleshooting documentation --- .../DisplayHealthcheckAdministration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js b/src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js index 9fa7b4f98..52f820211 100644 --- a/src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js +++ b/src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js @@ -1329,7 +1329,7 @@ class DisplayHealthcheckAdministration extends Component {

Something wrong?

Hang in there! Depending your installation, you might need to check the documentation in order to run the healthcheck from the CLI

- + Read the documentation From 9e8f9f1ad7c2283ebebaa9b1e2a4f0929d2d33ce Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Wed, 12 Jun 2024 14:19:38 +0200 Subject: [PATCH 36/67] PB-33743 Fix padding icon on account recovery sidebar in the user workspace --- .../DisplayUserDetailsAccountRecovery.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/react-extension/components/UserDetails/DisplayUserDetailsAccountRecovery/DisplayUserDetailsAccountRecovery.js b/src/react-extension/components/UserDetails/DisplayUserDetailsAccountRecovery/DisplayUserDetailsAccountRecovery.js index e2835f6fc..b161ffec6 100644 --- a/src/react-extension/components/UserDetails/DisplayUserDetailsAccountRecovery/DisplayUserDetailsAccountRecovery.js +++ b/src/react-extension/components/UserDetails/DisplayUserDetailsAccountRecovery/DisplayUserDetailsAccountRecovery.js @@ -182,11 +182,13 @@ class DisplayUserDetailsAccountRecovery extends React.Component {

From e78d3b1ed96a6bdf06f48fdfbba2b5eb9cae670a Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Wed, 12 Jun 2024 16:24:44 +0200 Subject: [PATCH 37/67] PB-33751 Fix avatar in activity section --- src/less/components/usercard.less | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/less/components/usercard.less b/src/less/components/usercard.less index 47e7e04b3..415fa5855 100644 --- a/src/less/components/usercard.less +++ b/src/less/components/usercard.less @@ -74,8 +74,14 @@ .avatar { float: left; width: @avatar-size; + height: @avatar-size; margin-left: -100%; + svg { + width: @avatar-size; + height: @avatar-size; + } + img { width: @avatar-size; height: @avatar-size; @@ -91,6 +97,13 @@ margin-left: 0; float: left; width: (@avatar-size / 2); + height: (@avatar-size / 2); + align-items: center; + + svg { + width: (@avatar-size / 2); + height: (@avatar-size / 2); + } img { width: (@avatar-size / 2); From 98147b5c19b61047ae331e90af2d1a7dc817f292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 12 Jun 2024 14:43:43 +0200 Subject: [PATCH 38/67] PB-33750 - Fix passphrase entropy computation --- ...playPasswordPoliciesAdministration.test.js | 4 +- .../GenerateOrganizationKey.test.js | 2 +- .../CreateGpgKey/CreateGpgKey.test.js | 6 +- .../EnterNewPassphrase.test.js | 8 +-- .../GeneratePasswordPage.test.js | 2 +- src/shared/lib/Secret/SecretComplexity.js | 56 ++----------------- .../SecretGeneratorComplexity.js | 11 ++-- .../SecretGeneratorComplexity.test.js | 10 ++-- 8 files changed, 27 insertions(+), 72 deletions(-) diff --git a/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js b/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js index 29bbf83b0..7f06a1de1 100644 --- a/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js +++ b/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js @@ -161,8 +161,8 @@ describe("DisplayPasswordPoliciesAdministration", () => { it("As a logged in administrator I should see the expected entropy of the passphrase configurator change based on the current configuration", async() => { expect.assertions(2); - const passphraseEntropyWith9Words = "116.5 bits"; - const passphraseEntropyWith20Words = "259.0 bits"; + const passphraseEntropyWith9Words = "130.6 bits"; + const passphraseEntropyWith20Words = "290.2 bits"; await page.togglePassphrasePanel(); diff --git a/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js b/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js index dff4836ed..afb376836 100644 --- a/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js +++ b/src/react-extension/components/Administration/SelectAccountRecoveryOrganizationKey/GenerateOrganizationKey.test.js @@ -191,7 +191,7 @@ describe('As AD I can generate an ORK', () => { await page.type("test", page.nameField); await page.type("test@passbolt.com", page.emailField); - await page.type("almost fair passw", page.passphraseField); + await page.type("almost fair", page.passphraseField); await page.clickOnGenerateButton(() => { if (page.passphraseFieldError === null) { diff --git a/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js b/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js index 44995c420..e7b04d588 100644 --- a/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js +++ b/src/react-extension/components/Authentication/CreateGpgKey/CreateGpgKey.test.js @@ -262,12 +262,12 @@ describe("CreateGpgKey", () => { const page = new CreateGpgKeyPage(props); await page.fill("passphrase from breached data"); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); await page.generateKey(); expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); await page.fill("passphrase from breached tada"); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); }); it(`As AN on the setup workflow I can submit the from after having changed the passphrase used from a data breach`, async() => { @@ -280,7 +280,7 @@ describe("CreateGpgKey", () => { const page = new CreateGpgKeyPage(props); await page.fill("passphrase from breached data"); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); await page.generateKey(); expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); diff --git a/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js b/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js index ca99b0a57..4aa260ee2 100644 --- a/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js +++ b/src/react-extension/components/UserSetting/ChangeUserPassphrase/EnterNewPassphrase.test.js @@ -204,11 +204,11 @@ describe("As LU I should see the user confirm passphrase page", () => { expect.assertions(3); jest.spyOn(PownedService.prototype, "evaluateSecret").mockImplementation(() => passphraseIsInDictionnary()); await page.insertPassphrase('passphrase from breached data'); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); await page.update(() => true); expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); await page.insertPassphrase("passphrase from breached tada"); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); }); it(`As LU I can update my passphrase after having changed the passphrase used from a data breach`, async() => { @@ -224,7 +224,7 @@ describe("As LU I should see the user confirm passphrase page", () => { })); await page.insertPassphrase('passphrase from breached data'); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); await page.update(() => true); expect(page.passphraseComplexity.textContent).toContain("Entropy: 0.0"); @@ -249,7 +249,7 @@ describe("As LU I should see the user confirm passphrase page", () => { const spyOnPownedService = jest.spyOn(PownedService.prototype, "evaluateSecret").mockImplementation(() => passphraseIsInDictionnary()); await page.insertPassphrase('passphrase from breached data'); - expect(page.passphraseComplexity.textContent).toContain("Entropy: 136.3"); + expect(page.passphraseComplexity.textContent).toContain("Entropy: 137.9"); await page.update(() => true); await generateResolve(); expect(spyOnPownedService).not.toHaveBeenCalled(); diff --git a/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js b/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js index 80d1dce19..61ef953c6 100644 --- a/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js +++ b/src/react-quickaccess/components/GeneratePasswordPage/GeneratePasswordPage.test.js @@ -49,7 +49,7 @@ describe("Generate password", () => { jest.runAllTimers(); expect(page.title).toBe('Generate password'); - expect(page.complexityText).toBe('Fair Entropy: 111.1 bits'); + expect(page.complexityText).toBe('Fair Entropy: 111.4 bits'); await page.applyGeneratePassword(); expect(props.history.goBack).toHaveBeenCalledTimes(1); }); diff --git a/src/shared/lib/Secret/SecretComplexity.js b/src/shared/lib/Secret/SecretComplexity.js index 8dadca4d2..ae5f9a0f2 100644 --- a/src/shared/lib/Secret/SecretComplexity.js +++ b/src/shared/lib/Secret/SecretComplexity.js @@ -11,6 +11,7 @@ * @link https://www.passbolt.com Passbolt(tm) * @since 2.14.0 */ +import {SecretGeneratorComplexity} from "../SecretGenerator/SecretGeneratorComplexity"; import PwnedPasswords from "./PwnedPasswords"; const STRENGTH = [ @@ -85,33 +86,6 @@ export default class SecretComplexity { return Math.floor(random * (max - min + 1)) + min; } - /** - * Calculate the entropy regarding the given primitives. - * @param length {int} The number of characters - * @param maskSize {int} The number of possibility for each character - * @return {int} - */ - static calculEntropy(length, maskSize) { - return (length && maskSize) ? length * (Math.log(maskSize) / Math.log(2)) : 0; - } - - /** - * Mesure the entropy of a password. - * @param pwd {srtring} The password to test the entropy - * @return {int} - */ - static entropy(pwd = '') { - let maskSize = 0; - - for (const i in MASKS) { - if (pwd.match(MASKS[i].pattern)) { - maskSize += MASKS[i].size; - } - } - - return this.calculEntropy(pwd.length, maskSize); - } - /** * Get the entropy level regarding the mesure of the entropy. * @param txt {string} The text to work on @@ -119,7 +93,7 @@ export default class SecretComplexity { */ static getStrength(txt) { txt = txt || ""; - const entropy = this.entropy(txt); + const entropy = SecretGeneratorComplexity.entropyPassword(txt); const strength = STRENGTH.reduce((accumulator, item) => { if (!accumulator) { return item; } @@ -130,28 +104,6 @@ export default class SecretComplexity { return strength; } - /** - * Check if a text matches multiple masks. - * @param txt {string} The text to - * @returns {array} The list of masks as following : - * { - * alpha: true, - * uppercase: false, - * ... - * } - */ - static matchMasks(txt) { - const matches = {}; - for (const i in MASKS) { - matches[i] = false; - if (txt.match(MASKS[i].pattern)) { - matches[i] = true; - } - } - - return matches; - } - /** * Generate a password following the system settings. * @param {int} [length] (optional) The password length. Default 18. @@ -174,14 +126,14 @@ export default class SecretComplexity { * Try maximum 10 times. */ let j = 0; - const expectedEntropy = this.calculEntropy(length, mask.length); + const expectedEntropy = SecretGeneratorComplexity.calculEntropy(length, mask.length); do { secret = ''; for (let i = 0; i < length; i++) { secret += mask[this.randomRange(0, mask.length - 1)]; } - } while (this.entropy(secret) < expectedEntropy && j++ < 10); + } while (SecretGeneratorComplexity.entropyPassword(secret) < expectedEntropy && j++ < 10); return secret; } diff --git a/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.js b/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.js index 1e8201ceb..4d6389155 100644 --- a/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.js +++ b/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.js @@ -112,10 +112,11 @@ export const MASKS = { } }; -const NUMBER_OF_ASCII_CHARACTER = 128; const NUMBER_OF_WORD_CASE = 3; const LOOK_ALIKE_CHARS = ["O", "l", "|", "I", "0", "1"]; +const ALL_CHARS = Object.values(MASKS).flatMap(mask => mask.characters); + export const SecretGeneratorComplexity = { /** * Evaluate the maximum entropy a password can be with the given generator configuration. @@ -152,7 +153,9 @@ export const SecretGeneratorComplexity = { } } - return calculEntropy(passwordCharacters.length, maskSize); + const unknownMaskSet = new Set(passwordCharacters.filter(character => !ALL_CHARS.includes(character))); + + return calculEntropy(passwordCharacters.length, maskSize + unknownMaskSet.size); }, /** @@ -164,8 +167,8 @@ export const SecretGeneratorComplexity = { entropyPassphrase: (numberOfWords = 0, separator = '') => { const words = PassphraseGeneratorWords['en-UK']; // determine a constant for separator - const maskSize = (separator.length * NUMBER_OF_ASCII_CHARACTER) + words.length + NUMBER_OF_WORD_CASE; - return calculEntropy(numberOfWords, maskSize); + const wordMaskSize = words.length * NUMBER_OF_WORD_CASE; + return calculEntropy(numberOfWords, wordMaskSize) + SecretGeneratorComplexity.entropyPassword(separator); }, /** diff --git a/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.test.js b/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.test.js index 74cafde7f..306ab22bb 100644 --- a/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.test.js +++ b/src/shared/lib/SecretGenerator/SecretGeneratorComplexity.test.js @@ -68,7 +68,7 @@ describe("SecretGeneratorComplexity", () => { "aaaAAA1111": 59.54, "🇫🇷": 0, // A char which doesn't match the known masks. "😸😸😸😸😸😸😸😸": 50.58, - "😸😸😸😸😸😸😸🇫🇷": 50.58, + "😸😸😸😸😸😸😸🇫🇷": 50.72, "aA1(~:<😸": 59, }; @@ -82,10 +82,10 @@ describe("SecretGeneratorComplexity", () => { const entropyOf = (wordCount, spacing) => roundEntropy(SecretGeneratorComplexity.entropyPassphrase(wordCount, spacing)); //Currently, the tests are written considering that there are 7776 words in the dictionnary and 3 word cases - expect(entropyOf(5, "")).toBe(64.63); - expect(entropyOf(5, " ")).toBe(64.86); - expect(entropyOf(10, "")).toBe(129.25); - expect(entropyOf(10, " ")).toBe(129.72); + expect(entropyOf(5, "")).toBe(72.55); + expect(entropyOf(5, "; ")).toBe(77.19); + expect(entropyOf(10, "")).toBe(72.55 * 2); + expect(entropyOf(10, "; ")).toBe(149.74); }); }); From 8225f316f19deda7385ec324cba96b2c78fb4521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 12 Jun 2024 16:54:39 +0200 Subject: [PATCH 39/67] PB-33746 - Update NPM dependency "Braces" --- package-lock.json | 234 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 188 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5cedf2400..9b006523a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "4.8.0-alpha.12", "license": "AGPL-3.0", "dependencies": { - "@testing-library/dom": "^8.11.3", "debounce-promise": "^3.1.2", "grapheme-splitter": "^1.0.4", "html5-qrcode": "^2.3.8", @@ -41,6 +40,7 @@ "@babel/preset-env": "^7.22.9", "@babel/preset-react": "^7.22.5", "@babel/runtime": "^7.22.6", + "@testing-library/dom": "^8.11.3", "@testing-library/react": "^12.1.4", "babel-jest": "^29.6.2", "babel-loader": "^9.1.3", @@ -131,6 +131,7 @@ "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, "dependencies": { "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" @@ -494,6 +495,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -539,6 +541,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -3395,6 +3398,7 @@ "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3413,6 +3417,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3427,6 +3432,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3442,6 +3448,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3452,12 +3459,14 @@ "node_modules/@testing-library/dom/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3466,6 +3475,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3503,7 +3513,8 @@ "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==" + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.1", @@ -4306,6 +4317,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -4339,6 +4351,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "dependencies": { "deep-equal": "^2.0.5" } @@ -4347,6 +4360,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -4496,6 +4510,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4805,12 +4820,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4976,6 +4992,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -5025,6 +5042,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5214,6 +5232,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -5221,7 +5240,8 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/colorette": { "version": "2.0.20", @@ -5567,6 +5587,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -5610,6 +5631,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -5689,7 +5711,8 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -5938,6 +5961,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -6050,6 +6074,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -6923,10 +6948,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7025,6 +7051,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -7181,7 +7208,8 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -7205,6 +7233,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7242,6 +7271,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -7471,6 +7501,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -8118,6 +8149,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -8129,6 +8161,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8137,6 +8170,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -8145,6 +8179,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.1" }, @@ -8156,6 +8191,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8167,6 +8203,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8178,6 +8215,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -8628,6 +8666,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -8671,6 +8710,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -8686,6 +8726,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -8705,6 +8746,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -8729,6 +8771,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -8744,6 +8787,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8767,6 +8811,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8819,6 +8864,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8849,6 +8895,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -8857,6 +8904,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8910,6 +8958,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -8937,6 +8986,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8945,6 +8995,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -8968,6 +9019,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8988,6 +9040,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -9002,6 +9055,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, "dependencies": { "which-typed-array": "^1.1.11" }, @@ -9037,6 +9091,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9057,6 +9112,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -9083,7 +9139,8 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/isexe": { "version": "2.0.0", @@ -11632,6 +11689,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, "bin": { "lz-string": "bin/bin.js" } @@ -12120,6 +12178,7 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12128,6 +12187,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -12143,6 +12203,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -12151,6 +12212,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -12732,6 +12794,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12745,6 +12808,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -13121,7 +13185,8 @@ "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "node_modules/react-list": { "version": "0.8.17", @@ -13286,6 +13351,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -13718,6 +13784,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -13813,6 +13880,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -13989,6 +14057,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -14240,6 +14309,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -15066,6 +15136,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -15081,6 +15152,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -15100,6 +15172,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -15341,6 +15414,7 @@ "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, "requires": { "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" @@ -15607,7 +15681,8 @@ "@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true }, "@babel/helper-validator-option": { "version": "7.22.15", @@ -15641,6 +15716,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -17577,6 +17653,7 @@ "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -17592,6 +17669,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -17600,6 +17678,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -17609,6 +17688,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -17616,17 +17696,20 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -17653,7 +17736,8 @@ "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==" + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "dev": true }, "@types/babel__core": { "version": "7.20.1", @@ -18316,6 +18400,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -18343,6 +18428,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "requires": { "deep-equal": "^2.0.5" } @@ -18351,6 +18437,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "requires": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -18460,7 +18547,8 @@ "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true }, "babel-jest": { "version": "29.6.2", @@ -18688,12 +18776,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "broccoli-node-api": { @@ -18809,6 +18897,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -18835,6 +18924,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -18970,6 +19060,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -18977,7 +19068,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "colorette": { "version": "2.0.20", @@ -19241,6 +19333,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -19278,6 +19371,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -19333,7 +19427,8 @@ "dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true }, "dom-helpers": { "version": "5.2.1", @@ -19531,6 +19626,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -19620,7 +19716,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true }, "escodegen": { "version": "2.1.0", @@ -20249,9 +20346,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -20327,6 +20424,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "requires": { "is-callable": "^1.1.3" } @@ -20454,7 +20552,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "function.prototype.name": { "version": "1.1.5", @@ -20471,7 +20570,8 @@ "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true }, "gaze": { "version": "1.1.3", @@ -20497,6 +20597,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -20667,6 +20768,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "requires": { "get-intrinsic": "^1.1.3" } @@ -21171,6 +21273,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -21178,17 +21281,20 @@ "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true }, "has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "requires": { "get-intrinsic": "^1.1.1" } @@ -21196,17 +21302,20 @@ "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -21556,6 +21665,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "requires": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -21587,6 +21697,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -21596,6 +21707,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -21612,6 +21724,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -21630,6 +21743,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -21638,7 +21752,8 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true }, "is-core-module": { "version": "2.12.1", @@ -21653,6 +21768,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -21686,7 +21802,8 @@ "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -21710,6 +21827,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -21745,6 +21863,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -21762,12 +21881,14 @@ "is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true }, "is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -21782,6 +21903,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -21796,6 +21918,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -21804,6 +21927,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, "requires": { "which-typed-array": "^1.1.11" } @@ -21826,7 +21950,8 @@ "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==" + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true }, "is-weakref": { "version": "1.0.2", @@ -21841,6 +21966,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -21861,7 +21987,8 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "isexe": { "version": "2.0.0", @@ -23781,7 +23908,8 @@ "lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true }, "make-dir": { "version": "2.1.0", @@ -24151,12 +24279,14 @@ "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -24165,12 +24295,14 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -24582,6 +24714,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "requires": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -24591,7 +24724,8 @@ "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true } } }, @@ -24878,7 +25012,8 @@ "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "react-list": { "version": "0.8.17", @@ -25020,6 +25155,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -25332,6 +25468,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -25408,6 +25545,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "requires": { "internal-slot": "^1.0.4" } @@ -25545,6 +25683,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -26354,6 +26493,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -26366,6 +26506,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "requires": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -26382,6 +26523,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", From 77957f28e4c295a4bdacdcd42ca84193caf03f8f Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 18 Jun 2024 09:22:11 +0200 Subject: [PATCH 40/67] PB-33803 Fix button size and alignment for small screen on the resource workspace --- src/less/components/actionbar.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/less/components/actionbar.less b/src/less/components/actionbar.less index 2ac6d638e..98174b77f 100644 --- a/src/less/components/actionbar.less +++ b/src/less/components/actionbar.less @@ -43,11 +43,17 @@ /* hide icons if there is no space */ @media all and (max-width: 1024px) { + .header.third .main-action-wrapper { + button { + width: 4.8rem; + } + } .header.third .main-action-wrapper, .header.third .actions-wrapper { button { min-width: 1em; font-size: 1em; + height: 3.6rem; } button span + span { &.svg-icon { From d0e5b21067bc1fcf8c487b182b62e9343d9e2983 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 18 Jun 2024 10:03:14 +0200 Subject: [PATCH 41/67] PB-33802 Fix icon attention required in the resource grid --- src/less/components/tableview.less | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/less/components/tableview.less b/src/less/components/tableview.less index a55111e9b..12a906ccc 100644 --- a/src/less/components/tableview.less +++ b/src/less/components/tableview.less @@ -265,13 +265,18 @@ /* multi select and fav fix */ th.cell-favorite, - td.cell-favorite { + td.cell-favorite, + th.cell-attentionRequired, + td.cell-attentionRequired { button { display: flex; align-items: center; .cell-header { margin: 0; overflow: inherit; + .svg-icon.exclamation { + margin-top: .1rem; + } .cell-header-icon-sort { display: flex; margin: 0; From de2ee65db24326847866227226e82e515ee22789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Tue, 18 Jun 2024 12:45:51 +0000 Subject: [PATCH 42/67] PB-33796 - As a signed in user when I navigate to the resource workspace, my... --- .../contexts/ResourceWorkspaceContext.js | 2 -- .../contexts/ResourceWorkspaceContext.test.js | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.js b/src/react-extension/contexts/ResourceWorkspaceContext.js index f291d1fb0..223552bc0 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.js @@ -596,8 +596,6 @@ export class ResourceWorkspaceContextProvider extends React.Component { this.props.context.port.request("passbolt.folders.update-local-storage"); } this.props.context.port.request("passbolt.resources.update-local-storage"); - this.props.context.port.request("passbolt.groups.update-local-storage"); - this.props.context.port.request("passbolt.users.update-local-storage"); } /** RESOURCE SEARCH **/ diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.js index 99c71c384..49fb98843 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.js @@ -19,10 +19,12 @@ import {defaultProps, defaultAppContext} from "./ResourceWorkspaceContext.test.data"; import ResourceWorkspaceContextPage from "./ResourceWorkspaceContext.test.page"; import {ResourceWorkspaceFilterTypes} from "./ResourceWorkspaceContext"; +import {waitForTrue} from "../../../test/utils/waitFor"; describe("Resource Workspace Context", () => { let page; // The page to test against const context = defaultAppContext(); // The applicative context + const totalResourcesCount = context.resources.length; const mockContextRequest = (context, implementation) => jest.spyOn(context.port, 'request').mockImplementation(implementation); @@ -48,51 +50,61 @@ describe("Resource Workspace Context", () => { it("AS LU I should have an RECENTLY-MODIFIED filter when I went to /app/passwords with such a filter", async() => { await page.goToRecentlyModified(); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.RECENTLY_MODIFIED); }); it("AS LU I should have an SHARED-WITH-ME filter when I went to /app/passwords with such a filter", async() => { await page.goToShareWithMe(); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.SHARED_WITH_ME); }); it("AS LU I should have an EXPIRED filter when I went to /app/passwords/filter/expried with such a filter", async() => { await page.goToExpired(); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.EXPIRED); }); it("AS LU I should have an ITEMS-I-OWN filter when I went to /app/passwords with such a filter", async() => { await page.goToItemsIOwn(); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.ITEMS_I_OWN); }); it("AS LU I should have an FAVORITE filter when I went to /app/passwords with such a filter", async() => { await page.goToFavorite(); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.FAVORITE); }); it("AS LU I should have an TEXT filter when I went to /app/passwords with such a filter", async() => { await page.goToText("some text"); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.TEXT); }); it("AS LU I should have an GROUP filter when I went to /app/passwords with such a filter", async() => { await page.goToGroup({group: {id: 'some group id'}}); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.GROUP); }); it("AS LU I should have an TAG filter when I went to /app/passwords with such a filter", async() => { await page.goToTag({tag: {id: 'some tag id'}}); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.TAG); }); it("AS LU I should have an FOLDER filter when I went to /app/folders/{folder-id} with such a filter", async() => { await page.goToFolder(context.folders[0]); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.FOLDER); }); it("AS LU I should have an ROOT-FOLDER filter when I went to /app/folders/{folder-id} with such a filter", async() => { await page.goToRootFolder(); + await waitForTrue(() => page.filter.type !== ResourceWorkspaceFilterTypes.ALL && page.filter.type !== ResourceWorkspaceFilterTypes.NONE); expect(page.filter.type).toBe(ResourceWorkspaceFilterTypes.ROOT_FOLDER); }); }); @@ -131,6 +143,7 @@ describe("Resource Workspace Context", () => { it("AS LU I should have resources matching a text when the filter is TEXT", async() => { const expectedResourcesCount = 1; await page.goToText("docker"); + await waitForTrue(() => page.filteredResources.length !== totalResourcesCount); expect(page.filteredResources).toHaveLength(expectedResourcesCount); expect(page.filteredResources[0].name).toBe("Docker"); }); @@ -138,6 +151,7 @@ describe("Resource Workspace Context", () => { it("AS LU I should have resources matching a text when the filter is TEXT in folder name", async() => { const expectedResourcesCount = 1; await page.goToText("Accounting"); + await waitForTrue(() => page.filteredResources.length !== totalResourcesCount); expect(page.filteredResources).toHaveLength(expectedResourcesCount); expect(page.filteredResources[0].name).toBe("Inside Bank Password"); }); @@ -159,6 +173,7 @@ describe("Resource Workspace Context", () => { const charlieTag = {tag: {id: '1c8afebc-7e23-51bd-a0b6-2e695afeb32f'}}; const expectedResourcesCount = 1; await page.goToTag(charlieTag); + await waitForTrue(() => page.filteredResources.length !== totalResourcesCount); expect(page.filteredResources).toHaveLength(expectedResourcesCount); }); @@ -167,12 +182,14 @@ describe("Resource Workspace Context", () => { const privateFolder = context.folders.find(folder => folder.id === privateFolderId); const expectedResourcesCount = 1; await page.goToFolder(privateFolder); + await waitForTrue(() => page.filteredResources.length !== totalResourcesCount); expect(page.filteredResources).toHaveLength(expectedResourcesCount); }); it("AS LU I should have resources belonged to a root folder the filter is ROOT-FOLDER", async() => { const expectedResourcesCount = 16; await page.goToRootFolder(); + await waitForTrue(() => page.filteredResources.length !== totalResourcesCount); expect(page.filteredResources).toHaveLength(expectedResourcesCount); }); }); @@ -185,19 +202,24 @@ describe("Resource Workspace Context", () => { it("As LU I should have all resources as selected when the All Selection event has been fired", async() => { await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); await page.selectAll(); + await waitForTrue(() => page.filteredResources.length > 0); expect(page.selectedResources).toHaveLength(context.resources.length); }); it("As LU I should have none resources as selected when the None Selection event has been fired", async() => { await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); await page.selectAll(); + await waitForTrue(() => page.filteredResources.length > 0); await page.selectNone(); expect(page.selectedResources).toHaveLength(0); }); it("As LU I should have one selected resource when the Single Selection event has been fired", async() => { await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); const resourceToSelect = context.resources[0]; page.select(resourceToSelect); expect(page.selectedResources).toHaveLength(1); @@ -206,6 +228,7 @@ describe("Resource Workspace Context", () => { it("As LU I should have multiple resources as selected when the Multiple Selection event has been fired", async() => { await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); const resourcesToSelect = [context.resources[0], context.resources[3]]; await page.selectMultiple(resourcesToSelect); expect(page.selectedResources).toHaveLength(2); @@ -215,6 +238,7 @@ describe("Resource Workspace Context", () => { it("As LU I should have a range of selected resources when the Range Selection event has been fired", async() => { await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); const resourcesToSelect = [context.resources[0], context.resources[3]]; await page.selectRange(resourcesToSelect); const expectResourceMatch = (resource, index) => expect(resource).toBe(context.resources[index]); @@ -227,6 +251,7 @@ describe("Resource Workspace Context", () => { it("As LU, I should detail a folder when a folder is selected as filter", async() => { const folder = context.folders[0]; await page.goToFolder(folder); + await waitForTrue(() => page.filteredResources.length !== totalResourcesCount); expect(page.details.folder).toBe(folder); expect(page.lockDisplayDetail).toBeTruthy(); }); @@ -234,7 +259,9 @@ describe("Resource Workspace Context", () => { it("As LU, I should detail a resource when a resource is selected", async() => { const resource = context.resources[0]; await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); await page.select(resource); + await waitForTrue(() => page.selectedResources.length > 0); expect(page.details.resource).toBe(resource); expect(page.lockDisplayDetail).toBeTruthy(); }); @@ -249,6 +276,7 @@ describe("Resource Workspace Context", () => { it("As LU, I should detail nothing when several resources are selected", async() => { await page.goToAllItems(); + await waitForTrue(() => page.filteredResources.length === totalResourcesCount); await page.selectAll(); expect(page.details.folder).toBeNull(); expect(page.details.resource).toBeNull(); From 827ab7620b46787459ca24fac72f98865d21685d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Tue, 18 Jun 2024 17:02:16 +0000 Subject: [PATCH 43/67] PB-33796 - As a signed in user when I navigate to the resource workspace, my... --- .../FilterResourcesByGroups.js | 28 +++++++++++++++++-- .../FilterResourcesByGroups.test.js | 7 ++++- .../FilterResourcesByGroups.test.stories.js | 9 +++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.js b/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.js index da8f4b003..133112212 100644 --- a/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.js +++ b/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.js @@ -40,9 +40,27 @@ class FilterResourcesByGroups extends React.Component { get defaultState() { return { open: true, // open the group section + groups: null, // the groups the user is member of + loading: false, // is the data currently loading }; } + async componentDidMount() { + await this.loadGroupsData(); + } + + /** + * Loads the groups the current user is member of. + * @returns {Promise} + */ + async loadGroupsData() { + if (!this.state.loading) { + this.setState({loading: true}); + const groups = await this.props.context.port.request('passbolt.groups.find-my-groups'); + this.setState({groups, loading: false}); + } + } + /** * Bind callbacks methods */ @@ -54,9 +72,13 @@ class FilterResourcesByGroups extends React.Component { /** * Handle when the user click on the title. */ - handleTitleClickEvent() { + async handleTitleClickEvent() { const open = !this.state.open; this.setState({open}); + + if (open) { + await this.loadGroupsData(); + } } /** @@ -84,7 +106,7 @@ class FilterResourcesByGroups extends React.Component { * @returns {*|boolean} */ hasGroup() { - return this.props.context.groups && this.groups.length > 0; + return this.groups && this.groups.length > 0; } /** @@ -92,7 +114,7 @@ class FilterResourcesByGroups extends React.Component { * @returns {*} */ get groups() { - return this.props.context.groups.filter(group => group.my_group_user !== null); + return this.state.groups; } /** diff --git a/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.js b/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.js index e9f25f07b..990b8ce0b 100644 --- a/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.js +++ b/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.js @@ -19,6 +19,7 @@ import {defaultProps, groupsMock} from "./FilterResourcesByGroups.test.data"; import SidebarGroupFilterSectionPage from "./FilterResourcesByGroups.test.page"; import MockPort from "../../../test/mock/MockPort"; import {ResourceWorkspaceFilterTypes} from "../../../contexts/ResourceWorkspaceContext"; +import {waitForTrue} from "../../../../../test/utils/waitFor"; beforeEach(() => { jest.resetModules(); @@ -27,6 +28,7 @@ beforeEach(() => { describe("See groups", () => { let page; // The page to test against const props = defaultProps(); // The props to pass + props.context.port.addRequestListener('passbolt.groups.find-my-groups', async() => props.context.groups.filter(group => Boolean(group.my_group_user))); describe(' As LU I can see groups', () => { /** @@ -41,13 +43,15 @@ describe("See groups", () => { }); it('I should see the 10 groups made on the resource', async() => { + await waitForTrue(() => page.title.hyperlink !== null); await page.title.click(); await page.title.click(); expect(page.displayGroupList.exists()).toBeTruthy(); expect(page.displayGroupList.count()).toBe(9); }); - it('I should be able to identify each group name', () => { + it('I should be able to identify each group name', async() => { + await waitForTrue(() => page.title.hyperlink !== null); expect(page.displayGroupList.name(1)).toBe('Leadership team'); expect(page.displayGroupList.name(2)).toBe('Management'); expect(page.displayGroupList.name(3)).toBe('Marketing'); @@ -60,6 +64,7 @@ describe("See groups", () => { }); it('I should be able to see the filtered group name selected', async() => { + await waitForTrue(() => page.title.hyperlink !== null); await page.displayGroupList.click(page.displayGroupList.group(8)); expect(page.displayGroupList.groupSelected).not.toBeNull(); const state = { diff --git a/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.stories.js b/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.stories.js index c04e32e88..f1d8325e2 100644 --- a/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.stories.js +++ b/src/react-extension/components/Resource/FilterResourcesByGroups/FilterResourcesByGroups.test.stories.js @@ -3,6 +3,7 @@ import {MemoryRouter, Route} from "react-router-dom"; import AppContext from "../../../../shared/context/AppContext/AppContext"; import {ResourceWorkspaceFilterTypes} from "../../../contexts/ResourceWorkspaceContext"; import FilterResourcesByGroups from "./FilterResourcesByGroups"; +import MockPort from "../../../test/mock/MockPort"; export default { @@ -10,14 +11,20 @@ export default { component: FilterResourcesByGroups }; +const mockedPort = new MockPort(); const context = { groups: [ {id: 1, name: 'Group 1'}, {id: 2, name: 'Group 2'}, {id: 3, name: 'Group 3'} - ] + ], + port: mockedPort, }; + +mockedPort.addRequestListener("passbolt.groups.find-my-groups", () => context.groups); + + const Template = args => From d8b67c2675a4fa5d9ac2dfcb59cef9493b53a6b1 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Wed, 12 Jun 2024 13:54:34 +0200 Subject: [PATCH 44/67] PB-14173 As Logged Out user, I shouldn't be able to view a previously viewed password --- src/react-extension/contexts/ExtAppContext.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react-extension/contexts/ExtAppContext.js b/src/react-extension/contexts/ExtAppContext.js index 968a4515a..5242a75bb 100644 --- a/src/react-extension/contexts/ExtAppContext.js +++ b/src/react-extension/contexts/ExtAppContext.js @@ -350,6 +350,8 @@ class ExtAppContextProvider extends React.Component { const displayExpiredSession = () => { if (!this.state.isSessionLogoutByUser) { callback(); + // Flush resources to not leave sensitive data + this.setState({resources: []}); } }; this.props.port.on('passbolt.auth.after-logout', displayExpiredSession); From f6e6f9e0cdd51b3c93474f3cf3902940b5a07061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 19 Jun 2024 14:21:42 +0000 Subject: [PATCH 45/67] PB-33799 - As a signed in user when I display the share dialog, the... --- src/locales/en-UK/common.json | 2 + .../Inputs/Autocomplete/Autocomplete.js | 31 +-- .../Inputs/Autocomplete/Autocomplete.test.js | 4 +- .../Autocomplete/Autocomplete.test.page.js | 2 +- .../Inputs/Autocomplete/AutocompleteItem.js | 34 +++- .../components/Share/ShareDialog.js | 44 +---- .../components/Share/ShareDialog.test.data.js | 177 +----------------- .../components/Share/ShareDialog.test.js | 98 +++++----- .../components/Share/SharePermissionItem.js | 2 +- 9 files changed, 107 insertions(+), 287 deletions(-) diff --git a/src/locales/en-UK/common.json b/src/locales/en-UK/common.json index b79c6c852..694125d89 100644 --- a/src/locales/en-UK/common.json +++ b/src/locales/en-UK/common.json @@ -15,6 +15,8 @@ "{{count}} group has been found._other": "{{count}} groups have been found.", "{{count}} group has been synchronized._one": "{{count}} group has been synchronized.", "{{count}} group has been synchronized._other": "{{count}} groups have been synchronized.", + "{{count}} group member_one": "One group member", + "{{count}} group member_other": "{{count}} group members", "{{count}} group will be synchronized._one": "{{count}} group will be synchronized.", "{{count}} group will be synchronized._other": "{{count}} groups will be synchronized.", "{{count}} password has been imported successfully._one": "{{count}} password has been imported successfully.", diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.js b/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.js index a4763e8d9..37289dc34 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.js @@ -103,7 +103,7 @@ class Autocomplete extends Component { this.handleKeyDown = this.handleKeyDown.bind(this); this.selectNext = this.selectNext.bind(this); this.selectPrevious = this.selectPrevious.bind(this); - this.handleAutocompleteChangeDebounced = debounce(this.handleAutocompleteChange.bind(this), 150); + this.handleAutocompleteChangeDebounced = debounce(this.handleAutocompleteChange.bind(this), 300); this.handleSelect = this.handleSelect.bind(this); this.handleInputChange = this.handleInputChange.bind(this); } @@ -126,7 +126,8 @@ class Autocomplete extends Component { */ async autocompleteSearch(keyword) { if (!this.cache[keyword] || this.cache[keyword].cacheExpiry < (new Date()).getTime()) { - this.cache[keyword] = await this.getItems(keyword); + const results = await this.getItems(keyword); + this.cache[keyword] = results; this.cache[keyword].cacheExpiry = (new Date()).getTime() + this.cacheExpiry; } return this.cache[keyword]; @@ -194,22 +195,21 @@ class Autocomplete extends Component { const name = target.name; this.setState({ [name]: value - }, () => { - if (name === 'name') { - this.handleAfterNameUpdate(); - } }); + if (name === 'name') { + this.handleNameUpdate(value); + } } /** - * Handle after name update - * @param {ReactEvent} event The triggered event + * Handle name update + * @param {string} value * @return {void} */ - handleAfterNameUpdate() { - if (this.state.name) { - if (!this.state.name.endsWith(' ')) { - this.handleAutocompleteChangeDebounced(); + handleNameUpdate(value) { + if (value) { + if (!value.endsWith(' ')) { + this.handleAutocompleteChangeDebounced(value); } } else { this.closeAutocomplete(); @@ -218,14 +218,15 @@ class Autocomplete extends Component { /** * Handle autocomplete change + * @param {string} searchedName * @returns {Promise} */ - async handleAutocompleteChange() { - const keyword = this.state.name; + async handleAutocompleteChange(keyword) { if (!keyword) { this.closeAutocomplete(); return; } + try { const autocompleteItems = await this.autocompleteSearch(keyword); let selected = null; @@ -363,7 +364,7 @@ class Autocomplete extends Component {
    - {this.state.processing && + {this.state.processing && this.state.name && } {!this.state.processing && (!this.state.autocompleteItems || !this.state.autocompleteItems.length) && diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.js b/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.js index c992f9290..eedce74c9 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.js @@ -60,7 +60,7 @@ describe("See the Autocomplete", () => { it('As LU I should see an item', async() => { expect.assertions(6); const items = [ - {name: "group", groups_users: []} + {name: "group", user_count: 1} ]; const requestMockImpl = jest.fn(() => items); jest.spyOn(props, 'searchCallback').mockImplementation(requestMockImpl); @@ -77,7 +77,7 @@ describe("See the Autocomplete", () => { it('As LU I should see an item with the number of user into the group', async() => { expect.assertions(1); const items = [ - {name: "group", groups_users: groups} + {name: "group", user_count: groups.length} ]; const requestMockImpl = jest.fn(() => items); jest.spyOn(props, 'searchCallback').mockImplementation(requestMockImpl); diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.page.js b/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.page.js index e04ef21d1..1dcb46121 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.page.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/Autocomplete.test.page.js @@ -116,7 +116,7 @@ export default class AutocompletePage { async fillInput(data, inProgressFn = () => {}) { const dataInputEvent = {target: {value: data}}; fireEvent.change(this.input, dataInputEvent); - jest.advanceTimersByTime(150); + jest.advanceTimersByTime(300); await waitFor(inProgressFn); } } diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js index 7d8be9e5a..e6dd719de 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js @@ -16,7 +16,8 @@ import PropTypes from "prop-types"; import UserAvatar from "../../Avatar/UserAvatar"; import GroupAvatar from "../../Avatar/GroupAvatar"; import {isUserSuspended} from "../../../../../shared/utils/userUtils"; -import {Trans} from "react-i18next"; +import {Trans, withTranslation} from "react-i18next"; +import {withAppContext} from "../../../../../shared/context/AppContext/AppContext"; class AutocompleteItem extends Component { /** @@ -25,9 +26,23 @@ class AutocompleteItem extends Component { */ constructor(props) { super(props); + this.state = this.defaultState; this.bindCallbacks(); } + get defaultState() { + return { + gpgKey: null + }; + } + + async componentDidMount() { + if (this.props.user) { + const gpgKey = await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', this.props.user.id); + this.setState({gpgKey}); + } + } + /** * Bind callbacks methods * @return {void} @@ -53,16 +68,13 @@ class AutocompleteItem extends Component { * @returns {string} */ getSubtitle() { - if (this.props.user) { - const longId = this.props.user.gpgkey.fingerprint.substr(this.props.user.gpgkey.fingerprint.length - 16); + if (this.props.user && this.state.gpgKey) { + const longId = this.state.gpgKey.fingerprint.substr(this.state.gpgKey.fingerprint.length - 16); return longId.replace(/(.{4})/g, "$1 "); - } else { - if (this.props.group?.groups_users?.length > 1) { - return `${this.props.group.groups_users.length} group members`; - } else { - return `One group member`; - } + } else if (this.props.group) { + return this.props.t("{{count}} group member", {count: this.props.group.user_count}); } + return ""; } /** @@ -125,6 +137,7 @@ AutocompleteItem.defaultProps = { }; AutocompleteItem.propTypes = { + context: PropTypes.object, baseUrl: PropTypes.string, id: PropTypes.number, user: PropTypes.object, @@ -132,6 +145,7 @@ AutocompleteItem.propTypes = { selected: PropTypes.bool, onClick: PropTypes.func, canShowUserAsSuspended: PropTypes.bool.isRequired, // is the feature disableUser enabled? + t: PropTypes.func, // the translation function }; -export default AutocompleteItem; +export default withAppContext(withTranslation("common")(AutocompleteItem)); diff --git a/src/react-extension/components/Share/ShareDialog.js b/src/react-extension/components/Share/ShareDialog.js index a51b50bbb..0ef7f7756 100644 --- a/src/react-extension/components/Share/ShareDialog.js +++ b/src/react-extension/components/Share/ShareDialog.js @@ -268,34 +268,20 @@ class ShareDialog extends Component { /** * Get users or groups matching the given keyword * @param {string} keyword - * @returns {Promise} aros, + * @returns {Promise} */ async fetchAutocompleteItems(keyword) { keyword = keyword.toLowerCase(); - const words = (keyword && keyword.split(/\s+/)) || ['']; - // Test match of some escaped test words against the name / username - const escapeWord = word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const wordToRegex = word => new RegExp(escapeWord(word), 'i'); - const matchWord = (word, value) => wordToRegex(word).test(value); - - const matchUsernameProperty = (word, user) => matchWord(word, user.username); - const matchNameProperty = (word, user) => matchWord(word, user.profile.first_name) || matchWord(word, user.profile.last_name); - const matchUser = (word, user) => matchUsernameProperty(word, user) || matchNameProperty(word, user); - const matchUserText = user => words.every(word => matchUser(word, user)); - - const matchGroup = (word, group) => matchWord(word, group.name); - const matchGroupText = group => words.every(word => matchGroup(word, group)); + const matchingUsersAndGroups = await this.props.context.port.request('passbolt.share.search-aros', keyword); const permissions = this.state.permissions; - const hasPermissionsOnResources = aro_id => permissions.some(permission => permission.id === aro_id); let currentcount = 0; - const groups = this.props.context.groups.filter(group => { + const usersAndGroupsToDisplay = matchingUsersAndGroups.filter(userOrGroup => { const isMatching = currentcount < Autocomplete.DISPLAY_LIMIT - && matchGroupText(group) - && !hasPermissionsOnResources(group.id); + && !hasPermissionsOnResources(userOrGroup.id); if (isMatching) { currentcount++; @@ -304,27 +290,7 @@ class ShareDialog extends Component { return isMatching; }); - const users = this.props.context.users.filter(user => { - const isMatching = currentcount < Autocomplete.DISPLAY_LIMIT - && user.active === true - && matchUserText(user) - && !hasPermissionsOnResources(user.id); - - if (isMatching) { - currentcount++; - } - - return isMatching; - }); - - await Promise.all(users.map(async user => { - if (!user.gpgkey) { - user.gpgkey = { - fingerprint: await this.getFingerprintForUser(user.id) - }; - } - })); - return [...users, ...groups]; + return usersAndGroupsToDisplay; } /** diff --git a/src/react-extension/components/Share/ShareDialog.test.data.js b/src/react-extension/components/Share/ShareDialog.test.data.js index cd438715b..ade02aab0 100644 --- a/src/react-extension/components/Share/ShareDialog.test.data.js +++ b/src/react-extension/components/Share/ShareDialog.test.data.js @@ -2,7 +2,7 @@ import UserSettings from "../../../shared/lib/Settings/UserSettings"; import userSettingsFixture from "../../test/fixture/Settings/userSettings"; import {users, groups} from "../../contexts/UserWorkspaceContext.test.data"; import MockPort from "../../test/mock/MockPort"; -import {TEST_ROLE_ADMIN_ID, TEST_ROLE_USER_ID} from "../../../shared/models/entity/role/role.test.data"; +import {TEST_ROLE_USER_ID} from "../../../shared/models/entity/role/role.test.data"; import SiteSettings from "../../../shared/lib/Settings/SiteSettings"; import siteSettingsFixture from "../../test/fixture/Settings/siteSettings"; @@ -1539,181 +1539,6 @@ export const folders = [{ }, ]; -export const autocompleteResult = [ - { - "user_count": "2", - "id": "469edf9d-ca1e-5003-91d6-3a46755d5a50", - "name": "Administrator", - "deleted": false, - "created": "2016-01-29T13:39:25+00:00", - "modified": "2016-01-29T13:39:25+00:00", - "created_by": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "modified_by": "d57c10f5-639d-5160-9c81-8a0c6c4ec856" - }, { - "id": "f848277c-5398-58f8-a82a-72397af2d450", - "role_id": TEST_ROLE_USER_ID, - "username": "ada@passbolt.com", - "active": true, - "deleted": false, - "created": "2019-12-21T10:39:26+00:00", - "modified": "2020-01-21T10:39:26+00:00", - "profile": { - "id": "99522cc9-0acc-5ae2-b996-d03bded3c0a6", - "user_id": "f848277c-5398-58f8-a82a-72397af2d450", - "first_name": "Ada", - "last_name": "Lovelace", - "created": "2020-02-21T10:39:26+00:00", - "modified": "2020-02-21T10:39:26+00:00", - "avatar": { - "id": "88eebbef-d7bc-4471-8577-2ec9e55769f6", - "user_id": "f848277c-5398-58f8-a82a-72397af2d450", - "foreign_key": "99522cc9-0acc-5ae2-b996-d03bded3c0a6", - "model": "Avatar", - "filename": "ada.png", - "filesize": 170049, - "mime_type": "image\/png", - "extension": "png", - "hash": "97e36ab6528e26e3b9f988444ef490f125f49a39", - "path": "Avatar\/a7\/32\/96\/88eebbefd7bc447185772ec9e55769f6\/88eebbefd7bc447185772ec9e55769f6.png", - "adapter": "Local", - "created": "2020-02-21T10:39:28+00:00", - "modified": "2020-02-21T10:39:28+00:00", - "url": { - "medium": "img\/public\/Avatar\/a7\/32\/96\/88eebbefd7bc447185772ec9e55769f6\/88eebbefd7bc447185772ec9e55769f6.a99472d5.png", - "small": "img\/public\/Avatar\/a7\/32\/96\/88eebbefd7bc447185772ec9e55769f6\/88eebbefd7bc447185772ec9e55769f6.65a0ba70.png" - } - } - }, - "groups_users": [], - "role": { - "id": TEST_ROLE_USER_ID, - "name": "user", - "description": "Logged in user", - "created": "2012-07-04T13:39:25+00:00", - "modified": "2012-07-04T13:39:25+00:00" - }, - "gpgkey": { - "id": "04481719-5d9d-5e22-880a-a6b9270601d2", - "user_id": "f848277c-5398-58f8-a82a-72397af2d450", - "armored_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFXHTB8BEADAaRMUn++WVatrw3kQK7\/6S6DvBauIYcBateuFjczhwEKXUD6T\nhLm7nOv5\/TKzCpnB5WkP+UZyfT\/+jCC2x4+pSgog46jIOuigWBL6Y9F6KkedApFK\nxnF6cydxsKxNf\/V70Nwagh9ZD4W5ujy+RCB6wYVARDKOlYJnHKWqco7anGhWYj8K\nKaDT+7yM7LGy+tCZ96HCw4AvcTb2nXF197Btu2RDWZ\/0MhO+DFuLMITXbhxgQC\/e\naA1CS6BNS7F91pty7s2hPQgYg3HUaDogTiIyth8R5Inn9DxlMs6WDXGc6IElSfhC\nnfcICao22AlM6X3vTxzdBJ0hm0RV3iU1df0J9GoM7Y7y8OieOJeTI22yFkZpCM8i\ntL+cMjWyiID06dINTRAvN2cHhaLQTfyD1S60GXTrpTMkJzJHlvjMk0wapNdDM1q3\njKZC+9HAFvyVf0UsU156JWtQBfkE1lqAYxFvMR\/ne+kI8+6ueIJNcAtScqh0LpA5\nuvPjiIjvlZygqPwQ\/LUMgxS0P7sPNzaKiWc9OpUNl4\/P3XTboMQ6wwrZ3wOmSYuh\nFN8ez51U8UpHPSsI8tcHWx66WsiiAWdAFctpeR\/ZuQcXMvgEad57pz\/jNN2JHycA\n+awesPIJieX5QmG44sfxkOvHqkB3l193yzxu\/awYRnWinH71ySW4GJepPQARAQAB\ntB9BZGEgTG92ZWxhY2UgPGFkYUBwYXNzYm9sdC5jb20+iQJOBBMBCgA4AhsDBQsJ\nCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEA\/YOlY9MspcjrN92E1O1sV2bBU8FAl0b\nmi8ACgkQE1O1sV2bBU+Okw\/\/b\/PRVTz0\/hgdagcVNYPn\/lclDFuwwqanyvYu6y6M\nAiLVn6CUtxfU7GH2aSwZSr7D\/46TSlBHvxVvNlYROMx7odbLgq47OJxfUDG5OPi7\nLZgsuE8zijCPURZTZu20m+ratsieV0ziri+xJV09xJrjdkXHdX2PrkU0YeJxhE50\nJuMR1rf7EHfCp45nWbXoM4H+LnadGC1zSHa1WhSJkeaYw9jp1gh93BKD8+kmUrm6\ncKEjxN54YpgjFwSdA60b+BZgXbMgA37gNQCnZYjk7toaQClUbqLMaQxHPIjETB+Z\njJNKOYn740N2LTRtCi3ioraQNgXQEU7tWsXGS0tuMMN7w4ya1I6sYV3fCtfiyXFw\nfuYnjjGzn5hXtTjiOLJ+2kdy5OmNZc9wpf6IpKv7\/F2RUwLsBUfH4ondNNXscdkB\n6Zoj1Hxt16TpkHnYrKsSWtoOs90JnlwYbHnki6R\/gekYRSRSpD\/ybScQDRASQ0aO\nhbi71WuyFbLZF92P1mEK5GInJeiFjKaifvJ8F+oagI9hiYcHgX6ghktaPrANa2De\nOjmesQ0WjIHirzFKx3avYIkOFwKp8v6KTzynAEQ8XUqZmqEhNjEgVKHH0g3sC+EC\nZ\/HGLHsRRIN1siYnJGahrrkNs7lFI5LTqByHh52bismY3ADLemxH6Voq+DokvQn4\nHxS5Ag0EVcdMHwEQAMFWZvlswoC+dEFISBhJLz0XpTR5M84MCn19s\/ILjp6dGPbC\nvlGcT5Ol\/wL43T3hML8bzq18MRGgkzhwsBkUXO+E7jVePjuGFvRwS5W+QYwCuAmw\nDijDdMhrev1mrdVK61v\/2U9kt5faETW8ZIYIvAWLaw\/lMHbVmKOa35ZCIJWcNsrv\noro2kGUklM6Nq1JQyU+puGPHuvm+1ywZzpAH5q55pMgfO+9JjMU3XFs+eqv6LVyA\n\/Y6T7ZK1H8inbUPm\/26sSvmYsT\/4xNVosC\/ha9lFEAasz\/rbVg7thffje4LWOXJB\no40iBTlHsNbCGs5BfNC0wl719JDA4V8mwhGInNtETCrGwg3mBlDrk5jYrDq5IMVk\nyX4Z6T8Fd2fLHmUr2kFc4vC96tGQGhNrbAa\/EeaAkWMeFyp\/YOW0Z3X2tz5A+lm+\nqevJZ3HcQd+7ca6mPTrYSVVXhclwSkyCLlhRJwEwSxrn+a2ZToYNotLs1uEy6tOL\nbIyhFBQNsR6mTa2ttkd\/89wJ+r9s7XYDOyibTQyUGgOXu\/0l1K0jTREKlC91wKkm\ndw\/lJkjZCIMc\/KTHiB1e7f5NdFtxwErToEZOLVumop0FjRqzHoXZIR9OCSMUzUmM\nspGHalE71GfwB9DkAlgvoJPohyiipJ\/Paw3pOytZnb\/7A\/PoRSjELgDNPJhxABEB\nAAGJAjYEGAEKACACGwwWIQQD9g6Vj0yylyOs33YTU7WxXZsFTwUCXRuaPgAKCRAT\nU7WxXZsFTxX0EADAN9lreHgEvsl4JK89JqwBLjvGeXGTNmHsfczCTLAutVde+Lf0\nqACAhKhG0J8Omru2jVkUqPhkRcaTfaPKopT2KU8GfjKuuAlJ+BzH7oUq\/wy70t2h\nsglAYByv4y0emwnGyFC8VNw2Fe+Wil2y5d8DI8XHGp0bAXehjT2S7\/v1lEypeiiE\nNbhAnGG94Zywwwim0RltyNKXOgGeT4mroYxAL0zeTaX99Lch+DqyaeDq94g4sfhA\nVvGT2KJDT85vR3oNbB0U5wlbKPa+bUl8CokEDjqrDmdZOOs\/UO2mc45V3X5RNRtp\nNZMBGPJsxOKQExEOZncOVsY7ZqLrecuR8UJBQnhPd1aoz3HCJppaPI02uINWyQLs\nCogTf+nQWnLyN9qLrToriahNcZlDfuJCRVKTQ1gw1lkSN3IZRSkBuRYRe05US+C6\n8JMKHP+1XMKMgQM2XR7r4noMJKLaVUzfLXuPIWH2xNdgYXcIOSRjiANkIv4O7lWM\nxX9vD6LklijrepMl55Omu0bhF5rRn2VAubfxKhJs0eQn69+NWaVUrNMQ078nF+8G\nKT6vH32q9i9fpV38XYlwM9qEa0il5wfrSwPuDd5vmGgk9AOlSEzY2vE1kvp7lEt1\nTdb3ZfAajPMO3Iov5dwvm0zhJDQHFo7SFi5jH0Pgk4bAd9HBmB8sioxL4Q==\n=Kwft\n-----END PGP PUBLIC KEY BLOCK-----", - "bits": 4096, - "uid": "Ada Lovelace \u003Cada@passbolt.com\u003E", - "key_id": "5D9B054F", - "fingerprint": "03F60E958F4CB29723ACDF761353B5B15D9B054F", - "type": "RSA", - "expires": null, - "key_created": "2015-08-09T12:48:31+00:00", - "deleted": false, - "created": "2020-02-21T10:39:28+00:00", - "modified": "2020-02-21T10:39:28+00:00" - }, - "is_mfa_enabled": false, - "last_logged_in": "" - }, { - "id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "role_id": TEST_ROLE_ADMIN_ID, - "username": "admin@passbolt.com", - "active": true, - "deleted": false, - "created": "2020-02-21T10:39:26+00:00", - "modified": "2020-02-21T10:39:26+00:00", - "profile": { - "id": "92ccfd1b-6eb8-5e1c-a022-cf22463e8361", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "first_name": "Admin", - "last_name": "User", - "created": "2020-02-21T10:39:26+00:00", - "modified": "2020-02-21T10:39:26+00:00", - "avatar": {"url": {"medium": "img\/avatar\/user_medium.png", "small": "img\/avatar\/user.png"}} - }, - "groups_users": [{ - "id": "03e26ff8-81d2-5b7f-87e4-99bbc40e1f95", - "group_id": "428ed4cd-81b1-56af-aa7f-a7cbdbe227e4", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "15b5e2c6-164a-50e9-a46f-2b4a9ab9345a", - "group_id": "c9c8fd8e-a0fa-53f0-967b-42edca3d91e4", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "15f486f6-4f5a-53f7-82ca-974e0be74e95", - "group_id": "4ff007f6-80ec-5bf7-8f0a-46a17178db6f", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "2510a118-c838-5470-a0dd-aff268d4a2b6", - "group_id": "516c2db6-0aed-52d8-854f-b3f3499995e7", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "8e42567e-6e6e-54bc-b17b-0f5afde5b01c", - "group_id": "3feba74f-47da-5146-9d8f-76c7266c60ea", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "99fabba9-e069-59e6-a3b6-775436322b21", - "group_id": "a89b771e-62ab-5434-b2fa-950827439ac7", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "9c937007-8d53-532d-b02f-80f100139990", - "group_id": "faa73142-fb5e-5891-8b9f-4a00b3836fad", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "ad80b164-c30f-53e0-aac1-3040fa2f136d", - "group_id": "f16c507f-9105-502e-aa8a-ba24c36dbdcf", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "c8f4bc84-2ea2-5509-8d6a-6b7378b7fffa", - "group_id": "5fe7a6af-d97e-54f1-a4fc-b4b8bdb6e2ac", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }, { - "id": "d100fc5d-6685-50aa-897b-87ac816e28c8", - "group_id": "b7cbce9f-6a20-545b-b20a-fcf4092307df", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "is_admin": true, - "created": "2020-02-21T10:39:29+00:00" - }], - "role": { - "id": TEST_ROLE_ADMIN_ID, - "name": "admin", - "description": "Organization administrator", - "created": "2012-07-04T13:39:25+00:00", - "modified": "2012-07-04T13:39:25+00:00" - }, - "gpgkey": { - "id": "91d8a7fd-3ab3-5e98-a4a5-0d8694ff23b9", - "user_id": "d57c10f5-639d-5160-9c81-8a0c6c4ec856", - "armored_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFY06pcBEADjYRuq05Zatu4qYtXmexbrwtUdakNJJHPlWxcusohdTLUmSXrt\n7LegXBE3OjvV9HbdBQfbpjitFp8eJw5krYQmh1+w\/UYjb5Jy\/A7ma3oawzbVwNpL\nwuAafYma5LLLloZD\/OpYKprhWfW9FHKyq6t+AcH5CFs\/HvixdrdbAO7K1\/z6mgWc\nT6HBP5\/dGTseAlrvUDTsW1kzo6qsrOWoUunrqm31umsvcfNROtDKM16zgZl+GlYY\n1BxNcRKr1\/AcZUrp4zdSSc6IXrYjJ+1kgHz\/ZoSrKn5QiqEn7wQEveJu+jNGSv8j\nMvQgjq+AmzveJ\/4f+RQirbe9JOeDgzX7NqloRil3I0FPFoivbRU0PHi4N2q7sN8e\nYpXxXzuL+OEq1GQe5fTsSotQTRZUJxbdUS8DfPckQaK79HoybTQAgA6mgQf\/C+U0\nX2TiBUzgBuhayiW12kHmKyK02htDeRNOYs4bBMdeZhAFm+5C74LJ3FGQOHe+\/o2o\nBktk0rAZScjizijzNzJviRB\/3nAJSBW6NSNYcbnosk0ET2osg2tLvzegRI6+NQJE\nb0EpByTMypUDhCNKgg5aEDUVWcq4iucps\/1e6\/2vg2XVB7xdphT4\/K44ZeBHdFuf\nhGQvs8rkAPzpkpsEWKgpTR+hdhbMmNiL984Ywk98nNuzgfkgpcP57xawNwARAQAB\ntCtQYXNzYm9sdCBEZWZhdWx0IEFkbWluIDxhZG1pbkBwYXNzYm9sdC5jb20+iQJO\nBBMBCgA4AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEDB0XYRENHjPJAG0a\nWxszLtBkJtMFAl0bmoYACgkQWxszLtBkJtPnxg\/\/Q9WOWUGf7VOnfbaIix3NGAON\nI7rgXuLFc1E0vG20XWT2+C6xGskFwjoJbiyDrbMYnILGn7vDIn3MSoITemLjtt97\n\/lEXK7AgbJEWQWF1lxpXm0nCvjJ6h+qatGK96ncjcua6ecUut10A\/CACpuqxfKOh\nD6CaM5l\/ksEDtwvrv2MIaVajuCvwg+yUx0I0rfAQv0YTXbJ5MRn1pvOo3c6n5Q0z\n5eu\/iiG0UNNIE3Tk5KpT02MTMv5ullpt3mtNjMHH0\/TdPxCtUKVh4q34x3syiYLe\npaddf5Ctv9CL52VWfsG3qFPHp7euOFY8lfzuemoqD9jcE7QIJnkCmwtLXLQrE0O2\nRW\/y\/oXqrETXu2sFyHMr1Xw\/\/QeJgIv63LBGmcPOj93VyHIlcUDarM2oq2+DXKxr\nDs2xfnFKVCZwpSvecIfKXUKsnX3AGrpetoZdfw0jAUVI3nt6YCu8KvczXxetfjOV\n3HHXa40gtOZk5OoKbfuTjzQlpc1oaDyLH8PT1GYsN3wWoDs4zulh6uKDpSt+4z58\nH1BfPFlrO2uhZSfk3E83uBQXZcABeXNxCdrTCJm8P90sbjLu1TlaeOnrWwVT7Yq8\ni8LE7lbAXnT1HjQlDi8GB2+2EnZZmOX+Z84a16jDElZazUNsE8zT7OmyjuB7GGDb\nQEFYzkb9dr1j1sukzty5Ag0EVjTqlwEQAJ37C9s4pq4jvcEF3bJgL+q3SBolgBIp\nN1g1\/woi9vEiOh+7e08Kr8mEhF04cpRDbhY6dcZ8OIXIQ99fgdNXfehlAWnI56NE\n\/FOIyif8TvGBfO6yE35fKSskwGNdUZWIZ0U0pxSXQvB+KEGWlq2c3Uf\/jhTZDnLN\nvfDjnYmn5ycp5sVWhtAmKFha9NJ6LGA0D1MC+jcCJCKtQRGgVvlqOESFDmQ7Pu8\/\nayr2BO0URHJ0Ob30lHluCnoKIv50qGpL9BYuGAdCfLBHXzRQhHIbfc\/cTPkK1kTX\nX5x\/MkiEl88TeGN+yjNVS7qqdxYgs+QYnDDZqevhWEvVyXVQjcCWSIHfjL1x5Ndq\nYL6+ci\/OxyIFoPs4K2umN3JPmpFi+fIPh2CexKy6BnyE8oAgNvgdDb6ZOfAtvShZ\nPM7QG4LZal2+nYp4n7gJRh6kepTQT\/4Bua0xOtRQhgcI4nGtcCxEDRMMzjqbGYlc\nnciMjsiMg9LPpWPDA+xKrRZKYwVFy8vLx\/alOz\/h1BZjx2u7YmuaGENxE62Lfyh0\nxeoCBDTdnWEOQTH6LVsomVtUO1FVap1t5jkYSdpxBuHf8\/2Ye7N3FTMRKe9n4e75\nsAJ00utnMl6P2Zca9mM4T29PK+LPFx2G2h35DQ7MbEid1cAZ8QVR3UyoiR8+u9jM\nek+9uFCm+nAxABEBAAGJAjYEGAEKACACGwwWIQQMHRdhEQ0eM8kAbRpbGzMu0GQm\n0wUCXRuamQAKCRBbGzMu0GQm004PD\/9sFmFkdoSqwU\/En77+h0gt4knlgZbgj0iR\nromnknIwLKBbJJXksHmMPXJB9b3WZ\/gGV3pPVtDWDKg3NZW4HLK13w3s3wQ2ViVV\nA6FzABDSkI3YBqkkasLRZU7oN9XajdFfph5wLhDSgTCjSncGfcjVzPugWKLqPPih\nZO6mpqxSFYEhx+p\/O80Tlj90UsOFRdot7cqn5wOhXZtKsQ0RwaA\/uq\/sFe6UNKHG\n2RBgQfoj5JbazJbvlgMiWxhBalwZKQWs8IBh\/4ag8AFwwoJN+gOtNM9C4UCHu+yt\n0Tv2\/Tu+Apcj0oyFaKJD4uQUmChQ2fDRysqJEIhee+yL29mrdcB4jG7Q2rt8HbhY\nwlsHKgas0YIHdR6dUOCiyw72i0khwrd2PDgxKRu5+cob6wMSqXbIIxFLLLACHy2s\nKd6fQcg8FxoivEiF0lRfMi32A\/YWGJ\/k1OoFCzW55KFXqqBMptYZWh2Jezhttmid\nYHPc7jas7HEPnw3SvVM0gYAcmEVWWvjKfUpOhSYYkk\/B71w9RuIpPyyI7G2XI8Db\nG2ttngDIOL8njS6ybU9Og6yTNUoHL1wWEZN1b3fznKHcC9lyr8MIg00QNeDItt9i\nILCOkjoEdUdauqlRIa+EmUu+AL+JobrlQTzyrCIm7aaT3Hp9EyaEx5xvJDWtmjgf\nFYNCFtV1fw==\n=amwR\n-----END PGP PUBLIC KEY BLOCK-----", - "bits": 4096, - "uid": "Passbolt Default Admin \u003Cadmin@passbolt.com\u003E", - "key_id": "D06426D3", - "fingerprint": "0C1D1761110D1E33C9006D1A5B1B332ED06426D3", - "type": "RSA", - "expires": null, - "key_created": "2015-10-31T16:21:43+00:00", - "deleted": false, - "created": "2020-02-21T10:39:28+00:00", - "modified": "2020-02-21T10:39:28+00:00" - }, - "is_mfa_enabled": false, - "last_logged_in": "" - } -]; - export const mockResultsResources = { 'passbolt.share.get-resources': resources, 'passbolt.keyring.get-public-key-info-by-user': gpgKey, diff --git a/src/react-extension/components/Share/ShareDialog.test.js b/src/react-extension/components/Share/ShareDialog.test.js index 41fb0f877..d1eca05e8 100644 --- a/src/react-extension/components/Share/ShareDialog.test.js +++ b/src/react-extension/components/Share/ShareDialog.test.js @@ -17,7 +17,6 @@ */ import ShareDialogPage from "./ShareDialog.test.page"; import { - autocompleteResult, defaultAppContext, defaultProps, folders, mockResultsFolders, @@ -27,6 +26,7 @@ import {ActionFeedbackContext} from "../../contexts/ActionFeedbackContext"; import PassboltApiFetchError from "../../../shared/lib/Error/PassboltApiFetchError"; import {waitFor} from "@testing-library/react"; import NotifyError from "../Common/Error/NotifyError/NotifyError"; +import {waitForTrue} from "../../../../test/utils/waitFor"; beforeAll(() => { global.scrollTo = jest.fn(); @@ -69,18 +69,19 @@ describe("As Lu I should see the share dialog", () => { expect(page.title).toBe('Share 3 resources'); expect(page.count).toBe(11); - const requestKeyInfoMockImpl = () => ({ - fingerprint: "079D6F4FDA3BFDC2D8E562D8AA44B1DA4BFB36B6" - }); + const requestBextMockImpl = (request, option) => { + switch (request) { + case "passbolt.keyring.get-public-key-info-by-user": + return {fingerprint: "079D6F4FDA3BFDC2D8E562D8AA44B1DA4BFB36B6"}; + case "passbolt.share.search-aros": + return context.users.filter(user => user.username.indexOf(option) !== -1); + } + }; - mockContextRequest(requestKeyInfoMockImpl); + mockContextRequest(requestBextMockImpl); await page.searchName("adm"); jest.runOnlyPendingTimers(); - await waitFor(() => { - if (!page.userOrGroupAutocomplete(1)) { - throw new Error("Page is not ready yet."); - } - }); + await waitForTrue(() => Boolean(page.userOrGroupAutocomplete(1))); await page.selectUserOrGroup(1); expect(page.warningMessage).toBe('Click save to apply your pending changes.'); @@ -114,21 +115,23 @@ describe("As Lu I should see the share dialog", () => { it('As LU I can remove a permission', async() => { expect.assertions(2); + await waitForTrue(() => page.count !== 3); expect(page.count).toBe(11); await page.selectRemovePermission(1); expect(page.count).toBe(10); }); it('As LU I should see a processing feedback while submitting the form', async() => { - const requestAutocompleteResultMockImpl = jest.fn(() => autocompleteResult); - mockContextRequest(requestAutocompleteResultMockImpl); + const requestBextMockImpl = (request, option) => { + switch (request) { + case "passbolt.share.search-aros": + return context.users.filter(user => user.username.indexOf(option) !== -1); + } + }; + mockContextRequest(requestBextMockImpl); await page.searchName("adm"); jest.runOnlyPendingTimers(); - await waitFor(() => { - if (!page.userOrGroupAutocomplete(1)) { - throw new Error("Page is not ready yet."); - } - }); + await waitForTrue(() => Boolean(page.userOrGroupAutocomplete(1))); await page.selectUserOrGroup(1); // Mock the request function to make it the expected result @@ -183,15 +186,16 @@ describe("As Lu I should see the share dialog", () => { it('As LU I should see an error dialog if the submit operation fails for an unexpected reason', async() => { expect.assertions(1); - const requestAutocompleteResultMockImpl = jest.fn(() => autocompleteResult); - mockContextRequest(requestAutocompleteResultMockImpl); + const requestBextMockImpl = (request, option) => { + switch (request) { + case "passbolt.share.search-aros": + return context.users.filter(user => user.username.indexOf(option) !== -1); + } + }; + mockContextRequest(requestBextMockImpl); await page.searchName("adm"); jest.runOnlyPendingTimers(); - await waitFor(() => { - if (!page.userOrGroupAutocomplete(1)) { - throw new Error("Page is not ready yet."); - } - }); + await waitForTrue(() => Boolean(page.userOrGroupAutocomplete(1))); await page.selectUserOrGroup(1); // Mock the request function to make it return an error. @@ -229,15 +233,16 @@ describe("As Lu I should see the share dialog", () => { expect(page.subtitle).toBe('apache'); expect(page.count).toBe(11); - const requestAutocompleteResultMockImpl = jest.fn(() => autocompleteResult); - mockContextRequest(requestAutocompleteResultMockImpl); + const requestBextMockImpl = (request, option) => { + switch (request) { + case "passbolt.share.search-aros": + return context.users.filter(user => user.username.indexOf(option) !== -1); + } + }; + mockContextRequest(requestBextMockImpl); await page.searchName("adm"); jest.runOnlyPendingTimers(); - await waitFor(() => { - if (!page.userOrGroupAutocomplete(1)) { - throw new Error("Page is not ready yet."); - } - }); + await waitForTrue(() => Boolean(page.userOrGroupAutocomplete(1))); await page.selectUserOrGroup(1); expect(page.count).toBe(12); @@ -289,13 +294,19 @@ describe("As Lu I should see the share dialog", () => { }); mockContextRequest(requestKeyInfoMockImpl); + const requestBextMockImpl = (request, option) => { + switch (request) { + case "passbolt.keyring.get-public-key-info-by-user": + return {fingerprint: "079D6F4FDA3BFDC2D8E562D8AA44B1DA4BFB36B6"}; + case "passbolt.share.search-aros": + return context.users.filter(user => user.username.indexOf(option) !== -1); + } + }; + mockContextRequest(requestBextMockImpl); + await page.searchName("ad"); jest.runOnlyPendingTimers(); - await waitFor(() => { - if (!page.userOrGroupAutocomplete(2)) { - throw new Error("Page is not ready yet."); - } - }); + await waitForTrue(() => Boolean(page.userOrGroupAutocomplete(1))); await page.selectUserOrGroup(2); expect(page.count).toBe(3); @@ -337,15 +348,16 @@ describe("As Lu I should see the share dialog", () => { expect(page.exists()).toBeTruthy(); expect(page.title).toBe('Share 2 items'); - const requestAutocompleteResultMockImpl = jest.fn(() => autocompleteResult); - mockContextRequest(requestAutocompleteResultMockImpl); + const requestBextMockImpl = (request, option) => { + switch (request) { + case "passbolt.share.search-aros": + return context.users.filter(user => user.username.indexOf(option) !== -1); + } + }; + mockContextRequest(requestBextMockImpl); await page.searchName("adm"); jest.runOnlyPendingTimers(); - await waitFor(() => { - if (!page.userOrGroupAutocomplete(1)) { - throw new Error("Page is not ready yet."); - } - }); + await waitForTrue(() => Boolean(page.userOrGroupAutocomplete(1))); await page.selectUserOrGroup(1); await page.savePermissions(); diff --git a/src/react-extension/components/Share/SharePermissionItem.js b/src/react-extension/components/Share/SharePermissionItem.js index 95422148b..1bce2693a 100644 --- a/src/react-extension/components/Share/SharePermissionItem.js +++ b/src/react-extension/components/Share/SharePermissionItem.js @@ -149,7 +149,7 @@ class SharePermissionItem extends Component { * @returns {*} */ hasGpgKey() { - return this.state.gpgKey && this.state.gpgKey.fingerprint; + return this.state.gpgKey?.fingerprint; } getClassName() { From 0aa6af2d56bf49e7c19657d16f5e6e492f3b4f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Thu, 20 Jun 2024 07:16:27 +0000 Subject: [PATCH 46/67] PB-33798 - As a signed in user when I open the information section of the... --- .../DisplayResourceDetailsInformation.js | 68 ++++++++++++++----- .../DisplayResourceDetailsInformation.test.js | 41 ++++++++--- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js index 3168e5ee8..17bc0bcfa 100644 --- a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js +++ b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js @@ -53,7 +53,9 @@ class DisplayResourceDetailsInformation extends React.Component { return { open: true, previewedSecret: null, // The type of previewed secret - plaintextSecretDto: null // The current resource password decrypted + plaintextSecretDto: null, // The current resource password decrypted + creator: null, // the data of the resource creator + modifier: null, // the data of the resource creator }; } @@ -73,6 +75,9 @@ class DisplayResourceDetailsInformation extends React.Component { componentDidMount() { this.props.passwordExpiryContext.findSettings(); + if (this.state.open) { + this.loadUserInformation(); + } } /** @@ -83,6 +88,26 @@ class DisplayResourceDetailsInformation extends React.Component { this.handleResourceChange(prevProps.resourceWorkspaceContext.details.resource); } + /** + * Loads the information about the creator and the modifier of the current selected resource. + * @returns {Promise} + */ + async loadUserInformation() { + const resourceInformation = await this.props.context.port.request("passbolt.resources.find-details", this.resource.id); + const hasInformationChanged = this.resource.created_by !== resourceInformation.created_by + || this.resource.modified_by !== resourceInformation.modified_by; + + if (hasInformationChanged) { + //current selected resource might have changed and the information received doesn't match anymore. In such case we don't update the state. + return; + } + + this.setState({ + creator: resourceInformation?.creator, + modifier: resourceInformation?.modifier, + }); + } + /** * Check if the resource has changed and fetch * @param previousResource @@ -93,6 +118,22 @@ class DisplayResourceDetailsInformation extends React.Component { if ((hasResourceChanged || hasResourceUpdated) && this.state.open) { this.setState({plaintextSecretDto: null, previewedSecret: null}); } + + if (!hasResourceChanged) { + return; + } + + const hasModifierOrCreatorChanged = this.resource.created_by !== previousResource.created_by + || this.resource.modified_by !== previousResource.modified_by; + + if (!hasModifierOrCreatorChanged) { + return; + } + + this.setState({creator: null, modifier: null}); + if (this.state.open) { + this.loadUserInformation(); + } } /** @@ -134,6 +175,12 @@ class DisplayResourceDetailsInformation extends React.Component { handleTitleClickEvent() { const open = !this.state.open; this.setState({open}); + + if (!open) { + this.setState({creator: null, modifier: null, plaintextSecretDto: null, previewedSecret: null}); + } else { + this.loadUserInformation(); + } } /** @@ -144,21 +191,6 @@ class DisplayResourceDetailsInformation extends React.Component { this.displaySuccessNotification(this.translate("The username has been copied to clipboard")); } - /** - * Get a user username - * @param {string} userId The user id - */ - getUserUsername(userId) { - if (this.props.context.users) { - const user = this.props.context.users.find(item => item.id === userId); - if (user) { - return user.username; - } - } - - return ""; - } - /** * Get the folder name. * @param {string} folderId The folder id @@ -485,8 +517,8 @@ class DisplayResourceDetailsInformation extends React.Component { const canUsePasswordExpiry = this.props.passwordExpiryContext.isFeatureEnabled(); const canCopySecret = this.props.rbacContext.canIUseUiAction(uiActions.SECRETS_COPY); - const creatorUsername = this.getUserUsername(this.resource.created_by); - const modifierUsername = this.getUserUsername(this.resource.modified_by); + const creatorUsername = this.state.creator?.username || ""; + const modifierUsername = this.state.modifier?.username || ""; const createdDateTimeAgo = formatDateTimeAgo(this.resource.created, this.props.t, this.props.context.locale); const modifiedDateTimeAgo = formatDateTimeAgo(this.resource.modified, this.props.t, this.props.context.locale); const isPasswordPreviewed = this.isPasswordPreviewed(); diff --git a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.test.js b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.test.js index 771e57233..a9cdbab8e 100644 --- a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.test.js +++ b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.test.js @@ -28,16 +28,21 @@ import {defaultUserAppContext} from "../../../contexts/ExtAppContext.test.data"; import {TotpCodeGeneratorService} from "../../../../shared/services/otp/TotpCodeGeneratorService"; import {defaultTotpViewModelDto} from "../../../../shared/models/totp/TotpDto.test.data"; -beforeEach(() => { - jest.resetModules(); -}); - describe("DisplayResourceDetailsInformation", () => { - let page; // The page to test against - const props = defaultProps(); // The props to pass + let page, props; const mockContextRequest = implementation => jest.spyOn(props.context.port, 'request').mockImplementation(implementation); const copyClipboardMockImpl = jest.fn((message, data) => data); + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + props = defaultProps(); // The props to pass + + const user = props.context.users[0]; + const resourceWithContain = Object.assign({}, props.resourceWorkspaceContext.details.resource, {creator: user, modifier: user}); + props.context.port.addRequestListener("passbolt.resources.find-details", async() => resourceWithContain); + }); + /** * Given a selected resource having information * When I open the “Information” section of the secondary sidebar @@ -63,6 +68,7 @@ describe("DisplayResourceDetailsInformation", () => { const absoluteCreationDate = props.resourceWorkspaceContext.details.resource.created; const creationDate = DateTime.fromISO(absoluteCreationDate).toRelative(); expect.assertions(22); + await waitFor(() => {}); expect(page.displayInformationList.usernameLabel).toBe('Username'); expect(page.displayInformationList.username.textContent).toBe(props.resourceWorkspaceContext.details.resource.username); expect(page.displayInformationList.passwordLabel).toBe('Password'); @@ -96,6 +102,9 @@ describe("DisplayResourceDetailsInformation", () => { it('I cannot see the folder a resource is contained in if disbaled by RBAC', async() => { const props = propsWithDenyUiAction(); + const user = props.context.users[0]; + const resourceWithContain = Object.assign({}, props.resourceWorkspaceContext.details.resource, {creator: user, modifier: user}); + props.context.port.addRequestListener("passbolt.resources.find-details", async() => resourceWithContain); page = new DisplayResourceDetailsInformationPage(props); expect.assertions(1); expect(page.displayInformationList.location).toBeNull(); @@ -107,6 +116,9 @@ describe("DisplayResourceDetailsInformation", () => { isFeatureEnabled: () => false } }); + const user = props.context.users[0]; + const resourceWithContain = Object.assign({}, props.resourceWorkspaceContext.details.resource, {creator: user, modifier: user}); + props.context.port.addRequestListener("passbolt.resources.find-details", async() => resourceWithContain); page = new DisplayResourceDetailsInformationPage(props); expect.assertions(1); @@ -118,11 +130,7 @@ describe("DisplayResourceDetailsInformation", () => { it('AS LU, I should be able to copy the username of a resource to clipboard', async() => { expect.assertions(3); page = new DisplayResourceDetailsInformationPage(props); - await waitFor(() => { - }); - mockContextRequest(copyClipboardMockImpl); - jest.spyOn(ActionFeedbackContext._currentValue, 'displaySuccess').mockImplementation(() => { - }); + jest.spyOn(ActionFeedbackContext._currentValue, 'displaySuccess').mockImplementation(() => {}); await page.displayInformationList.click(page.displayInformationList.username); @@ -157,6 +165,9 @@ describe("DisplayResourceDetailsInformation", () => { it('AS LU, I cannot copy secret of resource if denied by RBAC', async() => { const props = propsWithDenyUiAction(); + const user = props.context.users[0]; + const resourceWithContain = Object.assign({}, props.resourceWorkspaceContext.details.resource, {creator: user, modifier: user}); + props.context.port.addRequestListener("passbolt.resources.find-details", async() => resourceWithContain); page = new DisplayResourceDetailsInformationPage(props); await waitFor(() => {}); @@ -199,6 +210,10 @@ describe("DisplayResourceDetailsInformation", () => { } }); const props = defaultProps({context}); + const user = props.context.users[0]; + const resourceWithContain = Object.assign({}, props.resourceWorkspaceContext.details.resource, {creator: user, modifier: user}); + props.context.port.addRequestListener("passbolt.resources.find-details", async() => resourceWithContain); + page = new DisplayResourceDetailsInformationPage(props); await waitFor(() => {}); @@ -208,6 +223,10 @@ describe("DisplayResourceDetailsInformation", () => { it('AS LU, I cannot preview secret of resource if denied by RBAC', async() => { const props = propsWithDenyUiAction(); + const user = props.context.users[0]; + const resourceWithContain = Object.assign({}, props.resourceWorkspaceContext.details.resource, {creator: user, modifier: user}); + props.context.port.addRequestListener("passbolt.resources.find-details", async() => resourceWithContain); + page = new DisplayResourceDetailsInformationPage(props); await waitFor(() => {}); From c7a248fdb0b1b8cd20160d9d9b3ae4dffad17651 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Thu, 20 Jun 2024 09:27:47 +0200 Subject: [PATCH 47/67] PB-33824 As a user I should not see other dialog open except the session expired --- .../HandleSessionExpired/HandleSessionExpired.js | 2 ++ .../HandleSessionExpired/HandleSessionExpired.test.data.js | 6 +++--- .../HandleSessionExpired/HandleSessionExpired.test.js | 3 ++- src/react-extension/contexts/DialogContext.js | 3 ++- src/react-extension/contexts/DialogContext.test.data.js | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.js b/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.js index c7ffff903..75ca62aeb 100644 --- a/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.js +++ b/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.js @@ -46,6 +46,8 @@ class HandleSessionExpired extends React.Component { * Handle the session expired event */ handleSessionExpiredEvent() { + // Close all dialogs before to open the session expired dialog + this.props.dialogContext.closeAll(); this.props.dialogContext.open(NotifyExpiredSession); } diff --git a/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.data.js b/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.data.js index 86bb4dcf5..63f9f549a 100644 --- a/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.data.js +++ b/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.data.js @@ -11,6 +11,8 @@ * @link https://www.passbolt.com Passbolt(tm) */ +import {defaultDialogContext} from "../../../contexts/DialogContext.test.data"; + /** * Default props * @returns {{}} @@ -20,9 +22,7 @@ export function defaultProps(props = {}) { context: { onExpiredSession: jest.fn(callback => callback()) }, - dialogContext: { - open: jest.fn() - } + dialogContext: defaultDialogContext() }; return Object.assign(defaultProps, props); } diff --git a/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.js b/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.js index d194d24a9..95613be71 100644 --- a/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.js +++ b/src/react-extension/components/Authentication/HandleSessionExpired/HandleSessionExpired.test.js @@ -35,8 +35,9 @@ describe("HandleSessionExpired", () => { }); it('As LU I should add a callback for session expired in the app context', () => { - expect.assertions(2); + expect.assertions(3); expect(props.context.onExpiredSession).toHaveBeenCalledWith(expect.any(Function)); + expect(props.dialogContext.closeAll).toHaveBeenCalled(); expect(props.dialogContext.open).toHaveBeenCalledWith(NotifyExpiredSession); }); }); diff --git a/src/react-extension/contexts/DialogContext.js b/src/react-extension/contexts/DialogContext.js index 5587d426e..6ec52444d 100644 --- a/src/react-extension/contexts/DialogContext.js +++ b/src/react-extension/contexts/DialogContext.js @@ -50,7 +50,8 @@ export default class DialogContextProvider extends React.Component { this.setState({dialogs: [...this.state.dialogs, {key: dialogKey, Dialog, DialogProps}]}); return dialogKey; }, - close: dialogKey => this.setState({dialogs: this.state.dialogs.filter(dialog => dialogKey !== dialog.key)}) + close: dialogKey => this.setState({dialogs: this.state.dialogs.filter(dialog => dialogKey !== dialog.key)}), + closeAll: () => this.setState({dialogs: []}) }; } diff --git a/src/react-extension/contexts/DialogContext.test.data.js b/src/react-extension/contexts/DialogContext.test.data.js index 3e58cd14d..dadd4d48a 100644 --- a/src/react-extension/contexts/DialogContext.test.data.js +++ b/src/react-extension/contexts/DialogContext.test.data.js @@ -21,7 +21,8 @@ export function defaultDialogContext(context = {}) { const defaultContext = { dialogs: [], open: jest.fn(), - close: jest.fn() + close: jest.fn(), + closeAll: jest.fn(), }; return Object.assign(defaultContext, context); } From cf081caf0b363b0c8a602596c912eac57a9380da Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 21 Jun 2024 08:57:21 +0200 Subject: [PATCH 48/67] PB-33825 - Upgrade vulnerable library ws --- package-lock.json | 14 +++++++------- package.json | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) mode change 100755 => 100644 package.json diff --git a/package-lock.json b/package-lock.json index 9b006523a..40f9b489d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15263,9 +15263,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -23629,7 +23629,7 @@ "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", - "ws": "^8.11.0", + "ws": "8.17.1", "xml-name-validator": "^4.0.0" }, "dependencies": { @@ -26592,9 +26592,9 @@ } }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 28e281db7..4da46ccd6 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ }, "glob-stream": { "glob-parent": "5.1.2" - } + }, + "ws": "8.17.1" }, "scripts": { "build": "npm run build-api-app", From 0bb59beb2cdb397ceefc69df5e6f0065c06c8b25 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Fri, 21 Jun 2024 14:32:48 +0200 Subject: [PATCH 49/67] PB-33833 As a user I should not see a grid size issue after a browser update --- src/shared/components/Table/Context/TableContext.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/shared/components/Table/Context/TableContext.js b/src/shared/components/Table/Context/TableContext.js index 7c5941948..439e3652b 100644 --- a/src/shared/components/Table/Context/TableContext.js +++ b/src/shared/components/Table/Context/TableContext.js @@ -153,9 +153,12 @@ export default class TableContextProvider extends Component { * Handle window resize event */ handleWindowResizeEvent() { - this.setColumnsWidthFromActualWidth(this.state.tableviewWidth); - // Debounce the function to store the new columns width - this.handleChangeColumnsDebounced(); + // Prevent wrong calculation if the tableviewWidth is not set + if (this.state.tableviewWidth !== null) { + this.setColumnsWidthFromActualWidth(this.state.tableviewWidth); + // Debounce the function to store the new columns width + this.handleChangeColumnsDebounced(); + } } /** From 14b96be5e786ae0d308c241b90d1ee7db780cb43 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Mon, 24 Jun 2024 07:58:11 +0200 Subject: [PATCH 50/67] PB-23294 As LU I should not see a comment overlapping --- src/less/components/comments.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/less/components/comments.less b/src/less/components/comments.less index d8cf64135..3e0ac6d98 100644 --- a/src/less/components/comments.less +++ b/src/less/components/comments.less @@ -28,6 +28,7 @@ margin-left: 1.6rem; margin-top: 0; border-radius: .3rem; + overflow-wrap: break-word; } /* picture */ From 2e08eef69684b09777e6d98d958f5eedfd1bd408 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 25 Jun 2024 11:13:19 +0200 Subject: [PATCH 51/67] PB-33816 - Follow-up: in the information section the location icon folder should be the icon \"shared\" if relevant --- .../DisplayResourceDetailsInformation.js | 23 +++++++++++++++++-- ...DisplayResourceFolderDetailsInformation.js | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js index 17bc0bcfa..361960cd5 100644 --- a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js +++ b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js @@ -71,6 +71,7 @@ class DisplayResourceDetailsInformation extends React.Component { this.handleTotpClick = this.handleTotpClick.bind(this); this.handlePreviewTotpButtonClick = this.handlePreviewTotpButtonClick.bind(this); this.handleGoToResourceUriClick = this.handleGoToResourceUriClick.bind(this); + this.isFolderParentShared = this.isFolderParentShared.bind(this); } componentDidMount() { @@ -101,7 +102,6 @@ class DisplayResourceDetailsInformation extends React.Component { //current selected resource might have changed and the information received doesn't match anymore. In such case we don't update the state. return; } - this.setState({ creator: resourceInformation?.creator, modifier: resourceInformation?.modifier, @@ -211,6 +211,25 @@ class DisplayResourceDetailsInformation extends React.Component { return ""; } + /** + * Check if folder parent is shared + * @returns {boolean} + */ + isFolderParentShared() { + let isShared = false; + console.log(this.props.context.folders) + console.log(this.resource.folder_parent_id) + + if (this.resource.folder_parent_id !== null && this.props.context.folders) { + const folder = this.props.context.folders.find(item => item.id === this.resource.folder_parent_id); + if (folder) { + return !folder.personal; + } + } + + return isShared; + } + /** * Handle copy password click. */ @@ -623,7 +642,7 @@ class DisplayResourceDetailsInformation extends React.Component { Location diff --git a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js index c50be3481..f9c28f29c 100644 --- a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js +++ b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js @@ -162,7 +162,7 @@ class DisplayResourceFolderDetailsInformation extends React.Component { Location From 1b3dd84a8631679d84fa26a5e337081268127545 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 25 Jun 2024 13:21:09 +0200 Subject: [PATCH 52/67] PB-33816 - root should not have shared icon --- .../DisplayResourceFolderDetailsInformation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js index f9c28f29c..ebf050c17 100644 --- a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js +++ b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js @@ -162,7 +162,7 @@ class DisplayResourceFolderDetailsInformation extends React.Component { Location From 90ecd8606f5ab196344e7b3a211ec03c27f4955d Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 25 Jun 2024 13:27:43 +0200 Subject: [PATCH 53/67] PB-33816 - fix lint --- .../DisplayResourceDetailsInformation.js | 6 ++---- .../DisplayResourceFolderDetailsInformation.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js index 361960cd5..057f25592 100644 --- a/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js +++ b/src/react-extension/components/ResourceDetails/DisplayResourceDetails/DisplayResourceDetailsInformation.js @@ -216,9 +216,7 @@ class DisplayResourceDetailsInformation extends React.Component { * @returns {boolean} */ isFolderParentShared() { - let isShared = false; - console.log(this.props.context.folders) - console.log(this.resource.folder_parent_id) + const isShared = false; if (this.resource.folder_parent_id !== null && this.props.context.folders) { const folder = this.props.context.folders.find(item => item.id === this.resource.folder_parent_id); @@ -226,7 +224,7 @@ class DisplayResourceDetailsInformation extends React.Component { return !folder.personal; } } - + return isShared; } diff --git a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js index ebf050c17..b08c1b61d 100644 --- a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js +++ b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js @@ -162,7 +162,7 @@ class DisplayResourceFolderDetailsInformation extends React.Component { Location From a29a31bdbff52fbfc8be4bae821f290ef84aa124 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 25 Jun 2024 14:23:43 +0200 Subject: [PATCH 54/67] PB-33857 Get folder hierarchy from resourceWorkspaceContext --- .../contexts/ResourceWorkspaceContext.js | 39 ++++++++++++++++++- .../contexts/ResourceWorkspaceContext.test.js | 9 +++++ .../ResourceWorkspaceContext.test.page.js | 8 ++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.js b/src/react-extension/contexts/ResourceWorkspaceContext.js index 223552bc0..c3907754c 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.js @@ -84,6 +84,7 @@ export const ResourceWorkspaceContext = React.createContext({ onGoToResourceUriRequested: () => {}, // Whenever the users wants to follow a resource uri onChangeColumnView: () => {}, // Whenever the users wants to show or hide a column onChangeColumnsSettings: () => {}, // Whenever the user change the columns configuration + getHierarchyFolderCache: () => {}, // Whenever the need to get folder hierarchy }); /** @@ -158,7 +159,8 @@ export class ResourceWorkspaceContextProvider extends React.Component { onResourcesToExport: this.handleResourcesToExportChange.bind(this), // Whenever resources and/or folder have to be exported onGoToResourceUriRequested: this.onGoToResourceUriRequested.bind(this), // Whenever the users wants to follow a resource uri onChangeColumnView: this.handleChangeColumnView.bind(this), // Whenever the users wants to show or hide a column - onChangeColumnsSettings: this.handleChangeColumnsSettings.bind(this) // Whenever the user change the columns configuration + onChangeColumnsSettings: this.handleChangeColumnsSettings.bind(this), // Whenever the user change the columns configuration + getHierarchyFolderCache: this.getHierarchyFolderCache.bind(this) // Whenever the need to get folder hierarchy }; } @@ -169,6 +171,7 @@ export class ResourceWorkspaceContextProvider extends React.Component { this.resources = null; // A cache of the last known list of resources from the App context this.folders = null; // A cache of the last known list of folders from the App context this.foldersMapById = {}; // A cache of the last known list of folders map by ID from the App context + this.hierarchyFolderCache = {}; // A cache of the last known list of folders hierarchy by ID from the App context } /** @@ -248,6 +251,7 @@ export class ResourceWorkspaceContextProvider extends React.Component { result[folder.id] = folder; return result; }, {}); + this.hierarchyFolderCache = {}; await this.refreshSearchFilter(); await this.updateDetails(); } @@ -1170,6 +1174,39 @@ export class ResourceWorkspaceContextProvider extends React.Component { await this.gridResourceUserSetting.setSetting(gridUserSettingEntity); } + /** + * Get the hierarchy of a folder by ID in cache + * @param {string} id The id of the folder + * @returns {*[]} + */ + getHierarchyFolderCache(id) { + if (typeof this.hierarchyFolderCache[id] === "undefined") { + this.hierarchyFolderCache[id] = this.getHierarchyFolder(id); + } + return this.hierarchyFolderCache[id]; + } + + /** + * Get the hierarchy of a folder by ID in cache + * @param {string} id The id of the folder + * @returns {*[]} + */ + getHierarchyFolder(id) { + const hierarchy = []; + let currentFolderId = id; + while (currentFolderId) { + const folder = this.foldersMapById[currentFolderId]; + // Prevent issue if foldersMapById is not loaded yet + if (!folder) { + return hierarchy; + } + hierarchy.unshift(folder); + currentFolderId = folder.folder_parent_id; + } + return hierarchy; + } + + /** * Render the component * @returns {JSX} diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.js index 49fb98843..43f4370f0 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.js @@ -397,4 +397,13 @@ describe("Resource Workspace Context", () => { expect(page.columnsResourceSetting.toDto()).toStrictEqual(mergedColumnsSetting); }); }); + + describe("As LU I should be able to get the folder hierarchy", () => { + it("As LU I should be able to show a resource column", async() => { + expect.assertions(1); + await page.goToAllItems(); + const hierarchy = page.getHierarchyFolderCache(context.folders[1].id); + expect(hierarchy.length).toStrictEqual(2); + }); + }); }); diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.page.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.page.js index ec66225b2..fdef230c8 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.page.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.page.js @@ -275,6 +275,14 @@ export default class ResourceWorkspaceContextPage { await this.resourceWorkspaceContext.onChangeColumnsSettings(columnsSetting); } + /** + * get the folder hierarchy + * @param {string} id The id of the folder + */ + getHierarchyFolderCache(id) { + return this.resourceWorkspaceContext.getHierarchyFolderCache(id); + } + /** * Returns the rendering of the page * @param appContext a app context From 057c6396cd4b5c09a561e2cb46f6aedda8e44e1e Mon Sep 17 00:00:00 2001 From: Pierre Colart Date: Wed, 26 Jun 2024 12:09:21 +0000 Subject: [PATCH 55/67] Feature/pb 33816 follow up in the information section the location icon folder should be the icon shared if relevant --- ...DisplayResourceFolderDetailsInformation.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js index b08c1b61d..c9dcf18cb 100644 --- a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js +++ b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js @@ -110,6 +110,24 @@ class DisplayResourceFolderDetailsInformation extends React.Component { return ""; } + + /** + * Check if folder parent is shared + * @returns {boolean} + */ + isFolderParentShared() { + const isShared = false; + + if (this.folder.folder_parent_id !== null && this.props.context.folders) { + const folder = this.props.context.folders.find(item => item.id === this.folder.folder_parent_id); + if (folder) { + return !folder.personal; + } + } + + return isShared; + } + /** * Render the component * @returns {JSX} @@ -162,7 +180,7 @@ class DisplayResourceFolderDetailsInformation extends React.Component { Location From e330a6a056508e5dea150e6a4013161b824e4623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 26 Jun 2024 13:21:29 +0000 Subject: [PATCH 56/67] PB-33815 - Selecting a group should not trigger a refresh of the local storage... --- .../contexts/ResourceWorkspaceContext.js | 10 +++++++--- .../contexts/ResourceWorkspaceContext.test.js | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.js b/src/react-extension/contexts/ResourceWorkspaceContext.js index 223552bc0..5ff25ea6c 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.js @@ -229,9 +229,13 @@ export class ResourceWorkspaceContextProvider extends React.Component { const hasFilterChanged = !this.isFilterEqual(previousFilter, this.state.filter); if (hasFilterChanged) { // Avoid a side-effect whenever one inputs a specific resource url (it unselect the resource otherwise ) - const isNotNonePreviousFilter = previousFilter.type !== ResourceWorkspaceFilterTypes.NONE; - if (isNotNonePreviousFilter) { - await this.unselectAll(); + const isNonePreviousFilter = previousFilter.type === ResourceWorkspaceFilterTypes.NONE; + if (isNonePreviousFilter) { + return; + } + await this.unselectAll(); + + if (this.state.filter.type !== ResourceWorkspaceFilterTypes.GROUP) { await this.populateDebounced(); } } diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.js index 49fb98843..bf6412716 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.js @@ -158,12 +158,16 @@ describe("Resource Workspace Context", () => { it("AS LU I should have resources belonged to a group when the filter is GROUP", async() => { const mockGroupResources = context.resources.slice(0, 3); - const expectedResourcesCount = 3; + const expectedResourcesCount = mockGroupResources.length; const leadershipTeamGroup = {group: {id: "516c2db6-0aed-52d8-854f-b3f3499995e7"}}; - mockContextRequest(context, (path, args) => { - const isGroupResourcesRequest = path === "passbolt.resources.find-all" && args.filters; - return isGroupResourcesRequest ? mockGroupResources : context.port.request; + + context.port.addRequestListener("passbolt.resources.find-all", async() => mockGroupResources); + context.port.addRequestListener("passbolt.resources.update-local-storage", async() => { + if (page.filter.type === ResourceWorkspaceFilterTypes.GROUP) { + throw new Error("'passbolt.resources.update-local-storage' should have been called after filtering by GROUP"); + } }); + await page.goToAllItems(); await page.goToGroup(leadershipTeamGroup); expect(page.filteredResources).toHaveLength(expectedResourcesCount); From 09f11996eb203358f51e7b14a5941fe7ad1d46f5 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Tue, 25 Jun 2024 16:33:01 +0200 Subject: [PATCH 57/67] PB-33853 As a signed-in user I should see location in grid --- src/less/components/tableview.less | 6 ++ .../DisplayResourcesList.js | 27 ++++++ .../DisplayResourcesList.test.data.js | 14 ++- .../DisplayResourcesList.test.js | 30 +++++- .../DisplayResourcesList.test.page.js | 8 ++ .../contexts/ResourceWorkspaceContext.js | 26 ++++- .../ResourceWorkspaceContext.test.data.js | 10 +- .../contexts/ResourceWorkspaceContext.test.js | 15 +-- src/shared/components/Table/CellLocation.js | 95 +++++++++++++++++++ .../models/column/ColumnLocationModel.js | 38 ++++++++ src/shared/models/column/ColumnModel.js | 1 + .../columnsResourceSettingCollection.js | 3 +- 12 files changed, 253 insertions(+), 20 deletions(-) create mode 100644 src/shared/components/Table/CellLocation.js create mode 100644 src/shared/models/column/ColumnLocationModel.js diff --git a/src/less/components/tableview.less b/src/less/components/tableview.less index 12a906ccc..9fdfd2e7d 100644 --- a/src/less/components/tableview.less +++ b/src/less/components/tableview.less @@ -342,5 +342,11 @@ } } } + + td.cell-location { + span.caret { + margin: 0 .5rem; + } + } } } diff --git a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.js b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.js index 92f9ecde1..128679a34 100644 --- a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.js +++ b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.js @@ -54,6 +54,8 @@ import {withPasswordExpiry} from "../../../contexts/PasswordExpirySettingsContex import CellDate from "../../../../shared/components/Table/CellDate"; import CellExpiryDate from "../../../../shared/components/Table/CellExpiryDate"; import CellHeaderDefault from "../../../../shared/components/Table/CellHeaderDefault"; +import ColumnLocationModel from "../../../../shared/models/column/ColumnLocationModel"; +import CellLocation from "../../../../shared/components/Table/CellLocation"; /** * This component allows to display the filtered resources into a grid @@ -114,6 +116,7 @@ class DisplayResourcesList extends React.Component { this.getPreviewTotp = this.getPreviewTotp.bind(this); this.isPasswordResources = this.isPasswordResources.bind(this); this.isTotpResources = this.isTotpResources.bind(this); + this.handleLocationClick = this.handleLocationClick.bind(this); } /** @@ -137,6 +140,9 @@ class DisplayResourcesList extends React.Component { } this.defaultColumns.push(new ColumnUriModel({cellRenderer: {component: CellLink, props: {onClick: this.handleGoToResourceUriClick}}, headerCellRenderer: {component: CellHeaderDefault, props: {label: this.translate("URI")}}})); this.defaultColumns.push(new ColumnModifiedModel({cellRenderer: {component: CellDate, props: {locale: this.props.context.locale, t: this.props.t}}, headerCellRenderer: {component: CellHeaderDefault, props: {label: this.translate("Modified")}}})); + if (this.canUseFolders) { + this.defaultColumns.push(new ColumnLocationModel({getValue: resource => this.props.resourceWorkspaceContext.getHierarchyFolderCache(resource.folder_parent_id), cellRenderer: {component: CellLocation, props: {onClick: this.handleLocationClick, t: this.props.t}}, headerCellRenderer: {component: CellHeaderDefault, props: {label: this.translate("Location")}}})); + } } /** @@ -214,6 +220,15 @@ class DisplayResourcesList extends React.Component { this.listRef = React.createRef(); } + /** + * Check if the user can use folders. + * @returns {boolean} + */ + get canUseFolders() { + return this.props.context.siteSettings.canIUse("folders") + && this.props.rbacContext.canIUseUiAction(uiActions.FOLDERS_USE); + } + /** * Handle the All resources selection * @param event The DOM event @@ -669,6 +684,18 @@ class DisplayResourcesList extends React.Component { } } + /** + * Handle the user click on location folder from the grid. + */ + handleLocationClick(folderId) { + if (folderId) { + this.props.history.push(`/app/folders/view/${folderId}`); + } else { // Case of root folder + const filter = {type: ResourceWorkspaceFilterTypes.ROOT_FOLDER}; + this.props.history.push(`/app/passwords`, {filter}); + } + } + /** * Display success notification (toaster) * @param message diff --git a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.data.js b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.data.js index dc6097550..bb3a6ac45 100644 --- a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.data.js +++ b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.data.js @@ -100,7 +100,19 @@ export function propsWithFilteredResourcesAndColumnsHidden(data = {}) { {id: "password", label: "Password", position: 6, show: true}, {id: "totp", label: "TOTP", position: 7, show: false}, {id: "uri", label: "URI", position: 8, show: true}, - {id: "modified", label: "Modified", position: 9, show: false}]), + {id: "modified", label: "Modified", position: 9, show: false}, + {id: "location", label: "Location", position: 10, show: true}]), + getHierarchyFolderCache: () => [{ + "id": "9e03fd73-04c0-5514-95fa-1a6cf2c7c093", + "name": "Accounting", + "folder_parent_id": null, + "personal": false + }, { + "id": "6592f71b-8874-5e91-bf6d-829b8ad188f5", + "name": "Bank", + "folder_parent_id": "9e03fd73-04c0-5514-95fa-1a6cf2c7c093", + "personal": false + }], }), ...data }); diff --git a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js index 3b932cdec..a07d42385 100644 --- a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js +++ b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js @@ -331,16 +331,38 @@ describe("Display Resources", () => { }); describe('As LU, I should open the uri of a resource.', () => { - it('As LU, I should be able to follow the uri of a resource', async() => { + it('As LU, I should be able to follow the uri of a resource', async () => { const props = propsWithFilteredResources(); const page = new DisplayResourcesListPage(props); - await waitFor(() => {}); - jest.spyOn(props.resourceWorkspaceContext, 'onGoToResourceUriRequested').mockImplementationOnce(() => {}); + await waitFor(() => { + }); + jest.spyOn(props.resourceWorkspaceContext, 'onGoToResourceUriRequested').mockImplementationOnce(() => { + }); await page.resource(1).selectUri(); expect(props.resourceWorkspaceContext.onGoToResourceUriRequested).toHaveBeenCalled(); }); }); + describe('As LU, I should go to the folder location of a resource.', () => { + it('As LU, I should be able to go to the folder root if a resource is not in a folder', async() => { + expect.assertions(1); + const props = propsWithFilteredResources(); + const page = new DisplayResourcesListPage(props); + await waitFor(() => {}); + await page.resource(1).selectLocation(); + expect(page.resource(1).locationLink).toStrictEqual("root"); + }); + + it('As LU, I should be able to go to the folder location of a resource', async() => { + expect.assertions(1); + const props = propsWithFilteredResourcesAndColumnsHidden(); + const page = new DisplayResourcesListPage(props); + await waitFor(() => {}); + await page.resource(1).selectLocation(); + expect(page.resource(1).locationLink).toStrictEqual("Accounting›Bank"); + }); + }); + describe('As LU, I should resize columns of a resource.', () => { it('As LU, I should be able to resize a column of a resource with mouse move', async() => { const props = propsWithFilteredResources(); @@ -459,7 +481,7 @@ describe("Display Resources", () => { await waitFor(() => {}); // 6 columns should be displayed - expect(page.columnsCount).toStrictEqual(6); + expect(page.columnsCount).toStrictEqual(7); expect(page.columns(3).name).toStrictEqual(""); expect(page.columns(4).name).toStrictEqual("Name"); expect(page.columns(5).name).toStrictEqual("Password"); diff --git a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.page.js b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.page.js index 21bf0d0fd..3c3621f9b 100644 --- a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.page.js +++ b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.page.js @@ -123,6 +123,9 @@ export default class DisplayResourcesListPage { get copyTotpLink() { return element.querySelector('.cell-totp .secret-copy button'); }, + get locationLink() { + return element.querySelector('.cell-location button').textContent; + }, async selectFavorite() { const favorite = element.querySelector('.cell-favorite button'); fireEvent.click(favorite, leftClick); @@ -158,6 +161,11 @@ export default class DisplayResourcesListPage { fireEvent.click(uri, leftClick); await waitFor(() => {}); }, + async selectLocation() { + const location = element.querySelector('.cell-location button'); + fireEvent.click(location, leftClick); + await waitFor(() => {}); + }, async select() { fireEvent.click(element, leftClick); await waitFor(() => {}); diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.js b/src/react-extension/contexts/ResourceWorkspaceContext.js index c3907754c..d4c7918b4 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.js @@ -27,6 +27,8 @@ import GridResourceUserSettingService from "../../shared/services/gridResourceUserSetting/GridResourceUserSettingService"; import ColumnsResourceSettingCollection from "../../shared/models/entity/resource/columnsResourceSettingCollection"; import {withPasswordExpiry} from "./PasswordExpirySettingsContext"; +import {withRbac} from "../../shared/context/Rbac/RbacContext"; +import {uiActions} from "../../shared/services/rbacs/uiActionEnumeration"; /** * Context related to resources ( filter, current selections, etc.) @@ -591,12 +593,20 @@ export class ResourceWorkspaceContextProvider extends React.Component { } } + /** + * Check if the user can use folders. + * @returns {boolean} + */ + get canUseFolders() { + return this.props.context.siteSettings.canIUse("folders") + && this.props.rbacContext.canIUseUiAction(uiActions.FOLDERS_USE); + } /** * Populate the context with initial data such as resources and folders */ populate() { - if (this.props.context.siteSettings.canIUse("folders")) { + if (this.canUseFolders) { this.props.context.port.request("passbolt.folders.update-local-storage"); } this.props.context.port.request("passbolt.resources.update-local-storage"); @@ -882,8 +892,7 @@ export class ResourceWorkspaceContextProvider extends React.Component { * Navigate to the appropriate url after some resources selection operation */ redirectAfterSelection() { - const canUseFolders = this.props.context.siteSettings.canIUse('folders'); - const contentLoaded = this.resources !== null && (!canUseFolders || this.folders !== null); + const contentLoaded = this.resources !== null && (!this.canUseFolders || this.folders !== null); if (!contentLoaded) { return; } @@ -1121,6 +1130,9 @@ export class ResourceWorkspaceContextProvider extends React.Component { if (!this.hasAttentionRequiredColumn()) { columnsResourceSetting.removeById("attentionRequired"); } + if (!this.canUseFolders) { + columnsResourceSetting.removeById("location"); + } const sorter = gridUserSettingEntity?.sorter || this.state.sorter; // process the search after the grid setting is loaded this.setState({columnsResourceSetting, sorter}, async() => { @@ -1180,6 +1192,11 @@ export class ResourceWorkspaceContextProvider extends React.Component { * @returns {*[]} */ getHierarchyFolderCache(id) { + // When resources are not in a folder + if (id === null) { + return []; + } + // Process the hierarchy with a cache map by folder id if (typeof this.hierarchyFolderCache[id] === "undefined") { this.hierarchyFolderCache[id] = this.getHierarchyFolder(id); } @@ -1228,10 +1245,11 @@ ResourceWorkspaceContextProvider.propTypes = { history: PropTypes.object, actionFeedbackContext: PropTypes.object, passwordExpiryContext: PropTypes.object, // the password expiry contexts + rbacContext: PropTypes.any, // The role based access control context loadingContext: PropTypes.object // The loading context }; -export default withAppContext(withPasswordExpiry(withLoading(withActionFeedback(withRouter(ResourceWorkspaceContextProvider))))); +export default withAppContext(withRbac(withPasswordExpiry(withLoading(withActionFeedback(withRouter(ResourceWorkspaceContextProvider)))))); /** * Resource Workspace Context Consumer HOC diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js index 6eacd9fbf..53142e2df 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.data.js @@ -9,6 +9,7 @@ import {defaultResourceDto} from "../../shared/models/entity/resource/resourceEn import ColumnsResourceSettingCollection from "../../shared/models/entity/resource/columnsResourceSettingCollection"; import {defaultUserAppContext} from "./ExtAppContext.test.data"; import {defaultPasswordExpirySettingsContext} from "./PasswordExpirySettingsContext.test.data"; +import {defaultUserRbacContext} from "../../shared/context/Rbac/RbacContext.test.data"; /** * @deprecated should use defaultUserAppContext. @@ -25,11 +26,10 @@ export function defaultAppContext(appContext) { * Default props */ export function defaultProps() { - const defaultData = { + return { passwordExpiryContext: defaultPasswordExpirySettingsContext(), + rbacContext: defaultUserRbacContext() }; - - return defaultData; } /** @@ -54,7 +54,8 @@ export function defaultResourceWorkspaceContext(data = {}) { {id: "password", label: "Password", position: 6, show: true}, {id: "totp", label: "TOTP", position: 7, show: true}, {id: "uri", label: "URI", position: 8, show: true}, - {id: "modified", label: "Modified", position: 9, show: true}]), + {id: "modified", label: "Modified", position: 9, show: true}, + {id: "location", label: "Location", position: 10, show: true}]), filter: { type: ResourceWorkspaceFilterTypes.ALL }, @@ -82,6 +83,7 @@ export function defaultResourceWorkspaceContext(data = {}) { onLockDetail: jest.fn(), onChangeColumnView: jest.fn(), onChangeColumnsSettings: jest.fn(), + getHierarchyFolderCache: jest.fn(() => []), ...data }; } diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.test.js b/src/react-extension/contexts/ResourceWorkspaceContext.test.js index 43f4370f0..e19d2c59a 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.test.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.test.js @@ -313,9 +313,10 @@ describe("Resource Workspace Context", () => { {id: "password", label: "Password", position: 6, show: true}, {id: "totp", label: "TOTP", position: 7, show: true}, {id: "uri", label: "URI", position: 8, show: true}, - {id: "modified", label: "Modified", position: 9, show: true} + {id: "modified", label: "Modified", position: 9, show: true}, + {id: "location", label: "Location", position: 10, show: true} ]; - expect(page.columnsResourceSetting.items.length).toStrictEqual(9); + expect(page.columnsResourceSetting.items.length).toStrictEqual(10); expect(page.columnsResourceSetting.toDto()).toStrictEqual(defaultColumnsSetting); }); @@ -330,7 +331,8 @@ describe("Resource Workspace Context", () => { {id: "password", label: "Password", width: 300, position: 6, show: true}, {id: "totp", label: "TOTP", position: 7, width: 190, show: true}, {id: "uri", label: "URI", position: 8, show: false}, - {id: "modified", label: "Modified", width: 250, position: 9, show: true} + {id: "modified", label: "Modified", width: 250, position: 9, show: true}, + {id: "location", label: "Location", position: 10, show: true} ]; const sorter = { propertyName: 'name', @@ -346,7 +348,7 @@ describe("Resource Workspace Context", () => { }); await page.goToAllItems(); await page.goToRootFolder(); - expect(page.columnsResourceSetting.items.length).toStrictEqual(9); + expect(page.columnsResourceSetting.items.length).toStrictEqual(10); expect(page.columnsResourceSetting.toDto()).toStrictEqual(columnsSetting); expect(page.sorter.toDto()).toStrictEqual(sorter); }); @@ -388,12 +390,13 @@ describe("Resource Workspace Context", () => { {id: "password", label: "Password", position: 3, width: 100, show: true}, {id: "totp", label: "TOTP", position: 5, width: 190, show: true}, {id: "uri", label: "URI", position: 4, width: 300, show: true}, - {id: "modified", label: "Modified", position: 5, width: 250, show: true} + {id: "modified", label: "Modified", position: 5, width: 250, show: true}, + {id: "location", label: "Location", position: 10, show: true} ]; await page.goToAllItems(); await page.onChangeColumnView("name", false); await page.onChangeColumnsSettings(columnsSetting); - expect(page.columnsResourceSetting.length).toStrictEqual(9); + expect(page.columnsResourceSetting.length).toStrictEqual(10); expect(page.columnsResourceSetting.toDto()).toStrictEqual(mergedColumnsSetting); }); }); diff --git a/src/shared/components/Table/CellLocation.js b/src/shared/components/Table/CellLocation.js new file mode 100644 index 000000000..78409cac4 --- /dev/null +++ b/src/shared/components/Table/CellLocation.js @@ -0,0 +1,95 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import React, {Component} from "react"; +import PropTypes from "prop-types"; +import Icon from "../Icons/Icon"; + +/** + * This component represents a table cell location + */ +class CellLocation extends Component { + /** + * Handle click + * @param event + * @param {string} id The folder id + */ + handleClick(event, id) { + event.stopPropagation(); + this.props.onClick(id); + } + + /** + * Get the value + * @return {Object} + */ + get value() { + return this.props.value; + } + + /** + * Get the last folder + * @returns {*} + */ + get lastFolder() { + return this.value[this.value.length - 1]; + } + + /** + * Render the component + * @return {React.JSX.Element|null} + */ + render() { + // return empty array if a resource have no folder parent + if (this.value.length === 0) { + return ( + + ); + } + return ( +
    + +
    + ); + } +} + +CellLocation.propTypes = { + value: PropTypes.array.isRequired, // The value to display + onClick: PropTypes.func, // The onClick event function + t: PropTypes.func, // the translation function +}; + +export default CellLocation; diff --git a/src/shared/models/column/ColumnLocationModel.js b/src/shared/models/column/ColumnLocationModel.js new file mode 100644 index 000000000..89ff20119 --- /dev/null +++ b/src/shared/models/column/ColumnLocationModel.js @@ -0,0 +1,38 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ + +import ColumnModel, {ColumnModelTypes} from "./ColumnModel"; + +/** + * Model related to the column location use only with the UI + */ +class ColumnLocationModel extends ColumnModel { + /** + * Constructor + * @param {Object} columnDto + */ + constructor(columnDto = {}) { + columnDto.id = ColumnModelTypes.LOCATION; + columnDto.field = ColumnModelTypes.LOCATION; + columnDto.width = columnDto.width || 210; + columnDto.defaultWidth = 210; + columnDto.resizable = true; + columnDto.draggable = true; + columnDto.sortable = false; + super(columnDto); + } +} + +export default ColumnLocationModel; + diff --git a/src/shared/models/column/ColumnModel.js b/src/shared/models/column/ColumnModel.js index 49f5fc2e4..6f617c217 100644 --- a/src/shared/models/column/ColumnModel.js +++ b/src/shared/models/column/ColumnModel.js @@ -42,6 +42,7 @@ export const ColumnModelTypes = { CHECKBOX: 'checkbox', EXPIRED: 'expired', FAVORITE: 'favorite', + LOCATION: 'location', MODIFIED: 'modified', NAME: 'name', PASSWORD: 'password', diff --git a/src/shared/models/entity/resource/columnsResourceSettingCollection.js b/src/shared/models/entity/resource/columnsResourceSettingCollection.js index e4f795022..833cf92f3 100644 --- a/src/shared/models/entity/resource/columnsResourceSettingCollection.js +++ b/src/shared/models/entity/resource/columnsResourceSettingCollection.js @@ -34,7 +34,8 @@ class ColumnsResourceSettingCollection extends ColumnsSettingCollection { {id: "password", label: "Password", position: 6, show: true}, {id: "totp", label: "TOTP", position: 7, show: true}, {id: "uri", label: "URI", position: 8, show: true}, - {id: "modified", label: "Modified", position: 9, show: true} + {id: "modified", label: "Modified", position: 9, show: true}, + {id: "location", label: "Location", position: 10, show: true} ]); } } From 46612fb34df8511c00fd17875a4209fe2d978bfe Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Thu, 27 Jun 2024 16:47:46 +0200 Subject: [PATCH 58/67] PB-33880 As a user I should see tooltip always visible in any position --- src/less/components/tooltips-portal.less | 79 +++++++++ src/less/themes/common/ext_app.less | 1 + .../Common/Tooltip/TooltipPortal.js | 163 ++++++++++++++++++ src/shared/components/Table/CellLocation.js | 35 +++- 4 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 src/less/components/tooltips-portal.less create mode 100644 src/react-extension/components/Common/Tooltip/TooltipPortal.js diff --git a/src/less/components/tooltips-portal.less b/src/less/components/tooltips-portal.less new file mode 100644 index 000000000..b9af9b9ab --- /dev/null +++ b/src/less/components/tooltips-portal.less @@ -0,0 +1,79 @@ +/* + Tooltips Portal +*/ +/* Tooltip Portal container */ +.tooltip-portal { + display: flex; + cursor: pointer; + + // Tooltip focus + &:focus, :focus-visible { + outline: @outline-default-border; + } + + span { + .ellipsis(); + } +} + +/* Tooltip text */ +.tooltip-portal-text { + position: fixed; + display: flex; + flex-direction: column; + z-index: 99; + max-width: 24rem; + width: max-content; + padding: .4rem .8rem; + background: @tooltip-background; + color: @tooltip-text-color; + border-radius: .3rem; + font-size: 1.2rem; + line-height: 1.6rem; + font-weight: normal; + text-align: left; + + &::after { + content: " "; + position: absolute; + border: .5rem solid transparent; + } + + /* only right for now */ + + &.top { + &::after { + left: calc(50% - 5px); + top: 100%; + border-top-color: @tooltip-background; + } + } + + &.right { + &::after { + bottom: calc(50% - 5px); + right: 100%; + border-right-color: @tooltip-background; + } + } + + &.bottom { + &::after { + right: calc(50% - 5px); + bottom: 100%; + border-bottom-color: @tooltip-background; + } + } + + &.left { + &::after { + top: calc(50% - 5px); + left: 100%; + border-left-color: @tooltip-background; + } + } + + .folder-level span.caret { + margin-right: .5rem; + } +} \ No newline at end of file diff --git a/src/less/themes/common/ext_app.less b/src/less/themes/common/ext_app.less index 15679379d..9748c98c1 100644 --- a/src/less/themes/common/ext_app.less +++ b/src/less/themes/common/ext_app.less @@ -27,6 +27,7 @@ @import "../../components/sidebar-help.less"; @import "../../components/chips.less"; @import "../../components/range.less"; +@import "../../components/tooltips-portal.less"; // Extra components @import "../../components/activity.less"; diff --git a/src/react-extension/components/Common/Tooltip/TooltipPortal.js b/src/react-extension/components/Common/Tooltip/TooltipPortal.js new file mode 100644 index 000000000..30398faaf --- /dev/null +++ b/src/react-extension/components/Common/Tooltip/TooltipPortal.js @@ -0,0 +1,163 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import React, {Component} from "react"; +import PropTypes from "prop-types"; +import {createPortal} from "react-dom"; + +const MARGIN = 10; + +class TooltipPortal extends Component { + /** + * Default constructor + * @param props Component props + */ + constructor(props) { + super(props); + this.state = this.defaultState; + this.bindCallbacks(); + this.createRefs(); + } + + /** + * Returns the component default state + */ + get defaultState() { + return { + hasToDisplayTooltip: false, // boolean to display or not the tooltip (better for performance) + direction: "", // boolean to display or not the tooltip + top: 0, + left: 0 + }; + } + + /** + * Initialize the bindCallback + */ + bindCallbacks() { + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.handleMouseLeave = this.handleMouseLeave.bind(this); + this.findBestPosition = this.findBestPosition.bind(this); + } + + /** + * Create DOM nodes or React elements references in order to be able to access them programmatically. + */ + createRefs() { + this.tooltipRef = React.createRef(); + this.tooltipTextRef = React.createRef(); + } + + /** + * Handle mouse over event + */ + handleMouseEnter() { + this.setState({hasToDisplayTooltip: true}, this.findBestPosition); + } + + /** + * Handle mouse out event + */ + handleMouseLeave() { + this.setState({hasToDisplayTooltip: false, direction: ""}); + } + + /** + * Find the best position to display the tooltip + */ + findBestPosition() { + const tooltipContainer = this.tooltipRef.current.getBoundingClientRect(); + const tooltipText = this.tooltipTextRef.current.getBoundingClientRect(); + const {innerHeight, innerWidth} = window; + // Tooltip top position center with tooltip container + const topTooltipText = tooltipContainer.top + (tooltipContainer.height / 2) - (tooltipText.height / 2); + // Tooltip left position center with tooltip container + const leftTooltipText = tooltipContainer.left + (tooltipContainer.width / 2) - (tooltipText.width / 2); + + // Check if the tooltip is contained in the inner height of the window + if (topTooltipText + tooltipText.height <= innerHeight && topTooltipText >= 0) { + if (tooltipContainer.right + tooltipText.width <= innerWidth) { + // If tooltip is visible on the right of the container + this.setState({direction: 'right', left: tooltipContainer.right + MARGIN, top: topTooltipText}); + } else if (tooltipContainer.left - tooltipText.width > 0) { + // If tooltip is visible on the left of the container + this.setState({direction: 'left', left: tooltipContainer.left - tooltipText.width - MARGIN, top: topTooltipText}); + } + } else if (leftTooltipText >= 0 && leftTooltipText + tooltipText.width <= innerWidth) { + // If tooltip is not visible side of the container but visible on top or at the bottom + if (tooltipContainer.top - tooltipText.height <= 0) { + // If tooltip is visible on the bottom of the container + this.setState({direction: 'bottom', left: leftTooltipText, top: tooltipContainer.bottom + MARGIN}); + } else if (tooltipContainer.top - tooltipText.height <= innerHeight) { + // If tooltip is visible on the top of the container + this.setState({direction: 'top', left: leftTooltipText, top: tooltipContainer.top - tooltipText.height - MARGIN}); + } + } else { + // If tooltip cannot be visible entirely side of the container + this.findPositionWithMoreSpace(tooltipContainer, tooltipText, topTooltipText, leftTooltipText); + } + } + + /** + * Check which side have more space around the container and display the tooltip + * @param tooltipContainer + * @param tooltipText + * @param topTooltipText + * @param leftTooltipText + */ + findPositionWithMoreSpace(tooltipContainer, tooltipText, topTooltipText, leftTooltipText) { + const {innerHeight, innerWidth} = window; + const topSpace = innerHeight - tooltipContainer.top + innerWidth - leftTooltipText; + const bottomSpace = innerHeight - tooltipContainer.bottom + innerWidth - leftTooltipText; + const rightSpace = innerWidth - tooltipContainer.right + innerHeight - topTooltipText; + const leftSpace = innerWidth - tooltipContainer.left + innerHeight - topTooltipText; + + // Try to display the tooltip entirely + if (rightSpace > leftSpace && rightSpace > topSpace && rightSpace > bottomSpace) { + this.setState({direction: 'right', left: tooltipContainer.right - (tooltipContainer.width / 2), top: topTooltipText}); + } else if (leftSpace > topSpace && leftSpace > bottomSpace) { + this.setState({direction: 'left', left: tooltipContainer.left + (tooltipContainer.width / 2), top: topTooltipText}); + } else if (topSpace > bottomSpace) { + this.setState({direction: 'top', left: leftTooltipText, top: tooltipContainer.top - tooltipText.height}); + } else { + this.setState({direction: 'bottom', left: leftTooltipText, top: tooltipContainer.bottom}); + } + } + + /** + * Render the component + * @return {JSX} + */ + render() { + return ( +
    + + {this.props.children} + + {this.state.hasToDisplayTooltip && + createPortal( + {this.props.message} + , document.body) + } +
    + ); + } +} + +TooltipPortal.propTypes = { + children: PropTypes.any, + message: PropTypes.any.isRequired, +}; + +export default TooltipPortal; diff --git a/src/shared/components/Table/CellLocation.js b/src/shared/components/Table/CellLocation.js index 78409cac4..94b95a80a 100644 --- a/src/shared/components/Table/CellLocation.js +++ b/src/shared/components/Table/CellLocation.js @@ -14,6 +14,7 @@ import React, {Component} from "react"; import PropTypes from "prop-types"; import Icon from "../Icons/Icon"; +import TooltipPortal from "../../../react-extension/components/Common/Tooltip/TooltipPortal"; /** * This component represents a table cell location @@ -22,7 +23,7 @@ class CellLocation extends Component { /** * Handle click * @param event - * @param {string} id The folder id + * @param {string | null} id The folder id */ handleClick(event, id) { event.stopPropagation(); @@ -45,6 +46,20 @@ class CellLocation extends Component { return this.value[this.value.length - 1]; } + /** + * Get the tooltip hierarchy folder message + */ + get tooltipHierarchyFolder() { + return this.value.map((folder, index) => +
    + {folder.folder_parent_id !== null && + + } + {folder.name} +
    + ); + } + /** * Render the component * @return {React.JSX.Element|null} @@ -53,16 +68,18 @@ class CellLocation extends Component { // return empty array if a resource have no folder parent if (this.value.length === 0) { return ( - + {this.props.t("root")}} direction="auto"> + + ); } return ( -
    + -
    + ); } } From 4028acb794756012cb07748f41033dddb1c17687 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 8 Jul 2024 07:01:19 +0000 Subject: [PATCH 59/67] Apply 1 suggestion(s) to 1 file(s) --- src/react-extension/contexts/ResourceWorkspaceContext.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-extension/contexts/ResourceWorkspaceContext.js b/src/react-extension/contexts/ResourceWorkspaceContext.js index d4c7918b4..fe569fd08 100644 --- a/src/react-extension/contexts/ResourceWorkspaceContext.js +++ b/src/react-extension/contexts/ResourceWorkspaceContext.js @@ -1189,7 +1189,7 @@ export class ResourceWorkspaceContextProvider extends React.Component { /** * Get the hierarchy of a folder by ID in cache * @param {string} id The id of the folder - * @returns {*[]} + * @returns {array} Array of folders */ getHierarchyFolderCache(id) { // When resources are not in a folder From e44cc81af27f6337553b259687bf86b6d5422907 Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Mon, 8 Jul 2024 11:52:49 +0000 Subject: [PATCH 60/67] PB-33919 - As a user searching for users to share a resource/folder with I can... --- .../Inputs/Autocomplete/AutocompleteItem.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js index e6dd719de..0b3649022 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js @@ -36,13 +36,6 @@ class AutocompleteItem extends Component { }; } - async componentDidMount() { - if (this.props.user) { - const gpgKey = await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', this.props.user.id); - this.setState({gpgKey}); - } - } - /** * Bind callbacks methods * @return {void} @@ -57,7 +50,7 @@ class AutocompleteItem extends Component { */ getTitle() { if (this.props.user) { - return `${this.props.user.profile.first_name} ${this.props.user.profile.last_name} (${this.props.user.username})`; + return `${this.props.user.profile.first_name} ${this.props.user.profile.last_name}`; } else { return `${this.props.group.name}`; } @@ -68,9 +61,8 @@ class AutocompleteItem extends Component { * @returns {string} */ getSubtitle() { - if (this.props.user && this.state.gpgKey) { - const longId = this.state.gpgKey.fingerprint.substr(this.state.gpgKey.fingerprint.length - 16); - return longId.replace(/(.{4})/g, "$1 "); + if (this.props.user) { + return this.props.user.username } else if (this.props.group) { return this.props.t("{{count}} group member", {count: this.props.group.user_count}); } @@ -148,4 +140,4 @@ AutocompleteItem.propTypes = { t: PropTypes.func, // the translation function }; -export default withAppContext(withTranslation("common")(AutocompleteItem)); +export default withAppContext(withTranslation("common")(AutocompleteItem)); \ No newline at end of file From 0a3cd60b8559c6804c353c649f04e4b1634aad16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 3 Jul 2024 11:14:51 +0200 Subject: [PATCH 61/67] PB-33920 - As a user searching for users to share a resource/folder with I can see information icon next to a very long user full name --- src/less/components/form/share-autocomplete.less | 10 ++++++++-- .../Common/Inputs/Autocomplete/AutocompleteItem.js | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/less/components/form/share-autocomplete.less b/src/less/components/form/share-autocomplete.less index 71e0b14ed..be747f21d 100644 --- a/src/less/components/form/share-autocomplete.less +++ b/src/less/components/form/share-autocomplete.less @@ -108,8 +108,14 @@ } } - .autocomplete-item.suspended { - opacity: 0.5; + .autocomplete-item { + &.suspended { + opacity: 0.5; + } + + button { + max-width: 100%; + } } } diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js index 0b3649022..ec724732f 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js @@ -62,7 +62,7 @@ class AutocompleteItem extends Component { */ getSubtitle() { if (this.props.user) { - return this.props.user.username + return this.props.user.username; } else if (this.props.group) { return this.props.t("{{count}} group member", {count: this.props.group.user_count}); } @@ -112,8 +112,8 @@ class AutocompleteItem extends Component { }
    - {this.getTitle()}{this.isCurrentUserSuspended && (suspended)} - {this.getSubtitle()} + {this.getTitle()}{this.isCurrentUserSuspended && (suspended)} + {this.getSubtitle()}
    @@ -140,4 +140,4 @@ AutocompleteItem.propTypes = { t: PropTypes.func, // the translation function }; -export default withAppContext(withTranslation("common")(AutocompleteItem)); \ No newline at end of file +export default withAppContext(withTranslation("common")(AutocompleteItem)); From c08351ab0406caac4f42e52299af26bdb31a8adb Mon Sep 17 00:00:00 2001 From: Antony Bartolomucci Date: Wed, 3 Jul 2024 08:39:47 +0200 Subject: [PATCH 62/67] PB-33922 - Fix broken documentation links and unnecessary redirections --- .../ManageAccountRecoveryUserSettings.js | 2 +- .../DisplayAdministrationPasswordExpiry.js | 2 +- .../DisplayAdministrationPasswordExpiry.test.js | 2 +- .../DisplayAdministrationUserPassphrasePolicies.js | 2 +- .../DisplayAdministrationUserPassphrasePolicies.test.js | 2 +- .../DisplayEmailNotificationsAdministration.js | 2 +- .../DisplayMfaAdministration/DisplayMfaAdministration.js | 2 +- .../DisplayMfaPolicyAdministration.js | 2 +- .../DisplayMfaPolicyAdministration.test.js | 2 +- .../DisplayPasswordPoliciesAdministration.js | 2 +- .../DisplayPasswordPoliciesAdministration.test.js | 2 +- .../DisplayRbacAdministration/DisplayRbacAdministration.js | 2 +- .../DisplayRbacAdministration.test.js | 2 +- .../DisplaySelfRegistrationAdministration.js | 2 +- .../DisplaySelfRegistrationAdministration.test.js | 2 +- .../DisplayUserDirectoryAdministration.js | 2 +- .../ManageAccountRecoveryAdministrationSettings.js | 2 +- .../ManageSmtpAdministrationSettings.js | 2 +- .../Administration/ManageSsoSettings/ManageSsoSettings.js | 4 ++-- src/react-extension/components/Common/Footer/Footer.js | 2 +- .../components/Desktop/ImportAccountKit/ImportAccountKit.js | 2 +- .../Desktop/ImportAccountKit/ImportAccountKit.test.js | 2 +- .../components/Resource/ImportResources/ImportResources.js | 2 +- .../UserSetting/ChangeUserPassphrase/ConfirmPassphrase.js | 2 +- .../ExportAccountToDesktop/ExportAccountToDesktop.js | 2 +- .../ExportAccountToDesktop/ExportAccountToDesktop.test.js | 2 +- 26 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/react-extension/components/AccountRecovery/ManageAccountRecoveryUserSettings/ManageAccountRecoveryUserSettings.js b/src/react-extension/components/AccountRecovery/ManageAccountRecoveryUserSettings/ManageAccountRecoveryUserSettings.js index 168270a71..53b73acd6 100644 --- a/src/react-extension/components/AccountRecovery/ManageAccountRecoveryUserSettings/ManageAccountRecoveryUserSettings.js +++ b/src/react-extension/components/AccountRecovery/ManageAccountRecoveryUserSettings/ManageAccountRecoveryUserSettings.js @@ -288,7 +288,7 @@ class ManageAccountRecoveryUserSettings extends Component {
    Learn more diff --git a/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.js b/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.js index 8905b3762..0e797d532 100644 --- a/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.js +++ b/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.js @@ -143,7 +143,7 @@ class DisplayAdministrationPasswordExpiry extends React.PureComponent {

    About password expiry

    For more information about the password expiry, checkout the dedicated page on the help website.

    -
    + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.test.js b/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.test.js index a1de52094..7348639e7 100644 --- a/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.test.js +++ b/src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiry.test.js @@ -59,7 +59,7 @@ describe("DisplayAdministrationPasswordExpirySettingsPage", () => { const helpPageLink = page.helpPageLink; expect(helpPageLink).not.toBeNull(); expect(helpPageLink.getAttribute('rel')).toStrictEqual("noopener noreferrer"); - expect(helpPageLink.getAttribute('href')).toStrictEqual("https://help.passbolt.com/configure/password-expiry"); + expect(helpPageLink.getAttribute('href')).toStrictEqual("https://passbolt.com/docs/admin/password-configuration/password-expiry"); }); }); diff --git a/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.js b/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.js index c2a199629..ba211f5e8 100644 --- a/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.js +++ b/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.js @@ -168,7 +168,7 @@ class DisplayAdministrationUserPassphrasePolicies extends React.PureComponent {

    What is user passphrase policies?

    For more information about the user passphrase policies, checkout the dedicated page on the help website.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.test.js b/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.test.js index 75741abb5..e95b95ba5 100644 --- a/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.test.js +++ b/src/react-extension/components/Administration/DisplayAdministrationUserPassphrasePolicies/DisplayAdministrationUserPassphrasePolicies.test.js @@ -55,7 +55,7 @@ describe("DisplayAdministrationUserPassphrasePolicies", () => { const helpPageLink = page.helpPageLink; expect(helpPageLink).not.toBeNull(); expect(helpPageLink.getAttribute('rel')).toStrictEqual("noopener noreferrer"); - expect(helpPageLink.getAttribute('href')).toStrictEqual("https://help.passbolt.com/configure/user-passphrase-policies"); + expect(helpPageLink.getAttribute('href')).toStrictEqual("https://passbolt.com/docs/admin/authentication/user-passphrase-policies/"); }); it('As an administrator I should see the default settings', async() => { diff --git a/src/react-extension/components/Administration/DisplayEmailNotificationsAdministration/DisplayEmailNotificationsAdministration.js b/src/react-extension/components/Administration/DisplayEmailNotificationsAdministration/DisplayEmailNotificationsAdministration.js index 56cf5a1d2..7347fc763 100644 --- a/src/react-extension/components/Administration/DisplayEmailNotificationsAdministration/DisplayEmailNotificationsAdministration.js +++ b/src/react-extension/components/Administration/DisplayEmailNotificationsAdministration/DisplayEmailNotificationsAdministration.js @@ -491,7 +491,7 @@ class DisplayEmailNotificationsAdministration extends React.Component {

    Need some help?

    For more information about email notification, checkout the dedicated page on the help website.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayMfaAdministration/DisplayMfaAdministration.js b/src/react-extension/components/Administration/DisplayMfaAdministration/DisplayMfaAdministration.js index 67b360f10..6045cca0e 100644 --- a/src/react-extension/components/Administration/DisplayMfaAdministration/DisplayMfaAdministration.js +++ b/src/react-extension/components/Administration/DisplayMfaAdministration/DisplayMfaAdministration.js @@ -260,7 +260,7 @@ class DisplayMfaAdministration extends React.Component {

    Need help?

    Check out our Multi Factor Authentication configuration guide.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.js b/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.js index ad8af3375..302c540aa 100644 --- a/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.js +++ b/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.js @@ -153,7 +153,7 @@ class DisplayMfaPolicyAdministration extends React.Component {

    Need some help?

    For more information about MFA policy settings, checkout the dedicated page on the help website.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.test.js b/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.test.js index e605c22a4..50fdd7387 100644 --- a/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.test.js +++ b/src/react-extension/components/Administration/DisplayMfaPolicyAdministration/DisplayMfaPolicyAdministration.test.js @@ -70,7 +70,7 @@ describe("DisplayMfaPolicyAdministration", () => { expect(page.helpBoxTitle.textContent).toBe("Need some help?"); expect(page.helpBoxDescription.textContent).toBe("For more information about MFA policy settings, checkout the dedicated page on the help website."); expect(page.helpBoxButton.textContent).toEqual("Read the documentation"); - expect(page.helpBoxButton.getAttribute('href')).toEqual('https://help.passbolt.com/configure/mfa-policy'); + expect(page.helpBoxButton.getAttribute('href')).toEqual('https://passbolt.com/docs/admin/authentication/mfa-policy'); }); }); diff --git a/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.js b/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.js index 3a7584b92..271e7f66b 100644 --- a/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.js +++ b/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.js @@ -398,7 +398,7 @@ class DisplayPasswordPoliciesAdministration extends React.Component {

    What is password policy?

    For more information about the password policy settings, checkout the dedicated page on the help website.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js b/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js index 7f06a1de1..4331968c1 100644 --- a/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js +++ b/src/react-extension/components/Administration/DisplayPasswordPoliciesAdministration/DisplayPasswordPoliciesAdministration.test.js @@ -60,7 +60,7 @@ describe("DisplayPasswordPoliciesAdministration", () => { expect(page.helpBoxTitle.textContent).toBe("What is password policy?"); expect(page.helpBoxDescription.textContent).toBe("For more information about the password policy settings, checkout the dedicated page on the help website."); expect(page.helpBoxButton.textContent).toEqual("Read the documentation"); - expect(page.helpBoxButton.getAttribute('href')).toEqual('https://help.passbolt.com/configure/password-policies'); + expect(page.helpBoxButton.getAttribute('href')).toEqual('https://passbolt.com/docs/admin/password-configuration/password-policy/'); }); }); diff --git a/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.js b/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.js index 9b7ed0e02..44df58132 100644 --- a/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.js +++ b/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.js @@ -338,7 +338,7 @@ class DisplayRbacAdministration extends React.Component {

    Need help?

    Check out the Role Based Access Control documentation.

    - + Read RBAC doc diff --git a/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.test.js b/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.test.js index 8bc355df1..8392efcfa 100644 --- a/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.test.js +++ b/src/react-extension/components/Administration/DisplayRbacAdministration/DisplayRbacAdministration.test.js @@ -47,7 +47,7 @@ describe("DisplayRbacAdministration", () => { expect(page.helpBox).toBeDefined(); expect(page.helpBoxButton).toBeDefined(); expect(page.helpBoxButton.textContent).toEqual("Read RBAC doc"); - expect(page.helpBoxButton.getAttribute('href')).toEqual('https://help.passbolt.com/configure/rbac'); + expect(page.helpBoxButton.getAttribute('href')).toEqual('https://passbolt.com/docs/admin/role-based-access-control/'); expect(page.helpBoxButton.getAttribute('rel')).toEqual('noopener noreferrer'); }); diff --git a/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.js b/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.js index 071ffdbef..5f25a75b2 100644 --- a/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.js +++ b/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.js @@ -349,7 +349,7 @@ class DisplaySelfRegistrationAdministration extends React.Component {

    What is user self registration?

    User self registration enables users with an email from a whitelisted domain to create their passbolt account without prior admin invitation.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.test.js b/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.test.js index c43a7ee13..5fb5e8559 100644 --- a/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.test.js +++ b/src/react-extension/components/Administration/DisplaySelfRegistrationAdministration/DisplaySelfRegistrationAdministration.test.js @@ -73,7 +73,7 @@ describe("DisplaySelfRegistrationAdministration", () => { expect(page.helpBoxButton).toBeDefined(); expect(page.helpBoxButton.textContent).toEqual("Read the documentation"); - expect(page.helpBoxButton.getAttribute('href')).toEqual('https://help.passbolt.com/configure/self-registration'); + expect(page.helpBoxButton.getAttribute('href')).toEqual('https://passbolt.com/docs/admin/user-provisioning/self-registration/'); }); it('As a logged in administrator I can enable the User self registration setting', async() => { diff --git a/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js b/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js index 415768960..80647b6f4 100644 --- a/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js +++ b/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js @@ -692,7 +692,7 @@ class DisplayUserDirectoryAdministration extends React.Component {

    Need help?

    Check out our ldap configuration guide.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/ManageAccountRecoveryAdministrationSettings/ManageAccountRecoveryAdministrationSettings.js b/src/react-extension/components/Administration/ManageAccountRecoveryAdministrationSettings/ManageAccountRecoveryAdministrationSettings.js index 7f88ce693..bd146a0d0 100644 --- a/src/react-extension/components/Administration/ManageAccountRecoveryAdministrationSettings/ManageAccountRecoveryAdministrationSettings.js +++ b/src/react-extension/components/Administration/ManageAccountRecoveryAdministrationSettings/ManageAccountRecoveryAdministrationSettings.js @@ -404,7 +404,7 @@ class ManageAccountRecoveryAdministrationSettings extends React.Component {

    Need some help?

    For more information about account recovery, checkout the dedicated page on the help website.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/ManageSmtpAdministrationSettings/ManageSmtpAdministrationSettings.js b/src/react-extension/components/Administration/ManageSmtpAdministrationSettings/ManageSmtpAdministrationSettings.js index 6c20c5127..bb09d728e 100644 --- a/src/react-extension/components/Administration/ManageSmtpAdministrationSettings/ManageSmtpAdministrationSettings.js +++ b/src/react-extension/components/Administration/ManageSmtpAdministrationSettings/ManageSmtpAdministrationSettings.js @@ -466,7 +466,7 @@ export class ManageSmtpAdministrationSettings extends React.Component {

    Why do I need an SMTP server?

    Passbolt needs an smtp server in order to send invitation emails after an account creation and to send email notifications.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/ManageSsoSettings/ManageSsoSettings.js b/src/react-extension/components/Administration/ManageSsoSettings/ManageSsoSettings.js index 5009bf49d..f280f9a1a 100644 --- a/src/react-extension/components/Administration/ManageSsoSettings/ManageSsoSettings.js +++ b/src/react-extension/components/Administration/ManageSsoSettings/ManageSsoSettings.js @@ -214,13 +214,13 @@ class ManageSsoSettings extends React.Component {

    Important notice:

    Enabling SSO changes the security risks. For example an attacker with a local machine access maybe be able to access secrets, if the user is still logged in with the Identity provider. Make sure users follow screen lock best practices. - Learn more + Learn more

    Need some help?

    For more information about SSO, checkout the dedicated page on the help website.

    - + Read the documentation diff --git a/src/react-extension/components/Common/Footer/Footer.js b/src/react-extension/components/Common/Footer/Footer.js index 01b1403b2..8fa6045ac 100644 --- a/src/react-extension/components/Common/Footer/Footer.js +++ b/src/react-extension/components/Common/Footer/Footer.js @@ -19,7 +19,7 @@ import {withAppContext} from "../../../../shared/context/AppContext/AppContext"; import Tooltip from "../Tooltip/Tooltip"; const CREDITS_URL = "https://www.passbolt.com/credits"; -const UNSAFE_URL = "https://help.passbolt.com/faq/hosting/why-unsafe"; +const UNSAFE_URL = "https://www.passbolt.com/docs/hosting/faq/why-I-see-unsafe-mode-banner/"; /** * The application footer diff --git a/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.js b/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.js index 452f334f9..6928d0943 100644 --- a/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.js +++ b/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.js @@ -170,7 +170,7 @@ class ImportAccountKit extends React.Component { * @returns {Promise} */ openDocumentation() { - this.props.context.port.emit("passbolt.rendered.open-to-browser", "https://help.passbolt.com/configure/windows-app"); + this.props.context.port.emit("passbolt.rendered.open-to-browser", "https://www.passbolt.com/docs/user/quickstart/desktop/windows-app/"); } /** diff --git a/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.test.js b/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.test.js index 4cc85c102..7a0707876 100644 --- a/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.test.js +++ b/src/react-extension/components/Desktop/ImportAccountKit/ImportAccountKit.test.js @@ -48,7 +48,7 @@ describe("ImportAccountKit", () => { await page.click(page.getHelpMessage); expect(page.getHelpMessage).not.toBeNull(); - expect(props.context.port.emit).toHaveBeenCalledWith("passbolt.rendered.open-to-browser", "https://help.passbolt.com/configure/windows-app"); + expect(props.context.port.emit).toHaveBeenCalledWith("passbolt.rendered.open-to-browser", "https://www.passbolt.com/docs/user/quickstart/desktop/windows-app/"); }); it('As an unknown user I should be notified if I have uploaded a wrong file format', async() => { diff --git a/src/react-extension/components/Resource/ImportResources/ImportResources.js b/src/react-extension/components/Resource/ImportResources/ImportResources.js index b3bc7215f..f710ffdc7 100644 --- a/src/react-extension/components/Resource/ImportResources/ImportResources.js +++ b/src/react-extension/components/Resource/ImportResources/ImportResources.js @@ -453,7 +453,7 @@ class ImportResources extends Component {

    - Checkout the documentation to see what information is supported. + Checkout the documentation to see what information is supported.

    diff --git a/src/react-extension/components/UserSetting/ChangeUserPassphrase/ConfirmPassphrase.js b/src/react-extension/components/UserSetting/ChangeUserPassphrase/ConfirmPassphrase.js index 0e160cfe9..4206518d7 100644 --- a/src/react-extension/components/UserSetting/ChangeUserPassphrase/ConfirmPassphrase.js +++ b/src/react-extension/components/UserSetting/ChangeUserPassphrase/ConfirmPassphrase.js @@ -223,7 +223,7 @@ class ConfirmPassphrase extends React.Component {

    What if I forgot my passphrase?

    Unfortunately you need your passphrase in order to continue. If you forgot it, please contact your administrator.

    - + Learn more
    diff --git a/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.js b/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.js index 472ae0794..1b0585df2 100644 --- a/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.js +++ b/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.js @@ -128,7 +128,7 @@ class ExportAccountToDesktop extends React.Component {

    3. Open the application.

    4. Upload the account kit on the desktop app.

    5. And you are done!

    - + Read the documentation diff --git a/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.test.js b/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.test.js index d5cd2de20..47f3ae46f 100644 --- a/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.test.js +++ b/src/react-extension/components/UserSetting/ExportAccountToDesktop/ExportAccountToDesktop.test.js @@ -57,7 +57,7 @@ describe("ExportAccountToDesktop", () => { expect(page.helpBoxDescription[3].textContent).toEqual("4. Upload the account kit on the desktop app."); expect(page.helpBoxDescription[4].textContent).toEqual("5. And you are done!"); expect(page.helpBoxButton.textContent).toEqual("Read the documentation"); - expect(page.helpBoxButton.getAttribute('href')).toEqual('https://help.passbolt.com/configure/windows-app'); + expect(page.helpBoxButton.getAttribute('href')).toEqual('https://www.passbolt.com/docs/user/quickstart/desktop/windows-app/'); expect(page.helpBoxButton.getAttribute('rel')).toEqual('noopener noreferrer'); }); From 156525808694dccccd27f3244839a2d9cc5854d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Loegel?= Date: Wed, 10 Jul 2024 14:36:17 +0000 Subject: [PATCH 63/67] Draft: PB-33919 - As a user searching for users to share a resource/folder with I can... --- src/less/base/shimmer.less | 14 +- src/less/components/fingerprint.less | 24 +++ .../components/form/share-autocomplete.less | 19 +- .../components/permission/permissions.less | 7 +- .../tooltip/group-user-details.less | 26 +++ src/less/components/tooltips-portal.less | 2 +- src/less/components/tooltips.less | 3 +- src/less/themes/common/ext_app.less | 2 + .../Common/Fingerprint/Fingerprint.js | 36 ++++ .../Inputs/Autocomplete/AutocompleteItem.js | 38 +++- .../TooltipMessageFingerprintLoading.js | 32 +++ .../TooltipMessageGroupUserDetailsLoading.js | 35 ++++ .../Common/Tooltip/TooltipPortal.js | 2 + .../DisplayResourcesList.test.js | 2 +- .../components/Share/SharePermissionItem.js | 71 +++---- .../CreateUserGroup/CreateUserGroup.test.js | 13 +- .../CreateUserGroup.test.page.js | 43 +++- .../EditUserGroup/EditUserGroupItem.js | 192 +++++++----------- 18 files changed, 374 insertions(+), 187 deletions(-) create mode 100644 src/less/components/fingerprint.less create mode 100644 src/less/components/tooltip/group-user-details.less create mode 100644 src/react-extension/components/Common/Fingerprint/Fingerprint.js create mode 100644 src/react-extension/components/Common/Tooltip/TooltipMessageFingerprintLoading.js create mode 100644 src/react-extension/components/Common/Tooltip/TooltipMessageGroupUserDetailsLoading.js diff --git a/src/less/base/shimmer.less b/src/less/base/shimmer.less index d644d468c..8b53ce185 100644 --- a/src/less/base/shimmer.less +++ b/src/less/base/shimmer.less @@ -4,7 +4,7 @@ width:100%; height:100%; content: ''; - animation : shimmer 2s infinite; + animation: shimmer 2s infinite; background: linear-gradient( 45deg, rgba(@default-background, 0) 0, @@ -12,6 +12,18 @@ rgba(@default-background, 0.5) 50%, rgba(@default-background, 0) ); + + &.shimmer-tooltip { + animation : shimmer 10s infinite; + background: linear-gradient( + 45deg, + rgba(@tooltip-background, 0) 0, + rgba(@tooltip-background, 0.1) 30%, + rgba(@tooltip-background, 0.5) 50%, + rgba(@tooltip-background, 0) + ); + } + } @keyframes shimmer { 0% { diff --git a/src/less/components/fingerprint.less b/src/less/components/fingerprint.less new file mode 100644 index 000000000..2243f5ec1 --- /dev/null +++ b/src/less/components/fingerprint.less @@ -0,0 +1,24 @@ +.fingerprint { + .fingerprint-line { + text-transform: uppercase; + font-family: "Inconsolata"; + font-size: 1.5rem; + } + + &.skeleton { + position:relative; + .fingerprint-line { + margin: .3rem 0 0 0; + height: 1.3rem; + width: 18rem; + background: @tooltip-text-color; + opacity: 0.2; + .rounded(); + } + .shimmer { + position:absolute; + top:0; + left:0; + } + } +} diff --git a/src/less/components/form/share-autocomplete.less b/src/less/components/form/share-autocomplete.less index be747f21d..dc6a765b0 100644 --- a/src/less/components/form/share-autocomplete.less +++ b/src/less/components/form/share-autocomplete.less @@ -81,13 +81,14 @@ } .user, .group { - width: 75%; + width: calc(100% - 5.6rem); float: left; padding-left: 1em; padding-right: 1em; } .name { - display: block; + display: inline-block; + max-width: 100%; .ellipsis(); } .details { @@ -114,7 +115,19 @@ } button { - max-width: 100%; + width: 100%; + + & > span { + max-with:100; + } + } + + .user-fullname-container { + display: flex; + + .tooltip-portal { + margin-left: 1rem; + } } } } diff --git a/src/less/components/permission/permissions.less b/src/less/components/permission/permissions.less index 13b0a76c8..7a958dd80 100644 --- a/src/less/components/permission/permissions.less +++ b/src/less/components/permission/permissions.less @@ -63,11 +63,8 @@ } } - .tooltip { - display: flex; - align-items: center; - margin: 0 1rem 0 .6rem; - line-height: 1.6rem; + .tooltip-portal { + margin-left: 1rem; } .rights { diff --git a/src/less/components/tooltip/group-user-details.less b/src/less/components/tooltip/group-user-details.less new file mode 100644 index 000000000..486fa764d --- /dev/null +++ b/src/less/components/tooltip/group-user-details.less @@ -0,0 +1,26 @@ +.group-user-details-tooltip { + &.skeleton { + position:relative; + .email { + margin: .3rem 0 0 0; + height: 1.3rem; + width: 12rem; + background: @tooltip-text-color; + opacity: 0.2; + .rounded(); + } + .fingerprint-line { + margin: .3rem 0 0 0; + height: 1.3rem; + width: 18rem; + background: @tooltip-text-color; + opacity: 0.2; + .rounded(); + } + .shimmer { + position:absolute; + top:0; + left:0; + } + } +} diff --git a/src/less/components/tooltips-portal.less b/src/less/components/tooltips-portal.less index b9af9b9ab..54cb7b155 100644 --- a/src/less/components/tooltips-portal.less +++ b/src/less/components/tooltips-portal.less @@ -21,7 +21,7 @@ position: fixed; display: flex; flex-direction: column; - z-index: 99; + z-index: 1000; max-width: 24rem; width: max-content; padding: .4rem .8rem; diff --git a/src/less/components/tooltips.less b/src/less/components/tooltips.less index ce3a2abf5..79b28810d 100644 --- a/src/less/components/tooltips.less +++ b/src/less/components/tooltips.less @@ -108,5 +108,4 @@ transform: translate(0, -50%); } } - -} \ No newline at end of file +} diff --git a/src/less/themes/common/ext_app.less b/src/less/themes/common/ext_app.less index 9748c98c1..b00e2c199 100644 --- a/src/less/themes/common/ext_app.less +++ b/src/less/themes/common/ext_app.less @@ -34,6 +34,7 @@ @import "../../components/comments.less"; @import "../../components/error/error-details.less"; @import "../../components/error/http-error.less"; +@import "../../components/fingerprint.less"; @import "../../components/form/location.less"; @import "../../components/form/share-autocomplete.less"; @import "../../components/form/password.less"; @@ -49,6 +50,7 @@ @import "../../components/tableinfo.less"; @import "../../components/tabs.less"; @import "../../components/tag.less"; +@import "../../components/tooltip/group-user-details.less"; @import "../../components/usercard.less"; @import "../../components/provider-card.less"; @import "../../components/sso-buttons.less"; diff --git a/src/react-extension/components/Common/Fingerprint/Fingerprint.js b/src/react-extension/components/Common/Fingerprint/Fingerprint.js new file mode 100644 index 000000000..2429b8660 --- /dev/null +++ b/src/react-extension/components/Common/Fingerprint/Fingerprint.js @@ -0,0 +1,36 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import React, {Component} from "react"; +import PropTypes from "prop-types"; + +class Fingerprint extends Component { + /** + * Render the component + * @return {JSX} + */ + render() { + return ( +
    +
    {this.props.fingerprint?.substring(0, 20)?.replace(/.{4}/g, '$& ')}
    +
    {this.props.fingerprint?.substring(20)?.replace(/.{4}/g, '$& ')}
    +
    + ); + } +} + +Fingerprint.propTypes = { + fingerprint: PropTypes.string.isRequired, +}; + +export default Fingerprint; diff --git a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js index ec724732f..0b1112fff 100644 --- a/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js +++ b/src/react-extension/components/Common/Inputs/Autocomplete/AutocompleteItem.js @@ -18,6 +18,10 @@ import GroupAvatar from "../../Avatar/GroupAvatar"; import {isUserSuspended} from "../../../../../shared/utils/userUtils"; import {Trans, withTranslation} from "react-i18next"; import {withAppContext} from "../../../../../shared/context/AppContext/AppContext"; +import Icon from "../../../../../shared/components/Icons/Icon"; +import TooltipPortal from "../../Tooltip/TooltipPortal"; +import TooltipMessageFingerprintLoading from "../../Tooltip/TooltipMessageFingerprintLoading"; +import Fingerprint from "../../Fingerprint/Fingerprint"; class AutocompleteItem extends Component { /** @@ -30,9 +34,12 @@ class AutocompleteItem extends Component { this.bindCallbacks(); } + /** + * Returns the component default state + */ get defaultState() { return { - gpgKey: null + tooltipFingerprintMessage: null, }; } @@ -42,6 +49,7 @@ class AutocompleteItem extends Component { */ bindCallbacks() { this.handleClick = this.handleClick.bind(this); + this.onTooltipFingerprintMouseHover = this.onTooltipFingerprintMouseHover.bind(this); } /** @@ -69,6 +77,20 @@ class AutocompleteItem extends Component { return ""; } + /** + * Handle whenever the user passes its mouse hover the tooltip. + * @returns {Promise} + */ + async onTooltipFingerprintMouseHover() { + if (this.state.tooltipFingerprintMessage) { + return; + } + + const gpgkey = await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', this.props.user.id); + const tooltipFingerprintMessage = ; + this.setState({tooltipFingerprintMessage}); + } + /** * Get the autocomplete item classname * @returns {string} @@ -112,8 +134,18 @@ class AutocompleteItem extends Component { }
    - {this.getTitle()}{this.isCurrentUserSuspended && (suspended)} - {this.getSubtitle()} + + {this.getTitle()}{this.isCurrentUserSuspended && (suspended)} + {this.props.user && + } + direction="auto" + onMouseHover={this.onTooltipFingerprintMouseHover}> + + + } + + {this.getSubtitle()}
    diff --git a/src/react-extension/components/Common/Tooltip/TooltipMessageFingerprintLoading.js b/src/react-extension/components/Common/Tooltip/TooltipMessageFingerprintLoading.js new file mode 100644 index 000000000..6bcdffaca --- /dev/null +++ b/src/react-extension/components/Common/Tooltip/TooltipMessageFingerprintLoading.js @@ -0,0 +1,32 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import React, {Component} from "react"; + +class TooltipMessageFingerprintLoading extends Component { + /** + * Render the component + * @return {JSX} + */ + render() { + return ( +
    +
     
    +
     
    +
    +
    + ); + } +} + +export default TooltipMessageFingerprintLoading; diff --git a/src/react-extension/components/Common/Tooltip/TooltipMessageGroupUserDetailsLoading.js b/src/react-extension/components/Common/Tooltip/TooltipMessageGroupUserDetailsLoading.js new file mode 100644 index 000000000..5d6856cb1 --- /dev/null +++ b/src/react-extension/components/Common/Tooltip/TooltipMessageGroupUserDetailsLoading.js @@ -0,0 +1,35 @@ +/** + * Passbolt ~ Open source password manager for teams + * Copyright (c) Passbolt SA (https://www.passbolt.com) + * + * Licensed under GNU Affero General Public License version 3 of the or any later version. + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) + * @license https://opensource.org/licenses/AGPL-3.0 AGPL License + * @link https://www.passbolt.com Passbolt(tm) + * @since 4.9.0 + */ +import React, {Component} from "react"; + +class TooltipMessageGroupUserDetailsLoading extends Component { + /** + * Render the component + * @return {JSX} + */ + render() { + return ( +
    +
     
    +
    +
     
    +
     
    +
    +
    +
    + ); + } +} + +export default TooltipMessageGroupUserDetailsLoading; diff --git a/src/react-extension/components/Common/Tooltip/TooltipPortal.js b/src/react-extension/components/Common/Tooltip/TooltipPortal.js index 30398faaf..0fdcbcb97 100644 --- a/src/react-extension/components/Common/Tooltip/TooltipPortal.js +++ b/src/react-extension/components/Common/Tooltip/TooltipPortal.js @@ -62,6 +62,7 @@ class TooltipPortal extends Component { * Handle mouse over event */ handleMouseEnter() { + this.props?.onMouseHover(); this.setState({hasToDisplayTooltip: true}, this.findBestPosition); } @@ -158,6 +159,7 @@ class TooltipPortal extends Component { TooltipPortal.propTypes = { children: PropTypes.any, message: PropTypes.any.isRequired, + onMouseHover: PropTypes.func }; export default TooltipPortal; diff --git a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js index a07d42385..7d6b33467 100644 --- a/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js +++ b/src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.test.js @@ -331,7 +331,7 @@ describe("Display Resources", () => { }); describe('As LU, I should open the uri of a resource.', () => { - it('As LU, I should be able to follow the uri of a resource', async () => { + it('As LU, I should be able to follow the uri of a resource', async() => { const props = propsWithFilteredResources(); const page = new DisplayResourcesListPage(props); await waitFor(() => { diff --git a/src/react-extension/components/Share/SharePermissionItem.js b/src/react-extension/components/Share/SharePermissionItem.js index 1bce2693a..3494efc37 100644 --- a/src/react-extension/components/Share/SharePermissionItem.js +++ b/src/react-extension/components/Share/SharePermissionItem.js @@ -24,6 +24,9 @@ import Icon from "../../../shared/components/Icons/Icon"; import Tooltip from "../Common/Tooltip/Tooltip"; import Select from "../Common/Select/Select"; import {isUserSuspended} from "../../../shared/utils/userUtils"; +import TooltipPortal from "../Common/Tooltip/TooltipPortal"; +import TooltipMessageFingerprintLoading from "../Common/Tooltip/TooltipMessageFingerprintLoading"; +import Fingerprint from "../Common/Fingerprint/Fingerprint"; class SharePermissionItem extends Component { /** @@ -32,7 +35,7 @@ class SharePermissionItem extends Component { */ constructor(props) { super(props); - this.state = {}; + this.state = this.defaultState; if (!Number.isInteger(props.permissionType)) { throw new TypeError(this.translate("Invalid permission type for share permission item.")); } @@ -41,14 +44,12 @@ class SharePermissionItem extends Component { } /** - * Component did mount - * @returns {Promise} + * Returns the component default state */ - async componentDidMount() { - if (this.isUser()) { - const gpgKey = await this.findUserGpgKey(this.props.aro.profile.user_id); - this.setState({gpgKey}); - } + get defaultState() { + return { + tooltipFingerprintMessage: null, + }; } /** @@ -64,15 +65,7 @@ class SharePermissionItem extends Component { bindEventHandlers() { this.handleUpdate = this.handleUpdate.bind(this); this.handleDelete = this.handleDelete.bind(this); - } - - /** - * Find a user gpg key - * @param {string} userId - * @returns {Promise} - */ - async findUserGpgKey(userId) { - return await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', userId); + this.onTooltipFingerprintMouseHover = this.onTooltipFingerprintMouseHover.bind(this); } /** @@ -108,24 +101,17 @@ class SharePermissionItem extends Component { } /** - * Get the tooltip message - * @returns {JSX.Element} + * Handle whenever the user passes its mouse hover the tooltip. + * @returns {Promise} */ - get tooltipMessage() { - return <> -
    {this.props.aro.username}
    -
    {this.formatFingerprint(this.state.gpgKey.fingerprint)}
    - ; - } + async onTooltipFingerprintMouseHover() { + if (this.state.tooltipFingerprintMessage) { + return; + } - /** - * Format fingerprint - * @param {string} fingerprint An user finger print - * @returns {JSX.Element} - */ - formatFingerprint(fingerprint) { - const result = fingerprint.toUpperCase().replace(/.{4}/g, '$& '); - return <>{result.substr(0, 24)}
    {result.substr(25)}; + const gpgkey = await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', this.props.aro.id); + const tooltipFingerprintMessage = ; + this.setState({tooltipFingerprintMessage}); } /** @@ -144,14 +130,6 @@ class SharePermissionItem extends Component { return !(this.props.aro && this.props.aro.profile); } - /** - * Has a gpg key fingerprint - * @returns {*} - */ - hasGpgKey() { - return this.state.gpgKey?.fingerprint; - } - getClassName() { let className = 'row'; if (this.props.updated) { @@ -218,10 +196,13 @@ class SharePermissionItem extends Component {
    {this.getAroName()} - {this.hasGpgKey() && - - - + {this.isUser() && + } + direction="auto" + onMouseHover={this.onTooltipFingerprintMouseHover}> + + }
    diff --git a/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.js b/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.js index 8943b2442..dafbe7f44 100644 --- a/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.js +++ b/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.js @@ -66,12 +66,17 @@ describe("See the Create Dialog Group", () => { await page.createGroup.click(page.createGroup.userAutocomplete); await page.createGroup.selectFirstItem(2); + await page.createGroup.focus(page.createGroup.userInformationIcon(2)); + expect(page.createGroup.warningMessage).toBe('You need to click save for the changes to take place.'); expect(page.createGroup.count()).toBe(2); expect(page.createGroup.userFirstNameLastName(2)).toBe('Ada Lovelace'); - expect(page.createGroup.userEmail(2)).toBe('ada@passbolt.com'); - expect(page.createGroup.userFingerprint(2)).toBe('03F6 0E95 8F4C B297 23ACDF76 1353 B5B1 5D9B 054F '); + /* + * Commented following the usage of portal to display the tooltip. + * expect(page.createGroup.userEmail(2)).toBe('ada@passbolt.com'); + * expect(page.createGroup.userFingerprint(2)).toBe('03F6 0E95 8F4C B297 23ACDF76 1353 B5B1 5D9B 054F '); + */ const requestMockImpl = jest.fn((message, data) => data); mockContextRequest(context, requestMockImpl); @@ -166,7 +171,11 @@ describe("See the Create Dialog Group", () => { const requestGpgMockImpl = jest.fn(() => mockGpgKey); mockContextRequest(context, requestGpgMockImpl); page.createGroup.fillInput(page.createGroup.usernameInput, "ada"); + jest.runOnlyPendingTimers(); + await waitFor(() => {}); + + await page.createGroup.hoverAutocompleteItemInformationIcon(); expect(context.port.request).toHaveBeenCalledWith("passbolt.keyring.get-public-key-info-by-user", mockUsers[1].id); await waitFor(() => {}); diff --git a/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.page.js b/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.page.js index dfaad5c96..be4b23cb0 100644 --- a/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.page.js +++ b/src/react-extension/components/UserGroup/CreateUserGroup/CreateUserGroup.test.page.js @@ -168,7 +168,6 @@ class CreateGroupPageObject { /** * Returns the user email for the 'index' one - * @param index the display of the user email */ userEmail(index) { return this._container.querySelectorAll('.permissions.groups_users .row')[index - 1].querySelector('.email').textContent; @@ -182,6 +181,15 @@ class CreateGroupPageObject { return this._container.querySelectorAll('.permissions.groups_users .row')[index - 1].querySelector('.fingerprint').textContent; } + /** + * Return the information icon associated to the "index"ed permission list row + * @param {number} index the number of the row in the permission list + * @returns {HTMLElement} + */ + userInformationIcon(index) { + return this._container.querySelectorAll('.permissions.groups_users .row')[index - 1].querySelector('.tooltip-portal'); + } + /** * Returns the select rights for the 'index' one * @param index the display of the permission @@ -270,6 +278,34 @@ class CreateGroupPageObject { await waitFor(() => {}); } + /** + * Simulates a focus event on the given element + * @returns {Promise} + */ + async focus(element) { + fireEvent.focus(element); + await waitFor(() => {}); + } + + /** + * Simulates a mouse hover on the given element + * @returns {Promise} + */ + async hover(element) { + const event = new MouseEvent("hover"); + fireEvent(element, event); + await waitFor(() => {}); + } + + /** + * Simulates a mouse hover on the first autocomplete result item + * @returns {Promise} + */ + async hoverAutocompleteItemInformationIcon() { + const informationIcon = this.userAutocomplete?.querySelector(".tooltip-portal"); + await this.hover(informationIcon); + } + /** Click without wait for on the element */ clickWithoutWaitFor(element) { const leftClick = {button: 0}; @@ -300,8 +336,3 @@ class CreateGroupPageObject { await this.click(this.selectItem(index)); } } - - - - - diff --git a/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js b/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js index a90f844de..d5ff07a13 100644 --- a/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js +++ b/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js @@ -17,76 +17,40 @@ import {withAppContext} from "../../../../shared/context/AppContext/AppContext"; import UserAvatar from "../../Common/Avatar/UserAvatar"; import Icon from "../../../../shared/components/Icons/Icon"; import {Trans, withTranslation} from "react-i18next"; -import Tooltip from "../../Common/Tooltip/Tooltip"; import Select from "../../Common/Select/Select"; import {isUserSuspended} from "../../../../shared/utils/userUtils"; +import TooltipPortal from "../../Common/Tooltip/TooltipPortal"; +import Fingerprint from "../../Common/Fingerprint/Fingerprint"; +import TooltipMessageGroupUserDetailsLoading from "../../Common/Tooltip/TooltipMessageGroupUserDetailsLoading"; /** * This component allows to edit an user group */ class EditUserGroupItem extends Component { /** - * Constructor - * @param {Object} props + * Default constructor + * @param props The component props */ constructor(props) { super(props); this.state = this.defaultState; + this.bindCallbacks(); } /** - * Returns the default component state + * Returns the component default state */ get defaultState() { return { - user: null, - fingerprint: null, + tooltipFingerprintMessage: null, }; } /** - * Whenever the component is mounted + * Bind callbacks. */ - async componentDidMount() { - await this.populate(); - } - - /** - * Populate the component with initial data - * @returns {Promise} - */ - async populate() { - const user = this.props.groupUser.user; - const fingerprint = await this.getFingerprintForUser(user.id); - this.setState({user, fingerprint}); - } - - /** - * Find a user gpg key - * @param {string} userId - * @returns {Promise} - */ - async getFingerprintForUser(userId) { - const keyInfo = await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', userId); - return keyInfo.fingerprint; - } - - /** - * Returns true if the comopinent is ready to be displayed with all information - * @returns {boolean} - */ - get isReady() { - return this.state.user !== null && this.state.fingerprint !== null; - } - - /** - * Format fingerprint - * @param {string} fingerprint An user finger print - * @returns {JSX.Element} - */ - formatFingerprint(fingerprint) { - const result = fingerprint.toUpperCase().replace(/.{4}/g, '$& '); - return <>{result.substr(0, 24)}
    {result.substr(25)}; + bindCallbacks() { + this.onTooltipFingerprintMouseHover = this.onTooltipFingerprintMouseHover.bind(this); } /** @@ -94,21 +58,10 @@ class EditUserGroupItem extends Component { * @returns {string} */ getUserFullname() { - const user = this.state.user; + const user = this.props.groupUser.user; return `${user.profile.first_name} ${user.profile.last_name}`; } - /** - * Get the tooltip message - * @returns {JSX.Element} - */ - getTooltipMessage() { - return <> -
    {this.state.user.username}
    -
    {this.formatFingerprint(this.state.fingerprint)}
    - ; - } - /** * Returns true if the feature flag disableUser is enabled and the given user is suspended. * @param {object} user @@ -129,72 +82,75 @@ class EditUserGroupItem extends Component { ]; } + /** + * Handle whenever the user passes its mouse hover the tooltip. + * @returns {Promise} + */ + async onTooltipFingerprintMouseHover() { + if (this.state.tooltipFingerprintMessage) { + return; + } + + const gpgkey = await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', this.props.groupUser.user.id); + const tooltipFingerprintMessage =
    +
    {this.props.groupUser.user.username}
    + +
    ; + setTimeout(() => this.setState({tooltipFingerprintMessage}), 2000); + } + /** * Render the component */ render() { - const isReady = this.isReady; - const isSuspended = this.isUserSuspended(this.state.user); + const isSuspended = this.isUserSuspended(this.props.groupUser.user); return (
  • - {isReady && - <> - -
    -
    - {this.getUserFullname()}{isSuspended && (suspended)} - - - -
    -
    - {this.props.isMemberAdded && Will be added} - {this.props.isMemberChanged && !this.props.isMemberAdded && - Will be updated} - {!this.props.isMemberChanged && !this.props.isMemberAdded && Unchanged} - -
    -
    - -
    - this.props.onMemberRoleChange(event, this.props.groupUser)} + disabled={!this.props.areActionsAllowed} + direction={this.props.isLastItemDisplayed ? "top" : "bottom"}/> +
    + +
    + +
  • ); } From 984d2e139481469551e955dc5f6b69332d6195ba Mon Sep 17 00:00:00 2001 From: Cedric Alfonsi Date: Wed, 10 Jul 2024 16:01:48 +0000 Subject: [PATCH 64/67] PB-33921 Avoid gpgkeys sync when loading the autocomplete component --- .../UserGroup/EditUserGroup/EditUserGroup.js | 23 +------------------ .../EditUserGroup/EditUserGroupItem.js | 2 +- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroup.js b/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroup.js index 064f6f83a..65618fc36 100644 --- a/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroup.js +++ b/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroup.js @@ -31,7 +31,6 @@ import {maxSizeValidation} from '../../../lib/Error/InputValidator'; import Icon from "../../../../shared/components/Icons/Icon"; import {RESOURCE_GROUP_NAME_MAX_LENGTH} from "../../../../shared/constants/inputs.const"; - /** * This component allows to edit an user group */ @@ -150,25 +149,6 @@ class EditUserGroup extends Component { return this.props.context.users.find(user => user.id === userId); } - /** - * Find a user gpg key - * @param {string} userId - * @returns {Promise} - */ - async findUserGpgkey(userId) { - return await this.props.context.port.request('passbolt.keyring.get-public-key-info-by-user', userId); - } - - /** - * Decorate users with their associated gpg key - * @param users - * @returns {Promise} - */ - async decorateUsersWithGpgkey(users) { - const mapUserWithGpgkey = async user => Object.assign(user, {gpgkey: await this.findUserGpgkey(user.id)}); - return Promise.all(users.map(mapUserWithGpgkey)); - } - /** * The group to edit at component initialization * @type {object} @@ -652,7 +632,7 @@ class EditUserGroup extends Component { const matchText = user => words.every(word => matchUser(word, user)); let currentCount = 0; - const firstUsersMatched = this.props.context.users.filter(user => { + return this.props.context.users.filter(user => { const isUserMatching = currentCount < Autocomplete.DISPLAY_LIMIT && user.active === true && !this.isMember(user) @@ -663,7 +643,6 @@ class EditUserGroup extends Component { } return isUserMatching; }); - return this.decorateUsersWithGpgkey(firstUsersMatched); } /** diff --git a/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js b/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js index d5ff07a13..9f8b55cce 100644 --- a/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js +++ b/src/react-extension/components/UserGroup/EditUserGroup/EditUserGroupItem.js @@ -96,7 +96,7 @@ class EditUserGroupItem extends Component {
    {this.props.groupUser.user.username}
    ; - setTimeout(() => this.setState({tooltipFingerprintMessage}), 2000); + this.setState({tooltipFingerprintMessage}); } /** From 01452fe1e8de2deff52bb45b9a8e73124b9e8065 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Wed, 10 Jul 2024 11:46:47 +0200 Subject: [PATCH 65/67] PB-33847 As an administrator I can configure the LDAP integration to suspend deleted users --- .../DisplayUserDirectoryAdministration.js | 35 ++++++++++++++++++- ...ayUserDirectoryAdministration.test.data.js | 1 + .../models/userDirectory/UserDirectoryDTO.js | 1 + .../UserDirectoryDTO.test.data.js | 1 + .../userDirectory/UserDirectoryModel.js | 2 ++ .../UserDirectoryModel.test.data.js | 1 + 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js b/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js index 80647b6f4..4e802e068 100644 --- a/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js +++ b/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.js @@ -679,6 +679,38 @@ class DisplayUserDirectoryAdministration extends React.Component { +
    + +
    Define the behaviour when existing synchronized users are removed + from the users directory: +
    +
    +
    + + +
    +
    + + +
    +
    +
    @@ -692,7 +724,8 @@ class DisplayUserDirectoryAdministration extends React.Component {

    Need help?

    Check out our ldap configuration guide.

    - + Read the documentation diff --git a/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.test.data.js b/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.test.data.js index fcad36b78..0a01a0875 100644 --- a/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.test.data.js +++ b/src/react-extension/components/Administration/DisplayUserDirectoryAdministration/DisplayUserDirectoryAdministration.test.data.js @@ -170,6 +170,7 @@ export const mockModel = { createGroups: true, deleteGroups: true, updateGroups: true, + deleteUserBehavior: "delete", fieldsMapping: { ad: { user: { diff --git a/src/shared/models/userDirectory/UserDirectoryDTO.js b/src/shared/models/userDirectory/UserDirectoryDTO.js index 30d9fd809..e25cd1ce0 100644 --- a/src/shared/models/userDirectory/UserDirectoryDTO.js +++ b/src/shared/models/userDirectory/UserDirectoryDTO.js @@ -48,6 +48,7 @@ class UserDirectoryDTO { this.sync_groups_update = userDirectoryModel.updateGroups; this.fields_mapping = userDirectoryModel.fieldsMapping; this.field_fallbacks = userDirectoryModel.fallbackFields; + this.delete_user_behavior = userDirectoryModel.deleteUserBehavior; this.domains = { // DEFAULT DOMAIN diff --git a/src/shared/models/userDirectory/UserDirectoryDTO.test.data.js b/src/shared/models/userDirectory/UserDirectoryDTO.test.data.js index 42b3ebf62..a12eecbd1 100644 --- a/src/shared/models/userDirectory/UserDirectoryDTO.test.data.js +++ b/src/shared/models/userDirectory/UserDirectoryDTO.test.data.js @@ -38,6 +38,7 @@ export const mockedData = { user_path: undefined, user_custom_filters: undefined, users_parent_group: undefined, + delete_user_behavior: "delete", domains: { org_domain: { base_dn: "DC=passbolt,DC=local", diff --git a/src/shared/models/userDirectory/UserDirectoryModel.js b/src/shared/models/userDirectory/UserDirectoryModel.js index 1e0d983fa..a0ccafb4c 100644 --- a/src/shared/models/userDirectory/UserDirectoryModel.js +++ b/src/shared/models/userDirectory/UserDirectoryModel.js @@ -75,6 +75,7 @@ class UserDirectoryModel { this.createGroups = Boolean(userDirectoryDTO.sync_groups_create); this.deleteGroups = Boolean(userDirectoryDTO.sync_groups_delete); this.updateGroups = Boolean(userDirectoryDTO.sync_groups_update); + this.deleteUserBehavior = userDirectoryDTO.delete_user_behavior || "delete"; //Form field option this.userDirectoryToggle = Boolean(this.port) && Boolean(this.host) && userDirectoryDTO?.enabled; } @@ -123,6 +124,7 @@ class UserDirectoryModel { this.createGroups = true; this.deleteGroups = true; this.updateGroups = true; + this.deleteUserBehavior = "delete"; //Form field option this.userDirectoryToggle = false; } diff --git a/src/shared/models/userDirectory/UserDirectoryModel.test.data.js b/src/shared/models/userDirectory/UserDirectoryModel.test.data.js index 474c276b5..5fe564095 100644 --- a/src/shared/models/userDirectory/UserDirectoryModel.test.data.js +++ b/src/shared/models/userDirectory/UserDirectoryModel.test.data.js @@ -59,6 +59,7 @@ export const mockedDefaultData = (data = {}) => { authenticationType: "basic", fieldsMapping: defaultFieldsMapping(data.fieldsMapping), fallbackFields: defaultFallbackFields(data.fallbackFields), + deleteUserBehavior: "delete", }; delete data.fieldsMapping; From b6eac87973183da348790ce8702442076cbde2e1 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Fri, 12 Jul 2024 14:36:14 +0000 Subject: [PATCH 66/67] PB-25246 As signed-in user I should not see a blank page when I delete the... --- .../DisplayResourceFolderDetailsInformation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js index c9dcf18cb..ff0cc6117 100644 --- a/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js +++ b/src/react-extension/components/ResourceFolderDetails/DisplayResourceFolderDetails/DisplayResourceFolderDetailsInformation.js @@ -104,7 +104,7 @@ class DisplayResourceFolderDetailsInformation extends React.Component { if (this.props.context.folders) { const folder = this.props.context.folders.find(item => item.id === folderId); - return folder.name; + return folder?.name; } return ""; From 306537865ca21dc49f2f8fd328db6a819c3a1e40 Mon Sep 17 00:00:00 2001 From: Benjamin Monnot Date: Mon, 15 Jul 2024 14:58:47 +0200 Subject: [PATCH 67/67] PB-34068 Styleguide version bump to v4.9.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40f9b489d..6d5147124 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.12", + "version": "4.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.12", + "version": "4.9.0", "license": "AGPL-3.0", "dependencies": { "debounce-promise": "^3.1.2", diff --git a/package.json b/package.json index 4da46ccd6..c2593cb86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passbolt-styleguide", - "version": "4.8.0-alpha.12", + "version": "4.9.0", "license": "AGPL-3.0", "copyright": "Copyright 2023 Passbolt SA", "description": "Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.",