passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
571 lines (457 loc) • 21.5 kB
JavaScript
/**
* 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.2.0
*/
import { cleanup, screen } from "@testing-library/react";
import "../../../shared/lib/Secret/SecretComplexity";
import "../../../react-extension/test/lib/crypto/cryptoGetRandomvalues";
import { defaultProps } from "./ResourceCreatePage.test.data";
import { ConfirmCreatePageRuleVariations } from "../ConfirmCreatePage/ConfirmCreatePage";
import {
TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
TEST_RESOURCE_TYPE_V5_DEFAULT,
} from "../../../shared/models/entity/resourceType/resourceTypeEntity.test.data";
import ResourceCreatePagePage from "./ResourceCreatePage.test.page";
import { waitForTrue } from "../../../../test/utils/waitFor";
import { defaultResourceDto } from "../../../shared/models/entity/resource/resourceEntity.test.data";
import { DateTime } from "luxon";
import { createMemoryHistory } from "history";
import { defaultPasswordExpirySettingsContext } from "../../../react-extension/contexts/PasswordExpirySettingsContext.test.data";
import MetadataTypesSettingsEntity from "../../../shared/models/entity/metadata/metadataTypesSettingsEntity";
import { defaultMetadataTypesSettingsV6Dto } from "../../../shared/models/entity/metadata/metadataTypesSettingsEntity.test.data";
import { SECRET_DATA_OBJECT_TYPE } from "../../../shared/models/entity/secretData/secretDataEntity";
import { act } from "react";
// Reset the modules before each test.
beforeEach(() => {
jest.useFakeTimers();
jest.resetModules();
});
// Cleanup after each test.
afterEach(() => {
cleanup();
delete window.passbolt;
jest.clearAllTimers();
jest.useRealTimers();
});
describe("ResourceCreatePage", () => {
describe("Form initialization", () => {
it("should intialize the name and uri input fields with the active tab metadata", async () => {
expect.assertions(4);
const expectedData = {
name: "Passbolt Browser Extension Test",
uris: ["https://passbolt-browser-extension/test"],
secret_clear: "AAAAAAAAAAAAAAAAAA",
};
const props = defaultProps();
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", () => expectedData);
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(expectedData.name);
// Assert the form.
expect(page.name.value).toStrictEqual(expectedData.name);
expect(page.uri.value).toStrictEqual(expectedData.uris[0]);
expect(page.username.value).toStrictEqual(props.context.userSettings.username);
expect(page.password.value).toStrictEqual(expectedData.secret_clear);
});
it("should not initialize with chrome new tab metadata", async () => {
expect.assertions(2);
const expectedData = {
name: "newtab",
uris: ["chrome://newtab/"],
};
const props = defaultProps();
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", async () => expectedData);
const page = new ResourceCreatePagePage(props);
await waitForTrue(() => page.name.value === "");
// Assert the form.
expect(page.name.value).toStrictEqual("");
expect(page.uri.value).toStrictEqual("");
});
it("should not initialize with firefox new tab metadata", async () => {
const expectedData = {
name: "",
uris: ["about:newtab"],
};
const props = defaultProps();
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", async () => expectedData);
const page = new ResourceCreatePagePage(props);
await waitForTrue(() => page.name.value === "");
// Assert the form.
expect(page.name.value).toStrictEqual("");
expect(page.uri.value).toStrictEqual("");
});
});
describe("Form submission", () => {
it("should create a new password v4 on submit", async () => {
expect.assertions(2);
const fakeNow = DateTime.fromISO("2023-11-24T00:00:00.000Z");
jest.setSystemTime(fakeNow.toMillis());
const expectedResourceDto = {
resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
folder_parent_id: null,
metadata: {
resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
name: "Passbolt Browser Extension Test",
uris: ["https://passbolt-browser-extension/test"],
username: "test@passbolt.com",
},
expired: fakeNow.plus({ days: 30 }).plus({ milliseconds: 100 }).toJSDate().toISOString(),
};
const expectedSecretDto = {
password: "P4ssb0ltP4ssb0lt",
description: "",
resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
};
const props = defaultProps({}, true);
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", () => ({
name: expectedResourceDto.metadata.name,
uris: expectedResourceDto.metadata.uris,
}));
props.context.port.addRequestListener("passbolt.secrets.powned-password", () => 0);
let resourceCreated, secretCreated;
props.context.port.addRequestListener("passbolt.resources.create", (resourceDto, secretDto) => {
resourceCreated = resourceDto;
secretCreated = secretDto;
return defaultResourceDto();
});
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(expectedResourceDto.metadata.name);
// Fill the form empty fields"
await page.setFormWith({
username: "test@passbolt.com",
password: "P4ssb0ltP4ssb0lt",
});
// Submit the form.
await page.submitForm();
expect(resourceCreated).toStrictEqual(expectedResourceDto);
expect(secretCreated).toStrictEqual(expectedSecretDto);
});
it("should create a new password v5 on submit", async () => {
expect.assertions(2);
const fakeNow = DateTime.fromISO("2023-11-24T00:00:00.000Z");
jest.setSystemTime(fakeNow.toMillis());
const expectedResourceDto = {
resource_type_id: TEST_RESOURCE_TYPE_V5_DEFAULT,
folder_parent_id: null,
metadata: {
object_type: "PASSBOLT_RESOURCE_METADATA",
resource_type_id: TEST_RESOURCE_TYPE_V5_DEFAULT,
name: "Passbolt Browser Extension Test",
uris: ["https://passbolt-browser-extension/test"],
username: "test@passbolt.com",
},
expired: fakeNow.plus({ days: 30 }).plus({ milliseconds: 100 }).toJSDate().toISOString(),
};
const expectedSecretDto = {
password: "P4ssb0ltP4ssb0lt",
description: "",
resource_type_id: TEST_RESOURCE_TYPE_V5_DEFAULT,
object_type: SECRET_DATA_OBJECT_TYPE,
};
const metadataTypeSettings = new MetadataTypesSettingsEntity(defaultMetadataTypesSettingsV6Dto());
const props = defaultProps({ metadataTypeSettings }, true);
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", () => ({
name: expectedResourceDto.metadata.name,
uris: expectedResourceDto.metadata.uris,
}));
props.context.port.addRequestListener("passbolt.secrets.powned-password", () => 0);
let resourceCreated = false;
props.context.port.addRequestListener("passbolt.resources.create", (resourceDto, secretDto) => {
expect(resourceDto).toStrictEqual(expectedResourceDto);
expect(secretDto).toStrictEqual(expectedSecretDto);
resourceCreated = true;
return defaultResourceDto();
});
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(expectedResourceDto.metadata.name);
// Fill the form empty fields"
await page.setFormWith({
username: "test@passbolt.com",
password: "P4ssb0ltP4ssb0lt",
});
// Submit the form.
await page.submitForm();
await waitForTrue(() => resourceCreated);
});
it("should ask for password creation confirmation if the entropy is too low", async () => {
expect.assertions(4);
const props = defaultProps({ history: createMemoryHistory() }, true);
const preparedResource = {
name: "Passbolt Browser Extension Test",
uris: ["https://passbolt-browser-extension/test"],
};
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", async () => preparedResource);
props.context.port.addRequestListener("passbolt.secrets.powned-password", async () => 0);
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(preparedResource.name);
expect(page.complexityText).toBe("Quality Entropy: 0.0 bits");
await page.setFormWith({
password: "test",
username: "test@passbolt.com",
});
expect(page.complexityText).toBe("Very weak Entropy: 18.8 bits");
await page.submitForm();
await waitForTrue(() => props.history.location.pathname.toString() !== "/");
const expectedPageProps = {
resourceName: "Passbolt Browser Extension Test",
rule: ConfirmCreatePageRuleVariations.MINIMUM_ENTROPY,
};
expect(props.history.location.pathname.toString()).toStrictEqual(
"/webAccessibleResources/quickaccess/resources/confirm-create",
);
expect(props.history.location.state).toStrictEqual(expectedPageProps);
});
it("should ask for password creation confirmation if the passphrase is found in a data breach", async () => {
expect.assertions(4);
const props = defaultProps({ history: createMemoryHistory() }, true);
const preparedResource = {
name: "Passbolt Browser Extension Test",
uris: ["https://passbolt-browser-extension/test"],
};
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", async () => preparedResource);
props.context.port.addRequestListener("passbolt.secrets.powned-password", async () => 3);
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(preparedResource.name);
expect(page.complexityText).toBe("Quality Entropy: 0.0 bits");
await page.setFormWith({
password: "hello-world/hello-world",
username: "test@passbolt.com",
});
expect(page.complexityText).toBe("Strong Entropy: 112.9 bits");
await page.submitForm();
await waitForTrue(() => props.history.location.pathname.toString() !== "/");
const expectedPageProps = {
resourceName: "Passbolt Browser Extension Test",
rule: ConfirmCreatePageRuleVariations.IN_DICTIONARY,
};
expect(props.history.location.pathname.toString()).toStrictEqual(
"/webAccessibleResources/quickaccess/resources/confirm-create",
);
expect(props.history.location.state).toStrictEqual(expectedPageProps);
});
it("should create a new password on submit when the dictionary service is unreachable", async () => {
expect.assertions(3);
const fakeNow = DateTime.fromISO("2023-11-24T00:00:00.000Z");
jest.setSystemTime(fakeNow.toMillis());
// Assert the request to create a password has been called and contain the expected parameters.
const expectedResourceDto = {
resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
folder_parent_id: null,
metadata: {
resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
name: "Passbolt Browser Extension Test",
uris: ["https://passbolt-browser-extension/test"],
username: "test@passbolt.com",
},
expired: fakeNow.plus({ days: 30 }).plus({ milliseconds: 100 }).toJSDate().toISOString(),
};
const expectedSecretDto = {
password: "P4ssb0ltP4ssb0lt",
description: "",
resource_type_id: TEST_RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION,
};
const props = defaultProps({}, true);
let isPreparedResourceSet = false;
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", async () => {
isPreparedResourceSet = true;
return {
name: expectedResourceDto.metadata.name,
uris: expectedResourceDto.metadata.uris,
};
});
props.context.port.addRequestListener("passbolt.secrets.powned-password", async (password) => {
expect(password).toStrictEqual(expectedSecretDto.password);
throw new Error();
});
props.context.port.addRequestListener("passbolt.resources.create", async (resourceDto, secretDto) => {
expect(secretDto).toStrictEqual(expectedSecretDto);
expect(resourceDto).toStrictEqual(expectedResourceDto);
return defaultResourceDto();
});
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(expectedResourceDto.metadata.name);
await waitForTrue(() => page.name.value === expectedResourceDto.metadata.name);
await waitForTrue(() => isPreparedResourceSet);
// Fill the form empty fields
await page.setFormWith({
username: "test@passbolt.com",
password: "P4ssb0ltP4ssb0lt",
});
// Submit the form.
await page.submitForm();
});
});
describe("Form validation", () => {
it("should not submit the form if there is an error", async () => {
expect.assertions(2);
const resourceData = {
name: "",
uri: 42,
username: 42,
};
const props = defaultProps({
passwordExpiryContext: defaultPasswordExpirySettingsContext({ automatic_update: false }),
});
props.prepareResourceContext.consumePreparedResource.mockImplementation(() => resourceData);
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findAllByDisplayValue(42);
// Fill the form empty fields
await page.setFormWith({
password: "",
});
await page.submitForm();
expect(page.nameError.textContent).toStrictEqual("A name is required.");
expect(page.passwordError.textContent).toStrictEqual("A password is required.");
});
it("should display the error from the API", async () => {
expect.assertions(4);
const apiError = new Error();
apiError.name = "PassboltApiFetchError";
apiError.data = {
code: 400,
body: {
name: ["The name is invalid"],
uri: ["The uri is invalid", "The uri contains a forbidden scheme"],
username: ["The username is invalid"],
password: ["The password is too weak"],
},
};
const props = defaultProps(
{
passwordExpiryContext: defaultPasswordExpirySettingsContext({ automatic_update: false }),
},
false,
);
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", () => null);
props.context.port.addRequestListener("passbolt.resources.create", () => {
throw apiError;
});
props.prepareResourceContext.consumePreparedResource.mockImplementation(() => ({
name: "Resource name",
uri: "resource uri",
username: "resource username",
}));
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue("Resource name");
// Fill the form empty fields
await page.setFormWith({
password: "Here's a password!",
});
await page.submitForm();
// Wait until the text is found (This will ensure the state has been updated)
await screen.findByText("The name is invalid");
expect(page.nameError.textContent).toStrictEqual("The name is invalid");
expect(page.uriError.textContent).toStrictEqual("The uri is invalid, The uri contains a forbidden scheme");
expect(page.usernameError.textContent).toStrictEqual("The username is invalid");
expect(page.passwordError.textContent).toStrictEqual("The password is too weak");
});
it("should do nothing if user aborted the operation", async () => {
expect.assertions(5);
const error = new Error();
error.name = "UserAbortsOperationError";
const props = defaultProps(
{
passwordExpiryContext: defaultPasswordExpirySettingsContext({ automatic_update: false }),
},
false,
);
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", () => null);
let promiseRejecter;
props.context.port.addRequestListener("passbolt.resources.create", () => {
return new Promise((_resolve, reject) => {
promiseRejecter = reject;
});
});
const expectedValues = {
name: "Resource name",
uri: "resource uri",
username: "resource username",
password: "Here's a password!",
};
props.prepareResourceContext.consumePreparedResource.mockImplementation(() => expectedValues);
props.prepareResourceContext.lastGeneratedPassword = expectedValues.password;
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(expectedValues.name);
await page.submitForm();
expect(page.isSubmitting).toStrictEqual(true);
await act(() => promiseRejecter(error));
await waitForTrue(() => !page.isSubmitting);
expect(page.name.value).toStrictEqual(expectedValues.name);
expect(page.uri.value).toStrictEqual(expectedValues.uri);
expect(page.username.value).toStrictEqual(expectedValues.username);
expect(page.password.value).toStrictEqual(expectedValues.password);
});
it("should display the unexpected error after submitting", async () => {
expect.assertions(6);
const errorMessage = "This is an unexpected error";
const error = new Error(errorMessage);
error.name = "UnexpectedError";
const props = defaultProps(
{
passwordExpiryContext: defaultPasswordExpirySettingsContext({ automatic_update: false }),
},
false,
);
props.context.port.addRequestListener("passbolt.quickaccess.prepare-resource", () => null);
let promiseRejecter;
props.context.port.addRequestListener("passbolt.resources.create", () => {
return new Promise((_resolve, reject) => {
promiseRejecter = reject;
});
});
const expectedValues = {
name: "Resource name",
uri: "resource uri",
username: "resource username",
password: "Here's a password!",
};
props.prepareResourceContext.consumePreparedResource.mockImplementation(() => expectedValues);
props.prepareResourceContext.lastGeneratedPassword = expectedValues.password;
const page = new ResourceCreatePagePage(props);
// Wait until the input value is found (This will ensure the state has been updated)
await screen.findByDisplayValue(expectedValues.name);
await page.submitForm();
expect(page.isSubmitting).toStrictEqual(true);
await act(() => promiseRejecter(error));
await waitForTrue(() => !page.isSubmitting);
expect(page.name.value).toStrictEqual(expectedValues.name);
expect(page.uri.value).toStrictEqual(expectedValues.uri);
expect(page.username.value).toStrictEqual(expectedValues.username);
expect(page.password.value).toStrictEqual(expectedValues.password);
expect(page.unexpectedError).toStrictEqual(errorMessage);
});
});
describe("As LU I can navigate from the 'Resource Create' page", () => {
it("should allow to go back on the previous page", async () => {
expect.assertions(1);
const props = defaultProps();
props.history = createMemoryHistory({
initialEntries: ["/home", "/test"],
initialIndex: 1,
});
const initialPath = props.history.location.pathname.toString();
props.history.goBack();
const page = new ResourceCreatePagePage(props);
await page.clickOnBackButton();
await waitForTrue(() => props.history.location.pathname !== initialPath);
expect(props.history.location.pathname).toStrictEqual("/home");
});
});
});