diff --git a/apps/server/src/modules/board/index.ts b/apps/server/src/modules/board/index.ts index 0321fee95c2..003af17f13c 100644 --- a/apps/server/src/modules/board/index.ts +++ b/apps/server/src/modules/board/index.ts @@ -21,6 +21,7 @@ export { UserWithBoardRoles, isCard, isColumn, + isColumnBoard, isDrawingElement, isLinkElement, isRichTextElement, diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid-strategy.service.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid-strategy.service.ts index 6d3de082343..c24f8a68b18 100644 --- a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid-strategy.service.ts +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid-strategy.service.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common'; -import { CourseService } from '@modules/learnroom'; +import { type AnyBoardNode, BoardExternalReferenceType, BoardNodeService, isColumnBoard } from '@modules/board'; import { Group, GroupService } from '@modules/group'; +import { CourseService } from '@modules/learnroom'; +import { Injectable } from '@nestjs/common'; import { Course } from '@shared/domain/entity'; import { ToolContextType } from '../../../common/enum'; import { ContextExternalToolLaunchable } from '../../../context-external-tool/domain'; @@ -9,28 +10,46 @@ import { AutoParameterStrategy } from './auto-parameter.strategy'; @Injectable() export class AutoGroupExternalUuidStrategy implements AutoParameterStrategy { - constructor(private readonly courseService: CourseService, private readonly groupService: GroupService) {} + constructor( + private readonly courseService: CourseService, + private readonly groupService: GroupService, + private readonly boardNodeService: BoardNodeService + ) {} async getValue( _schoolExternalTool: SchoolExternalTool, contextExternalTool: ContextExternalToolLaunchable ): Promise { - if (contextExternalTool.contextRef.type !== ToolContextType.COURSE) { - return undefined; + switch (contextExternalTool.contextRef.type) { + case ToolContextType.BOARD_ELEMENT: { + const boardElement: AnyBoardNode = await this.boardNodeService.findById(contextExternalTool.contextRef.id); + const board: AnyBoardNode = await this.boardNodeService.findRoot(boardElement); + + if (!isColumnBoard(board) || board.context.type !== BoardExternalReferenceType.Course) { + return undefined; + } + + const uuid: string | undefined = await this.getExternalUuidFromCourse(board.context.id); + + return uuid; + } + case ToolContextType.COURSE: { + const uuid: string | undefined = await this.getExternalUuidFromCourse(contextExternalTool.contextRef.id); + + return uuid; + } + default: { + return undefined; + } } + } - const courseId = contextExternalTool.contextRef.id; + private async getExternalUuidFromCourse(courseId: string): Promise { const course: Course = await this.courseService.findById(courseId); const syncedGroup: Group | undefined = await this.getSyncedGroup(course); - if (!syncedGroup) { - return undefined; - } - const groupUuid = syncedGroup.externalSource?.externalId; - if (!groupUuid) { - return undefined; - } + const groupUuid = syncedGroup?.externalSource?.externalId; return groupUuid; } @@ -42,6 +61,7 @@ export class AutoGroupExternalUuidStrategy implements AutoParameterStrategy { } const syncedGroup = await this.groupService.findById(syncedGroupId); + return syncedGroup; } } diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid.strategy.spec.ts new file mode 100644 index 00000000000..db7f9351f25 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-external-uuid.strategy.spec.ts @@ -0,0 +1,418 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardExternalReferenceType, BoardNodeService } from '@modules/board'; +import { columnBoardFactory, externalToolElementFactory } from '@modules/board/testing'; +import { Group, GroupService } from '@modules/group'; +import { GroupEntity } from '@modules/group/entity'; +import { CourseService } from '@modules/learnroom'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Course } from '@shared/domain/entity'; +import { courseFactory, groupEntityFactory, groupFactory, setupEntities } from '@shared/testing'; +import { ToolContextType } from '../../../common/enum'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { contextExternalToolFactory } from '../../../context-external-tool/testing'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { schoolExternalToolFactory } from '../../../school-external-tool/testing'; +import { AutoGroupExternalUuidStrategy } from './auto-group-external-uuid-strategy.service'; + +describe(AutoGroupExternalUuidStrategy.name, () => { + let module: TestingModule; + let strategy: AutoGroupExternalUuidStrategy; + + let courseService: DeepMocked; + let groupService: DeepMocked; + let boardNodeService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + AutoGroupExternalUuidStrategy, + { + provide: CourseService, + useValue: createMock(), + }, + { + provide: GroupService, + useValue: createMock(), + }, + { + provide: BoardNodeService, + useValue: createMock(), + }, + ], + }).compile(); + + strategy = module.get(AutoGroupExternalUuidStrategy); + courseService = module.get(CourseService); + groupService = module.get(GroupService); + boardNodeService = module.get(BoardNodeService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getValue', () => { + describe('when the context is type course', () => { + describe('when the course is synced with a group that has an external ID', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: courseId, + type: ToolContextType.COURSE, + }, + }); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); + const course: Course = courseFactory.buildWithId( + { + name: 'Synced Course', + syncedWithGroup: groupEntity, + }, + courseId + ); + + const group: Group = groupFactory.build({ + id: groupEntity.id, + externalSource: { + externalId: groupEntity.externalSource?.externalId, + }, + }); + + courseService.findById.mockResolvedValue(course); + groupService.findById.mockResolvedValue(group); + + return { + group, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return the external ID from the synced group', async () => { + const { group, schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(group.externalSource?.externalId); + }); + }); + + describe('when the course is synced with a group that has no external ID', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: courseId, + type: ToolContextType.COURSE, + }, + }); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); + const course: Course = courseFactory.buildWithId( + { + name: 'Synced Course', + syncedWithGroup: groupEntity, + }, + courseId + ); + + const group: Group = groupFactory.build({ + id: groupEntity.id, + externalSource: { + externalId: undefined, + }, + }); + + courseService.findById.mockResolvedValue(course); + groupService.findById.mockResolvedValue(group); + + return { + group, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + + describe('when the course is not synced with any group', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: courseId, + type: ToolContextType.COURSE, + }, + }); + const course: Course = courseFactory.buildWithId( + { + name: 'Synced Course', + syncedWithGroup: undefined, + }, + courseId + ); + + courseService.findById.mockResolvedValue(course); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + }); + + describe('when the context is type board element', () => { + describe('when the boards course is synced with a group that has an external ID', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const boardElementId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: boardElementId, + type: ToolContextType.BOARD_ELEMENT, + }, + }); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); + const course: Course = courseFactory.buildWithId( + { + name: 'Synced Course', + syncedWithGroup: groupEntity, + }, + courseId + ); + const group: Group = groupFactory.build({ + id: groupEntity.id, + externalSource: { + externalId: groupEntity.externalSource?.externalId, + }, + }); + + const externalToolElement = externalToolElementFactory.build({ id: boardElementId }); + const board = columnBoardFactory.build({ + context: { + id: course.id, + type: BoardExternalReferenceType.Course, + }, + children: [externalToolElement], + }); + + boardNodeService.findById.mockResolvedValueOnce(externalToolElement); + boardNodeService.findRoot.mockResolvedValueOnce(board); + courseService.findById.mockResolvedValueOnce(course); + groupService.findById.mockResolvedValueOnce(group); + + return { + group, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return the external ID from the synced group', async () => { + const { group, schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(group.externalSource?.externalId); + }); + }); + + describe('when the boards has no course context', () => { + const setup = () => { + const boardElementId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: boardElementId, + type: ToolContextType.BOARD_ELEMENT, + }, + }); + + const externalToolElement = externalToolElementFactory.build({ id: boardElementId }); + const board = columnBoardFactory.build({ + context: { + id: new ObjectId().toHexString(), + type: BoardExternalReferenceType.User, + }, + children: [externalToolElement], + }); + + boardNodeService.findById.mockResolvedValueOnce(externalToolElement); + boardNodeService.findRoot.mockResolvedValueOnce(board); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + + describe('when the boards course is synced with a group that has no external ID', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const boardElementId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: boardElementId, + type: ToolContextType.BOARD_ELEMENT, + }, + }); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); + const course: Course = courseFactory.buildWithId( + { + name: 'Synced Course', + syncedWithGroup: groupEntity, + }, + courseId + ); + const group: Group = groupFactory.build({ + id: groupEntity.id, + externalSource: { + externalId: undefined, + }, + }); + + const externalToolElement = externalToolElementFactory.build({ id: boardElementId }); + const board = columnBoardFactory.build({ + context: { + id: course.id, + type: BoardExternalReferenceType.Course, + }, + children: [externalToolElement], + }); + + boardNodeService.findById.mockResolvedValueOnce(externalToolElement); + boardNodeService.findRoot.mockResolvedValueOnce(board); + courseService.findById.mockResolvedValueOnce(course); + groupService.findById.mockResolvedValueOnce(group); + + return { + group, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + + describe('when the boards course is not synced with any group', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const boardElementId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: boardElementId, + type: ToolContextType.BOARD_ELEMENT, + }, + }); + const course: Course = courseFactory.buildWithId( + { + name: 'Synced Course', + syncedWithGroup: undefined, + }, + courseId + ); + + const externalToolElement = externalToolElementFactory.build({ id: boardElementId }); + const board = columnBoardFactory.build({ + context: { + id: course.id, + type: BoardExternalReferenceType.Course, + }, + children: [externalToolElement], + }); + + boardNodeService.findById.mockResolvedValueOnce(externalToolElement); + boardNodeService.findRoot.mockResolvedValueOnce(board); + courseService.findById.mockResolvedValueOnce(course); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + }); + + describe('when the context is type media board', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + id: courseId, + type: ToolContextType.MEDIA_BOARD, + }, + }); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-uuid.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-uuid.strategy.spec.ts deleted file mode 100644 index ffae6e12e7a..00000000000 --- a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-group-uuid.strategy.spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { CourseService } from '@modules/learnroom'; -import { Course } from '@shared/domain/entity'; -import { Group, GroupService } from '@modules/group'; -import { GroupEntity } from '@modules/group/entity'; -import { courseFactory, groupEntityFactory, groupFactory, setupEntities } from '@shared/testing'; -import { ToolContextType } from '../../../common/enum'; -import { ContextExternalTool } from '../../../context-external-tool/domain'; -import { contextExternalToolFactory } from '../../../context-external-tool/testing'; -import { SchoolExternalTool } from '../../../school-external-tool/domain'; -import { schoolExternalToolFactory } from '../../../school-external-tool/testing'; -import { AutoGroupExternalUuidStrategy } from './auto-group-external-uuid-strategy.service'; - -describe(AutoGroupExternalUuidStrategy.name, () => { - let module: TestingModule; - let strategy: AutoGroupExternalUuidStrategy; - - let courseService: DeepMocked; - let groupService: DeepMocked; - - beforeAll(async () => { - await setupEntities(); - - module = await Test.createTestingModule({ - providers: [ - AutoGroupExternalUuidStrategy, - { - provide: CourseService, - useValue: createMock(), - }, - { - provide: GroupService, - useValue: createMock(), - }, - ], - }).compile(); - - strategy = module.get(AutoGroupExternalUuidStrategy); - courseService = module.get(CourseService); - groupService = module.get(GroupService); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('getValue', () => { - describe('when the context is type course', () => { - const setupExternalTools = () => { - const courseId = new ObjectId().toHexString(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - contextRef: { - id: courseId, - type: ToolContextType.COURSE, - }, - }); - return { courseId, schoolExternalTool, contextExternalTool }; - }; - - describe('when the course is synced with a group that has an external ID', () => { - const setup = () => { - const { courseId, schoolExternalTool, contextExternalTool } = setupExternalTools(); - const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); - const course: Course = courseFactory.buildWithId( - { - name: 'Synced Course', - syncedWithGroup: groupEntity, - }, - courseId - ); - - const groupDo: Group = groupFactory.build({ - id: groupEntity.id, - externalSource: { - externalId: groupEntity.externalSource?.externalId, - }, - }); - - courseService.findById.mockResolvedValue(course); - groupService.findById.mockResolvedValue(groupDo); - - return { - group: groupDo, - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return the external ID from the synced group', async () => { - const { group, schoolExternalTool, contextExternalTool } = setup(); - - const result = await strategy.getValue(schoolExternalTool, contextExternalTool); - - expect(result).toEqual(group.externalSource?.externalId); - }); - }); - - describe('when the course is synced with a group that has no external ID', () => { - const setup = () => { - const { courseId, schoolExternalTool, contextExternalTool } = setupExternalTools(); - - const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); - const course: Course = courseFactory.buildWithId( - { - name: 'Synced Course', - syncedWithGroup: groupEntity, - }, - courseId - ); - - const groupDo: Group = groupFactory.build({ - id: groupEntity.id, - externalSource: { - externalId: undefined, - }, - }); - - courseService.findById.mockResolvedValue(course); - groupService.findById.mockResolvedValue(groupDo); - - return { - group: groupDo, - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return undefined', async () => { - const { schoolExternalTool, contextExternalTool } = setup(); - - const result = await strategy.getValue(schoolExternalTool, contextExternalTool); - - expect(result).toBeUndefined(); - }); - }); - - describe('when the course is not synced with any group', () => { - const setup = () => { - const { courseId, schoolExternalTool, contextExternalTool } = setupExternalTools(); - - const course: Course = courseFactory.buildWithId( - { - name: 'Synced Course', - syncedWithGroup: undefined, - }, - courseId - ); - - courseService.findById.mockResolvedValue(course); - - return { - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return undefined', async () => { - const { schoolExternalTool, contextExternalTool } = setup(); - - const result = await strategy.getValue(schoolExternalTool, contextExternalTool); - - expect(result).toBeUndefined(); - }); - }); - }); - - describe('when the context is type board element', () => { - const setup = () => { - const courseId = new ObjectId().toHexString(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - contextRef: { - id: courseId, - type: ToolContextType.BOARD_ELEMENT, - }, - }); - - return { - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return undefined', async () => { - const { schoolExternalTool, contextExternalTool } = setup(); - - const result = await strategy.getValue(schoolExternalTool, contextExternalTool); - - expect(result).toBeUndefined(); - }); - }); - - describe('when the context is type media board', () => { - const setup = () => { - const courseId = new ObjectId().toHexString(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - contextRef: { - id: courseId, - type: ToolContextType.MEDIA_BOARD, - }, - }); - - return { - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return undefined', async () => { - const { schoolExternalTool, contextExternalTool } = setup(); - - const result = await strategy.getValue(schoolExternalTool, contextExternalTool); - - expect(result).toBeUndefined(); - }); - }); - }); -});