Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-1497 Delete groups that were deselected from provisioning #4683

Merged
merged 10 commits into from
Jan 9, 2024
3 changes: 2 additions & 1 deletion apps/server/src/modules/group/entity/group-user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Embeddable, ManyToOne } from '@mikro-orm/core';
import { Role, User } from '@shared/domain/entity';
import { Role } from '@shared/domain/entity/role.entity';
import { User } from '@shared/domain/entity/user.entity';

export interface GroupUserEntityProps {
user: User;
Expand Down
112 changes: 111 additions & 1 deletion apps/server/src/modules/group/repo/group.repo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { MongoMemoryDatabaseModule } from '@infra/database';
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { Test, TestingModule } from '@nestjs/testing';
import { ExternalSource, UserDO } from '@shared/domain/domainobject';
import { SchoolEntity, User } from '@shared/domain/entity';
import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity';
import {
cleanupCollections,
groupEntityFactory,
groupFactory,
roleFactory,
schoolFactory,
systemEntityFactory,
userDoFactory,
userFactory,
} from '@shared/testing';
Expand Down Expand Up @@ -268,6 +269,115 @@ describe('GroupRepo', () => {
});
});

describe('findGroupsBySchoolIdAndSystemIdAndGroupType', () => {
describe('when groups for the school exist', () => {
const setup = async () => {
const system: SystemEntity = systemEntityFactory.buildWithId();
const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system] });
const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, {
type: GroupEntityTypes.CLASS,
organization: school,
externalSource: {
system,
},
});
groups[1].type = GroupEntityTypes.COURSE;
groups[2].type = GroupEntityTypes.OTHER;

const otherSchool: SchoolEntity = schoolFactory.buildWithId({ systems: [system] });
const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, {
type: GroupEntityTypes.CLASS,
organization: otherSchool,
});

await em.persistAndFlush([school, system, ...groups, otherSchool, ...otherGroups]);
em.clear();

return {
school,
system,
otherSchool,
groups,
};
};

it('should return the groups', async () => {
const { school, system } = await setup();

const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType(
school.id,
system.id,
GroupTypes.CLASS
);

expect(result).toHaveLength(1);
});

it('should only return groups from the selected school', async () => {
const { school, system } = await setup();

const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType(
school.id,
system.id,
GroupTypes.CLASS
);

expect(result.every((group) => group.organizationId === school.id)).toEqual(true);
});

it('should only return groups from the selected system', async () => {
const { school, system } = await setup();

const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType(
school.id,
system.id,
GroupTypes.CLASS
);

expect(result.every((group) => group.externalSource?.systemId === system.id)).toEqual(true);
});

it('should return only groups of the given group type', async () => {
const { school, system } = await setup();

const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType(
school.id,
system.id,
GroupTypes.CLASS
);

expect(result).toEqual([expect.objectContaining<Partial<Group>>({ type: GroupTypes.CLASS })]);
});
});

describe('when no group exists', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
const system: SystemEntity = systemEntityFactory.buildWithId();

await em.persistAndFlush([school, system]);
em.clear();

return {
school,
system,
};
};

it('should return an empty array', async () => {
const { school, system } = await setup();

const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType(
school.id,
system.id,
GroupTypes.CLASS
);

expect(result).toHaveLength(0);
});
});
});

describe('save', () => {
describe('when a new object is provided', () => {
const setup = () => {
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/modules/group/repo/group.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ export class GroupRepo {
return domainObjects;
}

public async findGroupsBySchoolIdAndSystemIdAndGroupType(
schoolId: EntityId,
systemId: EntityId,
groupType: GroupTypes
): Promise<Group[]> {
const groupEntityType: GroupEntityTypes = GroupTypesToGroupEntityTypesMapping[groupType];

const scope: Scope<GroupEntity> = new GroupScope()
.byOrganizationId(schoolId)
.bySystemId(systemId)
.byTypes([groupEntityType]);

const entities: GroupEntity[] = await this.em.find(GroupEntity, scope.query);

const domainObjects: Group[] = entities.map((entity) => {
const props: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(entity);

return new Group(props);
});

return domainObjects;
}

public async save(domainObject: Group): Promise<Group> {
const entityProps: GroupEntityProps = GroupDomainMapper.mapDomainObjectToEntityProperties(domainObject, this.em);

Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/modules/group/repo/group.scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ describe(GroupScope.name, () => {
});
});

describe('bySystemId', () => {
describe('when id is undefined', () => {
it('should not add query', () => {
scope.bySystemId(undefined);

expect(scope.query).toEqual({});
});
});

describe('when id is defined', () => {
const setup = () => {
return {
id: new ObjectId().toHexString(),
};
};

it('should add query', () => {
const { id } = setup();

scope.bySystemId(id);

expect(scope.query).toEqual({ externalSource: { system: id } });
});
});
});

describe('byUserId', () => {
describe('when id is undefined', () => {
it('should not add query', () => {
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/modules/group/repo/group.scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export class GroupScope extends Scope<GroupEntity> {
return this;
}

bySystemId(id: EntityId | undefined): this {
if (id) {
this.addQuery({ externalSource: { system: id } });
}
return this;
}

byUserId(id: EntityId | undefined): this {
if (id) {
this.addQuery({ users: { user: new ObjectId(id) } });
Expand Down
42 changes: 42 additions & 0 deletions apps/server/src/modules/group/service/group.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,48 @@ describe('GroupService', () => {
});
});

describe('findGroupsBySchoolIdAndSystemIdAndGroupType', () => {
describe('when the school has groups of type class', () => {
const setup = () => {
const schoolId: string = new ObjectId().toHexString();
const systemId: string = new ObjectId().toHexString();
const groups: Group[] = groupFactory.buildList(3);

groupRepo.findGroupsBySchoolIdAndSystemIdAndGroupType.mockResolvedValue(groups);

return {
schoolId,
systemId,
groups,
};
};

it('should search for the groups', async () => {
const { schoolId, systemId } = setup();

await service.findGroupsBySchoolIdAndSystemIdAndGroupType(schoolId, systemId, GroupTypes.CLASS);

expect(groupRepo.findGroupsBySchoolIdAndSystemIdAndGroupType).toHaveBeenCalledWith(
schoolId,
systemId,
GroupTypes.CLASS
);
});

it('should return the groups', async () => {
const { schoolId, systemId, groups } = setup();

const result: Group[] = await service.findGroupsBySchoolIdAndSystemIdAndGroupType(
schoolId,
systemId,
GroupTypes.CLASS
);

expect(result).toEqual(groups);
});
});
});

describe('save', () => {
describe('when saving a group', () => {
const setup = () => {
Expand Down
14 changes: 14 additions & 0 deletions apps/server/src/modules/group/service/group.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ export class GroupService implements AuthorizationLoaderServiceGeneric<Group> {
return group;
}

public async findGroupsBySchoolIdAndSystemIdAndGroupType(
schoolId: EntityId,
systemId: EntityId,
groupType: GroupTypes
): Promise<Group[]> {
const group: Group[] = await this.groupRepo.findGroupsBySchoolIdAndSystemIdAndGroupType(
schoolId,
systemId,
groupType
);

return group;
}

public async save(group: Group): Promise<Group> {
const savedGroup: Group = await this.groupRepo.save(group);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ProvisioningOptionsInterface } from '../interface';
import { ProvisioningOptionsType } from './provisioning-options-type';

export abstract class BaseProvisioningOptions<T extends ProvisioningOptionsInterface> {
public isApplicable(provisioningOptions: ProvisioningOptionsInterface): provisioningOptions is T {
Expand All @@ -11,5 +12,7 @@ export abstract class BaseProvisioningOptions<T extends ProvisioningOptionsInter
return hasProperties;
}

abstract set(props: T): this;
public abstract get getType(): ProvisioningOptionsType;

public abstract set(props: T): this;
}
1 change: 1 addition & 0 deletions apps/server/src/modules/legacy-school/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { SchulConneXProvisioningOptions } from './schulconnex-provisionin-option
export { AnyProvisioningOptions, SchoolSystemOptions, SchoolSystemOptionsProps } from './school-system-options.do';
export { provisioningStrategyOptions } from './provisioning-strategy-options';
export { SchoolSystemOptionsBuilder } from './school-system-options.builder';
export { ProvisioningOptionsType } from './provisioning-options-type';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ProvisioningOptionsType {
SCHULCONNEX = 'schulconnex',
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy';
import { ProvisioningOptionsInterface } from '../interface';
import { ProvisioningStrategyInvalidOptionsLoggableException } from '../loggable';
import {
ProvisioningStrategyInvalidOptionsLoggableException,
ProvisioningStrategyNoOptionsLoggableException,
} from '../loggable';
import { SchoolSystemOptionsBuilder } from './school-system-options.builder';
import { AnyProvisioningOptions } from './school-system-options.do';
import { SchulConneXProvisioningOptions } from './schulconnex-provisionin-options.do';

describe(SchoolSystemOptionsBuilder.name, () => {
describe('getDefaultProvisioningOptions', () => {
describe('when the provisioning strategy has options', () => {
it('should have the correct options instance', () => {
const builder: SchoolSystemOptionsBuilder = new SchoolSystemOptionsBuilder(SystemProvisioningStrategy.SANIS);

const result: AnyProvisioningOptions = builder.getDefaultProvisioningOptions();

expect(result).toBeInstanceOf(SchulConneXProvisioningOptions);
});
});

describe('when the provisioning strategy has no options', () => {
it('should throw an error', () => {
const builder: SchoolSystemOptionsBuilder = new SchoolSystemOptionsBuilder(
SystemProvisioningStrategy.UNDEFINED
);

expect(() => builder.getDefaultProvisioningOptions()).toThrow(ProvisioningStrategyNoOptionsLoggableException);
});
});
});

describe('buildProvisioningOptions', () => {
describe('when the provisioning strategy is "SANIS" and the options are valid', () => {
const setup = () => {
Expand Down Expand Up @@ -52,21 +77,5 @@ describe(SchoolSystemOptionsBuilder.name, () => {
).toThrow(ProvisioningStrategyInvalidOptionsLoggableException);
});
});

describe('when the provisioning strategy has no options', () => {
it('should throw an error', () => {
const builder: SchoolSystemOptionsBuilder = new SchoolSystemOptionsBuilder(
SystemProvisioningStrategy.UNDEFINED
);

expect(() =>
builder.buildProvisioningOptions({
groupProvisioningClassesEnabled: true,
groupProvisioningCoursesEnabled: true,
groupProvisioningOtherEnabled: true,
})
).toThrow(ProvisioningStrategyInvalidOptionsLoggableException);
});
});
});
});
Loading
Loading