NGO-DB/ndb-core

View on GitHub
src/app/child-dev-project/attendance/attendance.service.spec.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { TestBed, waitForAsync } from "@angular/core/testing";

import { AttendanceService } from "./attendance.service";
import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service";
import { Database } from "../../core/database/database";
import { RecurringActivity } from "./model/recurring-activity";
import moment from "moment";
import { defaultInteractionTypes } from "../../core/config/default-config/default-interaction-types";
import { expectEntitiesToMatch } from "../../utils/expect-entity-data.spec";
import { EventNote } from "./model/event-note";
import { ChildrenService } from "../children/children.service";
import { School } from "../schools/model/school";
import { ChildSchoolRelation } from "../children/model/childSchoolRelation";
import { Child } from "../children/model/child";
import { Note } from "../notes/model/note";
import { DatabaseTestingModule } from "../../utils/database-testing.module";
import { Entity } from "../../core/entity/model/entity";

describe("AttendanceService", () => {
  let service: AttendanceService;

  let entityMapper: EntityMapperService;

  const meetingInteractionCategory = defaultInteractionTypes.find(
    (it) => it.isMeeting,
  );

  function createEvent(date: Date, activityIdWithPrefix: string): EventNote {
    const event = EventNote.create(date, "generated event");
    event.relatesTo = activityIdWithPrefix;
    event.category = defaultInteractionTypes.find(
      (t) => t.id === "COACHING_CLASS",
    );

    return event;
  }

  let activity1, activity2: RecurringActivity;
  let e1_1, e1_2, e1_3, e2_1: EventNote;

  beforeEach(waitForAsync(() => {
    activity1 = RecurringActivity.create("activity 1");
    activity2 = RecurringActivity.create("activity 2");

    e1_1 = createEvent(moment("2020-01-01").toDate(), activity1.getId());
    e1_2 = createEvent(moment("2020-01-02").toDate(), activity1.getId());
    e1_3 = createEvent(moment("2020-03-02").toDate(), activity1.getId());
    e2_1 = createEvent(moment("2020-01-01").toDate(), activity2.getId());

    TestBed.configureTestingModule({
      imports: [DatabaseTestingModule],
    });
    service = TestBed.inject(AttendanceService);

    entityMapper = TestBed.inject<EntityMapperService>(EntityMapperService);

    entityMapper.save(activity1);
    entityMapper.save(activity2);

    entityMapper.save(e1_1);
    entityMapper.save(e1_2);
    entityMapper.save(e1_3);
    entityMapper.save(e2_1);
  }));

  afterEach(() => TestBed.inject(Database).destroy());

  it("should be created", () => {
    expect(service).toBeTruthy();
  });

  it("gets events for a date", async () => {
    const actualEvents = await service.getEventsOnDate(
      moment("2020-01-01").toDate(),
    );
    expectEntitiesToMatch(actualEvents, [e1_1, e2_1]);
  });

  it("gets events including Notes for a date", async () => {
    const note1 = Note.create(
      moment("2020-01-01").toDate(),
      "manual event note 1",
    );
    note1.addChild("1");
    note1.addChild("2");
    note1.category = meetingInteractionCategory;
    await entityMapper.save(note1);

    const note2 = Note.create(
      moment("2020-01-02").toDate(),
      "manual event note 2",
    );
    note2.addChild("1");
    note2.category = meetingInteractionCategory;
    await entityMapper.save(note2);

    const nonMeetingNote = Note.create(
      moment("2020-01-02").toDate(),
      "manual event note 3",
    );
    nonMeetingNote.addChild("1");
    nonMeetingNote.category = defaultInteractionTypes.find((t) => !t.isMeeting);
    await entityMapper.save(nonMeetingNote);

    const actualEvents = await service.getEventsOnDate(
      moment("2020-01-01").toDate(),
      moment("2020-01-02").toDate(),
    );

    expectEntitiesToMatch(actualEvents, [e1_1, e1_2, e2_1, note1, note2]);
  });

  it("gets empty array for a date without events", async () => {
    const actualEvents = await service.getEventsOnDate(
      moment("2007-01-01").toDate(),
    );
    expect(actualEvents).toBeEmpty();
  });

  it("gets events and loads additional participants from linked schools", async () => {
    const linkedSchoolId = "test_school";
    await createChildrenInSchool(linkedSchoolId, ["2", "3"]);
    const date = new Date();

    const testNoteWithSchool = Note.create(date);
    testNoteWithSchool.children = ["1", "2"];
    testNoteWithSchool.schools = [linkedSchoolId];
    testNoteWithSchool.category = meetingInteractionCategory;
    await entityMapper.save(testNoteWithSchool);

    const actualEvents = await service.getEventsWithUpdatedParticipants(date);
    expect(actualEvents).toHaveSize(1);
    expect(actualEvents[0].children).toEqual(
      jasmine.arrayWithExactContents(["1", "2", "3"]),
    );
  });

  it("should create an event without the excluded participants", async () => {
    const linkedSchoolId = "test_school";
    await createChildrenInSchool(linkedSchoolId, ["excluded", "member"]);

    const activity = new RecurringActivity();
    activity.linkedGroups = [linkedSchoolId];
    activity.excludedParticipants = ["excluded"];
    activity.participants = ["direct", "excluded"];

    const event = await service.createEventForActivity(activity, new Date());
    expect(event.children).toEqual(
      jasmine.arrayWithExactContents(["member", "direct"]),
    );
  });

  it("gets events for an activity", async () => {
    const actualEvents = await service.getEventsForActivity(activity1.getId());
    expectEntitiesToMatch(actualEvents, [e1_1, e1_2, e1_3]);
  });

  it("getActivityAttendances creates record for each month when there is at least one event", async () => {
    const actualAttendances = await service.getActivityAttendances(activity1);

    expect(actualAttendances).toHaveSize(2);

    expect(
      moment(actualAttendances[0].periodFrom).isSame(
        moment("2020-01-01"),
        "day",
      ),
    ).toBeTrue();
    expectEntitiesToMatch(actualAttendances[0].events, [e1_1, e1_2]);
    expect(actualAttendances[0].activity).toEqual(activity1);

    expect(
      moment(actualAttendances[1].periodFrom).isSame(
        moment("2020-03-01"),
        "day",
      ),
    ).toBeTrue();
    expectEntitiesToMatch(actualAttendances[1].events, [e1_3]);
    expect(actualAttendances[1].activity).toEqual(activity1);
  });

  it("getAllActivityAttendancesForPeriod creates records for every activity with events in the given period", async () => {
    const actualAttendences = await service.getAllActivityAttendancesForPeriod(
      new Date(2020, 0, 1),
      new Date(2020, 0, 5),
    );

    expect(actualAttendences).toHaveSize(2);
    expectEntitiesToMatch(
      actualAttendences.find((t) => t.activity.getId() === activity1.getId())
        .events,
      [e1_1, e1_2],
    );
    expectEntitiesToMatch(
      actualAttendences.find((t) => t.activity.getId() === activity2.getId())
        .events,
      [e2_1],
    );

    expect(actualAttendences[0].periodFrom).toBeDate("2020-01-01");
    expect(actualAttendences[0].periodTo).toBeDate("2020-01-05");
    expect(actualAttendences[1].periodFrom).toBeDate("2020-01-01");
    expect(actualAttendences[1].periodTo).toBeDate("2020-01-05");
  });

  it("getActivitiesForChild gets all existing RecurringActivities where it is a participant", async () => {
    const testChildId = "c1";
    const testActivity1 = RecurringActivity.create("a1");
    testActivity1.participants.push(testChildId);

    await entityMapper.save(testActivity1);

    const actual = await service.getActivitiesForChild(testChildId);

    expectEntitiesToMatch(actual, [testActivity1]); // and does not include defaults activity1 or activity2
  });

  it("should return activities of a school that the child currently visits", async () => {
    const childSchoolRelation = new ChildSchoolRelation();
    childSchoolRelation.childId = "testChild";
    childSchoolRelation.schoolId = "testSchool";
    childSchoolRelation.start = moment("2020-01-01").toDate();
    const testActivity = RecurringActivity.create("new activity");
    testActivity.linkedGroups.push("testSchool");

    await entityMapper.saveAll([testActivity, childSchoolRelation]);

    const activities = await service.getActivitiesForChild("testChild");
    expectEntitiesToMatch(activities, [testActivity]);
  });

  it("should only return activities for active schools", async () => {
    const activeRelation1 = new ChildSchoolRelation();
    activeRelation1.childId = "testChild";
    activeRelation1.schoolId = "activeSchool1";
    activeRelation1.start = moment().subtract(1, "month").toDate();
    const activeRelation2 = new ChildSchoolRelation();
    activeRelation2.childId = "testChild";
    activeRelation2.schoolId = "activeSchool2";
    activeRelation2.start = new Date();
    const inactiveRelation = new ChildSchoolRelation();
    inactiveRelation.childId = "testChild";
    inactiveRelation.schoolId = "inactiveSchool";
    inactiveRelation.start = moment().subtract(1, "year").toDate();
    inactiveRelation.end = moment().subtract(1, "month").toDate();

    const activeActivity1 = RecurringActivity.create("active activity 1");
    activeActivity1.linkedGroups.push(activeRelation1.schoolId);
    const activeActivity2 = RecurringActivity.create("active activity 2");
    activeActivity2.linkedGroups.push(activeRelation2.schoolId);
    const inactiveActivity = RecurringActivity.create("inactive activity");
    inactiveActivity.linkedGroups.push(inactiveRelation.schoolId);

    await entityMapper.saveAll([
      activeRelation1,
      inactiveRelation,
      activeRelation2,
      activeActivity1,
      activeActivity2,
      inactiveActivity,
    ]);

    const activities = await service.getActivitiesForChild("testChild");
    expectEntitiesToMatch(activities, [activeActivity1, activeActivity2]);
  });

  it("should not return the same activity multiple times", async () => {
    const activity = new RecurringActivity();
    const relation = new ChildSchoolRelation();
    relation.schoolId = "test school";
    relation.childId = "test child";
    relation.start = new Date();
    activity.linkedGroups.push(relation.schoolId);
    activity.participants.push(relation.childId);

    await entityMapper.saveAll([activity, relation]);

    const activities = await service.getActivitiesForChild(relation.childId);

    expectEntitiesToMatch(activities, [activity]);
  });

  it("should include children from a linked school for event from activity", async () => {
    const activity = new RecurringActivity();
    const linkedSchool = new School();
    activity.linkedGroups.push(linkedSchool.getId());

    const childAttendingSchool = new ChildSchoolRelation();
    childAttendingSchool.childId = "child attending school";
    const mockQueryRelations = spyOn(
      TestBed.inject(ChildrenService),
      "queryActiveRelationsOf",
    ).and.resolveTo([childAttendingSchool]);

    const directlyAddedChild = new Child();
    activity.participants.push(directlyAddedChild.getId());
    const date = new Date();

    const event = await service.createEventForActivity(activity, date);

    expect(mockQueryRelations).toHaveBeenCalledWith(linkedSchool.getId(), date);
    expect(event.children).toHaveSize(2);
    expect(event.children).toContain(directlyAddedChild.getId());
    expect(event.children).toContain(childAttendingSchool.childId);
  });

  it("should not include duplicate children for event from activity", async () => {
    const activity = new RecurringActivity();
    const linkedSchool = new School();
    activity.linkedGroups.push(linkedSchool.getId());

    const duplicateChild = new Child();
    const duplicateChildRelation = new ChildSchoolRelation();
    duplicateChildRelation.childId = duplicateChild.getId();
    duplicateChildRelation.schoolId = linkedSchool.getId();
    const anotherRelation = new ChildSchoolRelation();
    anotherRelation.childId = Entity.createPrefixedId(
      Child.ENTITY_TYPE,
      "another_child_id",
    );
    anotherRelation.schoolId = linkedSchool.getId();
    await entityMapper.saveAll([duplicateChildRelation, anotherRelation]);

    const directlyAddedChild = new Child();
    activity.participants.push(
      directlyAddedChild.getId(),
      duplicateChild.getId(),
    );

    const event = await service.createEventForActivity(activity, new Date());

    expect(event.children).toHaveSize(3);
    expect(event.children).toContain(directlyAddedChild.getId());
    expect(event.children).toContain(duplicateChild.getId());
    expect(event.children).toContain(anotherRelation.childId);
  });

  it("should load the events for a date with date-picker format", async () => {
    const datePickerDate = new Date(
      moment("2021-04-05").toDate().setHours(0, 0, 0, 0),
    );
    const sameDayEvent = EventNote.create(
      moment("2021-04-05").toDate(),
      "Same Day Event",
    );
    sameDayEvent.category = meetingInteractionCategory;
    await entityMapper.save(sameDayEvent);
    const events = await service.getEventsOnDate(datePickerDate);
    expect(events).toHaveSize(1);
    expect(events[0].subject).toBe(sameDayEvent.subject);
  });

  async function createChildrenInSchool(
    schoolId: string,
    childrenIds: string[],
  ) {
    for (const childId of childrenIds) {
      const childSchool = new ChildSchoolRelation();
      childSchool.childId = childId;
      childSchool.schoolId = schoolId;
      childSchool.start = new Date();
      await entityMapper.save(childSchool);
    }
  }
});