Skip to content

Commit

Permalink
N21-1497 Delete groups that were deselected from provisioning (#4683)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinOehlerkingCap authored Jan 9, 2024
1 parent dc301b5 commit 5344554
Show file tree
Hide file tree
Showing 38 changed files with 858 additions and 79 deletions.
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

0 comments on commit 5344554

Please sign in to comment.