From 497048bac0639e6e7f09bd873b621dd225225a68 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Thu, 9 Jan 2025 17:40:55 +0100 Subject: [PATCH 1/9] implement feature check by board context --- .../uc/video-conference-create.uc.spec.ts | 61 +++++++++++++++++ .../uc/video-conference-create.uc.ts | 12 +++- .../uc/video-conference-end.uc.spec.ts | 57 ++++++++++++++++ .../uc/video-conference-end.uc.ts | 12 +++- .../uc/video-conference-info.uc.spec.ts | 66 +++++++++++++++++++ .../uc/video-conference-info.uc.ts | 12 +++- .../uc/video-conference-join.uc.spec.ts | 60 +++++++++++++++++ .../uc/video-conference-join.uc.ts | 12 +++- .../video-conference-api.module.ts | 3 +- 9 files changed, 286 insertions(+), 9 deletions(-) diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts index a2781a75a47..945c5ad4114 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts @@ -13,6 +13,7 @@ import { VideoConferenceOptions } from '../interface'; import { BBBService, VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef } from './dto'; import { VideoConferenceCreateUc } from './video-conference-create.uc'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceCreateUc', () => { let module: TestingModule; @@ -20,6 +21,7 @@ describe('VideoConferenceCreateUc', () => { let bbbService: DeepMocked; let userService: DeepMocked; let videoConferenceService: DeepMocked; + let boardContextApiHelperService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -37,6 +39,10 @@ describe('VideoConferenceCreateUc', () => { provide: VideoConferenceService, useValue: createMock(), }, + { + provide: BoardContextApiHelperService, + useValue: createMock(), + }, ], }).compile(); @@ -44,6 +50,7 @@ describe('VideoConferenceCreateUc', () => { bbbService = module.get(BBBService); userService = module.get(UserService); videoConferenceService = module.get(VideoConferenceService); + boardContextApiHelperService = module.get(BoardContextApiHelperService); }); afterAll(async () => { @@ -220,5 +227,59 @@ describe('VideoConferenceCreateUc', () => { expect(bbbService.create).not.toBeCalled(); }); }); + + describe('feature check', () => { + describe('when scope is a video conference element', () => { + const setup = (scopeName: VideoConferenceScope) => { + const user: UserDO = userDoFactory.buildWithId(); + + const scope: ScopeRef = { + scope: scopeName, + id: new ObjectId().toHexString(), + }; + + const options: VideoConferenceOptions = { + everyAttendeeJoinsMuted: true, + everybodyJoinsAsModerator: true, + moderatorMustApproveJoinRequests: true, + }; + + const scopeInfo: ScopeInfo = { + scopeId: scope.id, + scopeName, + title: 'title', + logoutUrl: 'logoutUrl', + }; + + bbbService.getMeetingInfo.mockRejectedValue(new Error('Meeting not found')); + userService.findById.mockResolvedValue(user); + videoConferenceService.getScopeInfo.mockResolvedValue(scopeInfo); + videoConferenceService.determineBbbRole.mockResolvedValue(BBBRole.MODERATOR); + + return { user, scope, options }; + }; + + it("should use the board context's schoolId", async () => { + const { user, scope, options } = setup(VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT); + + boardContextApiHelperService.getSchoolIdForBoardNode.mockResolvedValue('contextSchoolId'); + + await uc.createIfNotRunning(user.id!, scope, options); + + expect(boardContextApiHelperService.getSchoolIdForBoardNode).toBeCalledWith(scope.id); + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith('contextSchoolId'); + }); + + describe('when scope is not a video conference element', () => { + it("should use the user's schoolId", async () => { + const { user, scope, options } = setup(VideoConferenceScope.COURSE); + + await uc.createIfNotRunning(user.id!, scope, options); + + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith(user.schoolId); + }); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts index 6ff2b00d63f..6d6da369202 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts @@ -15,13 +15,16 @@ import { ErrorStatus } from '../error/error-status.enum'; import { VideoConferenceOptions } from '../interface'; import { VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef } from './dto'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; +import { VideoConferenceScope } from '@shared/domain/interface'; @Injectable() export class VideoConferenceCreateUc { constructor( private readonly bbbService: BBBService, private readonly userService: UserService, - private readonly videoConferenceService: VideoConferenceService + private readonly videoConferenceService: VideoConferenceService, + private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} async createIfNotRunning(currentUserId: EntityId, scope: ScopeRef, options: VideoConferenceOptions): Promise { @@ -47,7 +50,12 @@ export class VideoConferenceCreateUc { */ const user: UserDO = await this.userService.findById(currentUserId); - await this.verifyFeaturesEnabled(user.schoolId); + const schoolId = + scope.scope === VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT + ? await this.boardContextApiHelperService.getSchoolIdForBoardNode(scope.id) + : user.schoolId; + + await this.verifyFeaturesEnabled(schoolId); const scopeInfo: ScopeInfo = await this.videoConferenceService.getScopeInfo(currentUserId, scope.id, scope.scope); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts index 2c640f3980a..e04671472d4 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts @@ -12,6 +12,7 @@ import { ErrorStatus } from '../error/error-status.enum'; import { BBBService, VideoConferenceService } from '../service'; import { ScopeInfo, VideoConference, VideoConferenceState } from './dto'; import { VideoConferenceEndUc } from './video-conference-end.uc'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceEndUc', () => { let module: TestingModule; @@ -19,6 +20,7 @@ describe('VideoConferenceEndUc', () => { let bbbService: DeepMocked; let userService: DeepMocked; let videoConferenceService: DeepMocked; + let boardContextApiHelperService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -36,6 +38,10 @@ describe('VideoConferenceEndUc', () => { provide: VideoConferenceService, useValue: createMock(), }, + { + provide: BoardContextApiHelperService, + useValue: createMock(), + }, ], }).compile(); @@ -43,6 +49,7 @@ describe('VideoConferenceEndUc', () => { bbbService = module.get(BBBService); userService = module.get(UserService); videoConferenceService = module.get(VideoConferenceService); + boardContextApiHelperService = module.get(BoardContextApiHelperService); }); afterAll(async () => { @@ -166,5 +173,55 @@ describe('VideoConferenceEndUc', () => { expect(result.bbbResponse).toBe(bbbEndResponse); }); }); + + describe('feature check', () => { + describe('when scope is a video conference element', () => { + const setup = (scopeName: VideoConferenceScope) => { + const user: UserDO = userDoFactory.buildWithId(); + const scope = { scope: scopeName, id: new ObjectId().toHexString() }; + const scopeInfo: ScopeInfo = { + scopeId: scope.id, + scopeName: scopeName, + title: 'title', + logoutUrl: 'logoutUrl', + }; + + const bbbEndResponse: BBBResponse = { + response: { + returncode: BBBStatus.SUCCESS, + } as BBBBaseResponse, + }; + + userService.findById.mockResolvedValue(user); + videoConferenceService.throwOnFeaturesDisabled.mockResolvedValue(); + videoConferenceService.getScopeInfo.mockResolvedValue(scopeInfo); + bbbService.end.mockResolvedValue(bbbEndResponse); + videoConferenceService.determineBbbRole.mockResolvedValue(BBBRole.MODERATOR); + + return { user, scope }; + }; + + it("should use the board context's schoolId", async () => { + const { user, scope } = setup(VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT); + + boardContextApiHelperService.getSchoolIdForBoardNode.mockResolvedValue('contextSchoolId'); + + await uc.end(user.id!, scope); + + expect(boardContextApiHelperService.getSchoolIdForBoardNode).toBeCalledWith(scope.id); + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith('contextSchoolId'); + }); + + describe('when scope is not a video conference element', () => { + it("should use the user's schoolId", async () => { + const { user, scope } = setup(VideoConferenceScope.COURSE); + + await uc.end(user.id!, scope); + + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith(user.schoolId); + }); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts index 063e8382936..d74309208a8 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts @@ -7,13 +7,16 @@ import { BBBBaseMeetingConfig, BBBBaseResponse, BBBResponse, BBBRole, BBBService import { PermissionMapping } from '../mapper/video-conference.mapper'; import { VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef, VideoConference, VideoConferenceState } from './dto'; +import { VideoConferenceScope } from '@shared/domain/interface'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class VideoConferenceEndUc { constructor( private readonly bbbService: BBBService, private readonly userService: UserService, - private readonly videoConferenceService: VideoConferenceService + private readonly videoConferenceService: VideoConferenceService, + private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} async end(currentUserId: EntityId, scope: ScopeRef): Promise> { @@ -26,7 +29,12 @@ export class VideoConferenceEndUc { const user: UserDO = await this.userService.findById(currentUserId); const userId: string = user.id as string; - await this.videoConferenceService.throwOnFeaturesDisabled(user.schoolId); + const schoolId = + scope.scope === VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT + ? await this.boardContextApiHelperService.getSchoolIdForBoardNode(scope.id) + : user.schoolId; + + await this.videoConferenceService.throwOnFeaturesDisabled(schoolId); const scopeInfo: ScopeInfo = await this.videoConferenceService.getScopeInfo(userId, scope.id, scope.scope); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts index fd7a4e2ceb8..a5345f033f6 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts @@ -14,6 +14,7 @@ import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interf import { BBBService, VideoConferenceService } from '../service'; import { ScopeInfo, VideoConferenceInfo, VideoConferenceState } from './dto'; import { VideoConferenceInfoUc } from './video-conference-info.uc'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceInfoUc', () => { let module: TestingModule; @@ -21,6 +22,7 @@ describe('VideoConferenceInfoUc', () => { let bbbService: DeepMocked; let userService: DeepMocked; let videoConferenceService: DeepMocked; + let boardContextApiHelperService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -38,6 +40,10 @@ describe('VideoConferenceInfoUc', () => { provide: VideoConferenceService, useValue: createMock(), }, + { + provide: BoardContextApiHelperService, + useValue: createMock(), + }, ], }).compile(); @@ -45,6 +51,7 @@ describe('VideoConferenceInfoUc', () => { bbbService = module.get(BBBService); userService = module.get(UserService); videoConferenceService = module.get(VideoConferenceService); + boardContextApiHelperService = module.get(BoardContextApiHelperService); }); afterAll(async () => { @@ -373,5 +380,64 @@ describe('VideoConferenceInfoUc', () => { }); }); }); + + describe('feature check', () => { + describe('when scope is a video conference element', () => { + const setup = (scopeName: VideoConferenceScope) => { + const user: UserDO = userDoFactory.buildWithId(); + const currentUserId: string = user.id as string; + const scope = { scope: scopeName, id: new ObjectId().toHexString() }; + const scopeInfo: ScopeInfo = { + scopeId: scope.id, + scopeName: scopeName, + title: 'title', + logoutUrl: 'logoutUrl', + }; + const videoConferenceDO: VideoConferenceDO = videoConferenceDOFactory.buildWithId({ + options: { + everyAttendeeJoinsMuted: true, + everybodyJoinsAsModerator: true, + moderatorMustApproveJoinRequests: true, + }, + }); + + const bbbMeetingInfoResponse: BBBResponse = createBbbMeetingInfoSuccessResponse( + scope.id + ); + + userService.findById.mockResolvedValue(user); + videoConferenceService.throwOnFeaturesDisabled.mockResolvedValue(); + videoConferenceService.getScopeInfo.mockResolvedValue(scopeInfo); + videoConferenceService.findVideoConferenceByScopeIdAndScope.mockResolvedValue(videoConferenceDO); + bbbService.getMeetingInfo.mockResolvedValue(bbbMeetingInfoResponse); + videoConferenceService.hasExpertRole.mockResolvedValue(true); + videoConferenceService.canGuestJoin.mockReturnValue(true); + videoConferenceService.determineBbbRole.mockResolvedValue(BBBRole.VIEWER); + + return { user, scope }; + }; + + it("should use the board context's schoolId", async () => { + const { user, scope } = setup(VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT); + + boardContextApiHelperService.getSchoolIdForBoardNode.mockResolvedValue('contextSchoolId'); + + await uc.getMeetingInfo(user.id!, scope); + + expect(boardContextApiHelperService.getSchoolIdForBoardNode).toBeCalledWith(scope.id); + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith('contextSchoolId'); + }); + + describe('when scope is not a video conference element', () => { + it("should use the user's schoolId", async () => { + const { user, scope } = setup(VideoConferenceScope.COURSE); + + await uc.getMeetingInfo(user.id!, scope); + + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith(user.schoolId); + }); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts index 1aaebb99858..198941cef67 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts @@ -8,13 +8,16 @@ import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interf import { PermissionMapping } from '../mapper/video-conference.mapper'; import { VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef, VideoConferenceInfo, VideoConferenceState } from './dto'; +import { VideoConferenceScope } from '@shared/domain/interface'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class VideoConferenceInfoUc { constructor( private readonly bbbService: BBBService, private readonly userService: UserService, - private readonly videoConferenceService: VideoConferenceService + private readonly videoConferenceService: VideoConferenceService, + private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} async getMeetingInfo(currentUserId: EntityId, scope: ScopeRef): Promise { @@ -26,7 +29,12 @@ export class VideoConferenceInfoUc { */ const user: UserDO = await this.userService.findById(currentUserId); - await this.videoConferenceService.throwOnFeaturesDisabled(user.schoolId); + const schoolId = + scope.scope === VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT + ? await this.boardContextApiHelperService.getSchoolIdForBoardNode(scope.id) + : user.schoolId; + + await this.videoConferenceService.throwOnFeaturesDisabled(schoolId); const scopeInfo: ScopeInfo = await this.videoConferenceService.getScopeInfo(currentUserId, scope.id, scope.scope); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts index 655532d0cf2..e0e610b280e 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts @@ -14,6 +14,7 @@ import { VideoConferenceOptions } from '../interface'; import { BBBService, VideoConferenceService } from '../service'; import { VideoConferenceJoin, VideoConferenceState } from './dto'; import { VideoConferenceJoinUc } from './video-conference-join.uc'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceJoinUc', () => { let module: TestingModule; @@ -21,6 +22,7 @@ describe('VideoConferenceJoinUc', () => { let bbbService: DeepMocked; let userService: DeepMocked; let videoConferenceService: DeepMocked; + let boardContextApiHelperService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -38,6 +40,10 @@ describe('VideoConferenceJoinUc', () => { provide: VideoConferenceService, useValue: createMock(), }, + { + provide: BoardContextApiHelperService, + useValue: createMock(), + }, ], }).compile(); @@ -45,6 +51,7 @@ describe('VideoConferenceJoinUc', () => { bbbService = module.get(BBBService); userService = module.get(UserService); videoConferenceService = module.get(VideoConferenceService); + boardContextApiHelperService = module.get(BoardContextApiHelperService); }); afterAll(async () => { @@ -330,5 +337,58 @@ describe('VideoConferenceJoinUc', () => { }); }); }); + + describe('feature check', () => { + describe('when scope is a video conference element', () => { + const setup = (scopeName: VideoConferenceScope) => { + const user: UserDO = userDoFactory.buildWithId(); + const scope = { scope: scopeName, id: new ObjectId().toHexString() }; + const options: VideoConferenceOptions = { + everyAttendeeJoinsMuted: true, + everybodyJoinsAsModerator: true, + moderatorMustApproveJoinRequests: true, + }; + const videoConference: VideoConferenceDO = videoConferenceDOFactory.build({ options }); + + const bbbJoinResponse: BBBResponse = { + response: { + url: 'url', + }, + } as BBBResponse; + + userService.findById.mockResolvedValue(user); + videoConferenceService.getUserRoleAndGuestStatusByUserIdForBbb.mockResolvedValue({ + role: BBBRole.VIEWER, + isGuest: false, + }); + videoConferenceService.sanitizeString.mockReturnValue(`${user.firstName} ${user.lastName}`); + bbbService.join.mockResolvedValue(bbbJoinResponse.response.url); + videoConferenceService.findVideoConferenceByScopeIdAndScope.mockResolvedValue(videoConference); + + return { user, scope }; + }; + + it("should use the board context's schoolId", async () => { + const { user, scope } = setup(VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT); + + boardContextApiHelperService.getSchoolIdForBoardNode.mockResolvedValue('contextSchoolId'); + + await uc.join(user.id!, scope); + + expect(boardContextApiHelperService.getSchoolIdForBoardNode).toBeCalledWith(scope.id); + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith('contextSchoolId'); + }); + + describe('when scope is not a video conference element', () => { + it("should use the user's schoolId", async () => { + const { user, scope } = setup(VideoConferenceScope.COURSE); + + await uc.join(user.id!, scope); + + expect(videoConferenceService.throwOnFeaturesDisabled).toBeCalledWith(user.schoolId); + }); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts index f93030ba35d..8c5d8c489a3 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts @@ -7,19 +7,27 @@ import { BBBJoinConfigBuilder, BBBRole, BBBService } from '../bbb'; import { PermissionMapping } from '../mapper/video-conference.mapper'; import { VideoConferenceService } from '../service'; import { ScopeRef, VideoConferenceJoin, VideoConferenceState } from './dto'; +import { VideoConferenceScope } from '@shared/domain/interface'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class VideoConferenceJoinUc { constructor( private readonly bbbService: BBBService, private readonly userService: UserService, - private readonly videoConferenceService: VideoConferenceService + private readonly videoConferenceService: VideoConferenceService, + private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} async join(currentUserId: EntityId, scope: ScopeRef): Promise { const user: UserDO = await this.userService.findById(currentUserId); - await this.videoConferenceService.throwOnFeaturesDisabled(user.schoolId); + const schoolId = + scope.scope === VideoConferenceScope.VIDEO_CONFERENCE_ELEMENT + ? await this.boardContextApiHelperService.getSchoolIdForBoardNode(scope.id) + : user.schoolId; + + await this.videoConferenceService.throwOnFeaturesDisabled(schoolId); const { role, isGuest } = await this.videoConferenceService.getUserRoleAndGuestStatusByUserIdForBbb( currentUserId, diff --git a/apps/server/src/modules/video-conference/video-conference-api.module.ts b/apps/server/src/modules/video-conference/video-conference-api.module.ts index a94128120ae..1666991a447 100644 --- a/apps/server/src/modules/video-conference/video-conference-api.module.ts +++ b/apps/server/src/modules/video-conference/video-conference-api.module.ts @@ -1,4 +1,5 @@ import { AuthorizationModule } from '@modules/authorization'; +import { BoardContextApiHelperModule } from '@modules/board-context'; import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { VideoConferenceController } from './controller'; @@ -6,7 +7,7 @@ import { VideoConferenceCreateUc, VideoConferenceEndUc, VideoConferenceInfoUc, V import { VideoConferenceModule } from './video-conference.module'; @Module({ - imports: [VideoConferenceModule, UserModule, AuthorizationModule], + imports: [VideoConferenceModule, UserModule, AuthorizationModule, BoardContextApiHelperModule], controllers: [VideoConferenceController], providers: [VideoConferenceCreateUc, VideoConferenceJoinUc, VideoConferenceEndUc, VideoConferenceInfoUc], }) From 3a9c0c1ff8acbe0529664bc1f5f9d14a8e80110f Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Sat, 11 Jan 2025 21:37:35 +0100 Subject: [PATCH 2/9] implement board context features service --- .../board-context-api-helper.module.ts | 3 +- .../board-context-api-helper.service.spec.ts | 159 +++++++++++++++++- .../board-context-api-helper.service.ts | 74 +++++++- .../board/domain/types/board-feature.enum.ts | 3 + .../src/modules/board/domain/types/index.ts | 1 + 5 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 apps/server/src/modules/board/domain/types/board-feature.enum.ts diff --git a/apps/server/src/modules/board-context/board-context-api-helper.module.ts b/apps/server/src/modules/board-context/board-context-api-helper.module.ts index b43ed70e121..5dc166bd0f8 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.module.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.module.ts @@ -3,9 +3,10 @@ import { RoomModule } from '../room'; import { BoardContextApiHelperService } from './board-context-api-helper.service'; import { BoardModule } from '../board/board.module'; import { LearnroomModule } from '../learnroom'; +import { LegacySchoolModule } from '../legacy-school'; @Module({ - imports: [BoardModule, LearnroomModule, RoomModule], + imports: [BoardModule, LearnroomModule, RoomModule, LegacySchoolModule], providers: [BoardContextApiHelperService], exports: [BoardContextApiHelperService], }) diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts index 3332bf269c0..ef1b6260639 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts @@ -2,10 +2,15 @@ import { createMock } from '@golevelup/ts-jest'; import { AnyBoardNode, BoardExternalReferenceType, BoardNodeService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { RoomService } from '@modules/room'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; +import { CourseFeatures } from '@shared/domain/entity'; import { courseFactory, schoolEntityFactory, setupEntities } from '@shared/testing'; +import { BoardFeature } from '../board/domain'; import { cardFactory, columnBoardFactory, columnFactory } from '../board/testing'; +import { LegacySchoolService } from '../legacy-school'; import { roomFactory } from '../room/testing'; +import { VideoConferenceConfig } from '../video-conference'; import { BoardContextApiHelperService } from './board-context-api-helper.service'; describe('BoardContextApiHelperService', () => { @@ -14,6 +19,8 @@ describe('BoardContextApiHelperService', () => { let courseService: jest.Mocked; let roomService: jest.Mocked; let boardNodeService: jest.Mocked; + let legacySchoolService: jest.Mocked; + let configService: jest.Mocked>; beforeEach(async () => { await setupEntities(); @@ -32,6 +39,14 @@ describe('BoardContextApiHelperService', () => { provide: BoardNodeService, useValue: createMock(), }, + { + provide: LegacySchoolService, + useValue: createMock(), + }, + { + provide: ConfigService, + useValue: createMock(), + }, ], }).compile(); @@ -39,6 +54,8 @@ describe('BoardContextApiHelperService', () => { courseService = module.get(CourseService); roomService = module.get(RoomService); boardNodeService = module.get(BoardNodeService); + legacySchoolService = module.get(LegacySchoolService); + configService = module.get(ConfigService); }); afterAll(async () => { @@ -53,16 +70,14 @@ describe('BoardContextApiHelperService', () => { it('should return schoolId for course context', async () => { const school = schoolEntityFactory.build(); const course = courseFactory.build({ school }); - const cardNode = cardFactory.build(); - const columnNode = columnFactory.build(); - columnNode.addChild(cardNode); + const card = cardFactory.build(); + const column = columnFactory.build({ children: [card] }); const columnBoard = columnBoardFactory.build({ context: { type: BoardExternalReferenceType.Course, id: 'course.id' }, }); - columnBoard.addChild(columnNode); + columnBoard.addChild(column); - boardNodeService.findById.mockResolvedValueOnce(cardNode); - boardNodeService.findRoot.mockResolvedValueOnce(columnBoard); + boardNodeService.findById.mockResolvedValueOnce(card); boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard); courseService.findById.mockResolvedValueOnce(course); @@ -87,4 +102,136 @@ describe('BoardContextApiHelperService', () => { expect(result).toBe(room.schoolId); }); }); + + describe('getFeaturesForBoardNode', () => { + describe('when context is course', () => { + const setup = () => { + const course = courseFactory.build(); + const column = columnFactory.build(); + const columnBoard = columnBoardFactory.build({ + context: { type: BoardExternalReferenceType.Course, id: 'course.id' }, + children: [column], + }); + + courseService.findById.mockResolvedValueOnce(course); + boardNodeService.findById.mockResolvedValueOnce(column); + boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard); + + return { boardNode: column, course }; + }; + + describe('when video conference is enabled for course', () => { + it('should return video conference feature', async () => { + const { boardNode, course } = setup(); + + course.features = [CourseFeatures.VIDEOCONFERENCE]; + legacySchoolService.hasFeature.mockResolvedValueOnce(false); + configService.get.mockReturnValueOnce(false); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + }); + }); + + describe('when video conference is enabled for school', () => { + it('should return video conference feature', async () => { + const { boardNode, course } = setup(); + + course.features = []; + legacySchoolService.hasFeature.mockResolvedValueOnce(true); + configService.get.mockReturnValueOnce(false); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + }); + }); + + describe('when video conference is enabled for config', () => { + it('should return video conference feature', async () => { + const { boardNode, course } = setup(); + + course.features = []; + legacySchoolService.hasFeature.mockResolvedValueOnce(false); + configService.get.mockReturnValueOnce(true); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + }); + }); + + describe('when video conference is disabled entirely', () => { + it('should not return feature', async () => { + const { boardNode } = setup(); + + const course = courseFactory.build(); + courseService.findById.mockResolvedValueOnce(course); + legacySchoolService.hasFeature.mockResolvedValueOnce(false); + configService.get.mockReturnValueOnce(false); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([]); + }); + }); + }); + + describe('when context is room', () => { + const setup = () => { + const room = roomFactory.build(); + const column = columnFactory.build(); + const columnBoard = columnBoardFactory.build({ + context: { type: BoardExternalReferenceType.Room, id: 'room.id' }, + children: [column], + }); + + roomService.getSingleRoom.mockResolvedValueOnce(room); + boardNodeService.findById.mockResolvedValueOnce(column); + boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard); + + return { boardNode: column, room }; + }; + + describe('when video conference is enabled for school', () => { + it('should return video conference feature', async () => { + const { boardNode } = setup(); + + legacySchoolService.hasFeature.mockResolvedValueOnce(true); + configService.get.mockReturnValueOnce(false); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + }); + }); + + describe('when video conference is enabled for config', () => { + it('should return video conference feature', async () => { + const { boardNode } = setup(); + + legacySchoolService.hasFeature.mockResolvedValueOnce(false); + configService.get.mockReturnValueOnce(true); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + }); + }); + + describe('when video conference is disabled entirely', () => { + it('should not return feature', async () => { + const { boardNode } = setup(); + + legacySchoolService.hasFeature.mockResolvedValueOnce(false); + configService.get.mockReturnValueOnce(false); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([]); + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.ts index 6b560e71410..e58d197f6be 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.ts @@ -2,25 +2,42 @@ import { BoardExternalReference, BoardExternalReferenceType, BoardNodeService, C import { CourseService } from '@modules/learnroom'; import { RoomService } from '@modules/room'; import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; +import { EntityId, SchoolFeature } from '@shared/domain/types'; +import { BoardFeature } from '../board/domain'; +import { CourseFeatures } from '@shared/domain/entity'; +import { LegacySchoolService } from '../legacy-school'; +import { ConfigService } from '@nestjs/config'; +import { VideoConferenceConfig } from '../video-conference'; @Injectable() export class BoardContextApiHelperService { constructor( private readonly courseService: CourseService, private readonly roomService: RoomService, - private readonly boardNodeService: BoardNodeService + private readonly boardNodeService: BoardNodeService, + private readonly legacySchoolService: LegacySchoolService, + private readonly configService: ConfigService ) {} public async getSchoolIdForBoardNode(nodeId: EntityId): Promise { - const boardNode = await this.boardNodeService.findById(nodeId); - const board = await this.boardNodeService.findRoot(boardNode); - const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, board.id); - const schoolId = await this.getSchoolIdForBoard(columnBoard.context); + const boardContext = await this.getBoardContext(nodeId); + const schoolId = await this.getSchoolIdForBoardContext(boardContext); return schoolId; } - private async getSchoolIdForBoard(context: BoardExternalReference): Promise { + public async getFeaturesForBoardNode(nodeId: EntityId): Promise { + const boardContext = await this.getBoardContext(nodeId); + const features = await this.getFeaturesForBoardContext(boardContext); + return features; + } + + private async getBoardContext(nodeId: EntityId): Promise { + const boardNode = await this.boardNodeService.findById(nodeId, 1); + const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, boardNode.rootId, 1); + return columnBoard.context; + } + + private async getSchoolIdForBoardContext(context: BoardExternalReference): Promise { if (context.type === BoardExternalReferenceType.Course) { const course = await this.courseService.findById(context.id); @@ -35,4 +52,47 @@ export class BoardContextApiHelperService { /* istanbul ignore next */ throw new Error(`Unsupported board reference type ${context.type as string}`); } + + private async getFeaturesForBoardContext(context: BoardExternalReference): Promise { + const features: BoardFeature[] = []; + + if (context.type === BoardExternalReferenceType.Course) { + const course = await this.courseService.findById(context.id); + + if ( + (await this.isVideoConferenceEnabledForCourse(course.features)) || + (await this.isVideoConferenceEnabledForSchool(course.school.id)) || + this.isVideoConferenceEnabledForConfig() + ) { + features.push(BoardFeature.VIDEOCONFERENCE); + } + + return features; + } + + if (context.type === BoardExternalReferenceType.Room) { + const room = await this.roomService.getSingleRoom(context.id); + + if ((await this.isVideoConferenceEnabledForSchool(room.schoolId)) || this.isVideoConferenceEnabledForConfig()) { + features.push(BoardFeature.VIDEOCONFERENCE); + } + + return features; + } + + /* istanbul ignore next */ + throw new Error(`Unsupported board reference type ${context.type as string}`); + } + + private async isVideoConferenceEnabledForCourse(courseFeatures?: CourseFeatures[]): Promise { + return (courseFeatures ?? []).includes(CourseFeatures.VIDEOCONFERENCE); + } + + private async isVideoConferenceEnabledForSchool(schoolId: EntityId): Promise { + return this.legacySchoolService.hasFeature(schoolId, SchoolFeature.VIDEOCONFERENCE); + } + + private isVideoConferenceEnabledForConfig(): boolean { + return this.configService.get('FEATURE_VIDEOCONFERENCE_ENABLED'); + } } diff --git a/apps/server/src/modules/board/domain/types/board-feature.enum.ts b/apps/server/src/modules/board/domain/types/board-feature.enum.ts new file mode 100644 index 00000000000..fef75aff365 --- /dev/null +++ b/apps/server/src/modules/board/domain/types/board-feature.enum.ts @@ -0,0 +1,3 @@ +export enum BoardFeature { + VIDEOCONFERENCE = 'videoconference', +} diff --git a/apps/server/src/modules/board/domain/types/index.ts b/apps/server/src/modules/board/domain/types/index.ts index 32854076180..464251be496 100644 --- a/apps/server/src/modules/board/domain/types/index.ts +++ b/apps/server/src/modules/board/domain/types/index.ts @@ -1,6 +1,7 @@ export * from './any-board-node'; export * from './any-content-element'; export * from './board-external-reference'; +export * from './board-feature.enum'; export * from './board-layout.enum'; export * from './board-node-props'; export * from './board-node-type.enum'; From f9f1cabae00980223c35f04c133faf66921ff71c Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Mon, 13 Jan 2025 16:42:14 +0100 Subject: [PATCH 3/9] implement board features api --- .../board-context-api-helper.service.ts | 4 ++-- .../src/modules/board/board-api.module.ts | 10 +++++++++- .../api-test/board-lookup-in-room.api.spec.ts | 13 +++++++++++-- .../board/controller/board.controller.ts | 4 ++-- .../controller/dto/board/board.response.ts | 8 ++++++-- .../controller/mapper/board-response.mapper.ts | 5 +++-- .../gateway/board-collaboration.gateway.ts | 4 ++-- .../service/board-node-authorizable.service.ts | 2 +- apps/server/src/modules/board/uc/board.uc.ts | 18 ++++++++++++++---- 9 files changed, 50 insertions(+), 18 deletions(-) diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.ts index e58d197f6be..2d6b1baf85c 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.ts @@ -32,8 +32,8 @@ export class BoardContextApiHelperService { } private async getBoardContext(nodeId: EntityId): Promise { - const boardNode = await this.boardNodeService.findById(nodeId, 1); - const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, boardNode.rootId, 1); + const boardNode = await this.boardNodeService.findById(nodeId, 0); + const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, boardNode.rootId, 0); return columnBoard.context; } diff --git a/apps/server/src/modules/board/board-api.module.ts b/apps/server/src/modules/board/board-api.module.ts index e4fa5ac9cb0..693a5044990 100644 --- a/apps/server/src/modules/board/board-api.module.ts +++ b/apps/server/src/modules/board/board-api.module.ts @@ -14,9 +14,17 @@ import { import { BoardNodePermissionService } from './service'; import { BoardUc, CardUc, ColumnUc, ElementUc, SubmissionItemUc } from './uc'; import { RoomModule } from '../room'; +import { BoardContextApiHelperModule } from '../board-context'; @Module({ - imports: [BoardModule, LoggerModule, RoomMembershipModule, RoomModule, forwardRef(() => AuthorizationModule)], + imports: [ + BoardModule, + LoggerModule, + RoomMembershipModule, + RoomModule, + forwardRef(() => AuthorizationModule), + BoardContextApiHelperModule, + ], controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController], providers: [BoardUc, BoardNodePermissionService, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo], }) diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts index 0f1dea74740..07fe7acdf5e 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts @@ -2,7 +2,14 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing'; +import { + cleanupCollections, + groupEntityFactory, + roleFactory, + schoolEntityFactory, + TestApiClient, + userFactory, +} from '@shared/testing'; import { Permission, RoleName } from '@shared/domain/interface'; import { accountFactory } from '@src/modules/account/testing'; @@ -60,7 +67,8 @@ describe(`board lookup in room (api)`, () => { ], }); - const room = roomEntityFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); + const room = roomEntityFactory.buildWithId({ schoolId: school.id }); const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); @@ -76,6 +84,7 @@ describe(`board lookup in room (api)`, () => { userGroup, room, roomMembership, + school, ]); const columnBoardNode = columnBoardEntityFactory.build({ diff --git a/apps/server/src/modules/board/controller/board.controller.ts b/apps/server/src/modules/board/controller/board.controller.ts index 2a6f4e74cfe..566bed25145 100644 --- a/apps/server/src/modules/board/controller/board.controller.ts +++ b/apps/server/src/modules/board/controller/board.controller.ts @@ -60,9 +60,9 @@ export class BoardController { @Param() urlParams: BoardUrlParams, @CurrentUser() currentUser: ICurrentUser ): Promise { - const board = await this.boardUc.findBoard(currentUser.userId, urlParams.boardId); + const { board, features } = await this.boardUc.findBoard(currentUser.userId, urlParams.boardId); - const response = BoardResponseMapper.mapToResponse(board); + const response = BoardResponseMapper.mapToResponse(board, features); return response; } diff --git a/apps/server/src/modules/board/controller/dto/board/board.response.ts b/apps/server/src/modules/board/controller/dto/board/board.response.ts index 081dd5dea59..edab474f7c9 100644 --- a/apps/server/src/modules/board/controller/dto/board/board.response.ts +++ b/apps/server/src/modules/board/controller/dto/board/board.response.ts @@ -1,17 +1,18 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { BoardLayout } from '../../../domain'; +import { BoardFeature, BoardLayout } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; import { ColumnResponse } from './column.response'; export class BoardResponse { - constructor({ id, title, columns, timestamps, isVisible, layout }: BoardResponse) { + constructor({ id, title, columns, timestamps, isVisible, layout, features }: BoardResponse) { this.id = id; this.title = title; this.columns = columns; this.timestamps = timestamps; this.isVisible = isVisible; this.layout = layout; + this.features = features; } @ApiProperty({ @@ -36,4 +37,7 @@ export class BoardResponse { @ApiProperty() layout: BoardLayout; + + @ApiProperty({ enum: BoardFeature, isArray: true, enumName: 'BoardFeature' }) + features: BoardFeature[]; } diff --git a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts index 061f9576c9c..6118a490600 100644 --- a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts @@ -1,10 +1,10 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { Column, ColumnBoard } from '../../domain'; +import { BoardFeature, Column, ColumnBoard } from '../../domain'; import { BoardResponse, TimestampsResponse } from '../dto'; import { ColumnResponseMapper } from './column-response.mapper'; export class BoardResponseMapper { - static mapToResponse(board: ColumnBoard): BoardResponse { + static mapToResponse(board: ColumnBoard, features: BoardFeature[]): BoardResponse { const result = new BoardResponse({ id: board.id, title: board.title, @@ -21,6 +21,7 @@ export class BoardResponseMapper { timestamps: new TimestampsResponse({ lastUpdatedAt: board.updatedAt, createdAt: board.createdAt }), isVisible: board.isVisible, layout: board.layout, + features, }); return result; } diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index ca0c00aecbc..24a6c3d9aaa 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -216,8 +216,8 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { const emitter = this.buildBoardSocketEmitter({ socket, action: 'fetch-board' }); const { userId } = this.getCurrentUser(socket); try { - const board = await this.boardUc.findBoard(userId, data.boardId); - const responsePayload = BoardResponseMapper.mapToResponse(board); + const { board, features } = await this.boardUc.findBoard(userId, data.boardId); + const responsePayload = BoardResponseMapper.mapToResponse(board, features); await emitter.joinRoom(board); emitter.emitSuccess(responsePayload); } catch (err) { diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.ts index 4de17520369..c36b669fdfb 100644 --- a/apps/server/src/modules/board/service/board-node-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -52,7 +52,7 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService const rootIds = boardNodes.map((node) => node.rootId); const parentIds = boardNodes.map((node) => node.parentId).filter((defined) => defined) as EntityId[]; const boardNodeMap = await this.getBoardNodeMap([...rootIds, ...parentIds]); - const promises = boardNodes.map((boardNode) => { + const promises = boardNodes.map(async (boardNode) => { const rootNode = boardNodeMap[boardNode.rootId]; return this.boardContextService.getUsersWithBoardRoles(rootNode).then((users) => { return { id: boardNode.id, users }; diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 83d250541c4..071b89131ae 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -9,9 +9,17 @@ import { StorageLocation } from '@src/modules/files-storage/interface'; import { RoomService } from '@src/modules/room'; import { RoomMembershipService } from '@src/modules/room-membership'; import { CreateBoardBodyParams } from '../controller/dto'; -import { BoardExternalReference, BoardExternalReferenceType, BoardNodeFactory, Column, ColumnBoard } from '../domain'; +import { + BoardExternalReference, + BoardExternalReferenceType, + BoardFeature, + BoardNodeFactory, + Column, + ColumnBoard, +} from '../domain'; import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; import { StorageLocationReference } from '../service/internal'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class BoardUc { @@ -25,7 +33,8 @@ export class BoardUc { private readonly logger: LegacyLogger, private readonly courseRepo: CourseRepo, private readonly roomService: RoomService, - private readonly boardNodeFactory: BoardNodeFactory + private readonly boardNodeFactory: BoardNodeFactory, + private readonly boardContextApiHelperService: BoardContextApiHelperService ) { this.logger.setContext(BoardUc.name); } @@ -46,14 +55,15 @@ export class BoardUc { return board; } - async findBoard(userId: EntityId, boardId: EntityId): Promise { + async findBoard(userId: EntityId, boardId: EntityId): Promise<{ board: ColumnBoard; features: BoardFeature[] }> { this.logger.debug({ action: 'findBoard', userId, boardId }); // TODO set depth=2 to reduce data? const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); await this.boardPermissionService.checkPermission(userId, board, Action.read); + const features = await this.boardContextApiHelperService.getFeaturesForBoardNode(boardId); - return board; + return { board, features }; } async findBoardContext(userId: EntityId, boardId: EntityId): Promise { From e360d90b3b5c68e6b0658d0d209ff696ebc818d9 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 14 Jan 2025 11:54:06 +0100 Subject: [PATCH 4/9] fix linter errors --- .../board-context-api-helper.service.ts | 10 ++++---- apps/server/src/modules/board/uc/board.uc.ts | 25 +++++++++++-------- .../uc/video-conference-create.uc.spec.ts | 4 +-- .../uc/video-conference-create.uc.ts | 12 ++++++--- .../uc/video-conference-end.uc.spec.ts | 6 ++--- .../uc/video-conference-end.uc.ts | 6 ++--- .../uc/video-conference-info.uc.spec.ts | 7 +++--- .../uc/video-conference-info.uc.ts | 6 ++--- .../uc/video-conference-join.uc.spec.ts | 4 +-- .../uc/video-conference-join.uc.ts | 6 ++--- 10 files changed, 46 insertions(+), 40 deletions(-) diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.ts index 2d6b1baf85c..ae0ce0bd5d3 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.ts @@ -2,11 +2,11 @@ import { BoardExternalReference, BoardExternalReferenceType, BoardNodeService, C import { CourseService } from '@modules/learnroom'; import { RoomService } from '@modules/room'; import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { CourseFeatures } from '@shared/domain/entity'; import { EntityId, SchoolFeature } from '@shared/domain/types'; import { BoardFeature } from '../board/domain'; -import { CourseFeatures } from '@shared/domain/entity'; import { LegacySchoolService } from '../legacy-school'; -import { ConfigService } from '@nestjs/config'; import { VideoConferenceConfig } from '../video-conference'; @Injectable() @@ -60,7 +60,7 @@ export class BoardContextApiHelperService { const course = await this.courseService.findById(context.id); if ( - (await this.isVideoConferenceEnabledForCourse(course.features)) || + this.isVideoConferenceEnabledForCourse(course.features) || (await this.isVideoConferenceEnabledForSchool(course.school.id)) || this.isVideoConferenceEnabledForConfig() ) { @@ -84,11 +84,11 @@ export class BoardContextApiHelperService { throw new Error(`Unsupported board reference type ${context.type as string}`); } - private async isVideoConferenceEnabledForCourse(courseFeatures?: CourseFeatures[]): Promise { + private isVideoConferenceEnabledForCourse(courseFeatures?: CourseFeatures[]): boolean { return (courseFeatures ?? []).includes(CourseFeatures.VIDEOCONFERENCE); } - private async isVideoConferenceEnabledForSchool(schoolId: EntityId): Promise { + private isVideoConferenceEnabledForSchool(schoolId: EntityId): Promise { return this.legacySchoolService.hasFeature(schoolId, SchoolFeature.VIDEOCONFERENCE); } diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 071b89131ae..cc5cf9a7b98 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -5,6 +5,7 @@ import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo/course'; import { LegacyLogger } from '@src/core/logger'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { StorageLocation } from '@src/modules/files-storage/interface'; import { RoomService } from '@src/modules/room'; import { RoomMembershipService } from '@src/modules/room-membership'; @@ -19,7 +20,6 @@ import { } from '../domain'; import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; import { StorageLocationReference } from '../service/internal'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class BoardUc { @@ -39,7 +39,7 @@ export class BoardUc { this.logger.setContext(BoardUc.name); } - async createBoard(userId: EntityId, params: CreateBoardBodyParams): Promise { + public async createBoard(userId: EntityId, params: CreateBoardBodyParams): Promise { this.logger.debug({ action: 'createBoard', userId, title: params.title }); await this.checkReferenceWritePermission(userId, { type: params.parentType, id: params.parentId }); @@ -55,7 +55,10 @@ export class BoardUc { return board; } - async findBoard(userId: EntityId, boardId: EntityId): Promise<{ board: ColumnBoard; features: BoardFeature[] }> { + public async findBoard( + userId: EntityId, + boardId: EntityId + ): Promise<{ board: ColumnBoard; features: BoardFeature[] }> { this.logger.debug({ action: 'findBoard', userId, boardId }); // TODO set depth=2 to reduce data? @@ -66,7 +69,7 @@ export class BoardUc { return { board, features }; } - async findBoardContext(userId: EntityId, boardId: EntityId): Promise { + public async findBoardContext(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'findBoardContext', userId, boardId }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); @@ -75,7 +78,7 @@ export class BoardUc { return board.context; } - async deleteBoard(userId: EntityId, boardId: EntityId): Promise { + public async deleteBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'deleteBoard', userId, boardId }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); @@ -85,7 +88,7 @@ export class BoardUc { return board; } - async updateBoardTitle(userId: EntityId, boardId: EntityId, title: string): Promise { + public async updateBoardTitle(userId: EntityId, boardId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateBoardTitle', userId, boardId, title }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); @@ -95,7 +98,7 @@ export class BoardUc { return board; } - async createColumn(userId: EntityId, boardId: EntityId): Promise { + public async createColumn(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'createColumn', userId, boardId }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId, 1); @@ -108,7 +111,7 @@ export class BoardUc { return column; } - async moveColumn( + public async moveColumn( userId: EntityId, columnId: EntityId, targetBoardId: EntityId, @@ -126,7 +129,7 @@ export class BoardUc { return column; } - async copyBoard(userId: EntityId, boardId: EntityId, schoolId: EntityId): Promise { + public async copyBoard(userId: EntityId, boardId: EntityId, schoolId: EntityId): Promise { this.logger.debug({ action: 'copyBoard', userId, boardId }); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); @@ -148,7 +151,7 @@ export class BoardUc { return copyStatus; } - async updateVisibility(userId: EntityId, boardId: EntityId, isVisible: boolean): Promise { + public async updateVisibility(userId: EntityId, boardId: EntityId, isVisible: boolean): Promise { const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); await this.boardPermissionService.checkPermission(userId, board, Action.write); @@ -158,7 +161,7 @@ export class BoardUc { // ---- Move to shared service? (see apps/server/src/modules/sharing/uc/share-token.uc.ts) - private async checkReferenceWritePermission(userId: EntityId, context: BoardExternalReference) { + private async checkReferenceWritePermission(userId: EntityId, context: BoardExternalReference): Promise { const user = await this.authorizationService.getUserWithPermissions(userId); if (context.type === BoardExternalReferenceType.Course) { diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts index 945c5ad4114..eebc58ef322 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -6,14 +7,13 @@ import { UserDO } from '@shared/domain/domainobject'; import {} from '@shared/domain/entity'; import { VideoConferenceScope } from '@shared/domain/interface'; import { userDoFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBStatus } from '../bbb'; import { ErrorStatus } from '../error/error-status.enum'; import { VideoConferenceOptions } from '../interface'; import { BBBService, VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef } from './dto'; import { VideoConferenceCreateUc } from './video-conference-create.uc'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceCreateUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts index 6d6da369202..01d2d1be91c 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts @@ -1,7 +1,9 @@ import { UserService } from '@modules/user'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; +import { VideoConferenceScope } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBBaseMeetingConfig, BBBCreateConfigBuilder, @@ -15,8 +17,6 @@ import { ErrorStatus } from '../error/error-status.enum'; import { VideoConferenceOptions } from '../interface'; import { VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef } from './dto'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; -import { VideoConferenceScope } from '@shared/domain/interface'; @Injectable() export class VideoConferenceCreateUc { @@ -27,7 +27,11 @@ export class VideoConferenceCreateUc { private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} - async createIfNotRunning(currentUserId: EntityId, scope: ScopeRef, options: VideoConferenceOptions): Promise { + public async createIfNotRunning( + currentUserId: EntityId, + scope: ScopeRef, + options: VideoConferenceOptions + ): Promise { let bbbMeetingInfoResponse: BBBResponse | undefined; // try and catch based on legacy behavior try { @@ -98,7 +102,7 @@ export class VideoConferenceCreateUc { await this.videoConferenceService.throwOnFeaturesDisabled(schoolId); } - private throwIfNotModerator(role: BBBRole, errorMessage: string) { + private throwIfNotModerator(role: BBBRole, errorMessage: string): void { if (role !== BBBRole.MODERATOR) { throw new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION, errorMessage); } diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts index e04671472d4..ecde9127ce3 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -6,13 +7,12 @@ import { UserDO } from '@shared/domain/domainobject'; import {} from '@shared/domain/entity'; import { VideoConferenceScope } from '@shared/domain/interface'; import { userDoFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBBaseResponse, BBBResponse, BBBRole, BBBStatus } from '../bbb'; import { ErrorStatus } from '../error/error-status.enum'; import { BBBService, VideoConferenceService } from '../service'; import { ScopeInfo, VideoConference, VideoConferenceState } from './dto'; import { VideoConferenceEndUc } from './video-conference-end.uc'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceEndUc', () => { let module: TestingModule; @@ -181,7 +181,7 @@ describe('VideoConferenceEndUc', () => { const scope = { scope: scopeName, id: new ObjectId().toHexString() }; const scopeInfo: ScopeInfo = { scopeId: scope.id, - scopeName: scopeName, + scopeName, title: 'title', logoutUrl: 'logoutUrl', }; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts index d74309208a8..3d2bae8aef9 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts @@ -2,13 +2,13 @@ import { UserService } from '@modules/user'; import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; +import { VideoConferenceScope } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBBaseMeetingConfig, BBBBaseResponse, BBBResponse, BBBRole, BBBService } from '../bbb'; import { PermissionMapping } from '../mapper/video-conference.mapper'; import { VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef, VideoConference, VideoConferenceState } from './dto'; -import { VideoConferenceScope } from '@shared/domain/interface'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class VideoConferenceEndUc { @@ -19,7 +19,7 @@ export class VideoConferenceEndUc { private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} - async end(currentUserId: EntityId, scope: ScopeRef): Promise> { + public async end(currentUserId: EntityId, scope: ScopeRef): Promise> { /* need to be replace with const [authorizableUser, scopeResource]: [User, TeamEntity | Course] = await Promise.all([ this.authorizationService.getUserWithPermissions(userId), diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts index a5345f033f6..e10acdf8449 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -7,14 +8,13 @@ import {} from '@shared/domain/entity'; import { Permission, VideoConferenceScope } from '@shared/domain/interface'; import { userDoFactory } from '@shared/testing'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBStatus } from '../bbb'; import { ErrorStatus } from '../error/error-status.enum'; import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; import { BBBService, VideoConferenceService } from '../service'; import { ScopeInfo, VideoConferenceInfo, VideoConferenceState } from './dto'; import { VideoConferenceInfoUc } from './video-conference-info.uc'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceInfoUc', () => { let module: TestingModule; @@ -385,11 +385,10 @@ describe('VideoConferenceInfoUc', () => { describe('when scope is a video conference element', () => { const setup = (scopeName: VideoConferenceScope) => { const user: UserDO = userDoFactory.buildWithId(); - const currentUserId: string = user.id as string; const scope = { scope: scopeName, id: new ObjectId().toHexString() }; const scopeInfo: ScopeInfo = { scopeId: scope.id, - scopeName: scopeName, + scopeName, title: 'title', logoutUrl: 'logoutUrl', }; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts index 198941cef67..78c0c006352 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts @@ -2,14 +2,14 @@ import { UserService } from '@modules/user'; import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { UserDO, VideoConferenceDO, VideoConferenceOptionsDO } from '@shared/domain/domainobject'; +import { VideoConferenceScope } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBBaseMeetingConfig, BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBService } from '../bbb'; import { defaultVideoConferenceOptions, VideoConferenceOptions } from '../interface'; import { PermissionMapping } from '../mapper/video-conference.mapper'; import { VideoConferenceService } from '../service'; import { ScopeInfo, ScopeRef, VideoConferenceInfo, VideoConferenceState } from './dto'; -import { VideoConferenceScope } from '@shared/domain/interface'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class VideoConferenceInfoUc { @@ -20,7 +20,7 @@ export class VideoConferenceInfoUc { private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} - async getMeetingInfo(currentUserId: EntityId, scope: ScopeRef): Promise { + public async getMeetingInfo(currentUserId: EntityId, scope: ScopeRef): Promise { /* need to be replace with const [authorizableUser, scopeResource]: [User, TeamEntity | Course] = await Promise.all([ this.authorizationService.getUserWithPermissions(userId), diff --git a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts index e0e610b280e..5386253d0b4 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts @@ -1,4 +1,5 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -7,14 +8,13 @@ import {} from '@shared/domain/entity'; import { Permission, VideoConferenceScope } from '@shared/domain/interface'; import { userDoFactory } from '@shared/testing'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBJoinConfig, BBBJoinResponse, BBBResponse, BBBRole } from '../bbb'; import { ErrorStatus } from '../error/error-status.enum'; import { VideoConferenceOptions } from '../interface'; import { BBBService, VideoConferenceService } from '../service'; import { VideoConferenceJoin, VideoConferenceState } from './dto'; import { VideoConferenceJoinUc } from './video-conference-join.uc'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; describe('VideoConferenceJoinUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts index 8c5d8c489a3..b80de315dd4 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts @@ -2,13 +2,13 @@ import { UserService } from '@modules/user'; import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { UserDO, VideoConferenceDO } from '@shared/domain/domainobject'; +import { VideoConferenceScope } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { BBBJoinConfigBuilder, BBBRole, BBBService } from '../bbb'; import { PermissionMapping } from '../mapper/video-conference.mapper'; import { VideoConferenceService } from '../service'; import { ScopeRef, VideoConferenceJoin, VideoConferenceState } from './dto'; -import { VideoConferenceScope } from '@shared/domain/interface'; -import { BoardContextApiHelperService } from '@src/modules/board-context'; @Injectable() export class VideoConferenceJoinUc { @@ -19,7 +19,7 @@ export class VideoConferenceJoinUc { private readonly boardContextApiHelperService: BoardContextApiHelperService ) {} - async join(currentUserId: EntityId, scope: ScopeRef): Promise { + public async join(currentUserId: EntityId, scope: ScopeRef): Promise { const user: UserDO = await this.userService.findById(currentUserId); const schoolId = From 96ce039cc23ac950454fe0246cf0ea6473a2d194 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 15 Jan 2025 10:29:44 +0100 Subject: [PATCH 5/9] fix tests --- .../src/modules/board/uc/board.uc.spec.ts | 19 +++++++- ...rence-video-conference-element.api.spec.ts | 45 ++++++++++++------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index 276f3275458..5b57f339f21 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -7,6 +7,7 @@ import { CourseRepo } from '@shared/repo/course'; import { setupEntities, userFactory } from '@shared/testing'; import { courseFactory } from '@shared/testing/factory'; import { LegacyLogger } from '@src/core/logger'; +import { BoardContextApiHelperService } from '@src/modules/board-context'; import { RoomService } from '@src/modules/room'; import { RoomMembershipService } from '@src/modules/room-membership'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; @@ -24,6 +25,7 @@ describe(BoardUc.name, () => { let columnBoardService: DeepMocked; let courseRepo: DeepMocked; let boardNodeFactory: DeepMocked; + let boardContextApiHelperService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -61,6 +63,10 @@ describe(BoardUc.name, () => { provide: RoomMembershipService, useValue: createMock(), }, + { + provide: BoardContextApiHelperService, + useValue: createMock(), + }, { provide: LegacyLogger, useValue: createMock(), @@ -75,6 +81,7 @@ describe(BoardUc.name, () => { columnBoardService = module.get(ColumnBoardService); courseRepo = module.get(CourseRepo); boardNodeFactory = module.get(BoardNodeFactory); + boardContextApiHelperService = module.get(BoardContextApiHelperService); await setupEntities(); }); @@ -233,13 +240,21 @@ describe(BoardUc.name, () => { expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.read); }); - it('should return the column board object', async () => { + it('should call the board context api helper service to get features', async () => { + const { user, boardId } = globalSetup(); + + await uc.findBoard(user.id, boardId); + + expect(boardContextApiHelperService.getFeaturesForBoardNode).toHaveBeenCalledWith(boardId); + }); + + it('should return the column board object + features', async () => { const { user, board } = globalSetup(); boardNodeService.findByClassAndId.mockResolvedValueOnce(board); const result = await uc.findBoard(user.id, board.id); - expect(result).toEqual(board); + expect(result).toEqual({ board, features: [] }); }); }); diff --git a/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts b/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts index a622a760219..1bc23022be4 100644 --- a/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts +++ b/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts @@ -161,9 +161,10 @@ describe('VideoConferenceController (API)', () => { describe('when the logoutUrl is from a wrong origin', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); + const school = schoolEntityFactory.buildWithId({ features: [] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -235,9 +236,10 @@ describe('VideoConferenceController (API)', () => { describe('when conference params are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); + const school = schoolEntityFactory.buildWithId({ features: [] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -306,8 +308,9 @@ describe('VideoConferenceController (API)', () => { describe('when user has not the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2025-10-20'), }); @@ -376,9 +379,10 @@ describe('VideoConferenceController (API)', () => { describe('when user has the required permission in room scope', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -447,9 +451,10 @@ describe('VideoConferenceController (API)', () => { describe('when conference is for scope and scopeId is already running', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -527,9 +532,10 @@ describe('VideoConferenceController (API)', () => { describe('when scope and scopeId are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); + const school = schoolEntityFactory.buildWithId({ features: [] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -600,9 +606,10 @@ describe('VideoConferenceController (API)', () => { describe('when user has the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -676,9 +683,10 @@ describe('VideoConferenceController (API)', () => { describe('when conference is not running', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -761,9 +769,10 @@ describe('VideoConferenceController (API)', () => { describe('when scope and scopeId are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); + const school = schoolEntityFactory.buildWithId({ features: [] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -834,9 +843,10 @@ describe('VideoConferenceController (API)', () => { describe('when user has the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -908,9 +918,10 @@ describe('VideoConferenceController (API)', () => { describe('when guest want meeting info of conference without waiting room', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -993,9 +1004,10 @@ describe('VideoConferenceController (API)', () => { describe('when conference is not running', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -1079,9 +1091,10 @@ describe('VideoConferenceController (API)', () => { describe('when scope and scopeId are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); + const school = schoolEntityFactory.buildWithId({ features: [] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -1153,9 +1166,10 @@ describe('VideoConferenceController (API)', () => { describe('when a user without required permission wants to end a conference', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); @@ -1225,9 +1239,10 @@ describe('VideoConferenceController (API)', () => { describe('when a user with required permission wants to end a conference', () => { const setup = async () => { - const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const room = roomEntityFactory.build({ + schoolId: school.id, startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), }); From 9cee1eb8eed983df5e9e0807ef12d7cf0875de58 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 15 Jan 2025 11:11:09 +0100 Subject: [PATCH 6/9] fix tests --- apps/server/src/modules/board/board-ws-api.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/modules/board/board-ws-api.module.ts b/apps/server/src/modules/board/board-ws-api.module.ts index 663b1417b5d..5e9821ecde6 100644 --- a/apps/server/src/modules/board/board-ws-api.module.ts +++ b/apps/server/src/modules/board/board-ws-api.module.ts @@ -10,6 +10,7 @@ import { MetricsService } from './metrics/metrics.service'; import { BoardNodePermissionService } from './service'; import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc'; import { RoomModule } from '../room'; +import { BoardContextApiHelperModule } from '../board-context'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { RoomModule } from '../room'; UserModule, RoomMembershipModule, RoomModule, + BoardContextApiHelperModule, ], providers: [ BoardCollaborationGateway, From 3d5ae274cdafbd9ab55fc106753088c1405fb5eb Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 15 Jan 2025 12:14:05 +0100 Subject: [PATCH 7/9] apply requested changes --- .../board-context/board-context-api-helper.service.ts | 4 ++-- .../board/service/board-node-authorizable.service.ts | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.ts index ae0ce0bd5d3..9569ab11e6d 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.ts @@ -1,7 +1,7 @@ import { BoardExternalReference, BoardExternalReferenceType, BoardNodeService, ColumnBoard } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { RoomService } from '@modules/room'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CourseFeatures } from '@shared/domain/entity'; import { EntityId, SchoolFeature } from '@shared/domain/types'; @@ -81,7 +81,7 @@ export class BoardContextApiHelperService { } /* istanbul ignore next */ - throw new Error(`Unsupported board reference type ${context.type as string}`); + throw new BadRequestException(`Unsupported board reference type ${context.type as string}`); } private isVideoConferenceEnabledForCourse(courseFeatures?: CourseFeatures[]): boolean { diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.ts index c36b669fdfb..c6cc8197b6f 100644 --- a/apps/server/src/modules/board/service/board-node-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -24,7 +24,7 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService /** * @deprecated */ - async findById(id: EntityId): Promise { + public async findById(id: EntityId): Promise { const boardNode = await this.boardNodeRepo.findById(id, 1); const boardNodeAuthorizable = this.getBoardAuthorizable(boardNode); @@ -32,7 +32,7 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService return boardNodeAuthorizable; } - async getBoardAuthorizable(boardNode: AnyBoardNode): Promise { + public async getBoardAuthorizable(boardNode: AnyBoardNode): Promise { const rootNode = await this.boardNodeService.findRoot(boardNode, 1); const parentNode = await this.boardNodeService.findParent(boardNode, 1); const users = await this.boardContextService.getUsersWithBoardRoles(rootNode); @@ -48,15 +48,14 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService return boardNodeAuthorizable; } - async getBoardAuthorizables(boardNodes: AnyBoardNode[]): Promise { + public async getBoardAuthorizables(boardNodes: AnyBoardNode[]): Promise { const rootIds = boardNodes.map((node) => node.rootId); const parentIds = boardNodes.map((node) => node.parentId).filter((defined) => defined) as EntityId[]; const boardNodeMap = await this.getBoardNodeMap([...rootIds, ...parentIds]); const promises = boardNodes.map(async (boardNode) => { const rootNode = boardNodeMap[boardNode.rootId]; - return this.boardContextService.getUsersWithBoardRoles(rootNode).then((users) => { - return { id: boardNode.id, users }; - }); + const users = await this.boardContextService.getUsersWithBoardRoles(rootNode); + return { id: boardNode.id, users }; }); const results = await Promise.all(promises); From 6275e01daaf216d3ff0c768aad8692f95cd3a4d7 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 15 Jan 2025 15:57:49 +0100 Subject: [PATCH 8/9] fix feature check confitions --- .../board-context-api-helper.service.spec.ts | 81 +++++++++++-------- .../board-context-api-helper.service.ts | 8 +- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts index ef1b6260639..5481cc77dc6 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.spec.ts @@ -121,55 +121,57 @@ describe('BoardContextApiHelperService', () => { }; describe('when video conference is enabled for course', () => { - it('should return video conference feature', async () => { - const { boardNode, course } = setup(); + describe('and video conference is enabled for school and config', () => { + it('should return video conference feature', async () => { + const { boardNode, course } = setup(); - course.features = [CourseFeatures.VIDEOCONFERENCE]; - legacySchoolService.hasFeature.mockResolvedValueOnce(false); - configService.get.mockReturnValueOnce(false); + course.features = [CourseFeatures.VIDEOCONFERENCE]; + legacySchoolService.hasFeature.mockResolvedValueOnce(true); + configService.get.mockReturnValueOnce(true); - const result = await service.getFeaturesForBoardNode(boardNode.id); + const result = await service.getFeaturesForBoardNode(boardNode.id); - expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + }); }); - }); - describe('when video conference is enabled for school', () => { - it('should return video conference feature', async () => { - const { boardNode, course } = setup(); + describe('and video conference is disabled for school', () => { + it('should not return feature', async () => { + const { boardNode, course } = setup(); - course.features = []; - legacySchoolService.hasFeature.mockResolvedValueOnce(true); - configService.get.mockReturnValueOnce(false); + course.features = [CourseFeatures.VIDEOCONFERENCE]; + legacySchoolService.hasFeature.mockResolvedValueOnce(false); + configService.get.mockReturnValueOnce(true); - const result = await service.getFeaturesForBoardNode(boardNode.id); + const result = await service.getFeaturesForBoardNode(boardNode.id); - expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + expect(result).toEqual([]); + }); }); - }); - describe('when video conference is enabled for config', () => { - it('should return video conference feature', async () => { - const { boardNode, course } = setup(); + describe('and video conference is disabled for config', () => { + it('should not return feature', async () => { + const { boardNode, course } = setup(); - course.features = []; - legacySchoolService.hasFeature.mockResolvedValueOnce(false); - configService.get.mockReturnValueOnce(true); + course.features = [CourseFeatures.VIDEOCONFERENCE]; + legacySchoolService.hasFeature.mockResolvedValueOnce(true); + configService.get.mockReturnValueOnce(false); - const result = await service.getFeaturesForBoardNode(boardNode.id); + const result = await service.getFeaturesForBoardNode(boardNode.id); - expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + expect(result).toEqual([]); + }); }); }); - describe('when video conference is disabled entirely', () => { + describe('when video conference is disabled for course', () => { it('should not return feature', async () => { const { boardNode } = setup(); const course = courseFactory.build(); courseService.findById.mockResolvedValueOnce(course); - legacySchoolService.hasFeature.mockResolvedValueOnce(false); - configService.get.mockReturnValueOnce(false); + legacySchoolService.hasFeature.mockResolvedValueOnce(true); + configService.get.mockReturnValueOnce(true); const result = await service.getFeaturesForBoardNode(boardNode.id); @@ -194,12 +196,12 @@ describe('BoardContextApiHelperService', () => { return { boardNode: column, room }; }; - describe('when video conference is enabled for school', () => { + describe('when video conference is enabled for school and config', () => { it('should return video conference feature', async () => { const { boardNode } = setup(); legacySchoolService.hasFeature.mockResolvedValueOnce(true); - configService.get.mockReturnValueOnce(false); + configService.get.mockReturnValueOnce(true); const result = await service.getFeaturesForBoardNode(boardNode.id); @@ -207,8 +209,8 @@ describe('BoardContextApiHelperService', () => { }); }); - describe('when video conference is enabled for config', () => { - it('should return video conference feature', async () => { + describe('when video conference is disabled for school', () => { + it('should not return feature', async () => { const { boardNode } = setup(); legacySchoolService.hasFeature.mockResolvedValueOnce(false); @@ -216,7 +218,20 @@ describe('BoardContextApiHelperService', () => { const result = await service.getFeaturesForBoardNode(boardNode.id); - expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]); + expect(result).toEqual([]); + }); + }); + + describe('when video conference is disabled for config', () => { + it('should not return feature', async () => { + const { boardNode } = setup(); + + legacySchoolService.hasFeature.mockResolvedValueOnce(true); + configService.get.mockReturnValueOnce(false); + + const result = await service.getFeaturesForBoardNode(boardNode.id); + + expect(result).toEqual([]); }); }); diff --git a/apps/server/src/modules/board-context/board-context-api-helper.service.ts b/apps/server/src/modules/board-context/board-context-api-helper.service.ts index 9569ab11e6d..2d71d10d85c 100644 --- a/apps/server/src/modules/board-context/board-context-api-helper.service.ts +++ b/apps/server/src/modules/board-context/board-context-api-helper.service.ts @@ -60,9 +60,9 @@ export class BoardContextApiHelperService { const course = await this.courseService.findById(context.id); if ( - this.isVideoConferenceEnabledForCourse(course.features) || - (await this.isVideoConferenceEnabledForSchool(course.school.id)) || - this.isVideoConferenceEnabledForConfig() + this.isVideoConferenceEnabledForConfig() && + (await this.isVideoConferenceEnabledForSchool(course.school.id)) && + this.isVideoConferenceEnabledForCourse(course.features) ) { features.push(BoardFeature.VIDEOCONFERENCE); } @@ -73,7 +73,7 @@ export class BoardContextApiHelperService { if (context.type === BoardExternalReferenceType.Room) { const room = await this.roomService.getSingleRoom(context.id); - if ((await this.isVideoConferenceEnabledForSchool(room.schoolId)) || this.isVideoConferenceEnabledForConfig()) { + if (this.isVideoConferenceEnabledForConfig() && (await this.isVideoConferenceEnabledForSchool(room.schoolId))) { features.push(BoardFeature.VIDEOCONFERENCE); } From ea65a307a8271f8b37467cf09427381d1e1cc8cd Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Thu, 16 Jan 2025 11:42:16 +0100 Subject: [PATCH 9/9] fix linter error --- .../video-conference-video-conference-element.api.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts b/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts index 3a62a85f81e..40e245733c8 100644 --- a/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts +++ b/apps/server/src/modules/video-conference/controller/api-test/video-conference-video-conference-element.api.spec.ts @@ -2,7 +2,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Role, SchoolEntity, TargetModels, User, VideoConference } from '@shared/domain/entity'; +import { Role, TargetModels, User, VideoConference } from '@shared/domain/entity'; import { Permission, RoleName, VideoConferenceScope } from '@shared/domain/interface'; import { SchoolFeature } from '@shared/domain/types'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity';