NGO-DB/ndb-core

View on GitHub
src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts

Summary

Maintainability
B
6 hrs
Test Coverage
/*
 *     This file is part of ndb-core.
 *
 *     ndb-core is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     ndb-core is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with ndb-core.  If not, see <http://www.gnu.org/licenses/>.
 */

import { EntityMapperService } from "./entity-mapper.service";
import { Entity } from "../model/entity";
import { TestBed, waitForAsync } from "@angular/core/testing";
import { PouchDatabase } from "../../database/pouch-database";
import { DatabaseEntity } from "../database-entity.decorator";
import { Database } from "../../database/database";
import { CoreTestingModule } from "../../../utils/core-testing.module";
import { CurrentUserSubject } from "../../session/current-user-subject";
import { User } from "../../user/user";
import { TEST_USER } from "../../user/demo-user-generator.service";

describe("EntityMapperService", () => {
  let entityMapper: EntityMapperService;
  let testDatabase: PouchDatabase;

  const existingEntity = {
    _id: "Entity:existing-entity",
    label: "entity from database",
  };

  const existingEntity2 = {
    _id: "Entity:existing-entity2",
    label: "entity 2 from database",
  };

  beforeEach(waitForAsync(() => {
    testDatabase = PouchDatabase.create();

    TestBed.configureTestingModule({
      imports: [CoreTestingModule],
      providers: [
        { provide: Database, useValue: testDatabase },
        CurrentUserSubject,
        EntityMapperService,
      ],
    });
    entityMapper = TestBed.inject(EntityMapperService);

    return Promise.all([
      testDatabase.put(existingEntity),
      testDatabase.put(existingEntity2),
    ]);
  }));

  afterEach(async () => {
    await testDatabase.destroy();
  });

  function expectEntity(actualEntity, expectedEntity) {
    expect(actualEntity.getId()).toBe(expectedEntity._id);
    expect(actualEntity).toBeInstanceOf(Entity);
  }

  it("loads existing entity", async () => {
    const loadedEntity = await entityMapper.load<Entity>(
      Entity,
      existingEntity._id,
    );
    expectEntity(loadedEntity, existingEntity);
  });

  it("load multiple entities", async () => {
    const loadedEntities = await entityMapper.loadType<Entity>(Entity);
    expect(loadedEntities).toHaveSize(2);

    const entity1 = loadedEntities[0];
    const entity2 = loadedEntities[1];

    expectEntity(entity1, existingEntity);
    expectEntity(entity2, existingEntity2);
  });

  it("rejects promise when loading nonexistent entity", async () => {
    return entityMapper.load<Entity>(Entity, "nonexistent_id").catch((err) => {
      expect(err).withContext('"not found" error not defined').toBeDefined();
    });
  });

  it("returns empty array when loading non existing entity type ", async () => {
    class TestEntity extends Entity {
      static ENTITY_TYPE = "TestEntity";
    }

    const result = await entityMapper.loadType<TestEntity>(TestEntity);
    expect(result).toBeEmpty();
  });

  it("saves new entity and loads it", async () => {
    const entity = new Entity("test1");

    await entityMapper.save(entity);
    const loadedEntity = await entityMapper.load(Entity, entity.getId());
    expectEntity(loadedEntity, entity);
  });

  it("rejects promise when saving new entity with existing entityId", async () => {
    const duplicateEntity = new Entity(existingEntity._id);

    await entityMapper
      .save<Entity>(duplicateEntity)
      .then(() => {
        fail("unexpectedly succeeded to overwrite existing entity");
      })
      .catch(function (error) {
        expect(error).toBeDefined();
      });
  });

  it("saves new version of existing entity", async () => {
    const loadedEntity = await entityMapper.load(Entity, existingEntity._id);
    expect(loadedEntity.getId()).toEqual(existingEntity._id);

    await entityMapper.save<Entity>(loadedEntity);
  });

  it("removes existing entity", async () => {
    const loadedEntity = await entityMapper.load(Entity, existingEntity._id);
    await entityMapper.remove<Entity>(loadedEntity);

    await expectAsync(
      entityMapper.load(Entity, existingEntity._id),
    ).toBeRejected();
  });

  it("rejects promise removing nonexistent entity", () => {
    const nonexistingEntity = new Entity("nonexistent-entity");

    return entityMapper
      .remove<Entity>(nonexistingEntity)
      .then(() => fail("unexpectedly resolved promise"))
      .catch((error) => {
        expect(error).toBeDefined();
      });
  });

  it("loads entity for id given with and without prefix", async () => {
    const testId = "t1";
    const testEntity = new Entity(testId);
    await entityMapper.save(testEntity);

    const loadedByShortId = await entityMapper.load(
      Entity,
      testEntity.getId(true),
    );
    expect(loadedByShortId).toBeDefined();
    expect(loadedByShortId.getId().startsWith(Entity.ENTITY_TYPE)).toBeTrue();

    const loadedByFullId = await entityMapper.load(
      Entity,
      loadedByShortId.getId(),
    );
    expect(loadedByFullId.getId()).toBe(loadedByShortId.getId());
    expect(loadedByFullId._rev).toBe(loadedByShortId._rev);
  });

  it("publishes updates to any listeners", () => {
    const testId = "Entity:t1";
    const testEntity = new Entity(testId);
    entityMapper
      .save(testEntity, true)
      .then(() => entityMapper.remove(testEntity));

    return receiveUpdatesAndTestTypeAndId(undefined, testId);
  });

  it("publishes when an existing entity is updated", () => {
    entityMapper
      .load(Entity, existingEntity._id)
      .then((loadedEntity) => entityMapper.save(loadedEntity));

    return receiveUpdatesAndTestTypeAndId("update", existingEntity._id);
  });

  it("publishes when an existing entity is deleted", () => {
    entityMapper
      .load(Entity, existingEntity._id)
      .then((loadedEntity) => entityMapper.remove(loadedEntity));

    return receiveUpdatesAndTestTypeAndId("remove", existingEntity._id);
  });

  it("publishes when a new entity is being saved", () => {
    const testId = "Entity:t1";
    const testEntity = new Entity(testId);
    entityMapper.save(testEntity, true);

    return receiveUpdatesAndTestTypeAndId("new", testId);
  });

  it("correctly behaves when en empty array is given to the saveAll function", async () => {
    const result = await entityMapper.saveAll([]);
    expect(result).toHaveSize(0);
  });

  it("correctly saves an array of heterogeneous entities", async () => {
    const result = await entityMapper.saveAll([
      new MockEntityA("1"),
      new MockEntityA("10"),
      new MockEntityA("42"),
    ]);
    expect(result).toEqual([
      jasmine.objectContaining({
        ok: true,
        id: "EntityA:1",
      }),
      jasmine.objectContaining({
        ok: true,
        id: "EntityA:10",
      }),
      jasmine.objectContaining({
        ok: true,
        id: "EntityA:42",
      }),
    ]);
  });

  it("correctly saves an array of homogeneous entities", async () => {
    const result = await entityMapper.saveAll([
      new MockEntityA("1"),
      new MockEntityB("10"),
      new MockEntityA("42"),
    ]);
    expect(result).toEqual([
      jasmine.objectContaining({
        ok: true,
        id: "EntityA:1",
      }),
      jasmine.objectContaining({
        ok: true,
        id: "EntityB:10",
      }),
      jasmine.objectContaining({
        ok: true,
        id: "EntityA:42",
      }),
    ]);
  });

  it("sets the entityCreated property on save if it is a new entity & entityUpdated on subsequent saves", async () => {
    jasmine.clock().install();
    TestBed.inject(CurrentUserSubject).next(new User(TEST_USER));
    const id = "test_created";
    const entity = new Entity(id);

    const mockTime1 = 1;
    jasmine.clock().mockDate(new Date(mockTime1));
    await entityMapper.save(entity);
    const createdEntity = await entityMapper.load(Entity, id);

    expect(createdEntity.created?.at.getTime()).toEqual(mockTime1);
    expect(createdEntity.created?.by).toEqual(
      `${User.ENTITY_TYPE}:${TEST_USER}`,
    );
    expect(createdEntity.updated?.at.getTime()).toEqual(mockTime1);
    expect(createdEntity.updated?.by).toEqual(
      `${User.ENTITY_TYPE}:${TEST_USER}`,
    );

    const mockTime2 = mockTime1 + 1;
    jasmine.clock().mockDate(new Date(mockTime2));
    await entityMapper.save<Entity>(createdEntity);
    const updatedEntity = await entityMapper.load<Entity>(Entity, id);

    expect(updatedEntity.created?.at.getTime()).toEqual(mockTime1);
    expect(updatedEntity.updated?.at.getTime()).toEqual(mockTime2);

    jasmine.clock().uninstall();
  });

  function receiveUpdatesAndTestTypeAndId(type?: string, entityId?: string) {
    return new Promise<void>((resolve) => {
      entityMapper.receiveUpdates(Entity).subscribe((e) => {
        if (e) {
          if (type) {
            expect(e.type).toBe(type);
          }
          if (entityId) {
            expect(e.entity.getId()).toBe(entityId);
          }
          resolve();
        }
      });
    });
  }

  @DatabaseEntity("EntityA")
  class MockEntityA extends Entity {}

  @DatabaseEntity("EntityB")
  class MockEntityB extends Entity {}
});