From 0c0f8ed946d30325f2cc7d698627b51bf354b32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 27 Oct 2023 16:33:34 +0200 Subject: [PATCH 1/3] - add strategy pattern to auto parameters - add auto context name for board element --- .../auto-context-id.strategy.spec.ts | 56 ++++ .../auto-context-id.strategy.ts | 11 + .../auto-context-name.strategy.spec.ts | 194 ++++++++++++ .../auto-context-name.strategy.ts | 55 ++++ .../auto-parameter.strategy.ts | 9 + .../auto-school-id.strategy.spec.ts | 56 ++++ .../auto-school-id.strategy.ts | 15 + .../auto-school-number.strategy.spec.ts | 107 +++++++ .../auto-school-number.strategy.ts | 21 ++ .../service/auto-parameter-strategy/index.ts | 5 + .../abstract-launch.strategy.spec.ts | 287 +++++------------- .../abstract-launch.strategy.ts | 90 +++--- .../basic-tool-launch.strategy.spec.ts | 24 +- .../basic-tool-launch.strategy.ts | 0 .../{strategy => launch-strategy}/index.ts | 0 .../lti11-tool-launch.strategy.spec.ts | 30 +- .../lti11-tool-launch.strategy.ts | 20 +- .../oauth2-tool-launch.strategy.spec.ts | 26 +- .../oauth2-tool-launch.strategy.ts | 0 .../tool-launch-params.interface.ts | 2 +- .../tool-launch-strategy.interface.ts | 0 .../service/tool-launch.service.ts | 2 +- .../tool/tool-launch/tool-launch.module.ts | 18 +- 23 files changed, 735 insertions(+), 293 deletions(-) create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.spec.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-parameter.strategy.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.spec.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.spec.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.ts create mode 100644 apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/index.ts rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/abstract-launch.strategy.spec.ts (67%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/abstract-launch.strategy.ts (81%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/basic-tool-launch.strategy.spec.ts (90%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/basic-tool-launch.strategy.ts (100%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/index.ts (100%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/lti11-tool-launch.strategy.spec.ts (96%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/lti11-tool-launch.strategy.ts (92%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/oauth2-tool-launch.strategy.spec.ts (80%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/oauth2-tool-launch.strategy.ts (100%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/tool-launch-params.interface.ts (100%) rename apps/server/src/modules/tool/tool-launch/service/{strategy => launch-strategy}/tool-launch-strategy.interface.ts (100%) diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.spec.ts new file mode 100644 index 00000000000..d1865d231c2 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.spec.ts @@ -0,0 +1,56 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { contextExternalToolFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { AutoContextIdStrategy } from './auto-context-id.strategy'; + +describe(AutoContextIdStrategy.name, () => { + let module: TestingModule; + let strategy: AutoContextIdStrategy; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [AutoContextIdStrategy], + }).compile(); + + strategy = module.get(AutoContextIdStrategy); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getValue', () => { + const setup = () => { + const contextId = 'contextId'; + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + schoolToolRef: { + schoolToolId: schoolExternalTool.id as string, + }, + contextRef: { + id: contextId, + }, + }); + + return { + contextId, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return the context id', () => { + const { contextId, schoolExternalTool, contextExternalTool } = setup(); + + const result: string | undefined = strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(contextId); + }); + }); +}); diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.ts new file mode 100644 index 00000000000..a732d038522 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-id.strategy.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { AutoParameterStrategy } from './auto-parameter.strategy'; + +@Injectable() +export class AutoContextIdStrategy implements AutoParameterStrategy { + getValue(schoolExternalTool: SchoolExternalTool, contextExternalTool: ContextExternalTool): string | undefined { + return contextExternalTool.contextRef.id; + } +} diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts new file mode 100644 index 00000000000..96965d13521 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts @@ -0,0 +1,194 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ColumnBoardService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BoardExternalReferenceType, ColumnBoard, Course } from '@shared/domain'; +import { + columnBoardFactory, + contextExternalToolFactory, + courseFactory, + schoolExternalToolFactory, + setupEntities, +} from '@shared/testing'; +import { ToolContextType } from '../../../common/enum'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { ParameterTypeNotImplementedLoggableException } from '../../error'; +import { AutoContextNameStrategy } from './auto-context-name.strategy'; + +describe(AutoContextNameStrategy.name, () => { + let module: TestingModule; + let strategy: AutoContextNameStrategy; + + let courseService: DeepMocked; + let columnBoardService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + AutoContextNameStrategy, + { + provide: CourseService, + useValue: createMock(), + }, + { + provide: ColumnBoardService, + useValue: createMock(), + }, + ], + }).compile(); + + strategy = module.get(AutoContextNameStrategy); + courseService = module.get(CourseService); + columnBoardService = module.get(ColumnBoardService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getValue', () => { + describe('when the tool context is "course"', () => { + 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: 'testName', + }, + courseId + ); + + courseService.findById.mockResolvedValue(course); + + return { + schoolExternalTool, + contextExternalTool, + course, + }; + }; + + it('should return the course name', async () => { + const { schoolExternalTool, contextExternalTool, course } = setup(); + + const result: string | undefined = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(course.name); + }); + }); + + describe('when the tool context is "board element" and the board context is "course"', () => { + 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 course: Course = courseFactory.buildWithId({ + name: 'testName', + }); + + const columnBoard: ColumnBoard = columnBoardFactory.build({ + context: { + id: course.id, + type: BoardExternalReferenceType.Course, + }, + }); + + courseService.findById.mockResolvedValue(course); + columnBoardService.findById.mockResolvedValue(columnBoard); + + return { + schoolExternalTool, + contextExternalTool, + course, + }; + }; + + it('should return the course name', async () => { + const { schoolExternalTool, contextExternalTool, course } = setup(); + + const result: string | undefined = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(course.name); + }); + }); + + describe('when the tool context is "board element" and the board context is unknown', () => { + 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 columnBoard: ColumnBoard = columnBoardFactory.build({ + context: { + id: new ObjectId().toHexString(), + type: 'unknown' as unknown as BoardExternalReferenceType, + }, + }); + + columnBoardService.findById.mockResolvedValue(columnBoard); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result: string | undefined = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + + describe('when a lookup for a context name is not implemented', () => { + const setup = () => { + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + type: 'unknownContext' as unknown as ToolContextType, + }, + }); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should throw a ParameterNotImplementedLoggableException', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + await expect(strategy.getValue(schoolExternalTool, contextExternalTool)).rejects.toThrow( + ParameterTypeNotImplementedLoggableException + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts new file mode 100644 index 00000000000..1f3d1af05c4 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts @@ -0,0 +1,55 @@ +import { ColumnBoardService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { Injectable } from '@nestjs/common'; +import { BoardExternalReferenceType, ColumnBoard, Course, EntityId } from '@shared/domain'; +import { CustomParameterType, ToolContextType } from '../../../common/enum'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { ParameterTypeNotImplementedLoggableException } from '../../error'; +import { AutoParameterStrategy } from './auto-parameter.strategy'; + +@Injectable() +export class AutoContextNameStrategy implements AutoParameterStrategy { + constructor(private readonly courseService: CourseService, private readonly columnBoardService: ColumnBoardService) {} + + async getValue( + schoolExternalTool: SchoolExternalTool, + contextExternalTool: ContextExternalTool + ): Promise { + switch (contextExternalTool.contextRef.type) { + case ToolContextType.COURSE: { + const courseValue: string = await this.getCourseValue(contextExternalTool.contextRef.id); + + return courseValue; + } + case ToolContextType.BOARD_ELEMENT: { + const boardValue: string | undefined = await this.getBoardValue(contextExternalTool.contextRef.id); + + return boardValue; + } + default: { + throw new ParameterTypeNotImplementedLoggableException( + `${CustomParameterType.AUTO_CONTEXTNAME}/${contextExternalTool.contextRef.type as string}` + ); + } + } + } + + private async getCourseValue(courseId: EntityId): Promise { + const course: Course = await this.courseService.findById(courseId); + + return course.name; + } + + private async getBoardValue(boardId: EntityId): Promise { + const board: ColumnBoard = await this.columnBoardService.findById(boardId); + + if (board.context.type === BoardExternalReferenceType.Course) { + const courseName: string = await this.getCourseValue(board.context.id); + + return courseName; + } + + return undefined; + } +} diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-parameter.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-parameter.strategy.ts new file mode 100644 index 00000000000..5c5efbcc2b1 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-parameter.strategy.ts @@ -0,0 +1,9 @@ +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; + +export interface AutoParameterStrategy { + getValue( + schoolExternalTool: SchoolExternalTool, + contextExternalTool: ContextExternalTool + ): string | Promise | undefined; +} diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.spec.ts new file mode 100644 index 00000000000..2b184c51140 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.spec.ts @@ -0,0 +1,56 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { contextExternalToolFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { AutoSchoolIdStrategy } from './auto-school-id.strategy'; + +describe(AutoSchoolIdStrategy.name, () => { + let module: TestingModule; + let strategy: AutoSchoolIdStrategy; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [AutoSchoolIdStrategy], + }).compile(); + + strategy = module.get(AutoSchoolIdStrategy); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getValue', () => { + const setup = () => { + const schoolId = 'schoolId'; + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + schoolToolRef: { + schoolToolId: schoolExternalTool.id as string, + schoolId, + }, + }); + + return { + schoolId, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return the context id', () => { + const { schoolId, schoolExternalTool, contextExternalTool } = setup(); + + const result: string | undefined = strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(schoolId); + }); + }); +}); diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.ts new file mode 100644 index 00000000000..faad4be07d9 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-id.strategy.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { AutoParameterStrategy } from './auto-parameter.strategy'; + +@Injectable() +export class AutoSchoolIdStrategy implements AutoParameterStrategy { + getValue( + schoolExternalTool: SchoolExternalTool, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + contextExternalTool: ContextExternalTool + ): string | undefined { + return schoolExternalTool.schoolId; + } +} diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.spec.ts new file mode 100644 index 00000000000..91a01bbdd39 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.spec.ts @@ -0,0 +1,107 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LegacySchoolDo } from '@shared/domain'; +import { contextExternalToolFactory, legacySchoolDoFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { AutoSchoolNumberStrategy } from './auto-school-number.strategy'; + +describe(AutoSchoolNumberStrategy.name, () => { + let module: TestingModule; + let strategy: AutoSchoolNumberStrategy; + + let schoolService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + AutoSchoolNumberStrategy, + { + provide: LegacySchoolService, + useValue: createMock(), + }, + ], + }).compile(); + + strategy = module.get(AutoSchoolNumberStrategy); + schoolService = module.get(LegacySchoolService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getValue', () => { + describe('when the school has a school number', () => { + const setup = () => { + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + officialSchoolNumber: 'officialSchoolNumber', + }); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId: school.id as string, + }); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + schoolToolRef: { + schoolToolId: schoolExternalTool.id as string, + schoolId: school.id, + }, + }); + + schoolService.getSchoolById.mockResolvedValue(school); + + return { + school, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return the school number', async () => { + const { school, schoolExternalTool, contextExternalTool } = setup(); + + const result: string | undefined = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toEqual(school.officialSchoolNumber); + }); + }); + + describe('when the school does not have a school number', () => { + const setup = () => { + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + officialSchoolNumber: undefined, + }); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId: school.id as string, + }); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + schoolToolRef: { + schoolToolId: schoolExternalTool.id as string, + schoolId: school.id, + }, + }); + + schoolService.getSchoolById.mockResolvedValue(school); + + return { + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return undefined', async () => { + const { schoolExternalTool, contextExternalTool } = setup(); + + const result: string | undefined = await strategy.getValue(schoolExternalTool, contextExternalTool); + + expect(result).toBeUndefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.ts new file mode 100644 index 00000000000..7d9ad9dad65 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-school-number.strategy.ts @@ -0,0 +1,21 @@ +import { LegacySchoolService } from '@modules/legacy-school'; +import { Injectable } from '@nestjs/common'; +import { LegacySchoolDo } from '@shared/domain'; +import { ContextExternalTool } from '../../../context-external-tool/domain'; +import { SchoolExternalTool } from '../../../school-external-tool/domain'; +import { AutoParameterStrategy } from './auto-parameter.strategy'; + +@Injectable() +export class AutoSchoolNumberStrategy implements AutoParameterStrategy { + constructor(private readonly schoolService: LegacySchoolService) {} + + async getValue( + schoolExternalTool: SchoolExternalTool, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + contextExternalTool: ContextExternalTool + ): Promise { + const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); + + return school.officialSchoolNumber; + } +} diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/index.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/index.ts new file mode 100644 index 00000000000..619a0a6296c --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/index.ts @@ -0,0 +1,5 @@ +export * from './auto-parameter.strategy'; +export * from './auto-school-id.strategy'; +export * from './auto-context-id.strategy'; +export * from './auto-context-name.strategy'; +export * from './auto-school-number.strategy'; diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts similarity index 67% rename from apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts index 7ee237b3e93..04868663671 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts @@ -2,25 +2,15 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Course, EntityId, LegacySchoolDo } from '@shared/domain'; +import { EntityId } from '@shared/domain'; import { contextExternalToolFactory, - courseFactory, customParameterFactory, externalToolFactory, - legacySchoolDoFactory, schoolExternalToolFactory, - setupEntities, } from '@shared/testing'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; import { CustomParameterEntry } from '../../../common/domain'; -import { - CustomParameterLocation, - CustomParameterScope, - CustomParameterType, - ToolContextType, -} from '../../../common/enum'; +import { CustomParameterLocation, CustomParameterScope, CustomParameterType } from '../../../common/enum'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; @@ -33,6 +23,12 @@ import { ToolLaunchDataType, ToolLaunchRequest, } from '../../types'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from '../auto-parameter-strategy'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -69,33 +65,44 @@ class TestLaunchStrategy extends AbstractLaunchStrategy { } } -describe('AbstractLaunchStrategy', () => { +describe(AbstractLaunchStrategy.name, () => { let module: TestingModule; - let launchStrategy: TestLaunchStrategy; + let strategy: TestLaunchStrategy; - let schoolService: DeepMocked; - let courseService: DeepMocked; + let autoSchoolIdStrategy: DeepMocked; + let autoSchoolNumberStrategy: DeepMocked; + let autoContextIdStrategy: DeepMocked; + let autoContextNameStrategy: DeepMocked; beforeAll(async () => { - await setupEntities(); - module = await Test.createTestingModule({ providers: [ TestLaunchStrategy, { - provide: LegacySchoolService, - useValue: createMock(), + provide: AutoSchoolIdStrategy, + useValue: createMock(), + }, + { + provide: AutoSchoolNumberStrategy, + useValue: createMock(), }, { - provide: CourseService, - useValue: createMock(), + provide: AutoContextIdStrategy, + useValue: createMock(), + }, + { + provide: AutoContextNameStrategy, + useValue: createMock(), }, ], }).compile(); - launchStrategy = module.get(TestLaunchStrategy); - schoolService = module.get(LegacySchoolService); - courseService = module.get(CourseService); + strategy = module.get(TestLaunchStrategy); + + autoSchoolIdStrategy = module.get(AutoSchoolIdStrategy); + autoSchoolNumberStrategy = module.get(AutoSchoolNumberStrategy); + autoContextIdStrategy = module.get(AutoContextIdStrategy); + autoContextNameStrategy = module.get(AutoContextNameStrategy); }); afterAll(async () => { @@ -106,6 +113,7 @@ describe('AbstractLaunchStrategy', () => { describe('when parameters of every type are defined', () => { const setup = () => { const schoolId: string = new ObjectId().toHexString(); + const mockedAutoValue = 'mockedAutoValue'; // External Tool const globalCustomParameter = customParameterFactory.build({ @@ -139,6 +147,18 @@ describe('AbstractLaunchStrategy', () => { name: 'autoSchoolNumberParam', type: CustomParameterType.AUTO_SCHOOLNUMBER, }); + const autoContextIdCustomParameter = customParameterFactory.build({ + scope: CustomParameterScope.GLOBAL, + location: CustomParameterLocation.BODY, + name: 'autoSchoolNumberParam', + type: CustomParameterType.AUTO_CONTEXTID, + }); + const autoContextNameCustomParameter = customParameterFactory.build({ + scope: CustomParameterScope.GLOBAL, + location: CustomParameterLocation.BODY, + name: 'autoSchoolNumberParam', + type: CustomParameterType.AUTO_CONTEXTNAME, + }); const externalTool: ExternalTool = externalToolFactory.build({ parameters: [ @@ -147,6 +167,8 @@ describe('AbstractLaunchStrategy', () => { contextCustomParameter, autoSchoolIdCustomParameter, autoSchoolNumberCustomParameter, + autoContextIdCustomParameter, + autoContextNameCustomParameter, ], }); @@ -169,16 +191,6 @@ describe('AbstractLaunchStrategy', () => { parameters: [contextParameterEntry], }); - // Other - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId( - { - officialSchoolNumber: '1234', - }, - schoolId - ); - - schoolService.getSchoolById.mockResolvedValue(school); - const sortFn = (a: PropertyData, b: PropertyData) => { if (a.name < b.name) { return -1; @@ -189,17 +201,24 @@ describe('AbstractLaunchStrategy', () => { return 0; }; + autoSchoolIdStrategy.getValue.mockReturnValueOnce(mockedAutoValue); + autoSchoolNumberStrategy.getValue.mockResolvedValueOnce(mockedAutoValue); + autoContextIdStrategy.getValue.mockReturnValueOnce(mockedAutoValue); + autoContextNameStrategy.getValue.mockResolvedValueOnce(mockedAutoValue); + return { globalCustomParameter, schoolCustomParameter, autoSchoolIdCustomParameter, autoSchoolNumberCustomParameter, + autoContextIdCustomParameter, + autoContextNameCustomParameter, schoolParameterEntry, contextParameterEntry, externalTool, schoolExternalTool, contextExternalTool, - school, + mockedAutoValue, sortFn, }; }; @@ -211,15 +230,17 @@ describe('AbstractLaunchStrategy', () => { contextParameterEntry, autoSchoolIdCustomParameter, autoSchoolNumberCustomParameter, + autoContextIdCustomParameter, + autoContextNameCustomParameter, schoolParameterEntry, externalTool, schoolExternalTool, contextExternalTool, - school, + mockedAutoValue, sortFn, } = setup(); - const result: ToolLaunchData = await launchStrategy.createLaunchData('userId', { + const result: ToolLaunchData = await strategy.createLaunchData('userId', { externalTool, schoolExternalTool, contextExternalTool, @@ -248,135 +269,22 @@ describe('AbstractLaunchStrategy', () => { }, { name: autoSchoolIdCustomParameter.name, - value: school.id as string, + value: mockedAutoValue, location: PropertyLocation.BODY, }, { name: autoSchoolNumberCustomParameter.name, - value: school.officialSchoolNumber as string, + value: mockedAutoValue, location: PropertyLocation.BODY, }, { - name: concreteConfigParameter.name, - value: concreteConfigParameter.value, - location: concreteConfigParameter.location, - }, - ].sort(sortFn), - }); - }); - }); - - describe('when launching with context name parameter for the context "course"', () => { - const setup = () => { - const autoCourseNameCustomParameter = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoCourseNameParam', - type: CustomParameterType.AUTO_CONTEXTNAME, - }); - - const externalTool: ExternalTool = externalToolFactory.build({ - parameters: [autoCourseNameCustomParameter], - }); - - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - contextRef: { - type: ToolContextType.COURSE, - }, - }); - - const course: Course = courseFactory.buildWithId( - { - name: 'testName', - }, - contextExternalTool.contextRef.id - ); - - courseService.findById.mockResolvedValue(course); - - return { - autoCourseNameCustomParameter, - externalTool, - schoolExternalTool, - contextExternalTool, - course, - }; - }; - - it('should return ToolLaunchData with the course name as parameter value', async () => { - const { externalTool, schoolExternalTool, contextExternalTool, autoCourseNameCustomParameter, course } = - setup(); - - const result: ToolLaunchData = await launchStrategy.createLaunchData('userId', { - externalTool, - schoolExternalTool, - contextExternalTool, - }); - - expect(result).toEqual({ - baseUrl: externalTool.config.baseUrl, - type: ToolLaunchDataType.BASIC, - openNewTab: false, - properties: [ - { - name: autoCourseNameCustomParameter.name, - value: course.name, + name: autoContextIdCustomParameter.name, + value: mockedAutoValue, location: PropertyLocation.BODY, }, { - name: concreteConfigParameter.name, - value: concreteConfigParameter.value, - location: concreteConfigParameter.location, - }, - ], - }); - }); - }); - - describe('when launching with context id parameter', () => { - const setup = () => { - const autoContextIdCustomParameter = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoContextIdParam', - type: CustomParameterType.AUTO_CONTEXTID, - }); - - const externalTool: ExternalTool = externalToolFactory.build({ - parameters: [autoContextIdCustomParameter], - }); - - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - - return { - autoContextIdCustomParameter, - externalTool, - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return ToolLaunchData with the context id as parameter value', async () => { - const { externalTool, schoolExternalTool, contextExternalTool, autoContextIdCustomParameter } = setup(); - - const result: ToolLaunchData = await launchStrategy.createLaunchData('userId', { - externalTool, - schoolExternalTool, - contextExternalTool, - }); - - expect(result).toEqual({ - baseUrl: externalTool.config.baseUrl, - type: ToolLaunchDataType.BASIC, - openNewTab: false, - properties: [ - { - name: autoContextIdCustomParameter.name, - value: contextExternalTool.contextRef.id, + name: autoContextNameCustomParameter.name, + value: mockedAutoValue, location: PropertyLocation.BODY, }, { @@ -384,7 +292,7 @@ describe('AbstractLaunchStrategy', () => { value: concreteConfigParameter.value, location: concreteConfigParameter.location, }, - ], + ].sort(sortFn), }); }); }); @@ -413,7 +321,7 @@ describe('AbstractLaunchStrategy', () => { it('should return a ToolLaunchData with no custom parameters', async () => { const { externalTool, schoolExternalTool, contextExternalTool } = setup(); - const result: ToolLaunchData = await launchStrategy.createLaunchData('userId', { + const result: ToolLaunchData = await strategy.createLaunchData('userId', { externalTool, schoolExternalTool, contextExternalTool, @@ -454,11 +362,7 @@ describe('AbstractLaunchStrategy', () => { parameters: [], }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ - officialSchoolNumber: undefined, - }); - - schoolService.getSchoolById.mockResolvedValue(school); + autoSchoolNumberStrategy.getValue.mockResolvedValue(undefined); return { externalTool, @@ -471,7 +375,7 @@ describe('AbstractLaunchStrategy', () => { const { externalTool, schoolExternalTool, contextExternalTool } = setup(); const func = async () => - launchStrategy.createLaunchData('userId', { + strategy.createLaunchData('userId', { externalTool, schoolExternalTool, contextExternalTool, @@ -512,52 +416,7 @@ describe('AbstractLaunchStrategy', () => { const { externalTool, schoolExternalTool, contextExternalTool } = setup(); const func = async () => - launchStrategy.createLaunchData('userId', { - externalTool, - schoolExternalTool, - contextExternalTool, - }); - - await expect(func).rejects.toThrow(ParameterTypeNotImplementedLoggableException); - }); - }); - - describe('when a lookup for a context name is not implemented', () => { - const setup = () => { - const customParameterWithUnknownType = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoContextNameParam', - type: CustomParameterType.AUTO_CONTEXTNAME, - }); - const externalTool: ExternalTool = externalToolFactory.build({ - parameters: [customParameterWithUnknownType], - }); - - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - parameters: [], - }); - - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - contextRef: { - id: new ObjectId().toHexString(), - type: 'unknownContext' as unknown as ToolContextType, - }, - parameters: [], - }); - - return { - externalTool, - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should throw a ParameterNotImplementedLoggableException', async () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); - - const func = async () => - launchStrategy.createLaunchData('userId', { + strategy.createLaunchData('userId', { externalTool, schoolExternalTool, contextExternalTool, @@ -597,7 +456,7 @@ describe('AbstractLaunchStrategy', () => { }); toolLaunchDataDO.properties = [propertyData1, propertyData2]; - const result: ToolLaunchRequest = launchStrategy.createLaunchRequest(toolLaunchDataDO); + const result: ToolLaunchRequest = strategy.createLaunchRequest(toolLaunchDataDO); expect(result).toEqual({ method: LaunchRequestMethod.GET, @@ -622,7 +481,7 @@ describe('AbstractLaunchStrategy', () => { }); toolLaunchDataDO.properties = [bodyProperty1, bodyProperty2]; - const result: ToolLaunchRequest = launchStrategy.createLaunchRequest(toolLaunchDataDO); + const result: ToolLaunchRequest = strategy.createLaunchRequest(toolLaunchDataDO); expect(result.payload).toEqual(expectedPayload); }); @@ -642,7 +501,7 @@ describe('AbstractLaunchStrategy', () => { }); toolLaunchDataDO.properties = [pathProperty, queryProperty]; - const result: ToolLaunchRequest = launchStrategy.createLaunchRequest(toolLaunchDataDO); + const result: ToolLaunchRequest = strategy.createLaunchRequest(toolLaunchDataDO); expect(result.url).toEqual( `https://www.basic-baseurl.com/pre/${pathProperty.value}/post?${queryProperty.name}=${queryProperty.value}` diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts similarity index 81% rename from apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts index 63ba0680734..ce0f07c5731 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts @@ -1,27 +1,41 @@ import { Injectable } from '@nestjs/common'; -import { Course, EntityId, LegacySchoolDo } from '@shared/domain'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; +import { EntityId } from '@shared/domain'; import { URLSearchParams } from 'url'; import { CustomParameter, CustomParameterEntry } from '../../../common/domain'; -import { - CustomParameterLocation, - CustomParameterScope, - CustomParameterType, - ToolContextType, -} from '../../../common/enum'; +import { CustomParameterLocation, CustomParameterScope, CustomParameterType } from '../../../common/enum'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { MissingToolParameterValueLoggableException, ParameterTypeNotImplementedLoggableException } from '../../error'; import { ToolLaunchMapper } from '../../mapper'; import { LaunchRequestMethod, PropertyData, PropertyLocation, ToolLaunchData, ToolLaunchRequest } from '../../types'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoParameterStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from '../auto-parameter-strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; import { IToolLaunchStrategy } from './tool-launch-strategy.interface'; @Injectable() export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { - constructor(private readonly schoolService: LegacySchoolService, private readonly courseService: CourseService) {} + private readonly autoParameterStrategyMap: Map; + + constructor( + autoSchoolIdStrategy: AutoSchoolIdStrategy, + autoSchoolNumberStrategy: AutoSchoolNumberStrategy, + autoContextIdStrategy: AutoContextIdStrategy, + autoContextNameStrategy: AutoContextNameStrategy + ) { + this.autoParameterStrategyMap = new Map([ + [CustomParameterType.AUTO_SCHOOLID, autoSchoolIdStrategy], + [CustomParameterType.AUTO_SCHOOLNUMBER, autoSchoolNumberStrategy], + [CustomParameterType.AUTO_CONTEXTID, autoContextIdStrategy], + [CustomParameterType.AUTO_CONTEXTNAME, autoContextNameStrategy], + ]); + } public async createLaunchData(userId: EntityId, data: IToolLaunchParams): Promise { const launchData: ToolLaunchData = this.buildToolLaunchDataFromExternalTool(data.externalTool); @@ -207,43 +221,29 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { schoolExternalTool: SchoolExternalTool, contextExternalTool: ContextExternalTool ): Promise { - switch (customParameter.type) { - case CustomParameterType.AUTO_SCHOOLID: { - return schoolExternalTool.schoolId; - } - case CustomParameterType.AUTO_CONTEXTID: { - return contextExternalTool.contextRef.id; - } - case CustomParameterType.AUTO_CONTEXTNAME: { - switch (contextExternalTool.contextRef.type) { - case ToolContextType.COURSE: { - const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id); - - return course.name; - } - default: { - throw new ParameterTypeNotImplementedLoggableException( - `${customParameter.type}/${contextExternalTool.contextRef.type as string}` - ); - } - } - } - case CustomParameterType.AUTO_SCHOOLNUMBER: { - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); + if ( + customParameter.type === CustomParameterType.BOOLEAN || + customParameter.type === CustomParameterType.NUMBER || + customParameter.type === CustomParameterType.STRING + ) { + return customParameter.scope === CustomParameterScope.GLOBAL + ? customParameter.default + : matchingParameterEntry?.value; + } - return school.officialSchoolNumber; - } - case CustomParameterType.BOOLEAN: - case CustomParameterType.NUMBER: - case CustomParameterType.STRING: { - return customParameter.scope === CustomParameterScope.GLOBAL - ? customParameter.default - : matchingParameterEntry?.value; - } - default: { - throw new ParameterTypeNotImplementedLoggableException(customParameter.type); - } + const autoParameterStrategy: AutoParameterStrategy | undefined = this.autoParameterStrategyMap.get( + customParameter.type + ); + if (autoParameterStrategy) { + const autoValue: string | undefined = await autoParameterStrategy.getValue( + schoolExternalTool, + contextExternalTool + ); + + return autoValue; } + + throw new ParameterTypeNotImplementedLoggableException(customParameter.type); } private addProperty( diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts similarity index 90% rename from apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts index 3bb95b97755..db80f78498f 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts @@ -1,12 +1,16 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from '../auto-parameter-strategy'; import { BasicToolLaunchStrategy } from './basic-tool-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -19,12 +23,20 @@ describe('BasicToolLaunchStrategy', () => { providers: [ BasicToolLaunchStrategy, { - provide: LegacySchoolService, - useValue: createMock(), + provide: AutoSchoolIdStrategy, + useValue: createMock(), }, { - provide: CourseService, - useValue: createMock(), + provide: AutoSchoolNumberStrategy, + useValue: createMock(), + }, + { + provide: AutoContextIdStrategy, + useValue: createMock(), + }, + { + provide: AutoContextNameStrategy, + useValue: createMock(), }, ], }).compile(); diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts similarity index 100% rename from apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/index.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/index.ts similarity index 100% rename from apps/server/src/modules/tool/tool-launch/service/strategy/index.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/index.ts diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts similarity index 96% rename from apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts index 5113ff3cc76..ee2932cd535 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts @@ -1,4 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PseudonymService } from '@modules/pseudonym/service'; +import { UserService } from '@modules/user'; import { InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Pseudonym, RoleName, UserDO } from '@shared/domain'; @@ -9,10 +11,6 @@ import { userDoFactory, } from '@shared/testing'; import { pseudonymFactory } from '@shared/testing/factory/domainobject/pseudonym.factory'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { PseudonymService } from '@modules/pseudonym/service'; -import { UserService } from '@modules/user'; import { ObjectId } from 'bson'; import { Authorization } from 'oauth-1.0a'; import { LtiMessageType, LtiPrivacyPermission, LtiRole, ToolContextType } from '../../../common/enum'; @@ -20,6 +18,12 @@ import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from '../auto-parameter-strategy'; import { Lti11EncryptionService } from '../lti11-encryption.service'; import { Lti11ToolLaunchStrategy } from './lti11-tool-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -32,7 +36,7 @@ describe('Lti11ToolLaunchStrategy', () => { let pseudonymService: DeepMocked; let lti11EncryptionService: DeepMocked; - beforeEach(async () => { + beforeAll(async () => { module = await Test.createTestingModule({ providers: [ Lti11ToolLaunchStrategy, @@ -49,12 +53,20 @@ describe('Lti11ToolLaunchStrategy', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: AutoSchoolIdStrategy, + useValue: createMock(), + }, + { + provide: AutoSchoolNumberStrategy, + useValue: createMock(), + }, + { + provide: AutoContextIdStrategy, + useValue: createMock(), }, { - provide: CourseService, - useValue: createMock(), + provide: AutoContextNameStrategy, + useValue: createMock(), }, ], }).compile(); diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts similarity index 92% rename from apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts index 09d04e388f3..09516395234 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts @@ -1,15 +1,19 @@ +import { PseudonymService } from '@modules/pseudonym/service'; +import { UserService } from '@modules/user'; import { Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { EntityId, LtiPrivacyPermission, Pseudonym, RoleName, UserDO } from '@shared/domain'; import { RoleReference } from '@shared/domain/domainobject'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { PseudonymService } from '@modules/pseudonym/service'; -import { UserService } from '@modules/user'; import { Authorization } from 'oauth-1.0a'; import { LtiRole } from '../../../common/enum'; import { ExternalTool } from '../../../external-tool/domain'; import { LtiRoleMapper } from '../../mapper'; import { AuthenticationValues, LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from '../auto-parameter-strategy'; import { Lti11EncryptionService } from '../lti11-encryption.service'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -20,10 +24,12 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { private readonly userService: UserService, private readonly pseudonymService: PseudonymService, private readonly lti11EncryptionService: Lti11EncryptionService, - schoolService: LegacySchoolService, - courseService: CourseService + autoSchoolIdStrategy: AutoSchoolIdStrategy, + autoSchoolNumberStrategy: AutoSchoolNumberStrategy, + autoContextIdStrategy: AutoContextIdStrategy, + autoContextNameStrategy: AutoContextNameStrategy ) { - super(schoolService, courseService); + super(autoSchoolIdStrategy, autoSchoolNumberStrategy, autoContextIdStrategy, autoContextNameStrategy); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts similarity index 80% rename from apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts index bd97fafde71..8f81a1d0eb2 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts @@ -1,12 +1,16 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { CourseService } from '@modules/learnroom/service'; -import { LegacySchoolService } from '@modules/legacy-school'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { LaunchRequestMethod, PropertyData } from '../../types'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from '../auto-parameter-strategy'; import { OAuth2ToolLaunchStrategy } from './oauth2-tool-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -14,17 +18,25 @@ describe('OAuth2ToolLaunchStrategy', () => { let module: TestingModule; let strategy: OAuth2ToolLaunchStrategy; - beforeEach(async () => { + beforeAll(async () => { module = await Test.createTestingModule({ providers: [ OAuth2ToolLaunchStrategy, { - provide: LegacySchoolService, - useValue: createMock(), + provide: AutoSchoolIdStrategy, + useValue: createMock(), }, { - provide: CourseService, - useValue: createMock(), + provide: AutoSchoolNumberStrategy, + useValue: createMock(), + }, + { + provide: AutoContextIdStrategy, + useValue: createMock(), + }, + { + provide: AutoContextNameStrategy, + useValue: createMock(), }, ], }).compile(); diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts similarity index 100% rename from apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/tool-launch-params.interface.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts similarity index 100% rename from apps/server/src/modules/tool/tool-launch/service/strategy/tool-launch-params.interface.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts index a6d1b75d9cf..24e368476f5 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/tool-launch-params.interface.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-params.interface.ts @@ -1,6 +1,6 @@ +import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; -import { ContextExternalTool } from '../../../context-external-tool/domain'; export interface IToolLaunchParams { externalTool: ExternalTool; diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/tool-launch-strategy.interface.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-strategy.interface.ts similarity index 100% rename from apps/server/src/modules/tool/tool-launch/service/strategy/tool-launch-strategy.interface.ts rename to apps/server/src/modules/tool/tool-launch/service/launch-strategy/tool-launch-strategy.interface.ts diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts index 46d2efdeb70..abb5598796f 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts @@ -15,7 +15,7 @@ import { IToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, -} from './strategy'; +} from './launch-strategy'; @Injectable() export class ToolLaunchService { diff --git a/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts b/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts index 4ae6a3a38a5..b8ff7623586 100644 --- a/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts +++ b/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts @@ -1,14 +1,21 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { BoardModule } from '@modules/board'; import { LearnroomModule } from '@modules/learnroom'; import { LegacySchoolModule } from '@modules/legacy-school'; import { PseudonymModule } from '@modules/pseudonym'; import { UserModule } from '@modules/user'; +import { forwardRef, Module } from '@nestjs/common'; import { CommonToolModule } from '../common'; import { ContextExternalToolModule } from '../context-external-tool'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; import { Lti11EncryptionService, ToolLaunchService } from './service'; -import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy } from './service/strategy'; +import { + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, +} from './service/auto-parameter-strategy'; +import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy } from './service/launch-strategy'; @Module({ imports: [ @@ -20,13 +27,18 @@ import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrat UserModule, forwardRef(() => PseudonymModule), // i do not like this solution, the root problem is on other place but not detectable for me LearnroomModule, + BoardModule, ], providers: [ ToolLaunchService, + Lti11EncryptionService, BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, - Lti11EncryptionService, + AutoContextIdStrategy, + AutoContextNameStrategy, + AutoSchoolIdStrategy, + AutoSchoolNumberStrategy, ], exports: [ToolLaunchService], }) From 014e2149cd62bdde6b6b46c20abef81442f6c35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Wed, 1 Nov 2023 15:32:12 +0100 Subject: [PATCH 2/3] fix finding of board by child --- .../service/column-board.service.spec.ts | 82 +++++++++++++++++++ .../board/service/column-board.service.ts | 15 ++++ .../auto-context-name.strategy.spec.ts | 21 ++++- .../auto-context-name.strategy.ts | 16 ++-- backup/setup/external_tools.json | 4 +- 5 files changed, 127 insertions(+), 11 deletions(-) diff --git a/apps/server/src/modules/board/service/column-board.service.spec.ts b/apps/server/src/modules/board/service/column-board.service.spec.ts index 6ed6bb6f0a4..97b4a3d2578 100644 --- a/apps/server/src/modules/board/service/column-board.service.spec.ts +++ b/apps/server/src/modules/board/service/column-board.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { BoardExternalReference, BoardExternalReferenceType, @@ -106,6 +107,87 @@ describe(ColumnBoardService.name, () => { }); }); + describe('findByDescendant', () => { + describe('when searching a board for an element', () => { + const setup2 = () => { + const element = richTextElementFactory.build(); + const board: ColumnBoard = columnBoardFactory.build({ children: [element] }); + + boardDoRepo.getAncestorIds.mockResolvedValue([board.id]); + boardDoRepo.findById.mockResolvedValue(board); + + return { + element, + board, + }; + }; + + it('should search by the root id', async () => { + const { element, board } = setup2(); + + await service.findByDescendant(element); + + expect(boardDoRepo.findById).toHaveBeenCalledWith(board.id, 1); + }); + + it('should return the board', async () => { + const { element, board } = setup2(); + + const result = await service.findByDescendant(element); + + expect(result).toEqual(board); + }); + }); + + describe('when searching a board by itself', () => { + const setup2 = () => { + const board: ColumnBoard = columnBoardFactory.build({ children: [] }); + + boardDoRepo.getAncestorIds.mockResolvedValue([]); + boardDoRepo.findById.mockResolvedValue(board); + + return { + board, + }; + }; + + it('should search by the root id', async () => { + const { board } = setup2(); + + await service.findByDescendant(board); + + expect(boardDoRepo.findById).toHaveBeenCalledWith(board.id, 1); + }); + + it('should return the board', async () => { + const { board } = setup2(); + + const result = await service.findByDescendant(board); + + expect(result).toEqual(board); + }); + }); + + describe('when the root node is not a board', () => { + const setup2 = () => { + const element = richTextElementFactory.build(); + + boardDoRepo.getAncestorIds.mockResolvedValue([]); + boardDoRepo.findById.mockResolvedValue(element); + + return { + element, + }; + }; + + it('should throw a NotFoundLoggableException', async () => { + const { element } = setup2(); + + await expect(service.findByDescendant(element)).rejects.toThrow(NotFoundLoggableException); + }); + }); + }); + describe('getBoardObjectTitlesById', () => { describe('when asking for a list of boardObject-ids', () => { const setupBoards = () => { diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index f53f4f5f051..7a455e0fcfa 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -1,6 +1,8 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Injectable } from '@nestjs/common'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { + AnyBoardDo, BoardExternalReference, Card, Column, @@ -34,6 +36,19 @@ export class ColumnBoardService { return ids; } + async findByDescendant(boardDo: AnyBoardDo): Promise { + const ancestorIds: EntityId[] = await this.boardDoRepo.getAncestorIds(boardDo); + const idHierarchy: EntityId[] = [...ancestorIds, boardDo.id]; + const rootId: EntityId = idHierarchy[0]; + const rootBoardDo: AnyBoardDo = await this.boardDoRepo.findById(rootId, 1); + + if (rootBoardDo instanceof ColumnBoard) { + return rootBoardDo; + } + + throw new NotFoundLoggableException(ColumnBoard.name, 'id', rootId); + } + async getBoardObjectTitlesById(boardIds: EntityId[]): Promise> { const titleMap = this.boardDoRepo.getTitlesByIds(boardIds); return titleMap; diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts index 96965d13521..caa02d1c69b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts @@ -1,13 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { ColumnBoardService } from '@modules/board'; +import { ColumnBoardService, ContentElementService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, ColumnBoard, Course } from '@shared/domain'; +import { BoardExternalReferenceType, ColumnBoard, Course, ExternalToolElement } from '@shared/domain'; import { columnBoardFactory, contextExternalToolFactory, courseFactory, + externalToolElementFactory, schoolExternalToolFactory, setupEntities, } from '@shared/testing'; @@ -22,6 +23,7 @@ describe(AutoContextNameStrategy.name, () => { let strategy: AutoContextNameStrategy; let courseService: DeepMocked; + let contentElementService: DeepMocked; let columnBoardService: DeepMocked; beforeAll(async () => { @@ -34,6 +36,10 @@ describe(AutoContextNameStrategy.name, () => { provide: CourseService, useValue: createMock(), }, + { + provide: ContentElementService, + useValue: createMock(), + }, { provide: ColumnBoardService, useValue: createMock(), @@ -43,6 +49,7 @@ describe(AutoContextNameStrategy.name, () => { strategy = module.get(AutoContextNameStrategy); courseService = module.get(CourseService); + contentElementService = module.get(ContentElementService); columnBoardService = module.get(ColumnBoardService); }); @@ -106,6 +113,8 @@ describe(AutoContextNameStrategy.name, () => { name: 'testName', }); + const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const columnBoard: ColumnBoard = columnBoardFactory.build({ context: { id: course.id, @@ -114,7 +123,8 @@ describe(AutoContextNameStrategy.name, () => { }); courseService.findById.mockResolvedValue(course); - columnBoardService.findById.mockResolvedValue(columnBoard); + contentElementService.findById.mockResolvedValue(externalToolElement); + columnBoardService.findByDescendant.mockResolvedValue(columnBoard); return { schoolExternalTool, @@ -143,6 +153,8 @@ describe(AutoContextNameStrategy.name, () => { }, }); + const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const columnBoard: ColumnBoard = columnBoardFactory.build({ context: { id: new ObjectId().toHexString(), @@ -150,7 +162,8 @@ describe(AutoContextNameStrategy.name, () => { }, }); - columnBoardService.findById.mockResolvedValue(columnBoard); + contentElementService.findById.mockResolvedValue(externalToolElement); + columnBoardService.findByDescendant.mockResolvedValue(columnBoard); return { schoolExternalTool, diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts index 1f3d1af05c4..14d296d8b60 100644 --- a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts @@ -1,7 +1,7 @@ -import { ColumnBoardService } from '@modules/board'; +import { ColumnBoardService, ContentElementService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { Injectable } from '@nestjs/common'; -import { BoardExternalReferenceType, ColumnBoard, Course, EntityId } from '@shared/domain'; +import { AnyContentElementDo, BoardExternalReferenceType, ColumnBoard, Course, EntityId } from '@shared/domain'; import { CustomParameterType, ToolContextType } from '../../../common/enum'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; @@ -10,7 +10,11 @@ import { AutoParameterStrategy } from './auto-parameter.strategy'; @Injectable() export class AutoContextNameStrategy implements AutoParameterStrategy { - constructor(private readonly courseService: CourseService, private readonly columnBoardService: ColumnBoardService) {} + constructor( + private readonly courseService: CourseService, + private readonly contentElementService: ContentElementService, + private readonly columnBoardService: ColumnBoardService + ) {} async getValue( schoolExternalTool: SchoolExternalTool, @@ -41,8 +45,10 @@ export class AutoContextNameStrategy implements AutoParameterStrategy { return course.name; } - private async getBoardValue(boardId: EntityId): Promise { - const board: ColumnBoard = await this.columnBoardService.findById(boardId); + private async getBoardValue(elementId: EntityId): Promise { + const element: AnyContentElementDo = await this.contentElementService.findById(elementId); + + const board: ColumnBoard = await this.columnBoardService.findByDescendant(element); if (board.context.type === BoardExternalReferenceType.Course) { const courseName: string = await this.getCourseValue(board.context.id); diff --git a/backup/setup/external_tools.json b/backup/setup/external_tools.json index 22a582f9a09..2a3fd937a37 100644 --- a/backup/setup/external_tools.json +++ b/backup/setup/external_tools.json @@ -76,9 +76,9 @@ } }, "name": "LTI Test Tool", - "url": "https://www.tsugi.org/lti-test/tool.php", + "url": "https://saltire.lti.app", "config_type": "lti11", - "config_baseUrl": "https://www.tsugi.org/lti-test/tool.php", + "config_baseUrl": "https://saltire.lti.app/tool", "config_key": "12345", "config_secret": "secret", "config_lti_message_type": "basic-lti-launch-request", From 8cee1cec4c19f83b44cff4c6fee5f1a10559f226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Thu, 2 Nov 2023 16:39:11 +0100 Subject: [PATCH 3/3] fix test --- .../tool/tool-launch/service/tool-launch.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts index 3330b0c9f0e..e4f9eaa6113 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts @@ -21,7 +21,7 @@ import { IToolLaunchParams, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, -} from './strategy'; +} from './launch-strategy'; import { ToolLaunchService } from './tool-launch.service'; describe('ToolLaunchService', () => {