From 60afca715a6df3eee1371b3cfad380190afa9a2e Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Wed, 6 Dec 2023 14:11:44 +0100 Subject: [PATCH] N21-1464 extends provisioning of group types --- .../dto/response/group-type.response.ts | 2 + .../mapper/group-response.mapper.ts | 2 + .../src/modules/group/domain/group-types.ts | 2 + .../src/modules/group/entity/group.entity.ts | 2 + .../modules/group/repo/group-domain.mapper.ts | 6 +- .../src/modules/group/repo/group.repo.spec.ts | 75 +++++++++++++++-- .../src/modules/group/repo/group.repo.ts | 37 ++++++--- .../modules/group/repo/group.scope.spec.ts | 82 +++++++++++++++++++ .../src/modules/group/repo/group.scope.ts | 27 ++++++ .../group/service/group.service.spec.ts | 44 +++++++--- .../modules/group/service/group.service.ts | 10 +-- .../src/modules/group/uc/group.uc.spec.ts | 34 ++++++-- apps/server/src/modules/group/uc/group.uc.ts | 26 ++++-- .../service/oidc-provisioning.service.spec.ts | 6 +- .../oidc/service/oidc-provisioning.service.ts | 2 +- .../sanis/sanis-response.mapper.spec.ts | 33 +++++++- .../strategy/sanis/sanis-response.mapper.ts | 2 + 17 files changed, 331 insertions(+), 61 deletions(-) create mode 100644 apps/server/src/modules/group/repo/group.scope.spec.ts create mode 100644 apps/server/src/modules/group/repo/group.scope.ts diff --git a/apps/server/src/modules/group/controller/dto/response/group-type.response.ts b/apps/server/src/modules/group/controller/dto/response/group-type.response.ts index 54c32148ca1..210cbbb797d 100644 --- a/apps/server/src/modules/group/controller/dto/response/group-type.response.ts +++ b/apps/server/src/modules/group/controller/dto/response/group-type.response.ts @@ -1,3 +1,5 @@ export enum GroupTypeResponse { CLASS = 'class', + COURSE = 'course', + OTHER = 'other', } diff --git a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts index a61ee57c1a4..3b715b7db01 100644 --- a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts +++ b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts @@ -12,6 +12,8 @@ import { const typeMapping: Record = { [GroupTypes.CLASS]: GroupTypeResponse.CLASS, + [GroupTypes.COURSE]: GroupTypeResponse.COURSE, + [GroupTypes.OTHER]: GroupTypeResponse.OTHER, }; export class GroupResponseMapper { diff --git a/apps/server/src/modules/group/domain/group-types.ts b/apps/server/src/modules/group/domain/group-types.ts index fa1311a0495..23e11ca5af6 100644 --- a/apps/server/src/modules/group/domain/group-types.ts +++ b/apps/server/src/modules/group/domain/group-types.ts @@ -1,3 +1,5 @@ export enum GroupTypes { CLASS = 'class', + COURSE = 'course', + OTHER = 'other', } diff --git a/apps/server/src/modules/group/entity/group.entity.ts b/apps/server/src/modules/group/entity/group.entity.ts index 1d55aec991c..27d4319042c 100644 --- a/apps/server/src/modules/group/entity/group.entity.ts +++ b/apps/server/src/modules/group/entity/group.entity.ts @@ -8,6 +8,8 @@ import { GroupValidPeriodEntity } from './group-valid-period.entity'; export enum GroupEntityTypes { CLASS = 'class', + COURSE = 'course', + OTHER = 'other', } export interface GroupEntityProps { diff --git a/apps/server/src/modules/group/repo/group-domain.mapper.ts b/apps/server/src/modules/group/repo/group-domain.mapper.ts index 44fcd006bb1..ea9ab7b450c 100644 --- a/apps/server/src/modules/group/repo/group-domain.mapper.ts +++ b/apps/server/src/modules/group/repo/group-domain.mapper.ts @@ -6,10 +6,14 @@ import { GroupEntity, GroupEntityProps, GroupEntityTypes, GroupUserEntity, Group const GroupEntityTypesToGroupTypesMapping: Record = { [GroupEntityTypes.CLASS]: GroupTypes.CLASS, + [GroupEntityTypes.COURSE]: GroupTypes.COURSE, + [GroupEntityTypes.OTHER]: GroupTypes.OTHER, }; -const GroupTypesToGroupEntityTypesMapping: Record = { +export const GroupTypesToGroupEntityTypesMapping: Record = { [GroupTypes.CLASS]: GroupEntityTypes.CLASS, + [GroupTypes.COURSE]: GroupEntityTypes.COURSE, + [GroupTypes.OTHER]: GroupEntityTypes.OTHER, }; export class GroupDomainMapper { diff --git a/apps/server/src/modules/group/repo/group.repo.spec.ts b/apps/server/src/modules/group/repo/group.repo.spec.ts index 4fde48b0e63..a8e39f9d86e 100644 --- a/apps/server/src/modules/group/repo/group.repo.spec.ts +++ b/apps/server/src/modules/group/repo/group.repo.spec.ts @@ -91,7 +91,7 @@ describe('GroupRepo', () => { }); }); - describe('findByUser', () => { + describe('findByUserAndGroupTypes', () => { describe('when the user has groups', () => { const setup = async () => { const userEntity: User = userFactory.buildWithId(); @@ -99,6 +99,8 @@ describe('GroupRepo', () => { const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { users: [{ user: userEntity, role: roleFactory.buildWithId() }], }); + groups[1].type = GroupEntityTypes.COURSE; + groups[2].type = GroupEntityTypes.OTHER; const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2); @@ -114,12 +116,36 @@ describe('GroupRepo', () => { it('should return the groups', async () => { const { user, groups } = await setup(); - const result: Group[] = await repo.findByUser(user); + const result: Group[] = await repo.findByUserAndGroupTypes(user, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + GroupTypes.OTHER, + ]); expect(result.map((group) => group.id).sort((a, b) => a.localeCompare(b))).toEqual( groups.map((group) => group.id).sort((a, b) => a.localeCompare(b)) ); }); + + it('should return only groups of the given group types', async () => { + const { user } = await setup(); + + const result: Group[] = await repo.findByUserAndGroupTypes(user, [GroupTypes.CLASS]); + + expect(result).toEqual([expect.objectContaining>({ type: GroupTypes.CLASS })]); + }); + + describe('when no group type is given', () => { + it('should return all groups', async () => { + const { user, groups } = await setup(); + + const result: Group[] = await repo.findByUserAndGroupTypes(user); + + expect(result.map((group) => group.id).sort((a, b) => a.localeCompare(b))).toEqual( + groups.map((group) => group.id).sort((a, b) => a.localeCompare(b)) + ); + }); + }); }); describe('when the user has no groups exists', () => { @@ -140,21 +166,27 @@ describe('GroupRepo', () => { it('should return an empty array', async () => { const { user } = await setup(); - const result: Group[] = await repo.findByUser(user); + const result: Group[] = await repo.findByUserAndGroupTypes(user, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + GroupTypes.OTHER, + ]); expect(result).toHaveLength(0); }); }); }); - describe('findClassesForSchool', () => { - describe('when groups of type class for the school exist', () => { + describe('findBySchoolIdAndGroupTypes', () => { + describe('when groups for the school exist', () => { const setup = async () => { const school: SchoolEntity = schoolFactory.buildWithId(); const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { type: GroupEntityTypes.CLASS, organization: school, }); + groups[1].type = GroupEntityTypes.COURSE; + groups[2].type = GroupEntityTypes.OTHER; const otherSchool: SchoolEntity = schoolFactory.buildWithId(); const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { @@ -172,10 +204,14 @@ describe('GroupRepo', () => { }; }; - it('should return the group', async () => { + it('should return the groups', async () => { const { school, groups } = await setup(); - const result: Group[] = await repo.findClassesForSchool(school.id); + const result: Group[] = await repo.findBySchoolIdAndGroupTypes(school.id, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + GroupTypes.OTHER, + ]); expect(result).toHaveLength(groups.length); }); @@ -183,10 +219,31 @@ describe('GroupRepo', () => { it('should not return groups from another school', async () => { const { school, otherSchool } = await setup(); - const result: Group[] = await repo.findClassesForSchool(school.id); + const result: Group[] = await repo.findBySchoolIdAndGroupTypes(school.id, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + ]); expect(result.map((group) => group.organizationId)).not.toContain(otherSchool.id); }); + + it('should return only groups of the given group types', async () => { + const { school } = await setup(); + + const result: Group[] = await repo.findBySchoolIdAndGroupTypes(school.id, [GroupTypes.CLASS]); + + expect(result).toEqual([expect.objectContaining>({ type: GroupTypes.CLASS })]); + }); + + describe('when no group type is given', () => { + it('should return all groups', async () => { + const { school, groups } = await setup(); + + const result: Group[] = await repo.findBySchoolIdAndGroupTypes(school.id); + + expect(result).toHaveLength(groups.length); + }); + }); }); describe('when no group exists', () => { @@ -204,7 +261,7 @@ describe('GroupRepo', () => { it('should return an empty array', async () => { const { school } = await setup(); - const result: Group[] = await repo.findClassesForSchool(school.id); + const result: Group[] = await repo.findBySchoolIdAndGroupTypes(school.id, [GroupTypes.CLASS]); expect(result).toHaveLength(0); }); diff --git a/apps/server/src/modules/group/repo/group.repo.ts b/apps/server/src/modules/group/repo/group.repo.ts index c52ab7487d8..80f31c69be6 100644 --- a/apps/server/src/modules/group/repo/group.repo.ts +++ b/apps/server/src/modules/group/repo/group.repo.ts @@ -2,9 +2,11 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { type UserDO } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; -import { Group, GroupProps } from '../domain'; +import { Scope } from '@shared/repo'; +import { Group, GroupProps, GroupTypes } from '../domain'; import { GroupEntity, GroupEntityProps, GroupEntityTypes } from '../entity'; -import { GroupDomainMapper } from './group-domain.mapper'; +import { GroupDomainMapper, GroupTypesToGroupEntityTypesMapping } from './group-domain.mapper'; +import { GroupScope } from './group.scope'; @Injectable() export class GroupRepo { @@ -43,12 +45,17 @@ export class GroupRepo { return domainObject; } - public async findByUser(user: UserDO): Promise { - const entities: GroupEntity[] = await this.em.find(GroupEntity, { - users: { user: new ObjectId(user.id) }, - }); + public async findByUserAndGroupTypes(user: UserDO, groupTypes?: GroupTypes[]): Promise { + let groupEntityTypes: GroupEntityTypes[] | undefined; + if (groupTypes) { + groupEntityTypes = groupTypes.map((type: GroupTypes) => GroupTypesToGroupEntityTypesMapping[type]); + } + + const scope: Scope = new GroupScope().byUserId(user.id).byTypes(groupEntityTypes); + + const entities: GroupEntity[] = await this.em.find(GroupEntity, scope.query); - const domainObjects = entities.map((entity) => { + const domainObjects: Group[] = entities.map((entity) => { const props: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(entity); return new Group(props); @@ -57,13 +64,17 @@ export class GroupRepo { return domainObjects; } - public async findClassesForSchool(schoolId: EntityId): Promise { - const entities: GroupEntity[] = await this.em.find(GroupEntity, { - type: GroupEntityTypes.CLASS, - organization: schoolId, - }); + public async findBySchoolIdAndGroupTypes(schoolId: EntityId, groupTypes?: GroupTypes[]): Promise { + let groupEntityTypes: GroupEntityTypes[] | undefined; + if (groupTypes) { + groupEntityTypes = groupTypes.map((type: GroupTypes) => GroupTypesToGroupEntityTypesMapping[type]); + } + + const scope: Scope = new GroupScope().byOrganizationId(schoolId).byTypes(groupEntityTypes); + + const entities: GroupEntity[] = await this.em.find(GroupEntity, scope.query); - const domainObjects = entities.map((entity) => { + const domainObjects: Group[] = entities.map((entity) => { const props: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(entity); return new Group(props); diff --git a/apps/server/src/modules/group/repo/group.scope.spec.ts b/apps/server/src/modules/group/repo/group.scope.spec.ts new file mode 100644 index 00000000000..1088651853b --- /dev/null +++ b/apps/server/src/modules/group/repo/group.scope.spec.ts @@ -0,0 +1,82 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { GroupEntityTypes } from '../entity'; +import { GroupScope } from './group.scope'; + +describe(GroupScope.name, () => { + let scope: GroupScope; + + beforeEach(() => { + scope = new GroupScope(); + scope.allowEmptyQuery(true); + }); + + describe('byTypes', () => { + describe('when types is undefined', () => { + it('should not add query', () => { + scope.byTypes(undefined); + + expect(scope.query).toEqual({}); + }); + }); + + describe('when types is defined', () => { + it('should add query', () => { + scope.byTypes([GroupEntityTypes.COURSE, GroupEntityTypes.CLASS]); + + expect(scope.query).toEqual({ type: { $in: [GroupEntityTypes.COURSE, GroupEntityTypes.CLASS] } }); + }); + }); + }); + + describe('byOrganizationId', () => { + describe('when id is undefined', () => { + it('should not add query', () => { + scope.byOrganizationId(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.byOrganizationId(id); + + expect(scope.query).toEqual({ organization: id }); + }); + }); + }); + + describe('byUserId', () => { + describe('when id is undefined', () => { + it('should not add query', () => { + scope.byUserId(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.byUserId(id); + + expect(scope.query).toEqual({ users: { user: new ObjectId(id) } }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/group/repo/group.scope.ts b/apps/server/src/modules/group/repo/group.scope.ts new file mode 100644 index 00000000000..be0c6938aa5 --- /dev/null +++ b/apps/server/src/modules/group/repo/group.scope.ts @@ -0,0 +1,27 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { EntityId } from '@shared/domain/types'; +import { Scope } from '@shared/repo'; +import { GroupEntity, GroupEntityTypes } from '../entity'; + +export class GroupScope extends Scope { + byTypes(types: GroupEntityTypes[] | undefined): this { + if (types) { + this.addQuery({ type: { $in: types } }); + } + return this; + } + + byOrganizationId(id: EntityId | undefined): this { + if (id) { + this.addQuery({ organization: id }); + } + return this; + } + + byUserId(id: EntityId | undefined): this { + if (id) { + this.addQuery({ users: { user: new ObjectId(id) } }); + } + return this; + } +} diff --git a/apps/server/src/modules/group/service/group.service.spec.ts b/apps/server/src/modules/group/service/group.service.spec.ts index 8724a8723ce..51986e983d0 100644 --- a/apps/server/src/modules/group/service/group.service.spec.ts +++ b/apps/server/src/modules/group/service/group.service.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { UserDO } from '@shared/domain/domainobject'; import { groupFactory, userDoFactory } from '@shared/testing'; -import { Group } from '../domain'; +import { Group, GroupTypes } from '../domain'; import { GroupRepo } from '../repo'; import { GroupService } from './group.service'; @@ -121,13 +121,13 @@ describe('GroupService', () => { }); }); - describe('findByUser', () => { + describe('findGroupsByUserAndGroupTypes', () => { describe('when groups with the user exists', () => { const setup = () => { const user: UserDO = userDoFactory.buildWithId(); const groups: Group[] = groupFactory.buildList(2); - groupRepo.findByUser.mockResolvedValue(groups); + groupRepo.findByUserAndGroupTypes.mockResolvedValue(groups); return { user, @@ -138,17 +138,29 @@ describe('GroupService', () => { it('should return the groups', async () => { const { user, groups } = setup(); - const result: Group[] = await service.findByUser(user); + const result: Group[] = await service.findGroupsByUserAndGroupTypes(user, [GroupTypes.CLASS]); expect(result).toEqual(groups); }); + + it('should call the repo with given group types', async () => { + const { user } = setup(); + + await service.findGroupsByUserAndGroupTypes(user, [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER]); + + expect(groupRepo.findByUserAndGroupTypes).toHaveBeenCalledWith(user, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + GroupTypes.OTHER, + ]); + }); }); describe('when no groups with the user exists', () => { const setup = () => { const user: UserDO = userDoFactory.buildWithId(); - groupRepo.findByUser.mockResolvedValue([]); + groupRepo.findByUserAndGroupTypes.mockResolvedValue([]); return { user, @@ -158,20 +170,20 @@ describe('GroupService', () => { it('should return empty array', async () => { const { user } = setup(); - const result: Group[] = await service.findByUser(user); + const result: Group[] = await service.findGroupsByUserAndGroupTypes(user, [GroupTypes.CLASS]); expect(result).toEqual([]); }); }); }); - describe('findClassesForSchool', () => { + describe('findGroupsBySchoolIdAndGroupTypes', () => { describe('when the school has groups of type class', () => { const setup = () => { const schoolId: string = new ObjectId().toHexString(); const groups: Group[] = groupFactory.buildList(3); - groupRepo.findClassesForSchool.mockResolvedValue(groups); + groupRepo.findBySchoolIdAndGroupTypes.mockResolvedValue(groups); return { schoolId, @@ -182,15 +194,23 @@ describe('GroupService', () => { it('should call the repo', async () => { const { schoolId } = setup(); - await service.findClassesForSchool(schoolId); - - expect(groupRepo.findClassesForSchool).toHaveBeenCalledWith(schoolId); + await service.findGroupsBySchoolIdAndGroupTypes(schoolId, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + GroupTypes.OTHER, + ]); + + expect(groupRepo.findBySchoolIdAndGroupTypes).toHaveBeenCalledWith(schoolId, [ + GroupTypes.CLASS, + GroupTypes.COURSE, + GroupTypes.OTHER, + ]); }); it('should return the groups', async () => { const { schoolId, groups } = setup(); - const result: Group[] = await service.findClassesForSchool(schoolId); + const result: Group[] = await service.findGroupsBySchoolIdAndGroupTypes(schoolId, [GroupTypes.CLASS]); expect(result).toEqual(groups); }); diff --git a/apps/server/src/modules/group/service/group.service.ts b/apps/server/src/modules/group/service/group.service.ts index 62ec2ece9c8..82ce4942cc4 100644 --- a/apps/server/src/modules/group/service/group.service.ts +++ b/apps/server/src/modules/group/service/group.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { type UserDO } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; -import { Group } from '../domain'; +import { Group, GroupTypes } from '../domain'; import { GroupRepo } from '../repo'; @Injectable() @@ -32,14 +32,14 @@ export class GroupService implements AuthorizationLoaderServiceGeneric { return group; } - public async findByUser(user: UserDO): Promise { - const groups: Group[] = await this.groupRepo.findByUser(user); + public async findGroupsByUserAndGroupTypes(user: UserDO, groupTypes?: GroupTypes[]): Promise { + const groups: Group[] = await this.groupRepo.findByUserAndGroupTypes(user, groupTypes); return groups; } - public async findClassesForSchool(schoolId: EntityId): Promise { - const group: Group[] = await this.groupRepo.findClassesForSchool(schoolId); + public async findGroupsBySchoolIdAndGroupTypes(schoolId: EntityId, groupTypes: GroupTypes[]): Promise { + const group: Group[] = await this.groupRepo.findBySchoolIdAndGroupTypes(schoolId, groupTypes); return group; } diff --git a/apps/server/src/modules/group/uc/group.uc.spec.ts b/apps/server/src/modules/group/uc/group.uc.spec.ts index 03468c092f3..99a8202e22e 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -11,10 +11,12 @@ import { LegacySystemService, SystemDto } from '@modules/system'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { ReferencedEntityNotFoundLoggable } from '@shared/common/loggable'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { LegacySchoolDo, Page, UserDO } from '@shared/domain/domainobject'; import { SchoolYearEntity, User } from '@shared/domain/entity'; import { Permission, SortOrder } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; import { groupFactory, legacySchoolDoFactory, @@ -25,7 +27,6 @@ import { userDoFactory, userFactory, } from '@shared/testing'; -import { ReferencedEntityNotFoundLoggable } from '@shared/common/loggable'; import { Logger } from '@src/core/logger'; import { SchoolYearQueryType } from '../controller/dto/interface'; import { Group, GroupTypes } from '../domain'; @@ -34,6 +35,7 @@ import { GroupService } from '../service'; import { ClassInfoDto, ResolvedGroupDto } from './dto'; import { ClassRootType } from './dto/class-root-type'; import { GroupUc } from './group.uc'; +import any = jasmine.any; describe('GroupUc', () => { let module: TestingModule; @@ -210,9 +212,9 @@ describe('GroupUc', () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); authorizationService.hasAllPermissions.mockReturnValueOnce(false); classService.findAllByUserId.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); - groupService.findByUser.mockResolvedValueOnce([group, groupWithSystem]); + groupService.findGroupsByUserAndGroupTypes.mockResolvedValueOnce([group, groupWithSystem]); classService.findClassesForSchool.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); - groupService.findClassesForSchool.mockResolvedValueOnce([group, groupWithSystem]); + groupService.findGroupsBySchoolIdAndGroupTypes.mockResolvedValueOnce([group, groupWithSystem]); systemService.findById.mockResolvedValue(system); userService.findById.mockImplementation((userId: string): Promise => { if (userId === teacherUser.id) { @@ -361,6 +363,17 @@ describe('GroupUc', () => { total: 5, }); }); + + it('should call group service with allowed group types', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + + expect(groupService.findGroupsByUserAndGroupTypes).toHaveBeenCalledWith<[UserDO, GroupTypes[]]>( + expect.any(UserDO), + [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER] + ); + }); }); describe('when sorting by external source name in descending order', () => { @@ -568,7 +581,7 @@ describe('GroupUc', () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(adminUser); authorizationService.hasAllPermissions.mockReturnValueOnce(true); classService.findClassesForSchool.mockResolvedValueOnce([clazz]); - groupService.findClassesForSchool.mockResolvedValueOnce([group, groupWithSystem]); + groupService.findGroupsBySchoolIdAndGroupTypes.mockResolvedValueOnce([group, groupWithSystem]); systemService.findById.mockResolvedValue(system); userService.findById.mockImplementation((userId: string): Promise => { @@ -689,6 +702,17 @@ describe('GroupUc', () => { total: 3, }); }); + + it('should call group service with allowed group types', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + + expect(groupService.findGroupsBySchoolIdAndGroupTypes).toHaveBeenCalledWith<[EntityId, GroupTypes[]]>( + teacherUser.school.id, + [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER] + ); + }); }); describe('when sorting by external source name in descending order', () => { @@ -810,7 +834,7 @@ describe('GroupUc', () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); authorizationService.hasAllPermissions.mockReturnValueOnce(false); classService.findAllByUserId.mockResolvedValueOnce([clazz]); - groupService.findByUser.mockResolvedValueOnce([group]); + groupService.findGroupsByUserAndGroupTypes.mockResolvedValueOnce([group]); systemService.findById.mockResolvedValue(system); userService.findById.mockImplementation((userId: string): Promise => { diff --git a/apps/server/src/modules/group/uc/group.uc.ts b/apps/server/src/modules/group/uc/group.uc.ts index 637a9b97f7b..5ac89aff399 100644 --- a/apps/server/src/modules/group/uc/group.uc.ts +++ b/apps/server/src/modules/group/uc/group.uc.ts @@ -14,7 +14,7 @@ import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { LegacySystemService, SystemDto } from '@src/modules/system'; import { SchoolYearQueryType } from '../controller/dto/interface'; -import { Group, GroupUser } from '../domain'; +import { Group, GroupTypes, GroupUser } from '../domain'; import { UnknownQueryTypeLoggableException } from '../loggable'; import { GroupService } from '../service'; import { SortHelper } from '../util'; @@ -35,6 +35,8 @@ export class GroupUc { private readonly logger: Logger ) {} + private ALLOWED_GROUP_TYPES: GroupTypes[] = [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER]; + public async findAllClasses( userId: EntityId, schoolId: EntityId, @@ -85,7 +87,7 @@ export class GroupUc { const classInfosFromClasses = await this.findClassesForSchool(schoolId, schoolYearQueryType); if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { - classInfosFromGroups = await this.findGroupsOfTypeClassForSchool(schoolId); + classInfosFromGroups = await this.findGroupsForSchool(schoolId); } const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; @@ -102,7 +104,7 @@ export class GroupUc { const classInfosFromClasses = await this.findClassesForUser(userId, schoolYearQueryType); if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { - classInfosFromGroups = await this.findGroupsOfTypeClassForUser(userId); + classInfosFromGroups = await this.findGroupsForUser(userId); } const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; @@ -233,22 +235,28 @@ export class GroupUc { return teachers; } - private async findGroupsOfTypeClassForSchool(schoolId: EntityId): Promise { - const groupsOfTypeClass: Group[] = await this.groupService.findClassesForSchool(schoolId); + private async findGroupsForSchool(schoolId: EntityId): Promise { + const groups: Group[] = await this.groupService.findGroupsBySchoolIdAndGroupTypes( + schoolId, + this.ALLOWED_GROUP_TYPES + ); - const systemMap: Map = await this.findSystemNamesForGroups(groupsOfTypeClass); + const systemMap: Map = await this.findSystemNamesForGroups(groups); const classInfosFromGroups: ClassInfoDto[] = await Promise.all( - groupsOfTypeClass.map(async (group: Group): Promise => this.getClassInfoFromGroup(group, systemMap)) + groups.map(async (group: Group): Promise => this.getClassInfoFromGroup(group, systemMap)) ); return classInfosFromGroups; } - private async findGroupsOfTypeClassForUser(userId: EntityId): Promise { + private async findGroupsForUser(userId: EntityId): Promise { const user: UserDO = await this.userService.findById(userId); - const groupsOfTypeClass: Group[] = await this.groupService.findByUser(user); + const groupsOfTypeClass: Group[] = await this.groupService.findGroupsByUserAndGroupTypes( + user, + this.ALLOWED_GROUP_TYPES + ); const systemMap: Map = await this.findSystemNamesForGroups(groupsOfTypeClass); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts index 6f8a2e29ab7..f5d3c0dc7da 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts @@ -972,7 +972,7 @@ describe('OidcProvisioningService', () => { const externalGroups: ExternalGroupDto[] = [firstExternalGroup, secondExternalGroup]; userService.findByExternalId.mockResolvedValue(user); - groupService.findByUser.mockResolvedValue(existingGroups); + groupService.findGroupsByUserAndGroupTypes.mockResolvedValue(existingGroups); return { externalGroups, @@ -1029,7 +1029,7 @@ describe('OidcProvisioningService', () => { const externalGroups: ExternalGroupDto[] = [firstExternalGroup]; userService.findByExternalId.mockResolvedValue(user); - groupService.findByUser.mockResolvedValue(existingGroups); + groupService.findGroupsByUserAndGroupTypes.mockResolvedValue(existingGroups); return { externalGroups, @@ -1096,7 +1096,7 @@ describe('OidcProvisioningService', () => { const externalGroups: ExternalGroupDto[] = [firstExternalGroup]; userService.findByExternalId.mockResolvedValue(user); - groupService.findByUser.mockResolvedValue(existingGroups); + groupService.findGroupsByUserAndGroupTypes.mockResolvedValue(existingGroups); return { externalGroups, diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts index a96eba16dcb..98e6abb8223 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts @@ -231,7 +231,7 @@ export class OidcProvisioningService { throw new NotFoundLoggableException(UserDO.name, 'externalId', externalUserId); } - const existingGroupsOfUser: Group[] = await this.groupService.findByUser(user); + const existingGroupsOfUser: Group[] = await this.groupService.findGroupsByUserAndGroupTypes(user); const groupsFromSystem: Group[] = existingGroupsOfUser.filter( (existingGroup: Group) => existingGroup.externalSource?.systemId === systemId diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts index 14405d48c6d..4aed9170018 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts @@ -183,7 +183,7 @@ describe('SanisResponseMapper', () => { }); }); - describe('when no group type is provided', () => { + describe('when group type other is provided', () => { const setup = () => { const { sanisResponse } = setupSanisResponse(); sanisResponse.personenkontexte[0].gruppen![0]!.gruppe.typ = SanisGroupType.OTHER; @@ -193,12 +193,39 @@ describe('SanisResponseMapper', () => { }; }; - it('should not map the group', () => { + it('should map the group', () => { const { sanisResponse } = setup(); const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); - expect(result).toHaveLength(0); + expect(result).toEqual([ + expect.objectContaining>({ + type: GroupTypes.OTHER, + }), + ]); + }); + }); + + describe('when group type course is provided', () => { + const setup = () => { + const { sanisResponse } = setupSanisResponse(); + sanisResponse.personenkontexte[0].gruppen![0]!.gruppe.typ = SanisGroupType.COURSE; + + return { + sanisResponse, + }; + }; + + it('should map the group', () => { + const { sanisResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); + + expect(result).toEqual([ + expect.objectContaining>({ + type: GroupTypes.COURSE, + }), + ]); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts index 5a8f2f90b83..4ec69d54149 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts @@ -27,6 +27,8 @@ const GroupRoleMapping: Partial> = { const GroupTypeMapping: Partial> = { [SanisGroupType.CLASS]: GroupTypes.CLASS, + [SanisGroupType.COURSE]: GroupTypes.COURSE, + [SanisGroupType.OTHER]: GroupTypes.OTHER, }; @Injectable()