diff --git a/apps/server/src/modules/group/controller/group.controller.ts b/apps/server/src/modules/group/controller/group.controller.ts index e19f867c394..9bc3a037046 100644 --- a/apps/server/src/modules/group/controller/group.controller.ts +++ b/apps/server/src/modules/group/controller/group.controller.ts @@ -1,11 +1,12 @@ import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { Group } from '@modules/group'; import { Controller, ForbiddenException, Get, HttpStatus, Param, Query, UnauthorizedException } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; import { Page } from '@shared/domain/domainobject'; -import { IFindQuery } from '@shared/domain/interface'; +import { IFindOptions } from '@shared/domain/interface'; import { ErrorResponse } from '@src/core/error/dto'; -import { GroupUc } from '../uc'; +import { ClassGroupUc, GroupUc } from '../uc'; import { ClassInfoDto, ResolvedGroupDto } from '../uc/dto'; import { ClassCallerParams, @@ -24,7 +25,7 @@ import { GroupResponseMapper } from './mapper'; @Authenticate('jwt') @Controller('groups') export class GroupController { - constructor(private readonly groupUc: GroupUc) {} + constructor(private readonly groupUc: GroupUc, private readonly classGroupUc: ClassGroupUc) {} @ApiOperation({ summary: 'Get a list of classes and groups of type class for the current user.' }) @ApiResponse({ status: HttpStatus.OK, type: ClassInfoSearchListResponse }) @@ -38,13 +39,12 @@ export class GroupController { @Query() callerParams: ClassCallerParams, @CurrentUser() currentUser: ICurrentUser ): Promise { - const board: Page = await this.groupUc.findAllClasses( + const board: Page = await this.classGroupUc.findAllClasses( currentUser.userId, currentUser.schoolId, filterParams.type, callerParams.calledFrom, - pagination.skip, - pagination.limit, + pagination, sortingQuery.sortBy, sortingQuery.sortOrder ); @@ -86,13 +86,16 @@ export class GroupController { @Query() pagination: GroupPaginationParams, @Query() params: GroupParams ): Promise { - const query: IFindQuery = { pagination, nameQuery: params.nameQuery }; + const options: IFindOptions = { pagination }; + const groups: Page = await this.groupUc.getAllGroups( currentUser.userId, currentUser.schoolId, - query, + options, + params.nameQuery, params.availableGroupsForCourseSync ); + const response: GroupListResponse = GroupResponseMapper.mapToGroupListResponse(groups, pagination); return response; diff --git a/apps/server/src/modules/group/domain/index.ts b/apps/server/src/modules/group/domain/index.ts index 32b97d8f9f7..713cae7ac17 100644 --- a/apps/server/src/modules/group/domain/index.ts +++ b/apps/server/src/modules/group/domain/index.ts @@ -2,3 +2,4 @@ export * from './group'; export * from './group-user'; export * from './group-types'; export { GroupDeletedEvent } from './event'; +export { GroupFilter } from './interface'; diff --git a/apps/server/src/modules/group/domain/interface/group-filter.ts b/apps/server/src/modules/group/domain/interface/group-filter.ts new file mode 100644 index 00000000000..7baaffbf511 --- /dev/null +++ b/apps/server/src/modules/group/domain/interface/group-filter.ts @@ -0,0 +1,10 @@ +import { GroupTypes } from '@modules/group'; +import { EntityId } from '@shared/domain/types'; + +export interface GroupFilter { + userId?: EntityId; + schoolId?: EntityId; + systemId?: EntityId; + groupTypes?: GroupTypes[]; + nameQuery?: string; +} diff --git a/apps/server/src/modules/group/domain/interface/index.ts b/apps/server/src/modules/group/domain/interface/index.ts new file mode 100644 index 00000000000..e9556398024 --- /dev/null +++ b/apps/server/src/modules/group/domain/interface/index.ts @@ -0,0 +1 @@ +export { GroupFilter } from './group-filter'; diff --git a/apps/server/src/modules/group/group-api.module.ts b/apps/server/src/modules/group/group-api.module.ts index ab6de7f1bd8..613ae154602 100644 --- a/apps/server/src/modules/group/group-api.module.ts +++ b/apps/server/src/modules/group/group-api.module.ts @@ -10,7 +10,7 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { GroupController } from './controller'; import { GroupModule } from './group.module'; -import { GroupUc } from './uc'; +import { ClassGroupUc, GroupUc } from './uc'; @Module({ imports: [ @@ -26,6 +26,6 @@ import { GroupUc } from './uc'; LearnroomModule, ], controllers: [GroupController], - providers: [GroupUc], + providers: [GroupUc, ClassGroupUc], }) export class GroupApiModule {} 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 2dce82334ad..f42f18bce33 100644 --- a/apps/server/src/modules/group/repo/group.repo.spec.ts +++ b/apps/server/src/modules/group/repo/group.repo.spec.ts @@ -1,10 +1,10 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { School } from '@modules/school'; -import { SchoolEntityMapper } from '@modules/school/repo/mikro-orm/mapper'; import { Test, TestingModule } from '@nestjs/testing'; -import { ExternalSource, Page, UserDO } from '@shared/domain/domainobject'; +import { ExternalSource, Page } from '@shared/domain/domainobject'; import { Course as CourseEntity, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { IFindOptions } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; import { cleanupCollections, courseFactory, @@ -13,7 +13,6 @@ import { roleFactory, schoolEntityFactory, systemEntityFactory, - userDoFactory, userFactory, } from '@shared/testing'; import { Group, GroupProps, GroupTypes, GroupUser } from '../domain'; @@ -95,11 +94,11 @@ describe('GroupRepo', () => { }); }); - describe('findByUserAndGroupTypes', () => { + describe('findGroups', () => { describe('when the user has groups', () => { const setup = async () => { const userEntity: User = userFactory.buildWithId(); - const user: UserDO = userDoFactory.build({ id: userEntity.id }); + const userId: EntityId = userEntity.id; const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { users: [{ user: userEntity, role: roleFactory.buildWithId() }], }); @@ -114,20 +113,16 @@ describe('GroupRepo', () => { em.clear(); return { - user, + userId, groups, nameQuery, }; }; it('should return the groups', async () => { - const { user, groups } = await setup(); + const { userId, groups } = await setup(); - const result: Page = await repo.findByUserAndGroupTypes(user, [ - GroupTypes.CLASS, - GroupTypes.COURSE, - GroupTypes.OTHER, - ]); + const result: Page = await repo.findGroups({ userId }); expect(result.data.map((group) => group.id).sort((a, b) => a.localeCompare(b))).toEqual( groups.map((group) => group.id).sort((a, b) => a.localeCompare(b)) @@ -135,13 +130,9 @@ describe('GroupRepo', () => { }); it('should return groups according to pagination', async () => { - const { user, groups } = await setup(); + const { userId, groups } = await setup(); - const result: Page = await repo.findByUserAndGroupTypes( - user, - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - { pagination: { skip: 1, limit: 1 } } - ); + const result: Page = await repo.findGroups({ userId }, { pagination: { skip: 1, limit: 1 } }); expect(result.total).toEqual(groups.length); expect(result.data.length).toEqual(1); @@ -149,43 +140,27 @@ describe('GroupRepo', () => { }); it('should return groups according to name query', async () => { - const { user, groups, nameQuery } = await setup(); + const { userId, groups, nameQuery } = await setup(); - const result: Page = await repo.findByUserAndGroupTypes( - user, - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - { nameQuery } - ); + const result: Page = await repo.findGroups({ userId, nameQuery }); expect(result.data.length).toEqual(1); expect(result.data[0].id).toEqual(groups[1].id); }); it('should return only groups of the given group types', async () => { - const { user } = await setup(); + const { userId } = await setup(); - const result: Page = await repo.findByUserAndGroupTypes(user, [GroupTypes.CLASS]); + const result: Page = await repo.findGroups({ userId, groupTypes: [GroupTypes.CLASS] }); expect(result.data).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: Page = await repo.findByUserAndGroupTypes(user); - - expect(result.data.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', () => { + describe('when the user has no groups', () => { const setup = async () => { const userEntity: User = userFactory.buildWithId(); - const user: UserDO = userDoFactory.build({ id: userEntity.id }); + const userId: EntityId = userEntity.id; const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2); @@ -193,276 +168,252 @@ describe('GroupRepo', () => { em.clear(); return { - user, + userId, }; }; it('should return an empty array', async () => { - const { user } = await setup(); + const { userId } = await setup(); - const result: Page = await repo.findByUserAndGroupTypes(user, [ - GroupTypes.CLASS, - GroupTypes.COURSE, - GroupTypes.OTHER, - ]); + const result: Page = await repo.findGroups({ userId }); expect(result.data).toHaveLength(0); }); }); - }); - describe('findAvailableByUser', () => { - describe('when the user has groups', () => { + describe('when groups for the school exist', () => { const setup = async () => { - const userEntity: User = userFactory.buildWithId(); - const user: UserDO = userDoFactory.build({ id: userEntity.id }); - const groupUserEntity: GroupUserEmbeddable = new GroupUserEmbeddable({ - user: userEntity, - role: roleFactory.buildWithId(), - }); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const schoolId: EntityId = school.id; const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { - users: [groupUserEntity], + type: GroupEntityTypes.CLASS, + organization: school, }); - const nameQuery = groups[2].name.slice(-3); - const course: CourseEntity = courseFactory.build({ syncedWithGroup: groups[0] }); - const availableGroupsCount = 2; + groups[1].type = GroupEntityTypes.COURSE; + groups[2].type = GroupEntityTypes.OTHER; - const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2); + const nameQuery = groups[1].name.slice(-3); - await em.persistAndFlush([userEntity, ...groups, ...otherGroups, course]); + const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId(); + const otherSchoolId: EntityId = otherSchool.id; + const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { + type: GroupEntityTypes.CLASS, + organization: otherSchool, + }); + + await em.persistAndFlush([school, ...groups, otherSchool, ...otherGroups]); em.clear(); return { - user, + otherSchoolId, groups, - availableGroupsCount, nameQuery, + schoolId, }; }; - it('should return the available groups', async () => { - const { user, availableGroupsCount } = await setup(); + it('should return the groups', async () => { + const { schoolId, groups } = await setup(); - const result: Page = await repo.findAvailableByUser(user); + const result: Page = await repo.findGroups({ schoolId }); - expect(result.total).toEqual(availableGroupsCount); - expect(result.data.every((group) => group.users[0].userId === user.id)).toEqual(true); + expect(result.data).toHaveLength(groups.length); + }); + + it('should not return groups from another school', async () => { + const { schoolId, otherSchoolId } = await setup(); + + const result: Page = await repo.findGroups({ schoolId }); + + expect(result.data.map((group) => group.organizationId)).not.toContain(otherSchoolId); }); it('should return groups according to pagination', async () => { - const { user, groups, availableGroupsCount } = await setup(); + const { schoolId, groups } = await setup(); - const result: Page = await repo.findAvailableByUser(user, { pagination: { skip: 1, limit: 1 } }); + const result: Page = await repo.findGroups({ schoolId }, { pagination: { skip: 1, limit: 1 } }); - expect(result.total).toEqual(availableGroupsCount); + expect(result.total).toEqual(groups.length); expect(result.data.length).toEqual(1); - expect(result.data[0].id).toEqual(groups[2].id); + expect(result.data[0].id).toEqual(groups[1].id); }); it('should return groups according to name query', async () => { - const { user, groups, nameQuery } = await setup(); + const { schoolId, groups, nameQuery } = await setup(); - const result: Page = await repo.findAvailableByUser(user, { nameQuery }); + const result: Page = await repo.findGroups({ schoolId, nameQuery }); expect(result.data.length).toEqual(1); - expect(result.data[0].id).toEqual(groups[2].id); + expect(result.data[0].id).toEqual(groups[1].id); + }); + + it('should return only groups of the given group types', async () => { + const { schoolId } = await setup(); + + const result: Page = await repo.findGroups({ schoolId, groupTypes: [GroupTypes.CLASS] }); + + expect(result.data).toEqual([expect.objectContaining>({ type: GroupTypes.CLASS })]); }); }); - describe('when the user has no groups exists', () => { + describe('when no group exists', () => { const setup = async () => { - const userEntity: User = userFactory.buildWithId(); - const user: UserDO = userDoFactory.build({ id: userEntity.id }); - - const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const schoolId: EntityId = school.id; - await em.persistAndFlush([userEntity, ...otherGroups]); + await em.persistAndFlush(school); em.clear(); return { - user, + schoolId, }; }; it('should return an empty array', async () => { - const { user } = await setup(); + const { schoolId } = await setup(); - const result: Page = await repo.findAvailableByUser(user); + const result: Page = await repo.findGroups({ schoolId }); - expect(result.total).toEqual(0); + expect(result.data).toHaveLength(0); }); }); - }); - describe('findBySchoolIdAndGroupTypes', () => { - describe('when groups for the school exist', () => { + describe('when groups for the school and system exist', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const system: SystemEntity = systemEntityFactory.buildWithId(); + const systemId: EntityId = system.id; + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system] }); + const schoolId: EntityId = school.id; const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { type: GroupEntityTypes.CLASS, organization: school, + externalSource: { + system, + }, }); groups[1].type = GroupEntityTypes.COURSE; groups[2].type = GroupEntityTypes.OTHER; - const nameQuery = groups[1].name.slice(-3); - - const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId(); + const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system] }); const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { type: GroupEntityTypes.CLASS, organization: otherSchool, }); - const schoolDO: School = SchoolEntityMapper.mapToDo(school); - - await em.persistAndFlush([school, ...groups, otherSchool, ...otherGroups]); + await em.persistAndFlush([school, system, ...groups, otherSchool, ...otherGroups]); em.clear(); return { - otherSchool, + schoolId, + systemId, groups, - nameQuery, - schoolDO, }; }; it('should return the groups', async () => { - const { schoolDO, groups } = await setup(); - - const result: Page = await repo.findBySchoolIdAndGroupTypes(schoolDO, [ - GroupTypes.CLASS, - GroupTypes.COURSE, - GroupTypes.OTHER, - ]); - - expect(result.data).toHaveLength(groups.length); - }); - - it('should not return groups from another school', async () => { - const { schoolDO, otherSchool } = await setup(); + const { schoolId, systemId } = await setup(); - const result: Page = await repo.findBySchoolIdAndGroupTypes(schoolDO, [ - GroupTypes.CLASS, - GroupTypes.COURSE, - ]); + const result: Page = await repo.findGroups({ schoolId, systemId }); - expect(result.data.map((group) => group.organizationId)).not.toContain(otherSchool.id); + expect(result.total).toEqual(3); }); - it('should return groups according to pagination', async () => { - const { schoolDO, groups } = await setup(); + it('should only return groups from the selected school', async () => { + const { schoolId, systemId } = await setup(); - const result: Page = await repo.findBySchoolIdAndGroupTypes( - schoolDO, - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - { pagination: { skip: 1, limit: 1 } } - ); + const result: Page = await repo.findGroups({ schoolId, systemId }); - expect(result.total).toEqual(groups.length); - expect(result.data.length).toEqual(1); - expect(result.data[0].id).toEqual(groups[1].id); + expect(result.data.every((group) => group.organizationId === schoolId)).toEqual(true); }); - it('should return groups according to name query', async () => { - const { schoolDO, groups, nameQuery } = await setup(); + it('should only return groups from the selected system', async () => { + const { schoolId, systemId } = await setup(); - const result: Page = await repo.findBySchoolIdAndGroupTypes( - schoolDO, - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - { nameQuery } - ); + const result: Page = await repo.findGroups({ schoolId, systemId }); - expect(result.data.length).toEqual(1); - expect(result.data[0].id).toEqual(groups[1].id); + expect(result.data.every((group) => group.externalSource?.systemId === systemId)).toEqual(true); }); - it('should return only groups of the given group types', async () => { - const { schoolDO } = await setup(); + it('should return only groups of the given group type', async () => { + const { schoolId, systemId } = await setup(); - const result: Page = await repo.findBySchoolIdAndGroupTypes(schoolDO, [GroupTypes.CLASS]); + const result: Page = await repo.findGroups({ schoolId, systemId, groupTypes: [GroupTypes.CLASS] }); expect(result.data).toEqual([expect.objectContaining>({ type: GroupTypes.CLASS })]); }); - - describe('when no group type is given', () => { - it('should return all groups', async () => { - const { schoolDO, groups } = await setup(); - - const result: Page = await repo.findBySchoolIdAndGroupTypes(schoolDO); - - expect(result.data).toHaveLength(groups.length); - }); - }); }); describe('when no group exists', () => { const setup = async () => { const school: SchoolEntity = schoolEntityFactory.buildWithId(); - const schoolDO: School = SchoolEntityMapper.mapToDo(school); + const schoolId: EntityId = school.id; + const system: SystemEntity = systemEntityFactory.buildWithId(); + const systemId: EntityId = system.id; - await em.persistAndFlush(school); + await em.persistAndFlush([school, system]); em.clear(); return { - schoolDO, + schoolId, + systemId, }; }; it('should return an empty array', async () => { - const { schoolDO } = await setup(); + const { schoolId, systemId } = await setup(); - const result: Page = await repo.findBySchoolIdAndGroupTypes(schoolDO, [GroupTypes.CLASS]); + const result: Page = await repo.findGroups({ schoolId, systemId }); expect(result.data).toHaveLength(0); }); }); }); - describe('findAvailableBySchoolId', () => { - describe('when available groups for the school exist', () => { + describe('findAvailableGroups', () => { + describe('when the user has groups', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const userEntity: User = userFactory.buildWithId(); + const userId: EntityId = userEntity.id; + const groupUserEntity: GroupUserEmbeddable = new GroupUserEmbeddable({ + user: userEntity, + role: roleFactory.buildWithId(), + }); const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { - type: GroupEntityTypes.CLASS, - organization: school, + users: [groupUserEntity], }); const nameQuery = groups[2].name.slice(-3); - const course: CourseEntity = courseFactory.build({ school, syncedWithGroup: groups[0] }); + const course: CourseEntity = courseFactory.build({ syncedWithGroup: groups[0] }); const availableGroupsCount = 2; - const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId(); - const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { - type: GroupEntityTypes.CLASS, - organization: otherSchool, - }); - - const schoolDO: School = SchoolEntityMapper.mapToDo(school); + const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2); - await em.persistAndFlush([school, ...groups, otherSchool, ...otherGroups, course]); + await em.persistAndFlush([userEntity, ...groups, ...otherGroups, course]); em.clear(); + const defaultOptions: IFindOptions = { pagination: { skip: 0 } }; + return { - schoolDO, - otherSchool, + userId, groups, availableGroupsCount, nameQuery, + defaultOptions, }; }; - it('should return the available groups from selected school', async () => { - const { schoolDO, availableGroupsCount } = await setup(); + it('should return the available groups', async () => { + const { userId, availableGroupsCount, defaultOptions } = await setup(); - const result: Page = await repo.findAvailableBySchoolId(schoolDO); + const result: Page = await repo.findAvailableGroups({ userId }, defaultOptions); - expect(result.data).toHaveLength(availableGroupsCount); - expect(result.data.every((group) => group.organizationId === schoolDO.id)).toEqual(true); + expect(result.total).toEqual(availableGroupsCount); + expect(result.data.every((group) => group.users[0].userId === userId)).toEqual(true); }); it('should return groups according to pagination', async () => { - const { schoolDO, groups, availableGroupsCount } = await setup(); + const { userId, groups, availableGroupsCount } = await setup(); - const result: Page = await repo.findAvailableBySchoolId(schoolDO, { pagination: { skip: 1, limit: 1 } }); + const result: Page = await repo.findAvailableGroups({ userId }, { pagination: { skip: 1, limit: 1 } }); expect(result.total).toEqual(availableGroupsCount); expect(result.data.length).toEqual(1); @@ -470,143 +421,125 @@ describe('GroupRepo', () => { }); it('should return groups according to name query', async () => { - const { schoolDO, groups, nameQuery } = await setup(); + const { userId, groups, nameQuery, defaultOptions } = await setup(); - const result: Page = await repo.findAvailableBySchoolId(schoolDO, { nameQuery }); + const result: Page = await repo.findAvailableGroups({ userId, nameQuery }, defaultOptions); expect(result.data.length).toEqual(1); expect(result.data[0].id).toEqual(groups[2].id); }); }); - describe('when no group exists', () => { + describe('when the user has no groups exists', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId(); - const schoolDO: School = SchoolEntityMapper.mapToDo(school); + const userEntity: User = userFactory.buildWithId(); + const userId: EntityId = userEntity.id; - await em.persistAndFlush([school]); + const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2); + + await em.persistAndFlush([userEntity, ...otherGroups]); em.clear(); + const defaultOptions: IFindOptions = { pagination: { skip: 0 } }; + return { - schoolDO, + userId, + defaultOptions, }; }; it('should return an empty array', async () => { - const { schoolDO } = await setup(); + const { userId, defaultOptions } = await setup(); - const result: Page = await repo.findAvailableBySchoolId(schoolDO); + const result: Page = await repo.findAvailableGroups({ userId }, defaultOptions); expect(result.total).toEqual(0); }); }); - }); - describe('findGroupsBySchoolIdAndSystemIdAndGroupType', () => { - describe('when groups for the school exist', () => { + describe('when available groups for the school exist', () => { const setup = async () => { - const system: SystemEntity = systemEntityFactory.buildWithId(); - const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const schoolId: EntityId = school.id; const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { type: GroupEntityTypes.CLASS, organization: school, - externalSource: { - system, - }, }); - groups[1].type = GroupEntityTypes.COURSE; - groups[2].type = GroupEntityTypes.OTHER; + const nameQuery = groups[2].name.slice(-3); + const course: CourseEntity = courseFactory.build({ school, syncedWithGroup: groups[0] }); + const availableGroupsCount = 2; - const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system] }); + const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId(); const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { type: GroupEntityTypes.CLASS, organization: otherSchool, }); - await em.persistAndFlush([school, system, ...groups, otherSchool, ...otherGroups]); + await em.persistAndFlush([school, ...groups, otherSchool, ...otherGroups, course]); em.clear(); + const defaultOptions: IFindOptions = { pagination: { skip: 0 } }; + return { - school, - system, - otherSchool, + schoolId, groups, + availableGroupsCount, + nameQuery, + defaultOptions, }; }; - 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(); + it('should return the available groups from selected school', async () => { + const { schoolId, availableGroupsCount, defaultOptions } = await setup(); - const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType( - school.id, - system.id, - GroupTypes.CLASS - ); + const result: Page = await repo.findAvailableGroups({ schoolId }, defaultOptions); - expect(result.every((group) => group.organizationId === school.id)).toEqual(true); + expect(result.data).toHaveLength(availableGroupsCount); + expect(result.data.every((group) => group.organizationId === schoolId)).toEqual(true); }); - it('should only return groups from the selected system', async () => { - const { school, system } = await setup(); + it('should return groups according to pagination', async () => { + const { schoolId, groups, availableGroupsCount } = await setup(); - const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType( - school.id, - system.id, - GroupTypes.CLASS - ); + const result: Page = await repo.findAvailableGroups({ schoolId }, { pagination: { skip: 1, limit: 1 } }); - expect(result.every((group) => group.externalSource?.systemId === system.id)).toEqual(true); + expect(result.total).toEqual(availableGroupsCount); + expect(result.data.length).toEqual(1); + expect(result.data[0].id).toEqual(groups[2].id); }); - it('should return only groups of the given group type', async () => { - const { school, system } = await setup(); + it('should return groups according to name query', async () => { + const { schoolId, groups, nameQuery, defaultOptions } = await setup(); - const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType( - school.id, - system.id, - GroupTypes.CLASS - ); + const result: Page = await repo.findAvailableGroups({ schoolId, nameQuery }, defaultOptions); - expect(result).toEqual([expect.objectContaining>({ type: GroupTypes.CLASS })]); + expect(result.data.length).toEqual(1); + expect(result.data[0].id).toEqual(groups[2].id); }); }); describe('when no group exists', () => { const setup = async () => { const school: SchoolEntity = schoolEntityFactory.buildWithId(); - const system: SystemEntity = systemEntityFactory.buildWithId(); + const schoolId: EntityId = school.id; - await em.persistAndFlush([school, system]); + await em.persistAndFlush([school]); em.clear(); + const defaultOptions: IFindOptions = { pagination: { skip: 0 } }; + return { - school, - system, + schoolId, + defaultOptions, }; }; it('should return an empty array', async () => { - const { school, system } = await setup(); + const { schoolId, defaultOptions } = await setup(); - const result: Group[] = await repo.findGroupsBySchoolIdAndSystemIdAndGroupType( - school.id, - system.id, - GroupTypes.CLASS - ); + const result: Page = await repo.findAvailableGroups({ schoolId }, defaultOptions); - expect(result).toHaveLength(0); + expect(result.total).toEqual(0); }); }); }); diff --git a/apps/server/src/modules/group/repo/group.repo.ts b/apps/server/src/modules/group/repo/group.repo.ts index 52dc6233f59..3c92c427918 100644 --- a/apps/server/src/modules/group/repo/group.repo.ts +++ b/apps/server/src/modules/group/repo/group.repo.ts @@ -1,15 +1,14 @@ import { EntityData, EntityDictionary, EntityName, QueryOrder } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { School } from '@modules/school'; import { Injectable } from '@nestjs/common'; import { StringValidator } from '@shared/common'; -import { Page, type UserDO } from '@shared/domain/domainobject'; -import { IFindQuery } from '@shared/domain/interface'; +import { Page } from '@shared/domain/domainobject'; +import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { MongoPatterns } from '@shared/repo'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; -import { Group, GroupTypes } from '../domain'; -import { GroupEntity, GroupEntityTypes } from '../entity'; +import { Group, GroupFilter, GroupTypes } from '../domain'; +import { GroupEntity } from '../entity'; import { GroupDomainMapper, GroupTypesToGroupEntityTypesMapping } from './group-domain.mapper'; import { GroupScope } from './group.scope'; @@ -54,66 +53,25 @@ export class GroupRepo extends BaseDomainObjectRepo { return domainObject; } - public async findByUserAndGroupTypes( - user: UserDO, - groupTypes?: GroupTypes[], - query?: IFindQuery - ): Promise> { - const scope: GroupScope = new GroupScope().byUserId(user.id); - if (groupTypes) { - const groupEntityTypes = groupTypes.map((type: GroupTypes) => GroupTypesToGroupEntityTypesMapping[type]); - scope.byTypes(groupEntityTypes); - } - - const escapedName = query?.nameQuery?.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '').trim(); - if (StringValidator.isNotEmptyString(escapedName, true)) { - scope.byNameQuery(escapedName); - } - - const [entities, total] = await this.em.findAndCount(GroupEntity, scope.query, { - offset: query?.pagination?.skip, - limit: query?.pagination?.limit, - orderBy: { name: QueryOrder.ASC }, - }); - - const domainObjects: Group[] = entities.map((entity) => GroupDomainMapper.mapEntityToDo(entity)); - - const page: Page = new Page(domainObjects, total); - - return page; - } - - public async findAvailableByUser(user: UserDO, query?: IFindQuery): Promise> { - const pipelineStage: unknown[] = [{ $match: { users: { $elemMatch: { user: new ObjectId(user.id) } } } }]; - const availableGroups: Page = await this.findAvailableGroup( - pipelineStage, - query?.pagination?.skip, - query?.pagination?.limit, - query?.nameQuery - ); + public async findGroups(filter: GroupFilter, options?: IFindOptions): Promise> { + const scope: GroupScope = new GroupScope(); + scope.byUserId(filter.userId); + scope.byOrganizationId(filter.schoolId); + scope.bySystemId(filter.systemId); - return availableGroups; - } - - public async findBySchoolIdAndGroupTypes( - school: School, - groupTypes?: GroupTypes[], - query?: IFindQuery - ): Promise> { - const scope: GroupScope = new GroupScope().byOrganizationId(school.id); - if (groupTypes) { - const groupEntityTypes = groupTypes.map((type: GroupTypes) => GroupTypesToGroupEntityTypesMapping[type]); + if (filter.groupTypes) { + const groupEntityTypes = filter.groupTypes.map((type: GroupTypes) => GroupTypesToGroupEntityTypesMapping[type]); scope.byTypes(groupEntityTypes); } - const escapedName = query?.nameQuery?.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '').trim(); + const escapedName = filter.nameQuery?.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '').trim(); if (StringValidator.isNotEmptyString(escapedName, true)) { scope.byNameQuery(escapedName); } const [entities, total] = await this.em.findAndCount(GroupEntity, scope.query, { - offset: query?.pagination?.skip, - limit: query?.pagination?.limit, + offset: options?.pagination?.skip, + limit: options?.pagination?.limit, orderBy: { name: QueryOrder.ASC }, }); @@ -124,52 +82,23 @@ export class GroupRepo extends BaseDomainObjectRepo { return page; } - public async findAvailableBySchoolId(school: School, query?: IFindQuery): Promise> { - const pipelineStage: unknown[] = [{ $match: { organization: new ObjectId(school.id) } }]; - - const availableGroups: Page = await this.findAvailableGroup( - pipelineStage, - query?.pagination?.skip, - query?.pagination?.limit, - query?.nameQuery - ); - - return availableGroups; - } - - public async findGroupsBySchoolIdAndSystemIdAndGroupType( - schoolId: EntityId, - systemId: EntityId, - groupType: GroupTypes - ): Promise { - const groupEntityType: GroupEntityTypes = GroupTypesToGroupEntityTypesMapping[groupType]; - - const scope: GroupScope = new GroupScope() - .byOrganizationId(schoolId) - .bySystemId(systemId) - .byTypes([groupEntityType]); - - const entities: GroupEntity[] = await this.em.find(GroupEntity, scope.query); - - const domainObjects: Group[] = entities.map((entity) => GroupDomainMapper.mapEntityToDo(entity)); - - return domainObjects; - } - - private async findAvailableGroup( - pipelineStage: unknown[], - skip = 0, - limit?: number, - nameQuery?: string - ): Promise> { + public async findAvailableGroups(filter: GroupFilter, options?: IFindOptions): Promise> { + const pipeline: unknown[] = []; let nameRegexFilter = {}; - const pipeline: unknown[] = pipelineStage; - const escapedName = nameQuery?.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '').trim(); + const escapedName = filter.nameQuery?.replace(MongoPatterns.REGEX_MONGO_LANGUAGE_PATTERN_WHITELIST, '').trim(); if (StringValidator.isNotEmptyString(escapedName, true)) { nameRegexFilter = { name: { $regex: escapedName, $options: 'i' } }; } + if (filter.userId) { + pipeline.push({ $match: { users: { $elemMatch: { user: new ObjectId(filter.userId) } } } }); + } + + if (filter.schoolId) { + pipeline.push({ $match: { organization: new ObjectId(filter.schoolId) } }); + } + pipeline.push( { $match: nameRegexFilter }, { @@ -184,18 +113,18 @@ export class GroupRepo extends BaseDomainObjectRepo { { $sort: { name: 1 } } ); - if (limit) { + if (options?.pagination?.limit) { pipeline.push({ $facet: { total: [{ $count: 'count' }], - data: [{ $skip: skip }, { $limit: limit }], + data: [{ $skip: options.pagination?.skip }, { $limit: options.pagination.limit }], }, }); } else { pipeline.push({ $facet: { total: [{ $count: 'count' }], - data: [{ $skip: skip }], + data: [{ $skip: options?.pagination?.skip }], }, }); } 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 0ee2c08ba00..ca115317e9c 100644 --- a/apps/server/src/modules/group/service/group.service.spec.ts +++ b/apps/server/src/modules/group/service/group.service.spec.ts @@ -1,12 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { EventBus } from '@nestjs/cqrs'; -import { School } from '@modules/school'; -import { schoolFactory } from '@modules/school/testing'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { Page, UserDO } from '@shared/domain/domainobject'; -import { groupFactory, userDoFactory } from '@shared/testing'; +import { Page } from '@shared/domain/domainobject'; +import { EntityId } from '@shared/domain/types'; +import { groupFactory } from '@shared/testing'; import { Group, GroupDeletedEvent, GroupTypes } from '../domain'; import { GroupRepo } from '../repo'; import { GroupService } from './group.service'; @@ -130,246 +129,186 @@ describe('GroupService', () => { }); }); - describe('findGroupsByUserAndGroupTypes', () => { - describe('when groups with the user exists', () => { + describe('findGroups', () => { + describe('when groups exist', () => { const setup = () => { - const user: UserDO = userDoFactory.buildWithId(); + const userId: EntityId = new ObjectId().toHexString(); + const schoolId: EntityId = new ObjectId().toHexString(); + const systemId: EntityId = new ObjectId().toHexString(); + const nameQuery = 'name'; const groups: Group[] = groupFactory.buildList(2); const page: Page = new Page(groups, groups.length); - groupRepo.findByUserAndGroupTypes.mockResolvedValue(page); + groupRepo.findGroups.mockResolvedValue(page); return { - user, + userId, + schoolId, + systemId, + nameQuery, groups, }; }; - it('should return the groups', async () => { - const { user, groups } = setup(); + it('should return the groups for the user', async () => { + const { userId, groups } = setup(); - const result: Page = await service.findGroupsByUserAndGroupTypes(user, [GroupTypes.CLASS]); + const result: Page = await service.findGroups({ userId }); expect(result.data).toEqual(groups); }); - it('should call the repo with given group types', async () => { - const { user } = setup(); + it('should return the groups for school', async () => { + const { schoolId, groups } = setup(); - await service.findGroupsByUserAndGroupTypes(user, [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER]); + const result: Page = await service.findGroups({ schoolId }); - expect(groupRepo.findByUserAndGroupTypes).toHaveBeenCalledWith( - user, - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - undefined - ); - }); - }); - - describe('when no groups with the user exists', () => { - const setup = () => { - const user: UserDO = userDoFactory.buildWithId(); - - groupRepo.findByUserAndGroupTypes.mockResolvedValue(new Page([], 0)); - - return { - user, - }; - }; - - it('should return empty array', async () => { - const { user } = setup(); - - const result: Page = await service.findGroupsByUserAndGroupTypes(user, [GroupTypes.CLASS]); - - expect(result.data).toEqual([]); + expect(result.data).toEqual(groups); }); - }); - }); - - describe('findAvailableGroupByUser', () => { - describe('when available groups exist for user', () => { - const setup = () => { - const user: UserDO = userDoFactory.buildWithId(); - const groups: Group[] = groupFactory.buildList(2); - - groupRepo.findAvailableByUser.mockResolvedValue(new Page([groups[1]], 1)); - return { - user, - groups, - }; - }; - - it('should call repo', async () => { - const { user } = setup(); + it('should return the groups for school and system', async () => { + const { schoolId, systemId, groups } = setup(); - await service.findAvailableGroupsByUser(user); + const result: Page = await service.findGroups({ schoolId, systemId }); - expect(groupRepo.findAvailableByUser).toHaveBeenCalledWith(user, undefined); + expect(result.data).toEqual(groups); }); - it('should return groups', async () => { - const { user, groups } = setup(); + it('should call the repo with all given arguments', async () => { + const { userId, schoolId, systemId, nameQuery } = setup(); - const result: Page = await service.findAvailableGroupsByUser(user); - - expect(result.data).toEqual([groups[1]]); + await service.findGroups({ + userId, + schoolId, + systemId, + nameQuery, + groupTypes: [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], + }); + + expect(groupRepo.findGroups).toHaveBeenCalledWith( + { + userId, + schoolId, + systemId, + nameQuery, + groupTypes: [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], + }, + undefined + ); }); }); - describe('when no groups with the user exists', () => { + describe('when no groups exist', () => { const setup = () => { - const user: UserDO = userDoFactory.buildWithId(); + const userId: EntityId = new ObjectId().toHexString(); + const schoolId: EntityId = new ObjectId().toHexString(); + const systemId: EntityId = new ObjectId().toHexString(); - groupRepo.findAvailableByUser.mockResolvedValue(new Page([], 0)); + groupRepo.findGroups.mockResolvedValue(new Page([], 0)); return { - user, + userId, + schoolId, + systemId, }; }; - it('should return empty array', async () => { - const { user } = setup(); + it('should return empty array for user', async () => { + const { userId } = setup(); - const result: Page = await service.findAvailableGroupsByUser(user); + const result: Page = await service.findGroups({ userId }); expect(result.data).toEqual([]); }); - }); - }); - describe('findGroupsBySchoolIdAndGroupTypes', () => { - describe('when the school has groups of type class', () => { - const setup = () => { - const school: School = schoolFactory.build(); - const groups: Group[] = groupFactory.buildList(3); - const page: Page = new Page(groups, groups.length); + it('should return empty array for school', async () => { + const { schoolId } = setup(); - groupRepo.findBySchoolIdAndGroupTypes.mockResolvedValue(page); + const result: Page = await service.findGroups({ schoolId }); - return { - school, - groups, - }; - }; - - it('should call the repo', async () => { - const { school } = setup(); - - await service.findGroupsBySchoolIdAndGroupTypes(school, [ - GroupTypes.CLASS, - GroupTypes.COURSE, - GroupTypes.OTHER, - ]); - - expect(groupRepo.findBySchoolIdAndGroupTypes).toHaveBeenCalledWith( - school, - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - undefined - ); + expect(result.data).toEqual([]); }); - it('should return the groups', async () => { - const { school, groups } = setup(); + it('should return empty array for school and system', async () => { + const { schoolId, systemId } = setup(); - const result: Page = await service.findGroupsBySchoolIdAndGroupTypes(school, [GroupTypes.CLASS]); + const result: Page = await service.findGroups({ schoolId, systemId }); - expect(result.data).toEqual(groups); + expect(result.data).toEqual([]); }); }); }); - describe('findAvailableGroupBySchoolId', () => { - describe('when available groups exist for school', () => { + describe('findAvailableGroups', () => { + describe('when available groups exist', () => { const setup = () => { - const school: School = schoolFactory.build(); + const userId: EntityId = new ObjectId().toHexString(); + const schoolId: EntityId = new ObjectId().toHexString(); + const nameQuery = 'name'; const groups: Group[] = groupFactory.buildList(2); - groupRepo.findAvailableBySchoolId.mockResolvedValue(new Page([groups[1]], 1)); + groupRepo.findAvailableGroups.mockResolvedValue(new Page([groups[1]], 1)); return { - school, + userId, + schoolId, + nameQuery, groups, }; }; - it('should call repo', async () => { - const { school } = setup(); + it('should return groups for user', async () => { + const { userId, groups } = setup(); - await service.findAvailableGroupsBySchoolId(school); + const result: Page = await service.findAvailableGroups({ userId }); - expect(groupRepo.findAvailableBySchoolId).toHaveBeenCalledWith(school, undefined); + expect(result.data).toEqual([groups[1]]); }); - it('should return groups', async () => { - const { school, groups } = setup(); + it('should return groups for school', async () => { + const { schoolId, groups } = setup(); - const result: Page = await service.findAvailableGroupsBySchoolId(school); + const result: Page = await service.findAvailableGroups({ schoolId }); expect(result.data).toEqual([groups[1]]); }); - }); - - describe('when no groups with the user exists', () => { - const setup = () => { - const school: School = schoolFactory.build(); - groupRepo.findAvailableBySchoolId.mockResolvedValue(new Page([], 0)); - - return { - school, - }; - }; - - it('should return empty array', async () => { - const { school } = setup(); + it('should call repo', async () => { + const { userId, schoolId, nameQuery } = setup(); - const result: Page = await service.findAvailableGroupsBySchoolId(school); + await service.findAvailableGroups({ userId, schoolId, nameQuery }); - expect(result.data).toEqual([]); + expect(groupRepo.findAvailableGroups).toHaveBeenCalledWith({ userId, schoolId, nameQuery }, undefined); }); }); - }); - describe('findGroupsBySchoolIdAndSystemIdAndGroupType', () => { - describe('when the school has groups of type class', () => { + describe('when no groups exist', () => { const setup = () => { - const schoolId: string = new ObjectId().toHexString(); - const systemId: string = new ObjectId().toHexString(); - const groups: Group[] = groupFactory.buildList(3); + const userId: EntityId = new ObjectId().toHexString(); + const schoolId: EntityId = new ObjectId().toHexString(); - groupRepo.findGroupsBySchoolIdAndSystemIdAndGroupType.mockResolvedValue(groups); + groupRepo.findAvailableGroups.mockResolvedValue(new Page([], 0)); return { + userId, schoolId, - systemId, - groups, }; }; - it('should search for the groups', async () => { - const { schoolId, systemId } = setup(); + it('should return empty array for user', async () => { + const { userId } = setup(); - await service.findGroupsBySchoolIdAndSystemIdAndGroupType(schoolId, systemId, GroupTypes.CLASS); + const result: Page = await service.findAvailableGroups({ userId }); - expect(groupRepo.findGroupsBySchoolIdAndSystemIdAndGroupType).toHaveBeenCalledWith( - schoolId, - systemId, - GroupTypes.CLASS - ); + expect(result.data).toEqual([]); }); - it('should return the groups', async () => { - const { schoolId, systemId, groups } = setup(); + it('should return empty array for school', async () => { + const { schoolId } = setup(); - const result: Group[] = await service.findGroupsBySchoolIdAndSystemIdAndGroupType( - schoolId, - systemId, - GroupTypes.CLASS - ); + const result: Page = await service.findAvailableGroups({ schoolId }); - expect(result).toEqual(groups); + expect(result.data).toEqual([]); }); }); }); diff --git a/apps/server/src/modules/group/service/group.service.ts b/apps/server/src/modules/group/service/group.service.ts index a91db81b6f1..bbf41827624 100644 --- a/apps/server/src/modules/group/service/group.service.ts +++ b/apps/server/src/modules/group/service/group.service.ts @@ -1,12 +1,11 @@ import { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; -import { School } from '@modules/school'; import { Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { Page, type UserDO } from '@shared/domain/domainobject'; -import { IFindQuery } from '@shared/domain/interface'; +import { Page } from '@shared/domain/domainobject'; +import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { Group, GroupDeletedEvent, GroupTypes } from '../domain'; +import { Group, GroupDeletedEvent, GroupFilter } from '../domain'; import { GroupRepo } from '../repo'; @Injectable() @@ -35,52 +34,18 @@ export class GroupService implements AuthorizationLoaderServiceGeneric { return group; } - public async findGroupsByUserAndGroupTypes( - user: UserDO, - groupTypes?: GroupTypes[], - query?: IFindQuery - ): Promise> { - const groups: Page = await this.groupRepo.findByUserAndGroupTypes(user, groupTypes, query); + public async findGroups(filter: GroupFilter, options?: IFindOptions): Promise> { + const groups: Page = await this.groupRepo.findGroups(filter, options); return groups; } - public async findAvailableGroupsByUser(user: UserDO, query?: IFindQuery): Promise> { - const groups: Page = await this.groupRepo.findAvailableByUser(user, query); + public async findAvailableGroups(filter: GroupFilter, options?: IFindOptions): Promise> { + const groups: Page = await this.groupRepo.findAvailableGroups(filter, options); return groups; } - public async findGroupsBySchoolIdAndGroupTypes( - school: School, - groupTypes?: GroupTypes[], - query?: IFindQuery - ): Promise> { - const group: Page = await this.groupRepo.findBySchoolIdAndGroupTypes(school, groupTypes, query); - - return group; - } - - public async findAvailableGroupsBySchoolId(school: School, query?: IFindQuery): Promise> { - const groups: Page = await this.groupRepo.findAvailableBySchoolId(school, query); - - return groups; - } - - public async findGroupsBySchoolIdAndSystemIdAndGroupType( - schoolId: EntityId, - systemId: EntityId, - groupType: GroupTypes - ): Promise { - const group: Group[] = await this.groupRepo.findGroupsBySchoolIdAndSystemIdAndGroupType( - schoolId, - systemId, - groupType - ); - - return group; - } - public async save(group: Group): Promise { const savedGroup: Group = await this.groupRepo.save(group); diff --git a/apps/server/src/modules/group/uc/class-group.uc.spec.ts b/apps/server/src/modules/group/uc/class-group.uc.spec.ts new file mode 100644 index 00000000000..4e3f0d0ceee --- /dev/null +++ b/apps/server/src/modules/group/uc/class-group.uc.spec.ts @@ -0,0 +1,995 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Action, AuthorizationContext, AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { Class } from '@modules/class/domain'; +import { classFactory } from '@modules/class/domain/testing/factory/class.factory'; +import { ClassGroupUc } from '@modules/group/uc/class-group.uc'; +import { Course } from '@modules/learnroom/domain'; +import { CourseDoService } from '@modules/learnroom/service/course-do.service'; +import { courseFactory } from '@modules/learnroom/testing'; +import { SchoolYearService } from '@modules/legacy-school'; +import { ProvisioningConfig } from '@modules/provisioning'; +import { RoleService } from '@modules/role'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { School, SchoolService } from '@modules/school/domain'; +import { schoolFactory } from '@modules/school/testing'; +import { LegacySystemService, SystemDto } from '@modules/system'; +import { UserService } from '@modules/user'; +import { ForbiddenException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Page, UserDO } from '@shared/domain/domainobject'; +import { SchoolYearEntity, User } from '@shared/domain/entity'; +import { Permission, SortOrder } from '@shared/domain/interface'; +import { + groupFactory, + roleDtoFactory, + schoolYearFactory, + setupEntities, + UserAndAccountTestFactory, + userDoFactory, + userFactory, +} from '@shared/testing'; +import { ClassRequestContext, SchoolYearQueryType } from '../controller/dto/interface'; +import { Group, GroupFilter } from '../domain'; +import { UnknownQueryTypeLoggableException } from '../loggable'; +import { GroupService } from '../service'; +import { ClassInfoDto } from './dto'; +import { ClassRootType } from './dto/class-root-type'; + +describe('ClassGroupUc', () => { + let module: TestingModule; + let uc: ClassGroupUc; + + let groupService: DeepMocked; + let userService: DeepMocked; + let roleService: DeepMocked; + let classService: DeepMocked; + let systemService: DeepMocked; + let schoolService: DeepMocked; + let authorizationService: DeepMocked; + let schoolYearService: DeepMocked; + let courseService: DeepMocked; + let configService: DeepMocked>; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ClassGroupUc, + { + provide: GroupService, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: RoleService, + useValue: createMock(), + }, + { + provide: ClassService, + useValue: createMock(), + }, + { + provide: LegacySystemService, + useValue: createMock(), + }, + { + provide: SchoolService, + useValue: createMock(), + }, + { + provide: AuthorizationService, + useValue: createMock(), + }, + { + provide: SchoolYearService, + useValue: createMock(), + }, + { + provide: CourseDoService, + useValue: createMock(), + }, + { + provide: ConfigService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(ClassGroupUc); + groupService = module.get(GroupService); + userService = module.get(UserService); + roleService = module.get(RoleService); + classService = module.get(ClassService); + systemService = module.get(LegacySystemService); + schoolService = module.get(SchoolService); + authorizationService = module.get(AuthorizationService); + schoolYearService = module.get(SchoolYearService); + courseService = module.get(CourseDoService); + configService = module.get(ConfigService); + + await setupEntities(); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('findAllClasses', () => { + describe('when the user has no permission', () => { + const setup = () => { + const school: School = schoolFactory.build(); + const user: User = userFactory.buildWithId(); + const error = new ForbiddenException(); + + schoolService.getSchoolById.mockResolvedValue(school); + authorizationService.getUserWithPermissions.mockResolvedValue(user); + authorizationService.checkPermission.mockImplementation(() => { + throw error; + }); + authorizationService.hasAllPermissions.mockReturnValueOnce(false); + + return { + user, + error, + }; + }; + + it('should throw forbidden', async () => { + const { user, error } = setup(); + + const func = () => uc.findAllClasses(user.id, user.school.id); + + await expect(func).rejects.toThrow(error); + }); + }); + + describe('when accessing as a normal user', () => { + const setup = () => { + const school: School = schoolFactory.build({ permissions: { teacher: { STUDENT_LIST: true } } }); + const { studentUser } = UserAndAccountTestFactory.buildStudent(); + const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const teacherRole: RoleDto = roleDtoFactory.buildWithId({ + id: teacherUser.roles[0].id, + name: teacherUser.roles[0].name, + }); + const studentRole: RoleDto = roleDtoFactory.buildWithId({ + id: studentUser.roles[0].id, + name: studentUser.roles[0].name, + }); + const teacherUserDo: UserDO = userDoFactory.buildWithId({ + id: teacherUser.id, + lastName: teacherUser.lastName, + roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], + }); + const studentUserDo: UserDO = userDoFactory.buildWithId({ + id: studentUser.id, + lastName: studentUser.lastName, + roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], + }); + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); + const nextSchoolYear: SchoolYearEntity = schoolYearFactory.buildWithId({ + startDate: schoolYear.endDate, + }); + const clazz: Class = classFactory.build({ + name: 'A', + teacherIds: [teacherUser.id], + source: 'LDAP', + year: schoolYear.id, + }); + const successorClass: Class = classFactory.build({ + name: 'NEW', + teacherIds: [teacherUser.id], + year: nextSchoolYear.id, + }); + const classWithoutSchoolYear = classFactory.build({ + name: 'NoYear', + teacherIds: [teacherUser.id], + year: undefined, + }); + + const system: SystemDto = new SystemDto({ + id: new ObjectId().toHexString(), + displayName: 'External System', + type: 'oauth2', + }); + const group: Group = groupFactory.build({ + name: 'B', + users: [{ userId: teacherUser.id, roleId: teacherUser.roles[0].id }], + externalSource: undefined, + }); + const groupWithSystem: Group = groupFactory.build({ + name: 'C', + externalSource: { externalId: 'externalId', systemId: system.id }, + users: [ + { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, + { userId: studentUser.id, roleId: studentUser.roles[0].id }, + ], + }); + const synchronizedCourse: Course = courseFactory.build({ syncedWithGroup: group.id }); + + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); + authorizationService.hasAllPermissions.mockReturnValueOnce(false); + classService.findAllByUserId.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); + groupService.findGroups.mockResolvedValueOnce(new Page([group, groupWithSystem], 2)); + classService.findClassesForSchool.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); + groupService.findGroups.mockResolvedValueOnce(new Page([group, groupWithSystem], 2)); + systemService.findById.mockResolvedValue(system); + userService.findById.mockImplementation((userId: string): Promise => { + if (userId === teacherUser.id) { + return Promise.resolve(teacherUserDo); + } + + if (userId === studentUser.id) { + return Promise.resolve(studentUserDo); + } + + throw new Error(); + }); + userService.findByIdOrNull.mockImplementation((userId: string): Promise => { + if (userId === teacherUser.id) { + return Promise.resolve(teacherUserDo); + } + + if (userId === studentUser.id) { + return Promise.resolve(studentUserDo); + } + + throw new Error(); + }); + roleService.findById.mockImplementation((roleId: string): Promise => { + if (roleId === teacherUser.roles[0].id) { + return Promise.resolve(teacherRole); + } + + if (roleId === studentUser.roles[0].id) { + return Promise.resolve(studentRole); + } + + throw new Error(); + }); + schoolYearService.findById.mockResolvedValueOnce(schoolYear); + schoolYearService.findById.mockResolvedValueOnce(nextSchoolYear); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + configService.get.mockReturnValueOnce(true); + courseService.findBySyncedGroup.mockResolvedValueOnce([synchronizedCourse]); + courseService.findBySyncedGroup.mockResolvedValueOnce([]); + + return { + teacherUser, + school, + clazz, + successorClass, + classWithoutSchoolYear, + group, + groupWithSystem, + system, + schoolYear, + nextSchoolYear, + synchronizedCourse, + }; + }; + + it('should check the required permissions', async () => { + const { teacherUser, school } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id, SchoolYearQueryType.CURRENT_YEAR); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith<[User, School, AuthorizationContext]>( + teacherUser, + school, + { + action: Action.read, + requiredPermissions: [Permission.CLASS_VIEW, Permission.GROUP_VIEW], + } + ); + }); + + it('should check the access to the full list', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + + expect(authorizationService.hasAllPermissions).toHaveBeenCalledWith<[User, string[]]>(teacherUser, [ + Permission.CLASS_FULL_ADMIN, + Permission.GROUP_FULL_ADMIN, + ]); + }); + + describe('when accessing form course as a teacher', () => { + it('should call findClassesForSchool method from classService', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined, ClassRequestContext.COURSE); + + expect(classService.findClassesForSchool).toHaveBeenCalled(); + }); + }); + + describe('when accessing form class overview as a teacher', () => { + it('should call findAllByUserId method from classService', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined, ClassRequestContext.CLASS_OVERVIEW); + + expect(classService.findAllByUserId).toHaveBeenCalled(); + }); + }); + + describe('when no pagination is given', () => { + it('should return all classes sorted by name', async () => { + const { + teacherUser, + clazz, + successorClass, + classWithoutSchoolYear, + group, + groupWithSystem, + system, + schoolYear, + nextSchoolYear, + synchronizedCourse, + } = setup(); + + const result: Page = await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined); + + expect(result).toEqual>({ + data: [ + { + id: clazz.id, + name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, + externalSourceName: clazz.source, + teacherNames: [], + schoolYear: schoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: successorClass.id, + name: successorClass.gradeLevel + ? `${successorClass.gradeLevel}${successorClass.name}` + : successorClass.name, + type: ClassRootType.CLASS, + externalSourceName: successorClass.source, + teacherNames: [], + schoolYear: nextSchoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: classWithoutSchoolYear.id, + name: classWithoutSchoolYear.gradeLevel + ? `${classWithoutSchoolYear.gradeLevel}${classWithoutSchoolYear.name}` + : classWithoutSchoolYear.name, + type: ClassRootType.CLASS, + externalSourceName: classWithoutSchoolYear.source, + teacherNames: [], + isUpgradable: false, + studentCount: 2, + }, + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [{ id: synchronizedCourse.id, name: synchronizedCourse.name }], + }, + { + id: groupWithSystem.id, + name: groupWithSystem.name, + type: ClassRootType.GROUP, + externalSourceName: system.displayName, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + ], + total: 5, + }); + }); + + it('should call group service with userId and no pagination', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + + expect(groupService.findGroups).toHaveBeenCalledWith<[GroupFilter]>({ userId: teacherUser.id }); + }); + }); + + describe('when sorting by external source name in descending order', () => { + it('should return all classes sorted by external source name in descending order', async () => { + const { + teacherUser, + clazz, + classWithoutSchoolYear, + group, + groupWithSystem, + system, + schoolYear, + synchronizedCourse, + } = setup(); + + const result: Page = await uc.findAllClasses( + teacherUser.id, + teacherUser.school.id, + SchoolYearQueryType.CURRENT_YEAR, + undefined, + undefined, + 'externalSourceName', + SortOrder.desc + ); + + expect(result).toEqual>({ + data: [ + { + id: classWithoutSchoolYear.id, + name: classWithoutSchoolYear.gradeLevel + ? `${classWithoutSchoolYear.gradeLevel}${classWithoutSchoolYear.name}` + : classWithoutSchoolYear.name, + type: ClassRootType.CLASS, + externalSourceName: classWithoutSchoolYear.source, + teacherNames: [], + isUpgradable: false, + studentCount: 2, + }, + { + id: clazz.id, + name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, + externalSourceName: clazz.source, + teacherNames: [], + schoolYear: schoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: groupWithSystem.id, + name: groupWithSystem.name, + type: ClassRootType.GROUP, + externalSourceName: system.displayName, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [{ id: synchronizedCourse.id, name: synchronizedCourse.name }], + }, + ], + total: 4, + }); + }); + }); + + describe('when using pagination', () => { + it('should return the selected page', async () => { + const { teacherUser, group, synchronizedCourse } = setup(); + + const result: Page = await uc.findAllClasses( + teacherUser.id, + teacherUser.school.id, + SchoolYearQueryType.CURRENT_YEAR, + undefined, + { skip: 2, limit: 1 }, + 'name', + SortOrder.asc + ); + + expect(result).toEqual>({ + data: [ + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [{ id: synchronizedCourse.id, name: synchronizedCourse.name }], + }, + ], + total: 4, + }); + }); + }); + + describe('when querying for classes from next school year', () => { + it('should only return classes from next school year', async () => { + const { teacherUser, successorClass, nextSchoolYear } = setup(); + + const result: Page = await uc.findAllClasses( + teacherUser.id, + teacherUser.school.id, + SchoolYearQueryType.NEXT_YEAR + ); + + expect(result).toEqual>({ + data: [ + { + id: successorClass.id, + name: successorClass.gradeLevel + ? `${successorClass.gradeLevel}${successorClass.name}` + : successorClass.name, + externalSourceName: successorClass.source, + type: ClassRootType.CLASS, + teacherNames: [], + schoolYear: nextSchoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + ], + total: 1, + }); + }); + }); + + describe('when querying for archived classes', () => { + it('should only return classes from previous school years', async () => { + const { teacherUser } = setup(); + + const result: Page = await uc.findAllClasses( + teacherUser.id, + teacherUser.school.id, + SchoolYearQueryType.PREVIOUS_YEARS + ); + + expect(result).toEqual>({ + data: [], + total: 0, + }); + }); + }); + + describe('when querying for not existing type', () => { + it('should throw', async () => { + const { teacherUser } = setup(); + + const func = async () => + uc.findAllClasses(teacherUser.id, teacherUser.school.id, 'notAType' as SchoolYearQueryType); + + await expect(func).rejects.toThrow(UnknownQueryTypeLoggableException); + }); + }); + }); + + describe('when accessing as a user with elevated permission', () => { + const setup = (generateClasses = false) => { + const school: School = schoolFactory.build(); + const { studentUser } = UserAndAccountTestFactory.buildStudent(); + const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { adminUser } = UserAndAccountTestFactory.buildAdmin(); + const teacherRole: RoleDto = roleDtoFactory.buildWithId({ + id: teacherUser.roles[0].id, + name: teacherUser.roles[0].name, + }); + const studentRole: RoleDto = roleDtoFactory.buildWithId({ + id: studentUser.roles[0].id, + name: studentUser.roles[0].name, + }); + const adminUserDo: UserDO = userDoFactory.buildWithId({ + id: adminUser.id, + lastName: adminUser.lastName, + roles: [{ id: adminUser.roles[0].id, name: adminUser.roles[0].name }], + }); + const teacherUserDo: UserDO = userDoFactory.buildWithId({ + id: teacherUser.id, + lastName: teacherUser.lastName, + roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], + }); + const studentUserDo: UserDO = userDoFactory.buildWithId({ + id: studentUser.id, + lastName: studentUser.lastName, + roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], + }); + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); + let clazzes: Class[] = []; + if (generateClasses) { + clazzes = classFactory.buildList(11, { + name: 'A', + teacherIds: [teacherUser.id], + source: 'LDAP', + year: schoolYear.id, + }); + } + const clazz: Class = classFactory.build({ + name: 'A', + teacherIds: [teacherUser.id], + source: 'LDAP', + year: schoolYear.id, + }); + const system: SystemDto = new SystemDto({ + id: new ObjectId().toHexString(), + displayName: 'External System', + type: 'oauth2', + }); + const group: Group = groupFactory.build({ + name: 'B', + users: [{ userId: teacherUser.id, roleId: teacherUser.roles[0].id }], + externalSource: undefined, + }); + const groupWithSystem: Group = groupFactory.build({ + name: 'C', + externalSource: { externalId: 'externalId', systemId: system.id }, + users: [ + { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, + { userId: studentUser.id, roleId: studentUser.roles[0].id }, + ], + }); + + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(adminUser); + authorizationService.hasAllPermissions.mockReturnValueOnce(true); + classService.findClassesForSchool.mockResolvedValueOnce([...clazzes, clazz]); + groupService.findGroups.mockResolvedValueOnce(new Page([group, groupWithSystem], 2)); + systemService.findById.mockResolvedValue(system); + + userService.findById.mockImplementation((userId: string): Promise => { + if (userId === teacherUser.id) { + return Promise.resolve(teacherUserDo); + } + + if (userId === studentUser.id) { + return Promise.resolve(studentUserDo); + } + + if (userId === adminUser.id) { + return Promise.resolve(adminUserDo); + } + + throw new Error(); + }); + userService.findByIdOrNull.mockImplementation((userId: string): Promise => { + if (userId === teacherUser.id) { + return Promise.resolve(teacherUserDo); + } + + if (userId === studentUser.id) { + return Promise.resolve(studentUserDo); + } + + if (userId === adminUser.id) { + return Promise.resolve(adminUserDo); + } + + throw new Error(); + }); + roleService.findById.mockImplementation((roleId: string): Promise => { + if (roleId === teacherUser.roles[0].id) { + return Promise.resolve(teacherRole); + } + + if (roleId === studentUser.roles[0].id) { + return Promise.resolve(studentRole); + } + + throw new Error(); + }); + schoolYearService.findById.mockResolvedValue(schoolYear); + configService.get.mockReturnValueOnce(true); + courseService.findBySyncedGroup.mockResolvedValueOnce([]); + courseService.findBySyncedGroup.mockResolvedValueOnce([]); + + return { + adminUser, + teacherUser, + school, + clazz, + group, + groupWithSystem, + system, + schoolYear, + }; + }; + + it('should check the required permissions', async () => { + const { adminUser, school } = setup(); + + await uc.findAllClasses(adminUser.id, adminUser.school.id); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith<[User, School, AuthorizationContext]>( + adminUser, + school, + { + action: Action.read, + requiredPermissions: [Permission.CLASS_VIEW, Permission.GROUP_VIEW], + } + ); + }); + + it('should check the access to the full list', async () => { + const { adminUser } = setup(); + + await uc.findAllClasses(adminUser.id, adminUser.school.id); + + expect(authorizationService.hasAllPermissions).toHaveBeenCalledWith<[User, string[]]>(adminUser, [ + Permission.CLASS_FULL_ADMIN, + Permission.GROUP_FULL_ADMIN, + ]); + }); + + describe('when no pagination is given', () => { + it('should return all classes sorted by name', async () => { + const { adminUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); + + const result: Page = await uc.findAllClasses(adminUser.id, adminUser.school.id); + + expect(result).toEqual>({ + data: [ + { + id: clazz.id, + name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, + externalSourceName: clazz.source, + teacherNames: [], + schoolYear: schoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + { + id: groupWithSystem.id, + name: groupWithSystem.name, + type: ClassRootType.GROUP, + externalSourceName: system.displayName, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + ], + total: 3, + }); + }); + + it('should call group service with schoolId and no pagination', async () => { + const { teacherUser } = setup(); + + await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + + expect(groupService.findGroups).toHaveBeenCalledWith<[GroupFilter]>({ schoolId: teacherUser.school.id }); + }); + }); + + describe('when sorting by external source name in descending order', () => { + it('should return all classes sorted by external source name in descending order', async () => { + const { adminUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); + + const result: Page = await uc.findAllClasses( + adminUser.id, + adminUser.school.id, + undefined, + undefined, + undefined, + 'externalSourceName', + SortOrder.desc + ); + + expect(result).toEqual>({ + data: [ + { + id: clazz.id, + name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, + externalSourceName: clazz.source, + teacherNames: [], + schoolYear: schoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: groupWithSystem.id, + name: groupWithSystem.name, + type: ClassRootType.GROUP, + externalSourceName: system.displayName, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + ], + total: 3, + }); + }); + }); + + describe('when using pagination', () => { + it('should return the selected page', async () => { + const { adminUser, group } = setup(); + + const result: Page = await uc.findAllClasses( + adminUser.id, + adminUser.school.id, + undefined, + undefined, + { skip: 1, limit: 1 }, + 'name', + SortOrder.asc + ); + + expect(result).toEqual>({ + data: [ + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + ], + total: 3, + }); + }); + + it('should return classes with expected limit', async () => { + const { adminUser } = setup(true); + + const result: Page = await uc.findAllClasses( + adminUser.id, + adminUser.school.id, + undefined, + undefined, + { skip: 0, limit: 5 } + ); + + expect(result.data.length).toEqual(5); + }); + + it('should return all classes without limit', async () => { + const { adminUser } = setup(true); + + const result: Page = await uc.findAllClasses( + adminUser.id, + adminUser.school.id, + undefined, + undefined, + { skip: 0, limit: -1 } + ); + + expect(result.data.length).toEqual(14); + }); + }); + }); + + describe('when class has a user referenced which is not existing', () => { + const setup = () => { + const school: School = schoolFactory.build(); + const notFoundReferenceId = new ObjectId().toHexString(); + const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const teacherRole: RoleDto = roleDtoFactory.buildWithId({ + id: teacherUser.roles[0].id, + name: teacherUser.roles[0].name, + }); + + const teacherUserDo: UserDO = userDoFactory.buildWithId({ + id: teacherUser.id, + lastName: teacherUser.lastName, + roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], + }); + + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); + const clazz: Class = classFactory.build({ + name: 'A', + teacherIds: [teacherUser.id, notFoundReferenceId], + source: 'LDAP', + year: schoolYear.id, + }); + const system: SystemDto = new SystemDto({ + id: new ObjectId().toHexString(), + displayName: 'External System', + type: 'oauth2', + }); + const group: Group = groupFactory.build({ + name: 'B', + users: [ + { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, + { userId: notFoundReferenceId, roleId: teacherUser.roles[0].id }, + ], + externalSource: undefined, + }); + + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); + authorizationService.hasAllPermissions.mockReturnValueOnce(false); + classService.findAllByUserId.mockResolvedValueOnce([clazz]); + groupService.findGroups.mockResolvedValueOnce(new Page([group], 1)); + systemService.findById.mockResolvedValue(system); + + userService.findById.mockImplementation((userId: string): Promise => { + if (userId === teacherUser.id) { + return Promise.resolve(teacherUserDo); + } + + throw new Error(); + }); + userService.findByIdOrNull.mockImplementation((userId: string): Promise => { + if (userId === teacherUser.id) { + return Promise.resolve(teacherUserDo); + } + + if (userId === notFoundReferenceId) { + return Promise.resolve(null); + } + + throw new Error(); + }); + roleService.findById.mockImplementation((roleId: string): Promise => { + if (roleId === teacherUser.roles[0].id) { + return Promise.resolve(teacherRole); + } + + throw new Error(); + }); + schoolYearService.findById.mockResolvedValue(schoolYear); + configService.get.mockReturnValueOnce(true); + courseService.findBySyncedGroup.mockResolvedValueOnce([]); + + return { + teacherUser, + clazz, + group, + notFoundReferenceId, + schoolYear, + }; + }; + + it('should return class without missing user', async () => { + const { teacherUser, clazz, group, schoolYear } = setup(); + + const result = await uc.findAllClasses(teacherUser.id, teacherUser.school.id); + + expect(result).toEqual>({ + data: [ + { + id: clazz.id, + name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, + externalSourceName: clazz.source, + teacherNames: [], + schoolYear: schoolYear.name, + isUpgradable: false, + studentCount: 2, + }, + { + id: group.id, + name: group.name, + type: ClassRootType.GROUP, + teacherNames: [], + studentCount: 0, + synchronizedCourses: [], + }, + ], + total: 2, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/group/uc/class-group.uc.ts b/apps/server/src/modules/group/uc/class-group.uc.ts new file mode 100644 index 00000000000..5f7843c3885 --- /dev/null +++ b/apps/server/src/modules/group/uc/class-group.uc.ts @@ -0,0 +1,302 @@ +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { Class } from '@modules/class/domain'; +import { Course } from '@modules/learnroom/domain'; +import { CourseDoService } from '@modules/learnroom/service/course-do.service'; +import { SchoolYearService } from '@modules/legacy-school'; +import { ProvisioningConfig } from '@modules/provisioning'; +import { School, SchoolService } from '@modules/school/domain'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { SortHelper } from '@shared/common'; +import { Page, UserDO } from '@shared/domain/domainobject'; +import { SchoolYearEntity, User } from '@shared/domain/entity'; +import { Pagination, Permission, SortOrder } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; +import { LegacySystemService, SystemDto } from '@src/modules/system'; +import { ClassRequestContext, SchoolYearQueryType } from '../controller/dto/interface'; +import { Group, GroupFilter } from '../domain'; +import { UnknownQueryTypeLoggableException } from '../loggable'; +import { GroupService } from '../service'; +import { ClassInfoDto, ResolvedGroupUser } from './dto'; +import { GroupUcMapper } from './mapper/group-uc.mapper'; + +@Injectable() +export class ClassGroupUc { + constructor( + private readonly groupService: GroupService, + private readonly classService: ClassService, + private readonly systemService: LegacySystemService, + private readonly schoolService: SchoolService, + private readonly authorizationService: AuthorizationService, + private readonly schoolYearService: SchoolYearService, + private readonly courseService: CourseDoService, + private readonly configService: ConfigService + ) {} + + public async findAllClasses( + userId: EntityId, + schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType, + calledFrom?: ClassRequestContext, + pagination?: Pagination, + sortBy: keyof ClassInfoDto = 'name', + sortOrder: SortOrder = SortOrder.asc + ): Promise> { + const school: School = await this.schoolService.getSchoolById(schoolId); + + const user: User = await this.authorizationService.getUserWithPermissions(userId); + this.authorizationService.checkPermission( + user, + school, + AuthorizationContextBuilder.read([Permission.CLASS_VIEW, Permission.GROUP_VIEW]) + ); + + const canSeeFullList: boolean = this.authorizationService.hasAllPermissions(user, [ + Permission.CLASS_FULL_ADMIN, + Permission.GROUP_FULL_ADMIN, + ]); + + const calledFromCourse: boolean = + calledFrom === ClassRequestContext.COURSE && school.getPermissions()?.teacher?.STUDENT_LIST === true; + + let combinedClassInfo: ClassInfoDto[]; + if (canSeeFullList || calledFromCourse) { + combinedClassInfo = await this.findCombinedClassListForSchool(schoolId, schoolYearQueryType); + } else { + combinedClassInfo = await this.findCombinedClassListForUser(userId, schoolYearQueryType); + } + + combinedClassInfo.sort((a: ClassInfoDto, b: ClassInfoDto): number => + SortHelper.genericSortFunction(a[sortBy], b[sortBy], sortOrder) + ); + + const pageContent: ClassInfoDto[] = this.applyPagination(combinedClassInfo, pagination?.skip, pagination?.limit); + + const page: Page = new Page(pageContent, combinedClassInfo.length); + + return page; + } + + private async findCombinedClassListForSchool( + schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + let classInfosFromGroups: ClassInfoDto[] = []; + + const classInfosFromClasses = await this.findClassesForSchool(schoolId, schoolYearQueryType); + + if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { + classInfosFromGroups = await this.findGroupsForSchool(schoolId); + } + + const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; + + return combinedClassInfo; + } + + private async findCombinedClassListForUser( + userId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + let classInfosFromGroups: ClassInfoDto[] = []; + + const classInfosFromClasses = await this.findClassesForUser(userId, schoolYearQueryType); + + if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { + classInfosFromGroups = await this.findGroupsForUser(userId); + } + + const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; + + return combinedClassInfo; + } + + private async findClassesForSchool( + schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + const classes: Class[] = await this.classService.findClassesForSchool(schoolId); + + const classInfosFromClasses: ClassInfoDto[] = await this.getClassInfosFromClasses(classes, schoolYearQueryType); + + return classInfosFromClasses; + } + + private async findClassesForUser( + userId: EntityId, + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + const classes: Class[] = await this.classService.findAllByUserId(userId); + + const classInfosFromClasses: ClassInfoDto[] = await this.getClassInfosFromClasses(classes, schoolYearQueryType); + + return classInfosFromClasses; + } + + private async getClassInfosFromClasses( + classes: Class[], + schoolYearQueryType?: SchoolYearQueryType + ): Promise { + const currentYear: SchoolYearEntity = await this.schoolYearService.getCurrentSchoolYear(); + + const classesWithSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] = await this.addSchoolYearsToClasses( + classes + ); + + const filteredClassesForSchoolYear = classesWithSchoolYear.filter((classWithSchoolYear) => + this.isClassOfQueryType(currentYear, classWithSchoolYear.schoolYear, schoolYearQueryType) + ); + + const classInfosFromClasses: ClassInfoDto[] = this.mapClassInfosFromClasses(filteredClassesForSchoolYear); + + return classInfosFromClasses; + } + + private async addSchoolYearsToClasses(classes: Class[]): Promise<{ clazz: Class; schoolYear?: SchoolYearEntity }[]> { + const classesWithSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] = await Promise.all( + classes.map(async (clazz: Class) => { + let schoolYear: SchoolYearEntity | undefined; + if (clazz.year) { + schoolYear = await this.schoolYearService.findById(clazz.year); + } + + return { + clazz, + schoolYear, + }; + }) + ); + + return classesWithSchoolYear; + } + + private isClassOfQueryType( + currentYear: SchoolYearEntity, + schoolYear?: SchoolYearEntity, + schoolYearQueryType?: SchoolYearQueryType + ): boolean { + if (schoolYearQueryType === undefined) { + return true; + } + + if (schoolYear === undefined) { + return schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR; + } + + switch (schoolYearQueryType) { + case SchoolYearQueryType.CURRENT_YEAR: + return schoolYear.startDate === currentYear.startDate; + case SchoolYearQueryType.NEXT_YEAR: + return schoolYear.startDate > currentYear.startDate; + case SchoolYearQueryType.PREVIOUS_YEARS: + return schoolYear.startDate < currentYear.startDate; + default: + throw new UnknownQueryTypeLoggableException(schoolYearQueryType); + } + } + + private mapClassInfosFromClasses( + filteredClassesForSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] + ): ClassInfoDto[] { + const classInfosFromClasses: ClassInfoDto[] = filteredClassesForSchoolYear.map( + (classWithSchoolYear): ClassInfoDto => { + const teachers: UserDO[] = []; + + const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto( + classWithSchoolYear.clazz, + teachers, + classWithSchoolYear.schoolYear + ); + + return mapped; + } + ); + return classInfosFromClasses; + } + + private async findGroupsForSchool(schoolId: EntityId): Promise { + const filter: GroupFilter = { schoolId }; + + const groups: Page = await this.groupService.findGroups(filter); + + const classInfosFromGroups: ClassInfoDto[] = await this.getClassInfosFromGroups(groups.data); + + return classInfosFromGroups; + } + + private async findGroupsForUser(userId: EntityId): Promise { + const filter: GroupFilter = { userId }; + + const groups: Page = await this.groupService.findGroups(filter); + + const classInfosFromGroups: ClassInfoDto[] = await this.getClassInfosFromGroups(groups.data); + + return classInfosFromGroups; + } + + private async getClassInfosFromGroups(groups: Group[]): Promise { + const systemMap: Map = await this.findSystemNamesForGroups(groups); + + const classInfosFromGroups: ClassInfoDto[] = await Promise.all( + groups.map(async (group: Group): Promise => this.getClassInfoFromGroup(group, systemMap)) + ); + + return classInfosFromGroups; + } + + private async getClassInfoFromGroup(group: Group, systemMap: Map): Promise { + let system: SystemDto | undefined; + if (group.externalSource) { + system = systemMap.get(group.externalSource.systemId); + } + + const resolvedUsers: ResolvedGroupUser[] = []; + + let synchronizedCourses: Course[] = []; + if (this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) { + synchronizedCourses = await this.courseService.findBySyncedGroup(group); + } + + const mapped: ClassInfoDto = GroupUcMapper.mapGroupToClassInfoDto( + group, + resolvedUsers, + synchronizedCourses, + system + ); + + return mapped; + } + + private async findSystemNamesForGroups(groups: Group[]): Promise> { + const systemIds: EntityId[] = groups + .map((group: Group): string | undefined => group.externalSource?.systemId) + .filter((systemId: string | undefined): systemId is EntityId => systemId !== undefined); + + const uniqueSystemIds: EntityId[] = Array.from(new Set(systemIds)); + + const systems: Map = new Map(); + + await Promise.all( + uniqueSystemIds.map(async (systemId: string): Promise => { + const system: SystemDto = await this.systemService.findById(systemId); + + systems.set(systemId, system); + }) + ); + + return systems; + } + + private applyPagination(combinedClassInfo: ClassInfoDto[], skip = 0, limit?: number): ClassInfoDto[] { + let page: ClassInfoDto[]; + + if (limit === -1) { + page = combinedClassInfo.slice(skip); + } else { + page = combinedClassInfo.slice(skip, limit ? skip + limit : combinedClassInfo.length); + } + + return page; + } +} 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 6c4285122eb..cded865cb00 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -1,44 +1,31 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { Action, AuthorizationContext, AuthorizationService } from '@modules/authorization'; -import { ClassService } from '@modules/class'; -import { Class } from '@modules/class/domain'; -import { classFactory } from '@modules/class/domain/testing/factory/class.factory'; -import { Course } from '@modules/learnroom/domain'; -import { CourseDoService } from '@modules/learnroom/service/course-do.service'; -import { courseFactory } from '@modules/learnroom/testing'; -import { SchoolYearService } from '@modules/legacy-school'; +import { AuthorizationService } from '@modules/authorization'; import { ProvisioningConfig } from '@modules/provisioning'; import { RoleService } from '@modules/role'; import { RoleDto } from '@modules/role/service/dto/role.dto'; import { School, SchoolService } from '@modules/school/domain'; import { schoolFactory } from '@modules/school/testing'; -import { LegacySystemService, SystemDto } from '@modules/system'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { Page, UserDO } from '@shared/domain/domainobject'; -import { Role, SchoolYearEntity, User } from '@shared/domain/entity'; -import { IFindQuery, Permission, SortOrder } from '@shared/domain/interface'; +import { Role, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; import { groupFactory, roleDtoFactory, roleFactory, - schoolYearFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { ClassRequestContext, SchoolYearQueryType } from '../controller/dto/interface'; import { Group, GroupTypes } from '../domain'; -import { UnknownQueryTypeLoggableException } from '../loggable'; import { GroupService } from '../service'; -import { ClassInfoDto, ResolvedGroupDto } from './dto'; -import { ClassRootType } from './dto/class-root-type'; +import { ResolvedGroupDto } from './dto'; import { GroupUc } from './group.uc'; describe('GroupUc', () => { @@ -46,16 +33,11 @@ describe('GroupUc', () => { let uc: GroupUc; let groupService: DeepMocked; - let classService: DeepMocked; - let systemService: DeepMocked; let userService: DeepMocked; let roleService: DeepMocked; let schoolService: DeepMocked; let authorizationService: DeepMocked; - let schoolYearService: DeepMocked; - let courseService: DeepMocked; let configService: DeepMocked>; - // eslint-disable-next-line @typescript-eslint/no-unused-vars let logger: DeepMocked; beforeAll(async () => { @@ -66,14 +48,6 @@ describe('GroupUc', () => { provide: GroupService, useValue: createMock(), }, - { - provide: ClassService, - useValue: createMock(), - }, - { - provide: LegacySystemService, - useValue: createMock(), - }, { provide: UserService, useValue: createMock(), @@ -90,14 +64,6 @@ describe('GroupUc', () => { provide: AuthorizationService, useValue: createMock(), }, - { - provide: SchoolYearService, - useValue: createMock(), - }, - { - provide: CourseDoService, - useValue: createMock(), - }, { provide: ConfigService, useValue: createMock(), @@ -111,14 +77,10 @@ describe('GroupUc', () => { uc = module.get(GroupUc); groupService = module.get(GroupService); - classService = module.get(ClassService); - systemService = module.get(LegacySystemService); userService = module.get(UserService); roleService = module.get(RoleService); schoolService = module.get(SchoolService); authorizationService = module.get(AuthorizationService); - schoolYearService = module.get(SchoolYearService); - courseService = module.get(CourseDoService); configService = module.get(ConfigService); logger = module.get(Logger); @@ -133,894 +95,6 @@ describe('GroupUc', () => { jest.resetAllMocks(); }); - describe('findAllClasses', () => { - describe('when the user has no permission', () => { - const setup = () => { - const school: School = schoolFactory.build(); - const user: User = userFactory.buildWithId(); - const error = new ForbiddenException(); - - schoolService.getSchoolById.mockResolvedValue(school); - authorizationService.getUserWithPermissions.mockResolvedValue(user); - authorizationService.checkPermission.mockImplementation(() => { - throw error; - }); - authorizationService.hasAllPermissions.mockReturnValueOnce(false); - - return { - user, - error, - }; - }; - - it('should throw forbidden', async () => { - const { user, error } = setup(); - - const func = () => uc.findAllClasses(user.id, user.school.id); - - await expect(func).rejects.toThrow(error); - }); - }); - - describe('when accessing as a normal user', () => { - const setup = () => { - const school: School = schoolFactory.build({ permissions: { teacher: { STUDENT_LIST: true } } }); - const { studentUser } = UserAndAccountTestFactory.buildStudent(); - const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const teacherRole: RoleDto = roleDtoFactory.buildWithId({ - id: teacherUser.roles[0].id, - name: teacherUser.roles[0].name, - }); - const studentRole: RoleDto = roleDtoFactory.buildWithId({ - id: studentUser.roles[0].id, - name: studentUser.roles[0].name, - }); - const teacherUserDo: UserDO = userDoFactory.buildWithId({ - id: teacherUser.id, - lastName: teacherUser.lastName, - roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], - }); - const studentUserDo: UserDO = userDoFactory.buildWithId({ - id: studentUser.id, - lastName: studentUser.lastName, - roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], - }); - const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); - const nextSchoolYear: SchoolYearEntity = schoolYearFactory.buildWithId({ - startDate: schoolYear.endDate, - }); - const clazz: Class = classFactory.build({ - name: 'A', - teacherIds: [teacherUser.id], - source: 'LDAP', - year: schoolYear.id, - }); - const successorClass: Class = classFactory.build({ - name: 'NEW', - teacherIds: [teacherUser.id], - year: nextSchoolYear.id, - }); - const classWithoutSchoolYear = classFactory.build({ - name: 'NoYear', - teacherIds: [teacherUser.id], - year: undefined, - }); - - const system: SystemDto = new SystemDto({ - id: new ObjectId().toHexString(), - displayName: 'External System', - type: 'oauth2', - }); - const group: Group = groupFactory.build({ - name: 'B', - users: [{ userId: teacherUser.id, roleId: teacherUser.roles[0].id }], - externalSource: undefined, - }); - const groupWithSystem: Group = groupFactory.build({ - name: 'C', - externalSource: { externalId: 'externalId', systemId: system.id }, - users: [ - { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, - { userId: studentUser.id, roleId: studentUser.roles[0].id }, - ], - }); - const synchronizedCourse: Course = courseFactory.build({ syncedWithGroup: group.id }); - - schoolService.getSchoolById.mockResolvedValueOnce(school); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); - authorizationService.hasAllPermissions.mockReturnValueOnce(false); - classService.findAllByUserId.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); - groupService.findGroupsByUserAndGroupTypes.mockResolvedValueOnce(new Page([group, groupWithSystem], 2)); - classService.findClassesForSchool.mockResolvedValueOnce([clazz, successorClass, classWithoutSchoolYear]); - groupService.findGroupsBySchoolIdAndGroupTypes.mockResolvedValueOnce( - new Page([group, groupWithSystem], 2) - ); - systemService.findById.mockResolvedValue(system); - userService.findById.mockImplementation((userId: string): Promise => { - if (userId === teacherUser.id) { - return Promise.resolve(teacherUserDo); - } - - if (userId === studentUser.id) { - return Promise.resolve(studentUserDo); - } - - throw new Error(); - }); - userService.findByIdOrNull.mockImplementation((userId: string): Promise => { - if (userId === teacherUser.id) { - return Promise.resolve(teacherUserDo); - } - - if (userId === studentUser.id) { - return Promise.resolve(studentUserDo); - } - - throw new Error(); - }); - roleService.findById.mockImplementation((roleId: string): Promise => { - if (roleId === teacherUser.roles[0].id) { - return Promise.resolve(teacherRole); - } - - if (roleId === studentUser.roles[0].id) { - return Promise.resolve(studentRole); - } - - throw new Error(); - }); - schoolYearService.findById.mockResolvedValueOnce(schoolYear); - schoolYearService.findById.mockResolvedValueOnce(nextSchoolYear); - schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); - configService.get.mockReturnValueOnce(true); - courseService.findBySyncedGroup.mockResolvedValueOnce([synchronizedCourse]); - courseService.findBySyncedGroup.mockResolvedValueOnce([]); - - return { - teacherUser, - school, - clazz, - successorClass, - classWithoutSchoolYear, - group, - groupWithSystem, - system, - schoolYear, - nextSchoolYear, - synchronizedCourse, - }; - }; - - it('should check the required permissions', async () => { - const { teacherUser, school } = setup(); - - await uc.findAllClasses(teacherUser.id, teacherUser.school.id, SchoolYearQueryType.CURRENT_YEAR); - - expect(authorizationService.checkPermission).toHaveBeenCalledWith<[User, School, AuthorizationContext]>( - teacherUser, - school, - { - action: Action.read, - requiredPermissions: [Permission.CLASS_VIEW, Permission.GROUP_VIEW], - } - ); - }); - - it('should check the access to the full list', async () => { - const { teacherUser } = setup(); - - await uc.findAllClasses(teacherUser.id, teacherUser.school.id); - - expect(authorizationService.hasAllPermissions).toHaveBeenCalledWith<[User, string[]]>(teacherUser, [ - Permission.CLASS_FULL_ADMIN, - Permission.GROUP_FULL_ADMIN, - ]); - }); - - describe('when accessing form course as a teacher', () => { - it('should call findClassesForSchool method from classService', async () => { - const { teacherUser } = setup(); - - await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined, ClassRequestContext.COURSE); - - expect(classService.findClassesForSchool).toHaveBeenCalled(); - }); - }); - - describe('when accessing form class overview as a teacher', () => { - it('should call findAllByUserId method from classService', async () => { - const { teacherUser } = setup(); - - await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined, ClassRequestContext.CLASS_OVERVIEW); - - expect(classService.findAllByUserId).toHaveBeenCalled(); - }); - }); - - describe('when no pagination is given', () => { - it('should return all classes sorted by name', async () => { - const { - teacherUser, - clazz, - successorClass, - classWithoutSchoolYear, - group, - groupWithSystem, - system, - schoolYear, - nextSchoolYear, - synchronizedCourse, - } = setup(); - - const result: Page = await uc.findAllClasses(teacherUser.id, teacherUser.school.id, undefined); - - expect(result).toEqual>({ - data: [ - { - id: clazz.id, - name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, - type: ClassRootType.CLASS, - externalSourceName: clazz.source, - teacherNames: [], - schoolYear: schoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - { - id: successorClass.id, - name: successorClass.gradeLevel - ? `${successorClass.gradeLevel}${successorClass.name}` - : successorClass.name, - type: ClassRootType.CLASS, - externalSourceName: successorClass.source, - teacherNames: [], - schoolYear: nextSchoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - { - id: classWithoutSchoolYear.id, - name: classWithoutSchoolYear.gradeLevel - ? `${classWithoutSchoolYear.gradeLevel}${classWithoutSchoolYear.name}` - : classWithoutSchoolYear.name, - type: ClassRootType.CLASS, - externalSourceName: classWithoutSchoolYear.source, - teacherNames: [], - isUpgradable: false, - studentCount: 2, - }, - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [{ id: synchronizedCourse.id, name: synchronizedCourse.name }], - }, - { - id: groupWithSystem.id, - name: groupWithSystem.name, - type: ClassRootType.GROUP, - externalSourceName: system.displayName, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - ], - 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[], IFindQuery]>( - expect.any(UserDO), - [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER], - { pagination: { skip: 0 } } - ); - }); - }); - - describe('when sorting by external source name in descending order', () => { - it('should return all classes sorted by external source name in descending order', async () => { - const { - teacherUser, - clazz, - classWithoutSchoolYear, - group, - groupWithSystem, - system, - schoolYear, - synchronizedCourse, - } = setup(); - - const result: Page = await uc.findAllClasses( - teacherUser.id, - teacherUser.school.id, - SchoolYearQueryType.CURRENT_YEAR, - undefined, - undefined, - undefined, - 'externalSourceName', - SortOrder.desc - ); - - expect(result).toEqual>({ - data: [ - { - id: classWithoutSchoolYear.id, - name: classWithoutSchoolYear.gradeLevel - ? `${classWithoutSchoolYear.gradeLevel}${classWithoutSchoolYear.name}` - : classWithoutSchoolYear.name, - type: ClassRootType.CLASS, - externalSourceName: classWithoutSchoolYear.source, - teacherNames: [], - isUpgradable: false, - studentCount: 2, - }, - { - id: clazz.id, - name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, - type: ClassRootType.CLASS, - externalSourceName: clazz.source, - teacherNames: [], - schoolYear: schoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - { - id: groupWithSystem.id, - name: groupWithSystem.name, - type: ClassRootType.GROUP, - externalSourceName: system.displayName, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [{ id: synchronizedCourse.id, name: synchronizedCourse.name }], - }, - ], - total: 4, - }); - }); - }); - - describe('when using pagination', () => { - it('should return the selected page', async () => { - const { teacherUser, group, synchronizedCourse } = setup(); - - const result: Page = await uc.findAllClasses( - teacherUser.id, - teacherUser.school.id, - SchoolYearQueryType.CURRENT_YEAR, - undefined, - 2, - 1, - 'name', - SortOrder.asc - ); - - expect(result).toEqual>({ - data: [ - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [{ id: synchronizedCourse.id, name: synchronizedCourse.name }], - }, - ], - total: 4, - }); - }); - }); - - describe('when querying for classes from next school year', () => { - it('should only return classes from next school year', async () => { - const { teacherUser, successorClass, nextSchoolYear } = setup(); - - const result: Page = await uc.findAllClasses( - teacherUser.id, - teacherUser.school.id, - SchoolYearQueryType.NEXT_YEAR - ); - - expect(result).toEqual>({ - data: [ - { - id: successorClass.id, - name: successorClass.gradeLevel - ? `${successorClass.gradeLevel}${successorClass.name}` - : successorClass.name, - externalSourceName: successorClass.source, - type: ClassRootType.CLASS, - teacherNames: [], - schoolYear: nextSchoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - ], - total: 1, - }); - }); - }); - - describe('when querying for archived classes', () => { - it('should only return classes from previous school years', async () => { - const { teacherUser } = setup(); - - const result: Page = await uc.findAllClasses( - teacherUser.id, - teacherUser.school.id, - SchoolYearQueryType.PREVIOUS_YEARS - ); - - expect(result).toEqual>({ - data: [], - total: 0, - }); - }); - }); - - describe('when querying for not existing type', () => { - it('should throw', async () => { - const { teacherUser } = setup(); - - const func = async () => - uc.findAllClasses(teacherUser.id, teacherUser.school.id, 'notAType' as SchoolYearQueryType); - - await expect(func).rejects.toThrow(UnknownQueryTypeLoggableException); - }); - }); - }); - - describe('when accessing as a user with elevated permission', () => { - const setup = (generateClasses = false) => { - const school: School = schoolFactory.build(); - const { studentUser } = UserAndAccountTestFactory.buildStudent(); - const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const { adminUser } = UserAndAccountTestFactory.buildAdmin(); - const teacherRole: RoleDto = roleDtoFactory.buildWithId({ - id: teacherUser.roles[0].id, - name: teacherUser.roles[0].name, - }); - const studentRole: RoleDto = roleDtoFactory.buildWithId({ - id: studentUser.roles[0].id, - name: studentUser.roles[0].name, - }); - const adminUserDo: UserDO = userDoFactory.buildWithId({ - id: adminUser.id, - lastName: adminUser.lastName, - roles: [{ id: adminUser.roles[0].id, name: adminUser.roles[0].name }], - }); - const teacherUserDo: UserDO = userDoFactory.buildWithId({ - id: teacherUser.id, - lastName: teacherUser.lastName, - roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], - }); - const studentUserDo: UserDO = userDoFactory.buildWithId({ - id: studentUser.id, - lastName: studentUser.lastName, - roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], - }); - const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); - let clazzes: Class[] = []; - if (generateClasses) { - clazzes = classFactory.buildList(11, { - name: 'A', - teacherIds: [teacherUser.id], - source: 'LDAP', - year: schoolYear.id, - }); - } - const clazz: Class = classFactory.build({ - name: 'A', - teacherIds: [teacherUser.id], - source: 'LDAP', - year: schoolYear.id, - }); - const system: SystemDto = new SystemDto({ - id: new ObjectId().toHexString(), - displayName: 'External System', - type: 'oauth2', - }); - const group: Group = groupFactory.build({ - name: 'B', - users: [{ userId: teacherUser.id, roleId: teacherUser.roles[0].id }], - externalSource: undefined, - }); - const groupWithSystem: Group = groupFactory.build({ - name: 'C', - externalSource: { externalId: 'externalId', systemId: system.id }, - users: [ - { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, - { userId: studentUser.id, roleId: studentUser.roles[0].id }, - ], - }); - - schoolService.getSchoolById.mockResolvedValueOnce(school); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(adminUser); - authorizationService.hasAllPermissions.mockReturnValueOnce(true); - classService.findClassesForSchool.mockResolvedValueOnce([...clazzes, clazz]); - groupService.findGroupsBySchoolIdAndGroupTypes.mockResolvedValueOnce( - new Page([group, groupWithSystem], 2) - ); - systemService.findById.mockResolvedValue(system); - - userService.findById.mockImplementation((userId: string): Promise => { - if (userId === teacherUser.id) { - return Promise.resolve(teacherUserDo); - } - - if (userId === studentUser.id) { - return Promise.resolve(studentUserDo); - } - - if (userId === adminUser.id) { - return Promise.resolve(adminUserDo); - } - - throw new Error(); - }); - userService.findByIdOrNull.mockImplementation((userId: string): Promise => { - if (userId === teacherUser.id) { - return Promise.resolve(teacherUserDo); - } - - if (userId === studentUser.id) { - return Promise.resolve(studentUserDo); - } - - if (userId === adminUser.id) { - return Promise.resolve(adminUserDo); - } - - throw new Error(); - }); - roleService.findById.mockImplementation((roleId: string): Promise => { - if (roleId === teacherUser.roles[0].id) { - return Promise.resolve(teacherRole); - } - - if (roleId === studentUser.roles[0].id) { - return Promise.resolve(studentRole); - } - - throw new Error(); - }); - schoolYearService.findById.mockResolvedValue(schoolYear); - configService.get.mockReturnValueOnce(true); - courseService.findBySyncedGroup.mockResolvedValueOnce([]); - courseService.findBySyncedGroup.mockResolvedValueOnce([]); - - return { - adminUser, - teacherUser, - school, - clazz, - group, - groupWithSystem, - system, - schoolYear, - }; - }; - - it('should check the required permissions', async () => { - const { adminUser, school } = setup(); - - await uc.findAllClasses(adminUser.id, adminUser.school.id); - - expect(authorizationService.checkPermission).toHaveBeenCalledWith<[User, School, AuthorizationContext]>( - adminUser, - school, - { - action: Action.read, - requiredPermissions: [Permission.CLASS_VIEW, Permission.GROUP_VIEW], - } - ); - }); - - it('should check the access to the full list', async () => { - const { adminUser } = setup(); - - await uc.findAllClasses(adminUser.id, adminUser.school.id); - - expect(authorizationService.hasAllPermissions).toHaveBeenCalledWith<[User, string[]]>(adminUser, [ - Permission.CLASS_FULL_ADMIN, - Permission.GROUP_FULL_ADMIN, - ]); - }); - - describe('when no pagination is given', () => { - it('should return all classes sorted by name', async () => { - const { adminUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); - - const result: Page = await uc.findAllClasses(adminUser.id, adminUser.school.id); - - expect(result).toEqual>({ - data: [ - { - id: clazz.id, - name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, - type: ClassRootType.CLASS, - externalSourceName: clazz.source, - teacherNames: [], - schoolYear: schoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - { - id: groupWithSystem.id, - name: groupWithSystem.name, - type: ClassRootType.GROUP, - externalSourceName: system.displayName, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - ], - total: 3, - }); - }); - - it('should call group service with allowed group types', async () => { - const { teacherUser, school } = setup(); - - await uc.findAllClasses(teacherUser.id, teacherUser.school.id); - - expect(groupService.findGroupsBySchoolIdAndGroupTypes).toHaveBeenCalledWith<[School, GroupTypes[]]>(school, [ - GroupTypes.CLASS, - GroupTypes.COURSE, - GroupTypes.OTHER, - ]); - }); - }); - - describe('when sorting by external source name in descending order', () => { - it('should return all classes sorted by external source name in descending order', async () => { - const { adminUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); - - const result: Page = await uc.findAllClasses( - adminUser.id, - adminUser.school.id, - undefined, - undefined, - undefined, - undefined, - 'externalSourceName', - SortOrder.desc - ); - - expect(result).toEqual>({ - data: [ - { - id: clazz.id, - name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, - type: ClassRootType.CLASS, - externalSourceName: clazz.source, - teacherNames: [], - schoolYear: schoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - { - id: groupWithSystem.id, - name: groupWithSystem.name, - type: ClassRootType.GROUP, - externalSourceName: system.displayName, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - ], - total: 3, - }); - }); - }); - - describe('when using pagination', () => { - it('should return the selected page', async () => { - const { adminUser, group } = setup(); - - const result: Page = await uc.findAllClasses( - adminUser.id, - adminUser.school.id, - undefined, - undefined, - 1, - 1, - 'name', - SortOrder.asc - ); - - expect(result).toEqual>({ - data: [ - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - ], - total: 3, - }); - }); - - it('should return classes with expected limit', async () => { - const { adminUser } = setup(true); - - const result: Page = await uc.findAllClasses( - adminUser.id, - adminUser.school.id, - undefined, - undefined, - 0, - 5 - ); - - expect(result.data.length).toEqual(5); - }); - - it('should return all classes without limit', async () => { - const { adminUser } = setup(true); - - const result: Page = await uc.findAllClasses( - adminUser.id, - adminUser.school.id, - undefined, - undefined, - 0, - -1 - ); - - expect(result.data.length).toEqual(14); - }); - }); - }); - - describe('when class has a user referenced which is not existing', () => { - const setup = () => { - const school: School = schoolFactory.build(); - const notFoundReferenceId = new ObjectId().toHexString(); - const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); - - const teacherRole: RoleDto = roleDtoFactory.buildWithId({ - id: teacherUser.roles[0].id, - name: teacherUser.roles[0].name, - }); - - const teacherUserDo: UserDO = userDoFactory.buildWithId({ - id: teacherUser.id, - lastName: teacherUser.lastName, - roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], - }); - - const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); - const clazz: Class = classFactory.build({ - name: 'A', - teacherIds: [teacherUser.id, notFoundReferenceId], - source: 'LDAP', - year: schoolYear.id, - }); - const system: SystemDto = new SystemDto({ - id: new ObjectId().toHexString(), - displayName: 'External System', - type: 'oauth2', - }); - const group: Group = groupFactory.build({ - name: 'B', - users: [ - { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, - { userId: notFoundReferenceId, roleId: teacherUser.roles[0].id }, - ], - externalSource: undefined, - }); - - schoolService.getSchoolById.mockResolvedValueOnce(school); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); - authorizationService.hasAllPermissions.mockReturnValueOnce(false); - classService.findAllByUserId.mockResolvedValueOnce([clazz]); - groupService.findGroupsByUserAndGroupTypes.mockResolvedValueOnce(new Page([group], 1)); - systemService.findById.mockResolvedValue(system); - - userService.findById.mockImplementation((userId: string): Promise => { - if (userId === teacherUser.id) { - return Promise.resolve(teacherUserDo); - } - - throw new Error(); - }); - userService.findByIdOrNull.mockImplementation((userId: string): Promise => { - if (userId === teacherUser.id) { - return Promise.resolve(teacherUserDo); - } - - if (userId === notFoundReferenceId) { - return Promise.resolve(null); - } - - throw new Error(); - }); - roleService.findById.mockImplementation((roleId: string): Promise => { - if (roleId === teacherUser.roles[0].id) { - return Promise.resolve(teacherRole); - } - - throw new Error(); - }); - schoolYearService.findById.mockResolvedValue(schoolYear); - configService.get.mockReturnValueOnce(true); - courseService.findBySyncedGroup.mockResolvedValueOnce([]); - - return { - teacherUser, - clazz, - group, - notFoundReferenceId, - schoolYear, - }; - }; - - it('should return class without missing user', async () => { - const { teacherUser, clazz, group, schoolYear } = setup(); - - const result = await uc.findAllClasses(teacherUser.id, teacherUser.school.id); - - expect(result).toEqual>({ - data: [ - { - id: clazz.id, - name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, - type: ClassRootType.CLASS, - externalSourceName: clazz.source, - teacherNames: [], - schoolYear: schoolYear.name, - isUpgradable: false, - studentCount: 2, - }, - { - id: group.id, - name: group.name, - type: ClassRootType.GROUP, - teacherNames: [], - studentCount: 0, - synchronizedCourses: [], - }, - ], - total: 2, - }); - }); - }); - }); - describe('getGroup', () => { describe('when the user has no permission', () => { const setup = () => { @@ -1161,6 +235,63 @@ describe('GroupUc', () => { }); }); }); + + describe('when user in group is not found', () => { + const setup = () => { + const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { studentUser } = UserAndAccountTestFactory.buildStudent(); + const group: Group = groupFactory.build({ + users: [ + { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, + { userId: studentUser.id, roleId: studentUser.roles[0].id }, + ], + }); + const teacherRole: RoleDto = roleDtoFactory.build({ + id: teacherUser.roles[0].id, + name: teacherUser.roles[0].name, + }); + const studentRole: RoleDto = roleDtoFactory.build({ + id: studentUser.roles[0].id, + name: studentUser.roles[0].name, + }); + const teacherUserDo: UserDO = userDoFactory.build({ + id: teacherUser.id, + firstName: teacherUser.firstName, + lastName: teacherUser.lastName, + email: teacherUser.email, + roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], + }); + const studentUserDo: UserDO = userDoFactory.build({ + id: studentUser.id, + firstName: teacherUser.firstName, + lastName: studentUser.lastName, + email: studentUser.email, + roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], + }); + + groupService.findById.mockResolvedValueOnce(group); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); + userService.findById.mockResolvedValueOnce(teacherUserDo); + userService.findByIdOrNull.mockResolvedValueOnce(teacherUserDo); + roleService.findById.mockResolvedValueOnce(teacherRole); + userService.findById.mockResolvedValueOnce(studentUserDo); + userService.findByIdOrNull.mockResolvedValueOnce(null); + roleService.findById.mockResolvedValueOnce(studentRole); + + return { + teacherId: teacherUser.id, + group, + }; + }; + + it('should log missing user', async () => { + const { teacherId, group } = setup(); + + await uc.getGroup(teacherId, group.id); + + expect(logger.warning).toHaveBeenCalled(); + }); + }); }); describe('getAllGroups', () => { @@ -1217,10 +348,8 @@ describe('GroupUc', () => { schoolService.getSchoolById.mockResolvedValue(school); authorizationService.getUserWithPermissions.mockResolvedValue(user); authorizationService.hasAllPermissions.mockReturnValueOnce(true); - groupService.findAvailableGroupsBySchoolId.mockResolvedValue(new Page([availableGroupInSchool], 1)); - groupService.findGroupsBySchoolIdAndGroupTypes.mockResolvedValue( - new Page([groupInSchool, availableGroupInSchool], 2) - ); + groupService.findAvailableGroups.mockResolvedValue(new Page([availableGroupInSchool], 1)); + groupService.findGroups.mockResolvedValue(new Page([groupInSchool, availableGroupInSchool], 2)); userService.findByIdOrNull.mockResolvedValue(userDto); roleService.findById.mockResolvedValue(userRole); @@ -1328,7 +457,7 @@ describe('GroupUc', () => { it('should return all available groups for course sync', async () => { const { user, availableGroupInSchool, school } = setup(); - const response = await uc.getAllGroups(user.id, school.id, undefined, true); + const response = await uc.getAllGroups(user.id, school.id, undefined, undefined, true); expect(response).toMatchObject({ data: [ @@ -1391,10 +520,8 @@ describe('GroupUc', () => { schoolService.getSchoolById.mockResolvedValue(school); authorizationService.getUserWithPermissions.mockResolvedValue(user); authorizationService.hasAllPermissions.mockReturnValue(false); - groupService.findAvailableGroupsByUser.mockResolvedValue(new Page([availableTeachersGroup], 1)); - groupService.findGroupsByUserAndGroupTypes.mockResolvedValue( - new Page([teachersGroup, availableTeachersGroup], 2) - ); + groupService.findAvailableGroups.mockResolvedValue(new Page([availableTeachersGroup], 1)); + groupService.findGroups.mockResolvedValue(new Page([teachersGroup, availableTeachersGroup], 2)); userService.findByIdOrNull.mockResolvedValue(userDto); roleService.findById.mockResolvedValue(userRole); @@ -1502,7 +629,7 @@ describe('GroupUc', () => { it('should return all available groups for course sync the teacher is part of', async () => { const { user, availableTeachersGroup, school } = setup(); - const response = await uc.getAllGroups(user.id, school.id, undefined, true); + const response = await uc.getAllGroups(user.id, school.id, undefined, undefined, true); expect(response).toMatchObject({ data: [ diff --git a/apps/server/src/modules/group/uc/group.uc.ts b/apps/server/src/modules/group/uc/group.uc.ts index 9b3dae9c272..714307c6a6d 100644 --- a/apps/server/src/modules/group/uc/group.uc.ts +++ b/apps/server/src/modules/group/uc/group.uc.ts @@ -1,304 +1,43 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { ClassService } from '@modules/class'; -import { Class } from '@modules/class/domain'; -import { Course } from '@modules/learnroom/domain'; -import { CourseDoService } from '@modules/learnroom/service/course-do.service'; -import { SchoolYearService } from '@modules/legacy-school'; import { ProvisioningConfig } from '@modules/provisioning'; -import { RoleService } from '@modules/role'; -import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { RoleDto, RoleService } from '@modules/role'; import { School, SchoolService } from '@modules/school/domain'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { SortHelper } from '@shared/common'; +import { ReferencedEntityNotFoundLoggable } from '@shared/common/loggable'; import { Page, UserDO } from '@shared/domain/domainobject'; -import { SchoolYearEntity, User } from '@shared/domain/entity'; -import { IFindQuery, Permission, SortOrder } from '@shared/domain/interface'; +import { User } from '@shared/domain/entity'; +import { IFindOptions, Permission, SortOrder } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; -import { LegacySystemService, SystemDto } from '@src/modules/system'; -import { ClassRequestContext, SchoolYearQueryType } from '../controller/dto/interface'; -import { Group, GroupTypes, GroupUser } from '../domain'; -import { UnknownQueryTypeLoggableException } from '../loggable'; +import { Group, GroupFilter, GroupUser } from '../domain'; import { GroupService } from '../service'; -import { ClassInfoDto, ResolvedGroupDto, ResolvedGroupUser } from './dto'; +import { ResolvedGroupDto, ResolvedGroupUser } from './dto'; import { GroupUcMapper } from './mapper/group-uc.mapper'; @Injectable() export class GroupUc { constructor( private readonly groupService: GroupService, - private readonly classService: ClassService, - private readonly systemService: LegacySystemService, private readonly userService: UserService, private readonly roleService: RoleService, private readonly schoolService: SchoolService, private readonly authorizationService: AuthorizationService, - private readonly schoolYearService: SchoolYearService, - private readonly courseService: CourseDoService, private readonly configService: ConfigService, private readonly logger: Logger ) {} - private ALLOWED_GROUP_TYPES: GroupTypes[] = [GroupTypes.CLASS, GroupTypes.COURSE, GroupTypes.OTHER]; - - public async findAllClasses( - userId: EntityId, - schoolId: EntityId, - schoolYearQueryType?: SchoolYearQueryType, - calledFrom?: ClassRequestContext, - skip = 0, - limit?: number, - sortBy: keyof ClassInfoDto = 'name', - sortOrder: SortOrder = SortOrder.asc - ): Promise> { - const school: School = await this.schoolService.getSchoolById(schoolId); - + public async getGroup(userId: EntityId, groupId: EntityId): Promise { + const group: Group = await this.groupService.findById(groupId); const user: User = await this.authorizationService.getUserWithPermissions(userId); - this.authorizationService.checkPermission( - user, - school, - AuthorizationContextBuilder.read([Permission.CLASS_VIEW, Permission.GROUP_VIEW]) - ); - - const canSeeFullList: boolean = this.authorizationService.hasAllPermissions(user, [ - Permission.CLASS_FULL_ADMIN, - Permission.GROUP_FULL_ADMIN, - ]); - - const calledFromCourse: boolean = - calledFrom === ClassRequestContext.COURSE && school.getPermissions()?.teacher?.STUDENT_LIST === true; - - let combinedClassInfo: ClassInfoDto[]; - if (canSeeFullList || calledFromCourse) { - combinedClassInfo = await this.findCombinedClassListForSchool(school, schoolYearQueryType); - } else { - combinedClassInfo = await this.findCombinedClassListForUser(userId, schoolYearQueryType); - } - - combinedClassInfo.sort((a: ClassInfoDto, b: ClassInfoDto): number => - SortHelper.genericSortFunction(a[sortBy], b[sortBy], sortOrder) - ); - - const pageContent: ClassInfoDto[] = this.applyPagination(combinedClassInfo, skip, limit); - - const page: Page = new Page(pageContent, combinedClassInfo.length); - - return page; - } - - private async findCombinedClassListForSchool( - school: School, - schoolYearQueryType?: SchoolYearQueryType - ): Promise { - let classInfosFromGroups: ClassInfoDto[] = []; - - const classInfosFromClasses = await this.findClassesForSchool(school.id, schoolYearQueryType); - - if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { - classInfosFromGroups = await this.findGroupsForSchool(school); - } - - const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; - - return combinedClassInfo; - } - - private async findCombinedClassListForUser( - userId: EntityId, - schoolYearQueryType?: SchoolYearQueryType - ): Promise { - let classInfosFromGroups: ClassInfoDto[] = []; - - const classInfosFromClasses = await this.findClassesForUser(userId, schoolYearQueryType); - - if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { - classInfosFromGroups = await this.findGroupsForUser(userId); - } - - const combinedClassInfo: ClassInfoDto[] = [...classInfosFromClasses, ...classInfosFromGroups]; - - return combinedClassInfo; - } - - private async findClassesForSchool( - schoolId: EntityId, - schoolYearQueryType?: SchoolYearQueryType - ): Promise { - const classes: Class[] = await this.classService.findClassesForSchool(schoolId); - - const classInfosFromClasses: ClassInfoDto[] = await this.getClassInfosFromClasses(classes, schoolYearQueryType); - - return classInfosFromClasses; - } - - private async findClassesForUser( - userId: EntityId, - schoolYearQueryType?: SchoolYearQueryType - ): Promise { - const classes: Class[] = await this.classService.findAllByUserId(userId); - - const classInfosFromClasses: ClassInfoDto[] = await this.getClassInfosFromClasses(classes, schoolYearQueryType); - - return classInfosFromClasses; - } - - private async getClassInfosFromClasses( - classes: Class[], - schoolYearQueryType?: SchoolYearQueryType - ): Promise { - const currentYear: SchoolYearEntity = await this.schoolYearService.getCurrentSchoolYear(); - - const classesWithSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] = await this.addSchoolYearsToClasses( - classes - ); - - const filteredClassesForSchoolYear = classesWithSchoolYear.filter((classWithSchoolYear) => - this.isClassOfQueryType(currentYear, classWithSchoolYear.schoolYear, schoolYearQueryType) - ); - - const classInfosFromClasses: ClassInfoDto[] = this.mapClassInfosFromClasses(filteredClassesForSchoolYear); - - return classInfosFromClasses; - } - - private async addSchoolYearsToClasses(classes: Class[]): Promise<{ clazz: Class; schoolYear?: SchoolYearEntity }[]> { - const classesWithSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] = await Promise.all( - classes.map(async (clazz: Class) => { - let schoolYear: SchoolYearEntity | undefined; - if (clazz.year) { - schoolYear = await this.schoolYearService.findById(clazz.year); - } - - return { - clazz, - schoolYear, - }; - }) - ); - - return classesWithSchoolYear; - } - - private isClassOfQueryType( - currentYear: SchoolYearEntity, - schoolYear?: SchoolYearEntity, - schoolYearQueryType?: SchoolYearQueryType - ): boolean { - if (schoolYearQueryType === undefined) { - return true; - } - - if (schoolYear === undefined) { - return schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR; - } - - switch (schoolYearQueryType) { - case SchoolYearQueryType.CURRENT_YEAR: - return schoolYear.startDate === currentYear.startDate; - case SchoolYearQueryType.NEXT_YEAR: - return schoolYear.startDate > currentYear.startDate; - case SchoolYearQueryType.PREVIOUS_YEARS: - return schoolYear.startDate < currentYear.startDate; - default: - throw new UnknownQueryTypeLoggableException(schoolYearQueryType); - } - } - - private mapClassInfosFromClasses( - filteredClassesForSchoolYear: { clazz: Class; schoolYear?: SchoolYearEntity }[] - ): ClassInfoDto[] { - const classInfosFromClasses: ClassInfoDto[] = filteredClassesForSchoolYear.map( - (classWithSchoolYear): ClassInfoDto => { - const teachers: UserDO[] = []; - - const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto( - classWithSchoolYear.clazz, - teachers, - classWithSchoolYear.schoolYear - ); - return mapped; - } - ); - return classInfosFromClasses; - } - - private async findGroupsForSchool(school: School): Promise { - const groups: Page = await this.groupService.findGroupsBySchoolIdAndGroupTypes( - school, - this.ALLOWED_GROUP_TYPES - ); - - const classInfosFromGroups: ClassInfoDto[] = await this.getClassInfosFromGroups(groups.data); - - return classInfosFromGroups; - } - - private async findGroupsForUser(userId: EntityId): Promise { - const user: UserDO = await this.userService.findById(userId); - - const groups: Page = await this.groupService.findGroupsByUserAndGroupTypes(user, this.ALLOWED_GROUP_TYPES, { - pagination: { skip: 0 }, - }); + this.authorizationService.checkPermission(user, group, AuthorizationContextBuilder.read([Permission.GROUP_VIEW])); - const classInfosFromGroups: ClassInfoDto[] = await this.getClassInfosFromGroups(groups.data); - - return classInfosFromGroups; - } - - private async getClassInfosFromGroups(groups: Group[]): Promise { - const systemMap: Map = await this.findSystemNamesForGroups(groups); - - const classInfosFromGroups: ClassInfoDto[] = await Promise.all( - groups.map(async (group: Group): Promise => this.getClassInfoFromGroup(group, systemMap)) - ); - - return classInfosFromGroups; - } - - private async getClassInfoFromGroup(group: Group, systemMap: Map): Promise { - let system: SystemDto | undefined; - if (group.externalSource) { - system = systemMap.get(group.externalSource.systemId); - } - - const resolvedUsers: ResolvedGroupUser[] = []; - - let synchronizedCourses: Course[] = []; - if (this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) { - synchronizedCourses = await this.courseService.findBySyncedGroup(group); - } - - const mapped: ClassInfoDto = GroupUcMapper.mapGroupToClassInfoDto( - group, - resolvedUsers, - synchronizedCourses, - system - ); - - return mapped; - } - - private async findSystemNamesForGroups(groups: Group[]): Promise> { - const systemIds: EntityId[] = groups - .map((group: Group): string | undefined => group.externalSource?.systemId) - .filter((systemId: string | undefined): systemId is EntityId => systemId !== undefined); - - const uniqueSystemIds: EntityId[] = Array.from(new Set(systemIds)); - - const systems: Map = new Map(); - - await Promise.all( - uniqueSystemIds.map(async (systemId: string): Promise => { - const system: SystemDto = await this.systemService.findById(systemId); - - systems.set(systemId, system); - }) - ); + const resolvedUsers: ResolvedGroupUser[] = await this.findUsersForGroup(group); + const resolvedGroup: ResolvedGroupDto = GroupUcMapper.mapToResolvedGroupDto(group, resolvedUsers); - return systems; + return resolvedGroup; } private async findUsersForGroup(group: Group): Promise { @@ -307,11 +46,11 @@ export class GroupUc { const user: UserDO | null = await this.userService.findByIdOrNull(groupUser.userId); let resolvedGroup: ResolvedGroupUser | null = null; - /* TODO add this log back later - this.logger.warning( + if (!user) { + this.logger.warning( new ReferencedEntityNotFoundLoggable(Group.name, group.id, UserDO.name, groupUser.userId) - ); */ - if (user) { + ); + } else { const role: RoleDto = await this.roleService.findById(groupUser.roleId); resolvedGroup = new ResolvedGroupUser({ @@ -332,42 +71,11 @@ export class GroupUc { return resolvedGroupUsers; } - private applyPagination(combinedClassInfo: ClassInfoDto[], skip: number, limit: number | undefined): ClassInfoDto[] { - let page: ClassInfoDto[]; - - if (limit === -1) { - page = combinedClassInfo.slice(skip); - } else { - page = combinedClassInfo.slice(skip, limit ? skip + limit : combinedClassInfo.length); - } - - return page; - } - - public async getGroup(userId: EntityId, groupId: EntityId): Promise { - const group: Group = await this.groupService.findById(groupId); - - await this.checkPermission(userId, group); - - const resolvedUsers: ResolvedGroupUser[] = await this.findUsersForGroup(group); - const resolvedGroup: ResolvedGroupDto = GroupUcMapper.mapToResolvedGroupDto(group, resolvedUsers); - - return resolvedGroup; - } - - private async checkPermission(userId: EntityId, group: Group): Promise { - const user: User = await this.authorizationService.getUserWithPermissions(userId); - return this.authorizationService.checkPermission( - user, - group, - AuthorizationContextBuilder.read([Permission.GROUP_VIEW]) - ); - } - public async getAllGroups( userId: EntityId, schoolId: EntityId, - query?: IFindQuery, + options: IFindOptions = { pagination: { skip: 0 } }, + nameQuery?: string, availableGroupsForCourseSync?: boolean ): Promise> { const school: School = await this.schoolService.getSchoolById(schoolId); @@ -377,13 +85,17 @@ export class GroupUc { const canSeeFullList: boolean = this.authorizationService.hasAllPermissions(user, [Permission.GROUP_FULL_ADMIN]); - let groups: Page; + const filter: GroupFilter = { nameQuery }; + options.order = { name: SortOrder.asc }; + if (canSeeFullList) { - groups = await this.getGroupsForSchool(school, query, availableGroupsForCourseSync); + filter.schoolId = schoolId; } else { - groups = await this.getGroupsForUser(userId, query, availableGroupsForCourseSync); + filter.userId = userId; } + const groups: Page = await this.getGroups(filter, options, availableGroupsForCourseSync); + const resolvedGroups: ResolvedGroupDto[] = await Promise.all( groups.data.map(async (group: Group) => { const resolvedUsers: ResolvedGroupUser[] = await this.findUsersForGroup(group); @@ -398,32 +110,16 @@ export class GroupUc { return page; } - private async getGroupsForSchool( - school: School, - query?: IFindQuery, - availableGroupsForCourseSync?: boolean - ): Promise> { - let foundGroups: Page; - if (availableGroupsForCourseSync && this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) { - foundGroups = await this.groupService.findAvailableGroupsBySchoolId(school, query); - } else { - foundGroups = await this.groupService.findGroupsBySchoolIdAndGroupTypes(school, undefined, query); - } - - return foundGroups; - } - - private async getGroupsForUser( - userId: EntityId, - query?: IFindQuery, + private async getGroups( + filter: GroupFilter, + options: IFindOptions, availableGroupsForCourseSync?: boolean ): Promise> { let foundGroups: Page; - const user: UserDO = await this.userService.findById(userId); if (availableGroupsForCourseSync && this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) { - foundGroups = await this.groupService.findAvailableGroupsByUser(user, query); + foundGroups = await this.groupService.findAvailableGroups(filter, options); } else { - foundGroups = await this.groupService.findGroupsByUserAndGroupTypes(user, undefined, query); + foundGroups = await this.groupService.findGroups(filter, options); } return foundGroups; diff --git a/apps/server/src/modules/group/uc/index.ts b/apps/server/src/modules/group/uc/index.ts index 3f268fdf74c..da20bd0257e 100644 --- a/apps/server/src/modules/group/uc/index.ts +++ b/apps/server/src/modules/group/uc/index.ts @@ -1 +1,2 @@ -export * from './group.uc'; +export { GroupUc } from './group.uc'; +export { ClassGroupUc } from './class-group.uc'; diff --git a/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.spec.ts b/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.spec.ts index 57f2c9387a4..746c27c5a3e 100644 --- a/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.spec.ts +++ b/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Group, GroupService, GroupTypes } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; +import { Page } from '@shared/domain/domainobject'; import { groupFactory, schoolSystemOptionsFactory } from '@shared/testing'; import { SchoolSystemOptions, SchulConneXProvisioningOptions } from '../domain'; import { SchulconnexProvisioningOptionsUpdateService } from './schulconnex-provisioning-options-update.service'; @@ -50,8 +51,9 @@ describe(SchulconnexProvisioningOptionsUpdateService.name, () => { groupProvisioningCoursesEnabled: true, }); const group: Group = groupFactory.build({ type: GroupTypes.CLASS }); + const page: Page = new Page([group], 1); - groupService.findGroupsBySchoolIdAndSystemIdAndGroupType.mockResolvedValueOnce([group]); + groupService.findGroups.mockResolvedValueOnce(page); return { schoolSystemOptions, @@ -70,11 +72,11 @@ describe(SchulconnexProvisioningOptionsUpdateService.name, () => { schoolSystemOptions.provisioningOptions ); - expect(groupService.findGroupsBySchoolIdAndSystemIdAndGroupType).toHaveBeenCalledWith( - schoolSystemOptions.schoolId, - schoolSystemOptions.systemId, - GroupTypes.CLASS - ); + expect(groupService.findGroups).toHaveBeenCalledWith({ + schoolId: schoolSystemOptions.schoolId, + systemId: schoolSystemOptions.systemId, + groupTypes: [GroupTypes.CLASS], + }); }); it('should delete all classes', async () => { @@ -107,8 +109,9 @@ describe(SchulconnexProvisioningOptionsUpdateService.name, () => { groupProvisioningCoursesEnabled: false, }); const group: Group = groupFactory.build({ type: GroupTypes.COURSE }); + const page: Page = new Page([group], 1); - groupService.findGroupsBySchoolIdAndSystemIdAndGroupType.mockResolvedValueOnce([group]); + groupService.findGroups.mockResolvedValueOnce(page); return { schoolSystemOptions, @@ -127,11 +130,11 @@ describe(SchulconnexProvisioningOptionsUpdateService.name, () => { schoolSystemOptions.provisioningOptions ); - expect(groupService.findGroupsBySchoolIdAndSystemIdAndGroupType).toHaveBeenCalledWith( - schoolSystemOptions.schoolId, - schoolSystemOptions.systemId, - GroupTypes.COURSE - ); + expect(groupService.findGroups).toHaveBeenCalledWith({ + schoolId: schoolSystemOptions.schoolId, + systemId: schoolSystemOptions.systemId, + groupTypes: [GroupTypes.COURSE], + }); }); it('should delete all courses', async () => { @@ -164,8 +167,9 @@ describe(SchulconnexProvisioningOptionsUpdateService.name, () => { groupProvisioningCoursesEnabled: true, }); const group: Group = groupFactory.build({ type: GroupTypes.OTHER }); + const page: Page = new Page([group], 1); - groupService.findGroupsBySchoolIdAndSystemIdAndGroupType.mockResolvedValueOnce([group]); + groupService.findGroups.mockResolvedValueOnce(page); return { schoolSystemOptions, @@ -184,11 +188,11 @@ describe(SchulconnexProvisioningOptionsUpdateService.name, () => { schoolSystemOptions.provisioningOptions ); - expect(groupService.findGroupsBySchoolIdAndSystemIdAndGroupType).toHaveBeenCalledWith( - schoolSystemOptions.schoolId, - schoolSystemOptions.systemId, - GroupTypes.OTHER - ); + expect(groupService.findGroups).toHaveBeenCalledWith({ + schoolId: schoolSystemOptions.schoolId, + systemId: schoolSystemOptions.systemId, + groupTypes: [GroupTypes.OTHER], + }); }); it('should delete all other groups', async () => { diff --git a/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.ts b/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.ts index 580e38baffe..c1015e5d89f 100644 --- a/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.ts +++ b/apps/server/src/modules/legacy-school/service/schulconnex-provisioning-options-update.service.ts @@ -1,5 +1,6 @@ -import { Group, GroupService, GroupTypes } from '@modules/group'; +import { Group, GroupFilter, GroupService, GroupTypes } from '@modules/group'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Page } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; import { SchulConneXProvisioningOptions } from '../domain'; import { ProvisioningOptionsUpdateHandler } from './provisioning-options-update-handler'; @@ -30,11 +31,10 @@ export class SchulconnexProvisioningOptionsUpdateService } private async deleteGroups(schoolId: EntityId, systemId: EntityId, groupType: GroupTypes): Promise { - const groups: Group[] = await this.groupService.findGroupsBySchoolIdAndSystemIdAndGroupType( - schoolId, - systemId, - groupType - ); + const filter: GroupFilter = { schoolId, systemId, groupTypes: [groupType] }; + + const page: Page = await this.groupService.findGroups(filter); + const groups: Group[] = page.data; await Promise.all( groups.map(async (group: Group): Promise => { diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.spec.ts index a027c40ef36..e8c1f9f3d37 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.spec.ts @@ -6,7 +6,7 @@ import { SchoolSystemOptionsService, SchulConneXProvisioningOptions, } from '@modules/legacy-school'; -import { RoleService, RoleDto } from '@modules/role'; +import { RoleDto, RoleService } from '@modules/role'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -652,15 +652,24 @@ describe(SchulconnexGroupProvisioningService.name, () => { const externalGroups: ExternalGroupDto[] = [firstExternalGroup, secondExternalGroup]; userService.findByExternalId.mockResolvedValue(user); - groupService.findGroupsByUserAndGroupTypes.mockResolvedValue(new Page(existingGroups, 2)); + groupService.findGroups.mockResolvedValue(new Page(existingGroups, 2)); return { externalGroups, systemId, externalUserId, + user, }; }; + it('should find groups', async () => { + const { externalGroups, systemId, externalUserId, user } = setup(); + + await service.removeExternalGroupsAndAffiliation(externalUserId, externalGroups, systemId); + + expect(groupService.findGroups).toHaveBeenCalledWith({ userId: user.id }); + }); + it('should not save the group', async () => { const { externalGroups, systemId, externalUserId } = setup(); @@ -709,7 +718,7 @@ describe(SchulconnexGroupProvisioningService.name, () => { const externalGroups: ExternalGroupDto[] = [firstExternalGroup]; userService.findByExternalId.mockResolvedValue(user); - groupService.findGroupsByUserAndGroupTypes.mockResolvedValue(new Page(existingGroups, 2)); + groupService.findGroups.mockResolvedValue(new Page(existingGroups, 2)); return { externalGroups, @@ -788,7 +797,7 @@ describe(SchulconnexGroupProvisioningService.name, () => { const externalGroups: ExternalGroupDto[] = [firstExternalGroup]; userService.findByExternalId.mockResolvedValueOnce(user); - groupService.findGroupsByUserAndGroupTypes.mockResolvedValueOnce(new Page(existingGroups, 2)); + groupService.findGroups.mockResolvedValueOnce(new Page(existingGroups, 2)); groupService.save.mockResolvedValueOnce(secondExistingGroup); return { diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.ts index e84ea0c72d5..a25c8c4c6c7 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.ts @@ -1,5 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { Group, GroupService, GroupTypes, GroupUser } from '@modules/group'; +import { Group, GroupFilter, GroupService, GroupTypes, GroupUser } from '@modules/group'; import { LegacySchoolService, SchoolSystemOptionsService, @@ -175,7 +175,8 @@ export class SchulconnexGroupProvisioningService { throw new NotFoundLoggableException(UserDO.name, { externalId: externalUserId }); } - const existingGroupsOfUser: Page = await this.groupService.findGroupsByUserAndGroupTypes(user); + const filter: GroupFilter = { userId: user.id }; + const existingGroupsOfUser: Page = await this.groupService.findGroups(filter); const groupsFromSystem: Group[] = existingGroupsOfUser.data.filter( (existingGroup: Group) => existingGroup.externalSource?.systemId === systemId diff --git a/apps/server/src/shared/domain/interface/find-options.ts b/apps/server/src/shared/domain/interface/find-options.ts index 28822a35b0d..e1c9e005e8b 100644 --- a/apps/server/src/shared/domain/interface/find-options.ts +++ b/apps/server/src/shared/domain/interface/find-options.ts @@ -15,9 +15,4 @@ export interface IFindOptions { order?: SortOrderMap; } -export interface IFindQuery { - pagination?: Pagination; - nameQuery?: string; -} - export type SortOrderNumberType = Partial>;