passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
618 lines (531 loc) • 25.6 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 4.9.0
*/
import EntityV2 from "./entityV2";
import {
defaultAssociatedTestEntityV2Dto,
defaultTestEntityV2Dto,
minimalTestEntityV2Dto,
TestAssociatedCollection,
TestAssociatedEntityV2,
TestEntityV2,
TestWithAssociationEntityV2,
} from "./entityV2.test.data";
import * as assertEntityProperty from "../../../../../test/assert/assertEntityProperty";
import { v4 as uuid } from "uuid";
import EntitySchema from "./entitySchema";
import EntityValidationError from "./entityValidationError";
beforeEach(() => {
TestEntityV2._cachedSchema = {};
TestAssociatedEntityV2._cachedSchema = {};
jest.clearAllMocks();
});
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 associated 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)).toThrowEntityValidationError("associated_entity.details.id", "type");
expect(() => new TestEntityV2(dto)).not.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("call the marshall function prior to validate.", () => {
expect.assertions(1);
const dto = minimalTestEntityV2Dto({ name: "K4r3n" });
expect(() => new TestEntityV2(dto)).toThrowEntityValidationError("name", "karen");
});
it("validates the entity schema.", () => {
jest.spyOn(TestEntityV2.prototype, "validateSchema");
assertEntityProperty.string(TestEntityV2, "id");
assertEntityProperty.uuid(TestEntityV2, "id");
assertEntityProperty.nullable(TestEntityV2, "id");
assertEntityProperty.string(TestEntityV2, "name");
assertEntityProperty.nullable(TestEntityV2, "name");
expect(TestEntityV2.prototype.validateSchema).toHaveBeenCalled();
});
it("does not validate the entity schema or its associations if disabled by option.", () => {
expect.assertions(2);
jest.spyOn(TestEntityV2.prototype, "validateSchema");
jest.spyOn(TestAssociatedEntityV2.prototype, "validateSchema");
new TestEntityV2(defaultTestEntityV2Dto(), { validate: false });
expect(TestEntityV2.prototype.validateSchema).not.toHaveBeenCalled();
// For now instantiation of associations is implemented manually in entity constructor.
expect(TestAssociatedEntityV2.prototype.validateSchema).not.toHaveBeenCalled();
});
it("validates the entity build rules.", () => {
expect.assertions(2);
jest.spyOn(TestEntityV2.prototype, "validateBuildRules");
const dto = defaultTestEntityV2Dto({ name: "Karen" });
expect(() => new TestEntityV2(dto)).toThrowEntityValidationError("name", "karen");
expect(TestEntityV2.prototype.validateBuildRules).toHaveBeenCalled();
});
it("does not validate the entity build rules or its associations if disabled by option.", () => {
expect.assertions(2);
jest.spyOn(TestEntityV2.prototype, "validateBuildRules");
jest.spyOn(TestAssociatedEntityV2.prototype, "validateBuildRules");
new TestEntityV2(defaultTestEntityV2Dto(), { validate: false });
expect(TestEntityV2.prototype.validateBuildRules).not.toHaveBeenCalled();
// For now instantiation of associations is implemented manually in entity constructor.
expect(TestAssociatedEntityV2.prototype.validateBuildRules).not.toHaveBeenCalled();
});
});
describe("::validateSchema", () => {
it("throws an error if the getSchema function is not overridden.", () => {
expect.assertions(1);
const expectedError = new Error("The entity class should declare its schema.");
const dto = minimalTestEntityV2Dto({ name: "K4r3n" });
const entity = new EntityV2(dto, { validate: false });
expect(() => entity.validateSchema()).toThrow(expectedError);
});
it("validates the entity schema.", () => {
expect.assertions(1);
// Schema is better covered on the constructor as unit tests tools were implemented originally for it.
const entity = new TestEntityV2({ id: 42 }, { validate: false });
expect(() => entity.validateSchema()).toThrowEntityValidationError("id", "type");
});
it("retrieves and caches the schema on first validation.", () => {
expect.assertions(5);
jest.spyOn(TestEntityV2, "getSchema");
jest.spyOn(TestAssociatedEntityV2, "getSchema");
expect(TestEntityV2._cachedSchema.TestEntityV2).toBeUndefined();
expect(TestAssociatedEntityV2._cachedSchema.TestAssociatedEntityV2).toBeUndefined();
const entity = new TestEntityV2(defaultTestEntityV2Dto(), { validate: false });
entity.validateSchema();
expect(TestEntityV2._cachedSchema.TestEntityV2).toEqual(TestEntityV2.getSchema());
entity.validateSchema();
new TestEntityV2(defaultTestEntityV2Dto()); // ensure the cache is used even when validating entity from constructor.
expect(TestEntityV2.getSchema).toHaveBeenCalledTimes(2);
expect(TestAssociatedEntityV2.getSchema).toHaveBeenCalledTimes(5);
});
it("validates should remove association required if skipSchemaAssociationValidation option.", () => {
expect.assertions(1);
jest.spyOn(EntitySchema, "validate");
const entity = new TestWithAssociationEntityV2(
{
name: "test name",
associated_entity: defaultAssociatedTestEntityV2Dto(),
},
{
validate: false,
},
); // ensure the cache is used even when validating entity from constructor.
entity.validate({
skipSchemaAssociationValidation: true,
});
const expectedSchema = TestWithAssociationEntityV2.getSchema();
expectedSchema.required = ["name"];
expect(EntitySchema.validate).toHaveBeenCalledWith(entity.constructor.name, entity._props, expectedSchema);
});
it("validates should not association required if skipSchemaAssociationValidation option not exist.", () => {
expect.assertions(1);
jest.spyOn(EntitySchema, "validate");
const entity = new TestWithAssociationEntityV2(
{
name: "test name",
associated_entity: defaultAssociatedTestEntityV2Dto(),
},
{
validate: false,
},
); // ensure the cache is used even when validating entity from constructor.
entity.validate({
skipSchemaAssociationValidation: false,
});
expect(EntitySchema.validate).toHaveBeenCalledWith(
entity.constructor.name,
entity._props,
TestWithAssociationEntityV2.getSchema(),
);
});
});
describe("::validate", () => {
it("does not return error if there is no issue.", () => {
expect.assertions(1);
jest.spyOn(TestEntityV2.prototype, "validateSchema");
const entity = new TestEntityV2(minimalTestEntityV2Dto());
const result = entity.validate();
expect(result).toBeNull();
});
it("validates the entity schema.", () => {
expect.assertions(3);
jest.spyOn(TestEntityV2.prototype, "validateSchema");
const entity = new TestEntityV2({ id: 42 }, { validate: false });
const result = entity.validate();
expect(result.hasErrors()).toBeTruthy();
expect(result.hasError("id", "type")).toBeTruthy();
expect(TestEntityV2.prototype.validateSchema).toHaveBeenCalled();
});
it("validates the build rules.", () => {
expect.assertions(3);
jest.spyOn(TestEntityV2.prototype, "validateBuildRules");
const entity = new TestEntityV2({ name: "Karen" }, { validate: false });
const result = entity.validate();
expect(result.hasErrors()).toBeTruthy();
expect(result.hasError("name", "karen")).toBeTruthy();
expect(TestEntityV2.prototype.validateSchema).toHaveBeenCalled();
});
it("should skip required association from schema when option skipSchemaAssociationValidation exist.", () => {
expect.assertions(1);
const entity = new TestWithAssociationEntityV2(defaultTestEntityV2Dto(), {
validate: false,
});
const result = entity.validate({
skipSchemaAssociationValidation: true,
});
expect(result).toBeNull();
});
it("should not validate required association from schema when option skipSchemaAssociationValidation is false.", () => {
expect.assertions(2);
const entity = new TestWithAssociationEntityV2(defaultTestEntityV2Dto(), {
skipSchemaAssociationValidation: true,
validate: false,
});
const result = entity.validate();
expect(result.hasErrors()).toBeTruthy();
expect(result.hasError("associated_entity", "required")).toBeTruthy();
});
it("throws an error if the getSchema function is not overridden.", () => {
expect.assertions(1);
const expectedError = new Error("The entity class should declare its schema.");
const dto = minimalTestEntityV2Dto({ name: "K4r3n" });
const entity = new EntityV2(dto, { validate: false });
expect(() => entity.validate()).toThrow(expectedError);
});
});
describe("::validateAssociations", () => {
it("does not return error if validation association respect schema.", () => {
expect.assertions(1);
const entity = new TestWithAssociationEntityV2(defaultTestEntityV2Dto());
expect(() => entity.validateAssociations()).not.toThrowEntityValidationError();
});
it("throws a validation error if the association validation failed.", () => {
expect.assertions(1);
const entity = new TestWithAssociationEntityV2(
defaultTestEntityV2Dto({
associated_entity: {},
}),
{
validate: false,
},
);
const validationErrors = new EntityValidationError();
validationErrors.addAssociationError(
"associated_entity",
new EntityValidationError("id", "required", "Could not validate entity TestAssociatedEntityV2."),
);
expect(() => entity.validateAssociations()).toThrow(validationErrors);
});
});
describe("::associations", () => {
it("retrieves empty associations.", () => {
expect.assertions(1);
expect(EntityV2.associations).toStrictEqual({});
});
it("retrieves associations of an entity.", () => {
expect.assertions(1);
expect(TestEntityV2.associations).toStrictEqual({
associated_entity: TestAssociatedEntityV2,
associated_collection: TestAssociatedCollection,
});
});
});
describe("::get", () => {
it("returns a property value.", () => {
expect.assertions(1);
const entity = new TestEntityV2(minimalTestEntityV2Dto());
expect(entity.get("name")).toEqual("test name");
});
it("returns undefined if a property value was not defined.", () => {
expect.assertions(1);
const entity = new TestEntityV2(minimalTestEntityV2Dto());
expect(entity.get("id")).toBeUndefined();
});
it("throws if property name is not a string.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.get(42)).toThrow(TypeError);
});
it("throws if property not defined in entity schema.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.get("undefined_prop")).toThrow(
new Error('The property "undefined_prop" has no schema definition.'),
);
});
it("throws if property is relative to an object.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.get("object")).toThrow(
new Error('The property "associated_entity" should reference scalar properties only.'),
);
});
it("throws if property is relative to an array.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.get("array")).toThrow(
new Error('The property "associated_entity" should reference scalar properties only.'),
);
});
it("throws if property is relative to an association.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.get("associated_entity")).toThrow(
new Error('The property "associated_entity" should reference scalar properties only.'),
);
});
});
describe("::set", () => {
it("set a property value.", () => {
expect.assertions(1);
const entity = new TestEntityV2(minimalTestEntityV2Dto());
entity.set("name", "Updated name");
expect(entity.name).toEqual("Updated name");
});
it("throws if the value does not validate the schema.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("name", 42)).toThrowEntityValidationError("name", "type");
});
it("does not validate and set the property with a wrong value if the validation is disabled by option.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
entity.set("name", 42, { validate: false });
expect(entity.name).toBe(42);
});
it("throws if property name is not a string.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set(42)).toThrow(TypeError);
});
it("throws if property not defined in entity schema.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("undefined_prop")).toThrow(
new Error('The property "undefined_prop" has no schema definition.'),
);
});
it("throws if property is relative to an object.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("object")).toThrow(
new Error('The property "associated_entity" should reference scalar properties only.'),
);
});
it("set an association property value with dto.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const associatedTestEntityV2Dto = defaultAssociatedTestEntityV2Dto();
entity.set("associated_entity", associatedTestEntityV2Dto);
expect(entity.associatedEntity.toDto()).toStrictEqual(associatedTestEntityV2Dto);
});
it("set an association property value with entity.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const associatedTestEntityV2 = new TestAssociatedEntityV2(defaultAssociatedTestEntityV2Dto());
entity.set("associated_entity", associatedTestEntityV2);
expect(entity.associatedEntity).toStrictEqual(associatedTestEntityV2);
});
it("set an association property value with property in the association.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const id = uuid();
entity.set("associated_entity.id", id);
expect(entity.associatedEntity.id).toStrictEqual(id);
});
it("create and set an association property value with property in the association.", () => {
expect.assertions(2);
const entity = new TestEntityV2(minimalTestEntityV2Dto());
const id = uuid();
expect(entity.associatedEntity).toBeUndefined();
entity.set("associated_entity.id", id);
expect(entity.associatedEntity.id).toStrictEqual(id);
});
it("throws if association property value is not valid object.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("associated_entity", { id: "no uuid" })).toThrow(
new Error("Could not validate entity TestAssociatedEntityV2."),
);
});
it("throws if association property value is not valid entity.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("associated_entity", new EntityV2({ id: "no uuid" }, { validate: false }))).toThrow(
new Error("Could not validate entity TestAssociatedEntityV2."),
);
});
it("throws if association property value with property in the association is not valid.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("associated_entity.id", "no uuid")).toThrow(new Error("Could not validate property id."));
});
it("throws if association property value with property in an new association is not valid.", () => {
expect.assertions(1);
const entity = new TestEntityV2(minimalTestEntityV2Dto());
expect(() => entity.set("associated_entity.id", "no uuid")).toThrow(new Error("Could not validate property id."));
});
it("throws if array prop index is not defined.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("array")).toThrow(new Error('The property "array" has no index passed.'));
});
it("throws if array prop index has an incorrect format.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("array.[0]")).toThrow(
new Error('The property "array" has an invalid index format. Expected format: digits.'),
);
});
it("throws if array prop items does not respect items type.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("array.0", [])).toThrow(new Error("Could not validate property array."));
});
it("should set the array properties.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
entity.set("array.0", "test");
expect(entity.toDto().array[0]).toEqual("test");
});
it("should delete the array properties if null or undefined.", () => {
expect.assertions(2);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(entity.toDto().array.length).toEqual(2);
entity.set("array.1", null, { validate: false });
expect(entity.toDto().array.length).toEqual(1);
});
it("set an association collection property value with dto.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const associatedTestCollectionV2Dto = [defaultAssociatedTestEntityV2Dto()];
entity.set("associated_collection", associatedTestCollectionV2Dto);
expect(entity.associatedCollection.toDto()).toStrictEqual(associatedTestCollectionV2Dto);
});
it("set an association property value with collection.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const associatedTestCollectionV2Dto = [defaultAssociatedTestEntityV2Dto()];
const associatedTestCollectionV2 = new TestAssociatedCollection(associatedTestCollectionV2Dto);
entity.set("associated_collection", associatedTestCollectionV2);
expect(entity.associatedCollection).toStrictEqual(associatedTestCollectionV2);
});
it("set an association property value with property in the association collection.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const id = uuid();
entity.set("associated_collection.0.id", id);
expect(entity.associatedCollection.items[0].id).toStrictEqual(id);
});
it("throw if association property value with property in the association collection have no item at the index.", () => {
expect.assertions(2);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
const id = uuid();
expect(entity.associatedCollection).toBeDefined();
expect(() => entity.set("associated_collection.1.id", id)).toThrow(
new Error('The collection "associatedCollection" has no item at the index "1".'),
);
});
it("throws if association property value is not valid object.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.set("associated_collection.0.id", { id: "no uuid" })).toThrow(
new Error("Could not validate property id."),
);
});
});
describe("::diffProps", () => {
it("returns no differences between two similar entities.", () => {
expect.assertions(1);
const entity1 = new TestEntityV2(minimalTestEntityV2Dto());
const entity2 = new TestEntityV2(minimalTestEntityV2Dto());
const diff = entity1.diffProps(entity2);
expect(Object.keys(diff).length).toEqual(0);
});
it("returns differences when the property defined in entity 1 are similar to the property defined in a second entity having more property defined.", () => {
expect.assertions(5);
const entity1 = new TestEntityV2(minimalTestEntityV2Dto());
const entity2 = new TestEntityV2(defaultTestEntityV2Dto());
const diff = entity1.diffProps(entity2);
expect(Object.keys(diff).length).toEqual(4);
expect(diff.id).toEqual(entity2.id);
expect(diff.number).toEqual(entity2.get("number"));
expect(diff.integer).toEqual(entity2.get("integer"));
expect(diff.boolean).toEqual(entity2.get("boolean"));
});
it("returns differences when the property defined in entity 1 are not equal to the property defined in a second entity.", () => {
expect.assertions(2);
const entity1 = new TestEntityV2(defaultTestEntityV2Dto());
const entity2 = new TestEntityV2(minimalTestEntityV2Dto());
const diff = entity1.diffProps(entity2);
expect(Object.keys(diff).length).toEqual(4);
expect(diff).toEqual({ id: undefined, number: undefined, integer: undefined, boolean: undefined });
});
it("does not return difference on non scalar properties.", () => {
expect.assertions(5);
const entity1 = new TestEntityV2(defaultTestEntityV2Dto());
const entity2 = new TestEntityV2(defaultTestEntityV2Dto({ boolean: !entity1.get("boolean") }));
const diff = entity1.diffProps(entity2);
expect(Object.keys(diff).length).toEqual(4);
expect(diff.id).toEqual(entity2.id);
expect(diff.number).toEqual(entity2.get("number"));
expect(diff.integer).toEqual(entity2.get("integer"));
expect(diff.boolean).toEqual(entity2.get("boolean"));
});
it("throws if the compared entity is not of entity v2 type.", () => {
expect.assertions(1);
const entity = new TestEntityV2(defaultTestEntityV2Dto());
expect(() => entity.diffProps(42)).toThrow(TypeError);
});
});
describe("::hasDiffProps", () => {
it("returns false when the two entities have no differences.", () => {
expect.assertions(1);
const entity1 = new TestEntityV2(minimalTestEntityV2Dto());
const entity2 = new TestEntityV2(minimalTestEntityV2Dto());
expect(entity1.hasDiffProps(entity2)).toBeFalsy();
});
it("returns true when the two entities have no differences.", () => {
expect.assertions(1);
const entity1 = new TestEntityV2(defaultTestEntityV2Dto());
const entity2 = new TestEntityV2(minimalTestEntityV2Dto());
expect(entity1.hasDiffProps(entity2)).toBeTruthy();
});
});
});