passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
368 lines (315 loc) • 17.7 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2020 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) 2020 Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 2.11.0
*/
/**
* Unit tests on EditResource in regard of specifications
*/
import {ActionFeedbackContext} from "../../../contexts/ActionFeedbackContext";
import PassboltApiFetchError from "../../../../shared/lib/Error/PassboltApiFetchError";
import {waitFor} from "@testing-library/react";
import {defaultProps, mockResource} from "./EditResource.test.data";
import EditResourcePage from "./EditResource.test.page";
import "../../../test/lib/crypto/cryptoGetRandomvalues";
describe("See the Edit Resource", () => {
let page; // The page to test against
const props = defaultProps(); // The props to pass
props.onClose = jest.fn();
props.dialogContext.open = jest.fn();
const passwordEditDialogProps = {
id: "8e3874ae-4b40-590b-968a-418f704b9d9a"
};
const truncatedWarningMessage = "Warning: this is the maximum size for this field, make sure your data was not truncated.";
describe('As LU I can start adding a password', () => {
const mockContextRequest = implementation => jest.spyOn(props.context.port, 'request').mockImplementation(implementation);
/**
* I should see the edit password dialog
*/
beforeEach(() => {
jest.useFakeTimers();
jest.resetModules();
props.context.setContext({passwordEditDialogProps});
page = new EditResourcePage(props);
});
afterEach(() => {
jest.clearAllTimers();
});
it('matches the styleguide', () => {
// Dialog title exists and correct
expect(page.passwordEdit.exists()).toBeTruthy();
expect(page.title.header.textContent).toBe("Edit resource");
expect(page.title.subtitle.textContent).toBe(mockResource.name);
// Close button exists
expect(page.passwordEdit.dialogClose).not.toBeNull();
// Name input field exists.
expect(page.passwordEdit.name.value).toBe(mockResource.name);
// Uri input field exists.
expect(page.passwordEdit.uri.value).toBe(mockResource.uri);
// Username input field exists.
expect(page.passwordEdit.username.value).toBe(mockResource.username);
// Password input field exists
expect(page.passwordEdit.password).not.toBeNull();
expect(page.passwordEdit.password.value).toBe("");
expect(page.passwordEdit.password.getAttribute("type")).toBe("password");
const passwordInputStyle = window.getComputedStyle(page.passwordEdit.password);
expect(passwordInputStyle.background).toBe("white");
expect(passwordInputStyle.color).toBe("");
// Complexity label exists but is not yet defined.
expect(page.passwordEdit.complexityText.textContent).toBe("Quality");
// Password view button exists.
expect(page.passwordEdit.passwordViewButton).not.toBeNull();
expect(page.passwordEdit.passwordViewButton.classList.contains("eye-open")).toBe(true);
expect(page.passwordEdit.passwordViewButton.classList.contains("eye-close")).toBe(false);
// Password generate button exists.
expect(page.passwordEdit.passwordGenerateButton).not.toBeNull();
// Description textarea field exists
expect(page.passwordEdit.description.value).toBe(mockResource.description);
// Save button exists
expect(page.passwordEdit.saveButton.textContent).toBe("Save");
// Cancel button exists
expect(page.passwordEdit.cancelButton.textContent).toBe("Cancel");
});
it('generates password when clicking on the generate button.', async() => {
page.passwordEdit.focusInput(page.passwordEdit.password);
await waitFor(() => {
expect(page.passwordEdit.password.disabled).toBeFalsy();
});
await page.passwordEdit.click(page.passwordEdit.passwordGenerateButton);
expect(page.passwordEdit.complexityText.textContent).not.toBe("n/a (entropy: 0.0 bits)");
expect(page.passwordEdit.progressBar.classList.contains("not_available")).toBe(false);
});
it('views password when clicking on the view button.', async() => {
const passwordValue = "secret-decrypted";
// View password
await waitFor(() => {
expect(page.passwordEdit.password.disabled).toBeFalsy();
});
await page.passwordEdit.click(page.passwordEdit.passwordViewButton);
expect(page.passwordEdit.password.value).toBe(passwordValue);
let passwordInputType = page.passwordEdit.password.getAttribute("type");
expect(passwordInputType).toBe("text");
expect(page.passwordEdit.passwordViewButton.classList.contains("eye-open")).toBe(false);
expect(page.passwordEdit.passwordViewButton.classList.contains("eye-close")).toBe(true);
// Hide password
await page.passwordEdit.click(page.passwordEdit.passwordViewButton);
expect(page.passwordEdit.password.value).toBe(passwordValue);
passwordInputType = page.passwordEdit.password.getAttribute("type");
expect(passwordInputType).toBe("password");
expect(page.passwordEdit.passwordViewButton.classList.contains("eye-open")).toBe(true);
expect(page.passwordEdit.passwordViewButton.classList.contains("eye-close")).toBe(false);
});
it('requests the addon to edit a resource with encrypted description when clicking on the submit button.', async() => {
//Avoid to block with the beforeMount when checking current password
jest.runAllTimers();
// edit password
const resourceMeta = {
name: "Password name",
uri: "https://uri.dev",
username: "Password username",
password: "password-value12345",
description: "Password description"
};
// Fill the form
page.passwordEdit.fillInput(page.passwordEdit.name, resourceMeta.name);
page.passwordEdit.fillInput(page.passwordEdit.uri, resourceMeta.uri);
page.passwordEdit.fillInput(page.passwordEdit.username, resourceMeta.username);
page.passwordEdit.focusInput(page.passwordEdit.password);
await waitFor(() => {
expect(page.passwordEdit.password.disabled).toBeTruthy();
});
await page.passwordEdit.fillInputPassword(resourceMeta.password);
await page.passwordEdit.blurInput(page.passwordEdit.password);
expect(page.passwordEdit.complexityText.textContent).not.toBe("Quality");
expect(page.passwordEdit.progressBar.classList.contains("error")).toBe(false);
page.passwordEdit.fillInput(page.passwordEdit.description, resourceMeta.description);
const requestMockImpl = jest.fn();
mockContextRequest(requestMockImpl);
jest.spyOn(props.context.port, 'emit').mockImplementation(jest.fn());
jest.spyOn(ActionFeedbackContext._currentValue, 'displaySuccess').mockImplementation(() => {});
const onApiUpdateResourceMeta = {
id: "8e3874ae-4b40-590b-968a-418f704b9d9a",
name: resourceMeta.name,
uri: resourceMeta.uri,
username: resourceMeta.username,
description: resourceMeta.description,
resource_type_id: "669f8c64-242a-59fb-92fc-81f660975fd3"
};
await page.passwordEdit.click(page.passwordEdit.saveButton);
expect(props.context.port.request).toHaveBeenCalledWith("passbolt.resources.update", onApiUpdateResourceMeta, resourceMeta.password);
expect(ActionFeedbackContext._currentValue.displaySuccess).toHaveBeenCalled();
expect(props.context.port.emit).toHaveBeenNthCalledWith(1, "passbolt.resources.select-and-scroll-to", "8e3874ae-4b40-590b-968a-418f704b9d9a");
expect(props.onClose).toBeCalled();
});
xit('requests the addon to edit a resource with non encrypted description when clicking on the submit button.', async() => {
expect(page.passwordEdit.exists()).toBeTruthy();
// edit password
const resourceMeta = {
name: "Password name",
uri: "https://uri.dev",
username: "Password username",
password: "password-value",
description: "Password description"
};
// Fill the form
page.passwordEdit.fillInput(page.passwordEdit.name, resourceMeta.name);
page.passwordEdit.fillInput(page.passwordEdit.uri, resourceMeta.uri);
page.passwordEdit.fillInput(page.passwordEdit.username, resourceMeta.username);
page.passwordEdit.focusInput(page.passwordEdit.password);
await waitFor(() => {
expect(page.passwordEdit.password.classList).toContain("decrypted");
});
page.passwordEdit.fillInput(page.passwordEdit.password, resourceMeta.password);
page.passwordEdit.blurInput(page.passwordEdit.password);
expect(page.passwordEdit.complexityText.textContent).not.toBe("Complexity: n/aEntropy: NaN bits");
expect(page.passwordEdit.progressBar.classList.contains("not_available")).toBe(false);
page.passwordEdit.fillInput(page.passwordEdit.description, resourceMeta.description);
await page.passwordEdit.click(page.passwordEdit.descriptionEncryptedLock);
const requestMockImpl = jest.fn();
mockContextRequest(requestMockImpl);
jest.spyOn(props.context.port, 'emit').mockImplementation(jest.fn());
jest.spyOn(ActionFeedbackContext._currentValue, 'displaySuccess').mockImplementation(() => {});
const onApiUpdateResourceDto = {
id: "8e3874ae-4b40-590b-968a-418f704b9d9a",
name: resourceMeta.name,
uri: resourceMeta.uri,
username: resourceMeta.username,
description: '',
resource_type_id: "a28a04cd-6f53-518a-967c-9963bf9cec51"
};
const onApiUpdateSecretDto = {
description: resourceMeta.description,
password: resourceMeta.password
};
await page.passwordEdit.click(page.passwordEdit.saveButton);
expect(props.context.port.request).toHaveBeenCalledWith("passbolt.resources.update", onApiUpdateResourceDto, onApiUpdateSecretDto);
expect(ActionFeedbackContext._currentValue.displaySuccess).toHaveBeenCalled();
expect(props.context.port.emit).toHaveBeenNthCalledWith(1, "passbolt.resources.select-and-scroll-to", "8e3874ae-4b40-590b-968a-418f704b9d9a");
expect(props.onClose).toBeCalled();
});
xit('As LU I shouldn’t be able to submit the form if there is an invalid field', async() => {
expect(page.passwordEdit.exists()).toBeTruthy();
// empty the form
page.passwordEdit.fillInput(page.passwordEdit.name, "");
page.passwordEdit.focusInput(page.passwordEdit.password);
await waitFor(() => {
expect(page.passwordEdit.password.classList).toContain("decrypted");
});
await page.passwordEdit.fillInputPassword("");
page.passwordEdit.blurInput(page.passwordEdit.password);
await page.passwordEdit.click(page.passwordEdit.saveButton);
// Throw error message
expect(page.passwordEdit.nameErrorMessage.textContent).toBe("A name is required.");
expect(page.passwordEdit.passwordErrorMessage.textContent).toBe("A password is required.");
});
it('As LU I can stop editing a password by clicking on the cancel button', async() => {
expect(page.passwordEdit.exists()).toBeTruthy();
await page.passwordEdit.click(page.passwordEdit.cancelButton);
expect(props.onClose).toBeCalled();
});
it('As LU I can stop editing a password by closing the dialog', async() => {
expect(page.passwordEdit.exists()).toBeTruthy();
await page.passwordEdit.click(page.passwordEdit.dialogClose);
expect(props.onClose).toBeCalled();
});
it('As LU I can stop adding a password with the keyboard (escape)', async() => {
expect(page.passwordEdit.exists()).toBeTruthy();
await page.passwordEdit.escapeKey(page.passwordEdit.dialogClose);
expect(props.onClose).toBeCalled();
});
xit('As LU I should see an error dialog if the submit operation fails for an unexpected reason', async() => {
// Mock the request function to make it return an error.
page.passwordEdit.focusInput(page.passwordEdit.password);
await waitFor(() => {
expect(page.passwordEdit.password.classList).toContain("decrypted");
});
await page.passwordEdit.fillInputPassword("password");
page.passwordEdit.blurInput(page.passwordEdit.password);
jest.spyOn(props.context.port, 'request').mockImplementationOnce(() => {
throw new PassboltApiFetchError("Jest simulate API error.");
});
await page.passwordEdit.click(page.passwordEdit.saveButton);
// Throw general error message
expect(page.passwordEdit.errorDialog).not.toBeNull();
expect(page.passwordEdit.errorDialogMessage).not.toBeNull();
});
xit('As LU I cannot update the form fields and I should see a processing feedback while submitting the form', async() => {
// Mock the request function to make it the expected result
let updateResolve;
const requestMockImpl = jest.fn(() => new Promise(resolve => {
updateResolve = resolve;
}));
page.passwordEdit.focusInput(page.passwordEdit.password);
await waitFor(() => {
expect(page.passwordEdit.password.classList).toContain("decrypted");
});
await page.passwordEdit.fillInputPassword("password");
page.passwordEdit.blurInput(page.passwordEdit.password);
// Mock the request function to make it the expected result
mockContextRequest(requestMockImpl);
page.passwordEdit.clickWithoutWaitFor(page.passwordEdit.saveButton);
// API calls are made on submit, wait they are resolved.
await waitFor(() => {
expect(page.passwordEdit.name.getAttribute("disabled")).not.toBeNull();
expect(page.passwordEdit.uri.getAttribute("disabled")).not.toBeNull();
expect(page.passwordEdit.username.getAttribute("disabled")).not.toBeNull();
expect(page.passwordEdit.password.getAttribute("disabled")).not.toBeNull();
expect(page.passwordEdit.saveButton.getAttribute("disabled")).not.toBeNull();
expect(page.passwordEdit.saveButton.className).toBe("button primary disabled processing");
expect(page.passwordEdit.cancelButton.className).toBe("cancel disabled");
updateResolve();
});
});
it('As LU I should access to the password generator dialog', async() => {
jest.spyOn(props.dialogContext, 'open').mockImplementationOnce(jest.fn);
await page.passwordEdit.openPasswordGenerator();
expect(props.dialogContext.open).toBeCalled();
});
it("As a user I should see a feedback when the password, descriptions, name, username or uri fields content is truncated by a field limit", async() => {
expect.assertions(5);
await page.passwordEdit.fillInputPassword('a'.repeat(4097));
page.passwordEdit.fillInput(page.passwordEdit.description, 'a'.repeat(10000));
page.passwordEdit.fillInput(page.passwordEdit.name, 'a'.repeat(256));
page.passwordEdit.fillInput(page.passwordEdit.username, 'a'.repeat(255));
page.passwordEdit.fillInput(page.passwordEdit.uri, 'a'.repeat(1025));
await page.passwordEdit.keyUpInput(page.passwordEdit.password);
await page.passwordEdit.keyUpInput(page.passwordEdit.description);
await page.passwordEdit.keyUpInput(page.passwordEdit.username);
await page.passwordEdit.keyUpInput(page.passwordEdit.name);
await page.passwordEdit.keyUpInput(page.passwordEdit.uri);
expect(page.passwordEdit.passwordWarningMessage.textContent).toEqual(truncatedWarningMessage);
expect(page.passwordEdit.descriptionWarningMessage.textContent).toEqual(truncatedWarningMessage);
expect(page.passwordEdit.nameWarningMessage.textContent).toEqual(truncatedWarningMessage);
expect(page.passwordEdit.uriWarningMessage.textContent).toEqual(truncatedWarningMessage);
expect(page.passwordEdit.usernameWarningMessage.textContent).toEqual(truncatedWarningMessage);
});
it("As a signed-in user editing a password on the application, I should get warn when I enter a pwned password and not be blocked", async() => {
expect.assertions(2);
mockContextRequest(() => Promise.resolve(2));
await page.passwordEdit.fillInputPassword('hello-world');
await waitFor(() => {});
// we expect a warning to inform about powned password
expect(page.passwordEdit.pwnedWarningMessage.textContent).toEqual("The password is part of an exposed data breach.");
mockContextRequest(() => Promise.reject());
await page.passwordEdit.fillInputPassword('another test');
await waitFor(() => {});
// we expect a warning to inform about a network issue
expect(page.passwordEdit.pwnedWarningMessage.textContent).toEqual("The pwnedpasswords service is unavailable, your password might be part of an exposed data breach");
});
it("As a signed-in user editing a password on the application, I should see a complexity as Quality if the passphrase is empty", async() => {
expect.assertions(2);
await page.passwordEdit.fillInputPassword("");
await waitFor(() => {});
expect(page.passwordEdit.pwnedWarningMessage).toBeNull();
expect(page.passwordEdit.complexityText.textContent).toBe("Quality");
});
});
});