UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

429 lines (339 loc) 16.2 kB
/** * 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 5.11.0 */ /* eslint-disable no-unsanitized/property */ import UserEventsService from "../lib/User/UserEventsService"; import { domElementLoginWithNameAttributeUsername, domElementOnlyPassword, domElementOnlyUsername, domElementWithNoUsernamePassword, domElementLoginWithPlaceHolderAttributeUsername, domElementWithMultipleLogin, domGenericTextAndPassword, domWithMultiplePasswords, domMultipleUsernameCandidates, domNestedUsernamePassword, domTwoEmailInputsAndPassword, domUsernameByClass, domUsernameById, domSingleOTPField, domSingleOTPFieldWithUsernameAndPassword, domSingleOTPMultiField, domPartslink24ThreeFieldLogin, domSDRLoginForm, domBestKeywordOnFirstElement, domSameKeywordPriorityTwoElements, } from "../lib/InForm/InformManager.test.data"; import { defaultFormData } from "./Autofill.test.data"; import MockPort from "../../react-extension/test/mock/MockPort"; import AutofillPage from "./Autofill.test.page"; import { FAIL_STRING_SCENARIOS } from "../../../test/assert/assertEntityProperty"; import { TotpCodeGeneratorService } from "../../shared/services/otp/TotpCodeGeneratorService"; beforeEach(() => { jest.clearAllMocks(); Object.defineProperty(window, "port", { writable: true, value: new MockPort(), }); Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, writable: true, value: 100, }); jest.spyOn(UserEventsService, "autofill").mockReturnValue(); jest.spyOn(window.port, "emit").mockResolvedValue(); }); describe("Autofill::fillForm", () => { describe("Should autofill the form", () => { let formData; beforeEach(() => { formData = defaultFormData(); }); describe("With password and username elements", () => { it.each([ { label: "username and password", value: domElementLoginWithNameAttributeUsername }, { label: "nested username and password", value: domNestedUsernamePassword }, { label: "username by id", value: domUsernameById }, { label: "username by class", value: domUsernameByClass }, { label: "username by placeholder", value: domElementLoginWithPlaceHolderAttributeUsername }, ])("Should autofill both username and password for $label", ({ value: testCase }) => { expect.assertions(4); document.body.innerHTML = testCase; const page = new AutofillPage(); page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(2); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.username, formData.username); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.password, formData.secret); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); }); describe("With password element only", () => { it("Should autofill only password when no username field is found near password", () => { expect.assertions(3); document.body.innerHTML = domElementOnlyPassword; const page = new AutofillPage(); page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(1); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.password, formData.secret); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); }); describe("With no password element", () => { it("Should autofill username from document when no password element exists", () => { expect.assertions(3); document.body.innerHTML = domElementOnlyUsername; const page = new AutofillPage(); page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(1); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.username, formData.username); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); }); describe("With no suitable element", () => { it("Should catch and emit an error when no elements are found", () => { expect.assertions(2); document.body.innerHTML = domElementWithNoUsernamePassword; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: "Unable to find the input elements on this page.", }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); }); describe("With OTP element", () => { it("Should catch and emit an error when TOTP generation failed", () => { expect.assertions(3); document.body.innerHTML = domSingleOTPField; jest.spyOn(TotpCodeGeneratorService, "generate").mockReturnValueOnce(undefined); const formData = defaultFormData(); const page = new AutofillPage(); expect(() => page.fillForm(formData)).not.toThrow(); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: "Error while generating the TOTP.", }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); }); }); }); describe("Autofill::validateData", () => { ["username", "secret", "url"].forEach((parameterName) => { FAIL_STRING_SCENARIOS.forEach(({ scenario, value: parameterValue }) => { it(`Should catch and emit an error when ${parameterName} is ${scenario}`, () => { expect.assertions(2); const formData = defaultFormData({ [parameterName]: parameterValue }); const page = new AutofillPage(); page.fillForm(formData); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: `The parameter ${parameterName} is not valid`, }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); }); }); it(`Should catch and emit an error when otp is not an invalid object`, () => { expect.assertions(2); const formData = defaultFormData({ otp: {} }); const page = new AutofillPage(); page.fillForm(formData); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: `The parameter otp is not valid`, }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); it(`Should catch and emit an error when no credentials are not provided`, () => { expect.assertions(2); const formData = defaultFormData({ username: undefined, secret: undefined, otp: undefined }); const page = new AutofillPage(); page.fillForm(formData); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: `Either otp or username/secret parameters are required`, }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); }); describe("Autofill::isRequestInitiatedFromSameOrigin", () => { it("Should emit an error when the request is from a different origin", () => { expect.assertions(2); document.body.innerHTML = domElementLoginWithNameAttributeUsername; const formData = defaultFormData({ url: "https://passbolt.com/login" }); const page = new AutofillPage(); page.fillForm(formData); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: "The request is not initiated from same origin", }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); it("Should emit an error when the url is an empty string", () => { expect.assertions(2); document.body.innerHTML = domElementLoginWithNameAttributeUsername; const formData = defaultFormData({ url: "" }); const page = new AutofillPage(); page.fillForm(formData); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "ERROR", { name: "Error", message: "The request is not initiated from same origin", }); expect(UserEventsService.autofill).not.toHaveBeenCalled(); }); }); describe("Autofill::getPasswordElement", () => { it("Should skip hidden password fields and use the first visible one", () => { expect.assertions(3); document.body.innerHTML = domWithMultiplePasswords; const formData = defaultFormData(); const page = new AutofillPage(); // Only the second password is visible page.password.offsetWidth = 0; page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(2); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.username, formData.username); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.passwords[1], formData.secret); }); }); describe("Autofill::getUsernameElementBasedOnPasswordElement", () => { it("Should get the username field when all password fields are hidden", () => { expect.assertions(3); document.body.innerHTML = domElementWithMultipleLogin; const formData = defaultFormData(); const page = new AutofillPage(); // Hide all passwords page.passwords.forEach((password) => { password.offsetWidth = 0; }); page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(1); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.username, formData.username); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); }); describe("Autofill::getOTPElement", () => { [ { label: "the autocomplete attribute is set", value: domSingleOTPField }, { label: "there is a single OTP field represented by multiple inputs", value: domSingleOTPMultiField }, ].forEach(({ label, value: testCase }) => { it(`Should get the OTP field when ${label}`, () => { expect.assertions(3); document.body.innerHTML = testCase; const formData = defaultFormData(); const page = new AutofillPage(); const totp = page.generateTOTP(formData.otp); page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(1); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.otp, totp); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); }); it(`Should get the OTP field when there are username and password fields`, () => { expect.assertions(5); document.body.innerHTML = domSingleOTPFieldWithUsernameAndPassword; const formData = defaultFormData(); const page = new AutofillPage(); const totp = page.generateTOTP(formData.otp); page.fillForm(formData); expect(UserEventsService.autofill).toHaveBeenCalledTimes(3); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.username, formData.username); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.password, formData.secret); expect(UserEventsService.autofill).toHaveBeenCalledWith(page.otp, totp); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); }); describe("Autofill::extractUsernameElementWithFallback", () => { it("Should prefer the input with a username-related keyword in its id", () => { expect.assertions(1); document.body.innerHTML = domMultipleUsernameCandidates; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const expectedUsernameEl = document.querySelector("#user_email"); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedUsernameEl, formData.username); }); it("Should fall back to the first matching element when multiple candidates exist", () => { expect.assertions(2); document.body.innerHTML = domTwoEmailInputsAndPassword; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const firstEmailInput = document.querySelector("#first-field"); expect(UserEventsService.autofill).toHaveBeenCalledWith(firstEmailInput, formData.username); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); it("Should autofill only password when text input does not match username selectors", () => { expect.assertions(3); document.body.innerHTML = domGenericTextAndPassword; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const passwordEl = document.querySelector('input[type="password"]'); expect(UserEventsService.autofill).toHaveBeenCalledTimes(1); expect(UserEventsService.autofill).toHaveBeenCalledWith(passwordEl, formData.secret); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); it("Should prefer userLogin over accountLogin on partslink24 3-field form", () => { expect.assertions(4); document.body.innerHTML = domPartslink24ThreeFieldLogin; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const expectedUsernameEl = document.querySelector('[name="userLogin"]'); const expectedPasswordEl = document.querySelector('[type="password"]'); expect(UserEventsService.autofill).toHaveBeenCalledTimes(2); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedUsernameEl, formData.username); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedPasswordEl, formData.secret); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); it("Should autofill Italian 'Utente' username field on SDR login form", () => { expect.assertions(4); document.body.innerHTML = domSDRLoginForm; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const expectedUsernameEl = document.querySelector("#Utente"); const expectedPasswordEl = document.querySelector("#Password"); expect(UserEventsService.autofill).toHaveBeenCalledTimes(2); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedUsernameEl, formData.username); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedPasswordEl, formData.secret); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); it("Should pick the first element when it has the best keyword score", () => { expect.assertions(3); document.body.innerHTML = domBestKeywordOnFirstElement; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const expectedUsernameEl = document.querySelector('[name="username"]'); expect(UserEventsService.autofill).toHaveBeenCalledTimes(2); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedUsernameEl, formData.username); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); it("Should pick the first element in DOM order when two elements have the same keyword priority", () => { expect.assertions(3); document.body.innerHTML = domSameKeywordPriorityTwoElements; const formData = defaultFormData(); const page = new AutofillPage(); page.fillForm(formData); const expectedUsernameEl = document.querySelector('[name="main_user"]'); expect(UserEventsService.autofill).toHaveBeenCalledTimes(2); expect(UserEventsService.autofill).toHaveBeenCalledWith(expectedUsernameEl, formData.username); expect(window.port.emit).toHaveBeenCalledWith(formData.requestId, "SUCCESS"); }); });