From f2f0b368e636b997aaea4aa63157bd745c56fc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:13:10 +0200 Subject: [PATCH] N21-1248 Configure External Tools in Boards (#4471) - move tool reference to context external tool module - new endpoint for tool reference from context external tool id - add new tool reference controller - add board-element as context for context external tools --- .../element/external-tool-element.response.ts | 6 +- .../external-tool-element-response.mapper.ts | 2 +- .../service/feathers-roster.service.spec.ts | 16 +- .../service/feathers-roster.service.ts | 6 +- .../common/enum/tool-context-type.enum.ts | 1 + .../tool/common/mapper/context-type.mapper.ts | 1 + .../mapper/tool-status-response.mapper.ts | 14 + .../context-external-tool.module.ts | 27 +- .../api-test/tool-reference.api.spec.ts | 287 ++++++++++++++++++ .../context-external-tool-context.params.ts | 8 +- .../controller/dto/index.ts | 3 + .../tool-configuration-status.response.ts | 0 .../dto}/tool-reference-list.response.ts | 0 .../dto}/tool-reference.response.ts | 1 + .../controller/tool-reference.controller.ts | 68 +++++ .../context-external-tool/domain/index.ts | 1 + .../domain/tool-reference.ts | 0 .../entity/context-external-tool-type.enum.ts | 1 + .../context-external-tool-response.mapper.ts | 25 +- .../context-external-tool/mapper/index.ts | 1 + .../mapper/tool-reference.mapper.ts | 4 +- ...t-external-tool-validation.service.spec.ts | 8 +- ...ontext-external-tool-validation.service.ts | 6 +- .../context-external-tool.service.spec.ts | 6 +- .../service/context-external-tool.service.ts | 4 +- .../context-external-tool/service/index.ts | 1 + .../service/tool-reference.service.spec.ts | 132 ++++++++ .../service/tool-reference.service.ts | 50 +++ .../uc/context-external-tool.uc.spec.ts | 10 +- .../uc/context-external-tool.uc.ts | 14 +- .../tool/context-external-tool/uc/index.ts | 1 + .../uc/tool-reference.uc.spec.ts | 215 +++++++++++++ .../uc/tool-reference.uc.ts | 82 +++++ .../controller/api-test/tool.api.spec.ts | 142 +-------- .../controller/dto/response/index.ts | 3 - .../controller/tool.controller.ts | 38 +-- .../tool/external-tool/domain/index.ts | 1 - .../mapper/external-tool-response.mapper.ts | 24 +- .../tool/external-tool/mapper/index.ts | 1 - .../external-tool-logo-service.spec.ts | 10 +- .../service/external-tool-logo.service.ts | 14 +- .../external-tool-validation.service.spec.ts | 12 +- .../external-tool-validation.service.ts | 4 +- .../service/external-tool.service.spec.ts | 8 +- .../service/external-tool.service.ts | 2 +- .../uc/external-tool-configuration.uc.spec.ts | 28 +- .../uc/external-tool-configuration.uc.ts | 16 +- .../external-tool/uc/external-tool.uc.spec.ts | 4 +- .../tool/external-tool/uc/external-tool.uc.ts | 4 +- .../modules/tool/external-tool/uc/index.ts | 1 - .../uc/tool-reference.uc.spec.ts | 234 -------------- .../external-tool/uc/tool-reference.uc.ts | 102 ------- .../api-test/tool-school.api.spec.ts | 6 +- .../dto/school-external-tool.response.ts | 2 +- ...hool-external-tool-response.mapper.spec.ts | 2 +- .../school-external-tool-response.mapper.ts | 12 +- ...l-external-tool-validation.service.spec.ts | 4 +- ...school-external-tool-validation.service.ts | 4 +- .../school-external-tool.service.spec.ts | 20 +- .../service/school-external-tool.service.ts | 12 +- .../uc/school-external-tool.uc.spec.ts | 12 +- .../uc/school-external-tool.uc.ts | 14 +- .../src/modules/tool/tool-api.module.ts | 10 +- .../strategy/abstract-launch.strategy.spec.ts | 151 +++++++-- .../strategy/abstract-launch.strategy.ts | 19 +- .../lti11-tool-launch.strategy.spec.ts | 4 +- .../strategy/lti11-tool-launch.strategy.ts | 2 +- .../service/tool-launch.service.spec.ts | 32 +- .../service/tool-launch.service.ts | 20 +- .../tool-launch/uc/tool-launch.uc.spec.ts | 10 +- .../tool/tool-launch/uc/tool-launch.uc.ts | 8 +- ...ext-external-tool.repo.integration.spec.ts | 34 ++- .../context-external-tool.repo.ts | 4 + 73 files changed, 1246 insertions(+), 785 deletions(-) create mode 100644 apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts rename apps/server/src/modules/tool/{external-tool/controller/dto/response => context-external-tool/controller/dto}/tool-configuration-status.response.ts (100%) rename apps/server/src/modules/tool/{external-tool/controller/dto/response => context-external-tool/controller/dto}/tool-reference-list.response.ts (100%) rename apps/server/src/modules/tool/{external-tool/controller/dto/response => context-external-tool/controller/dto}/tool-reference.response.ts (96%) create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts rename apps/server/src/modules/tool/{external-tool => context-external-tool}/domain/tool-reference.ts (100%) rename apps/server/src/modules/tool/{external-tool => context-external-tool}/mapper/tool-reference.mapper.ts (80%) create mode 100644 apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts delete mode 100644 apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts delete mode 100644 apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts diff --git a/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts b/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts index 5f51a1a26ec..fc67b7631b6 100644 --- a/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { ContentElementType } from '@shared/domain'; import { TimestampsResponse } from '../timestamps.response'; @@ -7,8 +7,8 @@ export class ExternalToolElementContent { this.contextExternalToolId = props.contextExternalToolId; } - @ApiPropertyOptional() - contextExternalToolId?: string; + @ApiProperty({ type: String, required: true, nullable: true }) + contextExternalToolId: string | null; } export class ExternalToolElementResponse { diff --git a/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts index a907f4eb157..a27cab41d63 100644 --- a/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts @@ -18,7 +18,7 @@ export class ExternalToolElementResponseMapper implements BaseResponseMapper { id: element.id, timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), type: ContentElementType.EXTERNAL_TOOL, - content: new ExternalToolElementContent({ contextExternalToolId: element.contextExternalToolId }), + content: new ExternalToolElementContent({ contextExternalToolId: element.contextExternalToolId ?? null }), }); return result; diff --git a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts index ce4d5144a38..e2a8484d630 100644 --- a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts @@ -2,13 +2,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { DatabaseObjectNotFoundException } from '@mikro-orm/core'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { Course, Pseudonym, RoleName, LegacySchoolDo, UserDO, SchoolEntity } from '@shared/domain'; +import { Course, LegacySchoolDo, Pseudonym, RoleName, SchoolEntity, UserDO } from '@shared/domain'; import { contextExternalToolFactory, courseFactory, externalToolFactory, - pseudonymFactory, legacySchoolDoFactory, + pseudonymFactory, schoolExternalToolFactory, schoolFactory, setupEntities, @@ -249,10 +249,10 @@ describe('FeathersRosterService', () => { ]); contextExternalToolService.findAllByContext.mockResolvedValueOnce([otherContextExternalTool]); contextExternalToolService.findAllByContext.mockResolvedValueOnce([]); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(otherSchoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(otherExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(otherSchoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + externalToolService.findById.mockResolvedValueOnce(otherExternalTool); return { pseudonym, @@ -299,7 +299,7 @@ describe('FeathersRosterService', () => { await service.getUserGroups(pseudonym.pseudonym, clientId); - expect(schoolExternalToolService.getSchoolExternalToolById.mock.calls).toEqual([ + expect(schoolExternalToolService.findById.mock.calls).toEqual([ [schoolExternalTool.id], [otherSchoolExternalTool.id], ]); @@ -310,7 +310,7 @@ describe('FeathersRosterService', () => { await service.getUserGroups(pseudonym.pseudonym, clientId); - expect(externalToolService.findExternalToolById.mock.calls).toEqual([[externalToolId], [otherExternalTool.id]]); + expect(externalToolService.findById.mock.calls).toEqual([[externalToolId], [otherExternalTool.id]]); }); it('should return a group for each course where the tool of the users pseudonym is used', async () => { diff --git a/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts b/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts index a5fd359b6c1..e808a2fc59f 100644 --- a/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts +++ b/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts @@ -179,12 +179,10 @@ export class FeathersRosterService { ); for await (const contextExternalTool of contextExternalTools) { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById( - schoolExternalTool.toolId - ); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); const isRequiredTool: boolean = externalTool.id === externalToolId; if (isRequiredTool) { diff --git a/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts b/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts index 20e57d7bd60..4c930b57397 100644 --- a/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts +++ b/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts @@ -1,3 +1,4 @@ export enum ToolContextType { COURSE = 'course', + BOARD_ELEMENT = 'board-element', } diff --git a/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts b/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts index 00da0a8b36d..bf8fb537924 100644 --- a/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts +++ b/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts @@ -3,6 +3,7 @@ import { ToolContextType } from '../enum'; const typeMapping: Record = { [ToolContextType.COURSE]: AuthorizableReferenceType.Course, + [ToolContextType.BOARD_ELEMENT]: AuthorizableReferenceType.BoardNode, }; export class ContextTypeMapper { diff --git a/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts b/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts new file mode 100644 index 00000000000..c199fc6f307 --- /dev/null +++ b/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts @@ -0,0 +1,14 @@ +import { ToolConfigurationStatusResponse } from '../../context-external-tool/controller/dto/tool-configuration-status.response'; +import { ToolConfigurationStatus } from '../enum'; + +export const statusMapping: Record = { + [ToolConfigurationStatus.LATEST]: ToolConfigurationStatusResponse.LATEST, + [ToolConfigurationStatus.OUTDATED]: ToolConfigurationStatusResponse.OUTDATED, + [ToolConfigurationStatus.UNKNOWN]: ToolConfigurationStatusResponse.UNKNOWN, +}; + +export class ToolStatusResponseMapper { + static mapToResponse(status: ToolConfigurationStatus): ToolConfigurationStatusResponse { + return statusMapping[status]; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts index e3319512fc5..1afd639f1e7 100644 --- a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts +++ b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts @@ -1,25 +1,28 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; import { ContextExternalToolAuthorizableService, ContextExternalToolService, ContextExternalToolValidationService, + ToolReferenceService, } from './service'; -import { CommonToolModule } from '../common'; @Module({ - // TODO: remove authorization module here N21-1055 - imports: [ - CommonToolModule, - ExternalToolModule, - SchoolExternalToolModule, - LoggerModule, - forwardRef(() => AuthorizationModule), + imports: [CommonToolModule, ExternalToolModule, SchoolExternalToolModule, LoggerModule], + providers: [ + ContextExternalToolService, + ContextExternalToolValidationService, + ContextExternalToolAuthorizableService, + ToolReferenceService, + ], + exports: [ + ContextExternalToolService, + ContextExternalToolValidationService, + ContextExternalToolAuthorizableService, + ToolReferenceService, ], - providers: [ContextExternalToolService, ContextExternalToolValidationService, ContextExternalToolAuthorizableService], - exports: [ContextExternalToolService, ContextExternalToolValidationService, ContextExternalToolAuthorizableService], }) export class ContextExternalToolModule {} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts new file mode 100644 index 00000000000..f3072a95131 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts @@ -0,0 +1,287 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Course, Permission, SchoolEntity } from '@shared/domain'; +import { + cleanupCollections, + contextExternalToolEntityFactory, + courseFactory, + externalToolEntityFactory, + schoolExternalToolEntityFactory, + schoolFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { ServerTestModule } from '@src/modules/server'; +import { Response } from 'supertest'; +import { ToolContextType } from '../../../common/enum'; +import { ExternalToolEntity } from '../../../external-tool/entity'; +import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; +import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; +import { ContextExternalToolContextParams, ToolReferenceListResponse, ToolReferenceResponse } from '../dto'; +import { ToolConfigurationStatusResponse } from '../dto/tool-configuration-status.response'; + +describe('ToolReferenceController (API)', () => { + let app: INestApplication; + let em: EntityManager; + + let testApiClient: TestApiClient; + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleRef.createNestApplication(); + + await app.init(); + + em = app.get(EntityManager); + testApiClient = new TestApiClient(app, 'tools/tool-references'); + }); + + afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('[GET] tools/tool-references/:contextType/:contextId', () => { + describe('when user is not authenticated', () => { + it('should return unauthorized', async () => { + const response: Response = await testApiClient.get(`contextType/${new ObjectId().toHexString()}`); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when user has no access to a tool', () => { + const setup = async () => { + const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const params: ContextExternalToolContextParams = { + contextId: course.id, + contextType: ToolContextType.COURSE, + }; + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { loggedInClient, params }; + }; + + it('should filter out the tool', async () => { + const { loggedInClient, params } = await setup(); + + const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ data: [] }); + }); + }); + + describe('when user has access for a tool', () => { + const setup = async () => { + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.CONTEXT_TOOL_USER, + ]); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + logoBase64: 'logoBase64', + }); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + toolVersion: externalToolEntity.version, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + displayName: 'This is a test tool', + toolVersion: schoolExternalToolEntity.toolVersion, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const params: ContextExternalToolContextParams = { + contextId: course.id, + contextType: ToolContextType.COURSE, + }; + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { loggedInClient, params, contextExternalToolEntity, externalToolEntity }; + }; + + it('should return an ToolReferenceListResponse with data', async () => { + const { loggedInClient, params, contextExternalToolEntity, externalToolEntity } = await setup(); + + const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + data: [ + { + contextToolId: contextExternalToolEntity.id, + displayName: contextExternalToolEntity.displayName as string, + status: ToolConfigurationStatusResponse.LATEST, + logoUrl: `http://localhost:3030/api/v3/tools/external-tools/${externalToolEntity.id}/logo`, + openInNewTab: externalToolEntity.openNewTab, + }, + ], + }); + }); + }); + }); + + describe('[GET] tools/tool-references/context-external-tools/:contextExternalToolId', () => { + describe('when user is not authenticated', () => { + it('should return unauthorized', async () => { + const response: Response = await testApiClient.get(`context-external-tools/${new ObjectId().toHexString()}`); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when user has no access to a tool', () => { + const setup = async () => { + const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { + loggedInClient, + contextExternalToolId: contextExternalToolEntity.id, + }; + }; + + it('should filter out the tool', async () => { + const { loggedInClient, contextExternalToolId } = await setup(); + + const response: Response = await loggedInClient.get(`context-external-tools/${contextExternalToolId}`); + + expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN); + }); + }); + + describe('when user has access for a tool', () => { + const setup = async () => { + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.CONTEXT_TOOL_USER, + ]); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + logoBase64: 'logoBase64', + }); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + toolVersion: externalToolEntity.version, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + displayName: 'This is a test tool', + toolVersion: schoolExternalToolEntity.toolVersion, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { + loggedInClient, + contextExternalToolId: contextExternalToolEntity.id, + contextExternalToolEntity, + externalToolEntity, + }; + }; + + it('should return an ToolReferenceListResponse with data', async () => { + const { loggedInClient, contextExternalToolId, contextExternalToolEntity, externalToolEntity } = await setup(); + + const response: Response = await loggedInClient.get(`context-external-tools/${contextExternalToolId}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + contextToolId: contextExternalToolEntity.id, + displayName: contextExternalToolEntity.displayName as string, + status: ToolConfigurationStatusResponse.LATEST, + logoUrl: `http://localhost:3030/api/v3/tools/external-tools/${externalToolEntity.id}/logo`, + openInNewTab: externalToolEntity.openNewTab, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts index 7d20deef026..63850daa22b 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts @@ -8,6 +8,12 @@ export class ContextExternalToolContextParams { contextId!: string; @IsEnum(ToolContextType) - @ApiProperty({ nullable: false, required: true, example: ToolContextType.COURSE }) + @ApiProperty({ + enum: ToolContextType, + enumName: 'ToolContextType', + nullable: false, + required: true, + example: ToolContextType.COURSE, + }) contextType!: ToolContextType; } diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts index dfe16d84244..e6da4bb909f 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts @@ -3,3 +3,6 @@ export * from './context-external-tool-id.params'; export * from './context-external-tool-search-list.response'; export * from './context-external-tool-context.params'; export * from './context-external-tool.response'; +export * from './tool-reference-list.response'; +export * from './tool-reference.response'; +export * from './tool-configuration-status.response'; diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-configuration-status.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-configuration-status.response.ts similarity index 100% rename from apps/server/src/modules/tool/external-tool/controller/dto/response/tool-configuration-status.response.ts rename to apps/server/src/modules/tool/context-external-tool/controller/dto/tool-configuration-status.response.ts diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference-list.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference-list.response.ts similarity index 100% rename from apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference-list.response.ts rename to apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference-list.response.ts diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts similarity index 96% rename from apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference.response.ts rename to apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts index 24844d8bd2a..0ccefffa6ae 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference.response.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts @@ -20,6 +20,7 @@ export class ToolReferenceResponse { @ApiProperty({ enum: ToolConfigurationStatusResponse, + enumName: 'ToolConfigurationStatusResponse', nullable: false, required: true, description: 'The status of the tool', diff --git a/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts b/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts new file mode 100644 index 00000000000..c414f4423de --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts @@ -0,0 +1,68 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ICurrentUser } from '@src/modules/authentication'; +import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ToolReference } from '../domain'; +import { ContextExternalToolResponseMapper } from '../mapper'; +import { ToolReferenceUc } from '../uc'; +import { + ContextExternalToolContextParams, + ContextExternalToolIdParams, + ToolReferenceListResponse, + ToolReferenceResponse, +} from './dto'; + +@ApiTags('Tool') +@Authenticate('jwt') +@Controller('tools/tool-references') +export class ToolReferenceController { + constructor(private readonly toolReferenceUc: ToolReferenceUc) {} + + @Get('context-external-tools/:contextExternalToolId') + @ApiOperation({ summary: 'Get ExternalTool Reference for a given context external tool' }) + @ApiOkResponse({ + description: 'The Tool Reference has been successfully fetched.', + type: ToolReferenceResponse, + }) + @ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' }) + @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) + async getToolReference( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: ContextExternalToolIdParams + ): Promise { + const toolReference: ToolReference = await this.toolReferenceUc.getToolReference( + currentUser.userId, + params.contextExternalToolId + ); + + const toolReferenceResponse: ToolReferenceResponse = + ContextExternalToolResponseMapper.mapToToolReferenceResponse(toolReference); + + return toolReferenceResponse; + } + + @Get('/:contextType/:contextId') + @ApiOperation({ summary: 'Get ExternalTool References for a given context' }) + @ApiOkResponse({ + description: 'The Tool References has been successfully fetched.', + type: ToolReferenceListResponse, + }) + @ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' }) + @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) + async getToolReferencesForContext( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: ContextExternalToolContextParams + ): Promise { + const toolReferences: ToolReference[] = await this.toolReferenceUc.getToolReferencesForContext( + currentUser.userId, + params.contextType, + params.contextId + ); + + const toolReferenceResponses: ToolReferenceResponse[] = + ContextExternalToolResponseMapper.mapToToolReferenceResponses(toolReferences); + const toolReferenceListResponse = new ToolReferenceListResponse(toolReferenceResponses); + + return toolReferenceListResponse; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/index.ts b/apps/server/src/modules/tool/context-external-tool/domain/index.ts index a012e1d4002..557bc04788c 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/index.ts @@ -1,2 +1,3 @@ export * from './context-external-tool.do'; export * from './context-ref'; +export * from './tool-reference'; diff --git a/apps/server/src/modules/tool/external-tool/domain/tool-reference.ts b/apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts similarity index 100% rename from apps/server/src/modules/tool/external-tool/domain/tool-reference.ts rename to apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts diff --git a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts index 4d40ca6e84c..56753c354dc 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts @@ -1,3 +1,4 @@ export enum ContextExternalToolType { COURSE = 'course', + BOARD_ELEMENT = 'boardElement', } diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts index 601c960299d..07610a7a508 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts @@ -1,6 +1,7 @@ +import { ToolStatusResponseMapper } from '../../common/mapper/tool-status-response.mapper'; import { CustomParameterEntryParam, CustomParameterEntryResponse } from '../../school-external-tool/controller/dto'; -import { ContextExternalToolResponse } from '../controller/dto'; -import { ContextExternalTool } from '../domain'; +import { ContextExternalToolResponse, ToolReferenceResponse } from '../controller/dto'; +import { ContextExternalTool, ToolReference } from '../domain'; export class ContextExternalToolResponseMapper { static mapContextExternalToolResponse(contextExternalTool: ContextExternalTool): ContextExternalToolResponse { @@ -33,4 +34,24 @@ export class ContextExternalToolResponseMapper { return mapped; } + + static mapToToolReferenceResponses(toolReferences: ToolReference[]): ToolReferenceResponse[] { + const toolReferenceResponses: ToolReferenceResponse[] = toolReferences.map((toolReference: ToolReference) => + this.mapToToolReferenceResponse(toolReference) + ); + + return toolReferenceResponses; + } + + static mapToToolReferenceResponse(toolReference: ToolReference): ToolReferenceResponse { + const response = new ToolReferenceResponse({ + contextToolId: toolReference.contextToolId, + displayName: toolReference.displayName, + logoUrl: toolReference.logoUrl, + openInNewTab: toolReference.openInNewTab, + status: ToolStatusResponseMapper.mapToResponse(toolReference.status), + }); + + return response; + } } diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/index.ts b/apps/server/src/modules/tool/context-external-tool/mapper/index.ts index 1987491c4e0..427f02a713a 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/index.ts @@ -1,2 +1,3 @@ export * from './context-external-tool-request.mapper'; export * from './context-external-tool-response.mapper'; +export * from './tool-reference.mapper'; diff --git a/apps/server/src/modules/tool/external-tool/mapper/tool-reference.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts similarity index 80% rename from apps/server/src/modules/tool/external-tool/mapper/tool-reference.mapper.ts rename to apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts index ec982467578..be6e6b8ab12 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/tool-reference.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts @@ -1,6 +1,6 @@ -import { ExternalTool, ToolReference } from '../domain'; -import { ContextExternalTool } from '../../context-external-tool/domain'; import { ToolConfigurationStatus } from '../../common/enum'; +import { ExternalTool } from '../../external-tool/domain'; +import { ContextExternalTool, ToolReference } from '../domain'; export class ToolReferenceMapper { static mapToToolReference( diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts index c419154c020..41e849b3d79 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ValidationError } from '@mikro-orm/core'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory } from '@shared/testing'; -import { ValidationError } from '@mikro-orm/core'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; @@ -62,7 +62,7 @@ describe('ContextExternalToolValidationService', () => { describe('when no tool with the name exists in the context', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ displayName: 'Tool 1', @@ -93,9 +93,7 @@ describe('ContextExternalToolValidationService', () => { await service.validate(contextExternalTool); - expect(schoolExternalToolService.getSchoolExternalToolById).toBeCalledWith( - contextExternalTool.schoolToolRef.schoolToolId - ); + expect(schoolExternalToolService.findById).toBeCalledWith(contextExternalTool.schoolToolRef.schoolToolId); }); it('should call commonToolValidationService.checkCustomParameterEntries', async () => { diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts index af6d36840f7..3777273d18e 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts @@ -23,13 +23,11 @@ export class ContextExternalToolValidationService { await this.checkDuplicateInContext(contextExternalTool); - const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( + const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); - const loadedExternalTool: ExternalTool = await this.externalToolService.findExternalToolById( - loadedSchoolExternalTool.toolId - ); + const loadedExternalTool: ExternalTool = await this.externalToolService.findById(loadedSchoolExternalTool.toolId); this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, contextExternalTool); } diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts index 28cb093ae2d..819e2895896 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts @@ -130,7 +130,7 @@ describe('ContextExternalToolService', () => { }); }); - describe('getContextExternalToolById', () => { + describe('findById', () => { describe('when contextExternalToolId is given', () => { const setup = () => { const schoolId: string = legacySchoolDoFactory.buildWithId().id as string; @@ -151,7 +151,7 @@ describe('ContextExternalToolService', () => { it('should return a contextExternalTool', async () => { const { contextExternalTool } = setup(); - const result: ContextExternalTool = await service.getContextExternalToolById(contextExternalTool.id as string); + const result: ContextExternalTool = await service.findById(contextExternalTool.id as string); expect(result).toEqual(contextExternalTool); }); @@ -165,7 +165,7 @@ describe('ContextExternalToolService', () => { it('should throw a not found exception', async () => { setup(); - const func = () => service.getContextExternalToolById('unknownContextExternalToolId'); + const func = () => service.findById('unknownContextExternalToolId'); await expect(func()).rejects.toThrow(NotFoundException); }); diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts index 011e6db2f7a..63618191810 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { ContextExternalToolRepo } from '@shared/repo'; -import { ContextExternalToolQuery } from '../uc/dto/context-external-tool.types'; import { ContextExternalTool, ContextRef } from '../domain'; +import { ContextExternalToolQuery } from '../uc/dto/context-external-tool.types'; @Injectable() export class ContextExternalToolService { @@ -14,7 +14,7 @@ export class ContextExternalToolService { return contextExternalTools; } - async getContextExternalToolById(contextExternalToolId: EntityId): Promise { + async findById(contextExternalToolId: EntityId): Promise { const tool: ContextExternalTool = await this.contextExternalToolRepo.findById(contextExternalToolId); return tool; diff --git a/apps/server/src/modules/tool/context-external-tool/service/index.ts b/apps/server/src/modules/tool/context-external-tool/service/index.ts index 887cfbe7d9d..31fedbe42af 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/index.ts @@ -1,3 +1,4 @@ export * from './context-external-tool.service'; export * from './context-external-tool-validation.service'; export * from './context-external-tool-authorizable.service'; +export * from './tool-reference.service'; diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts new file mode 100644 index 00000000000..e434ab49527 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts @@ -0,0 +1,132 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; +import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ToolReference } from '../domain'; +import { ContextExternalToolService } from './context-external-tool.service'; +import { ToolReferenceService } from './tool-reference.service'; + +describe('ToolReferenceService', () => { + let module: TestingModule; + let service: ToolReferenceService; + + let externalToolService: DeepMocked; + let schoolExternalToolService: DeepMocked; + let contextExternalToolService: DeepMocked; + let commonToolService: DeepMocked; + let externalToolLogoService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ToolReferenceService, + { + provide: ExternalToolService, + useValue: createMock(), + }, + { + provide: SchoolExternalToolService, + useValue: createMock(), + }, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: CommonToolService, + useValue: createMock(), + }, + { + provide: ExternalToolLogoService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ToolReferenceService); + externalToolService = module.get(ExternalToolService); + schoolExternalToolService = module.get(SchoolExternalToolService); + contextExternalToolService = module.get(ContextExternalToolService); + commonToolService = module.get(CommonToolService); + externalToolLogoService = module.get(ExternalToolLogoService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getToolReference', () => { + describe('when a context external tool id is provided', () => { + const setup = () => { + const contextExternalToolId = new ObjectId().toHexString(); + const externalTool = externalToolFactory.buildWithId(); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(undefined, contextExternalToolId); + const logoUrl = 'logoUrl'; + + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + commonToolService.determineToolConfigurationStatus.mockReturnValue(ToolConfigurationStatus.OUTDATED); + externalToolLogoService.buildLogoUrl.mockReturnValue(logoUrl); + + return { + contextExternalToolId, + externalTool, + schoolExternalTool, + contextExternalTool, + logoUrl, + }; + }; + + it('should determine the tool status', async () => { + const { contextExternalToolId, externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.getToolReference(contextExternalToolId); + + expect(commonToolService.determineToolConfigurationStatus).toHaveBeenCalledWith( + externalTool, + schoolExternalTool, + contextExternalTool + ); + }); + + it('should build the logo url', async () => { + const { contextExternalToolId, externalTool } = setup(); + + await service.getToolReference(contextExternalToolId); + + expect(externalToolLogoService.buildLogoUrl).toHaveBeenCalledWith( + '/v3/tools/external-tools/{id}/logo', + externalTool + ); + }); + + it('should return the tool reference', async () => { + const { contextExternalToolId, logoUrl, contextExternalTool, externalTool } = setup(); + + const result: ToolReference = await service.getToolReference(contextExternalToolId); + + expect(result).toEqual({ + logoUrl, + displayName: contextExternalTool.displayName as string, + openInNewTab: externalTool.openNewTab, + status: ToolConfigurationStatus.OUTDATED, + contextToolId: contextExternalToolId, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts new file mode 100644 index 00000000000..02c6a08677e --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; +import { ExternalTool } from '../../external-tool/domain'; +import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ContextExternalTool, ToolReference } from '../domain'; +import { ToolReferenceMapper } from '../mapper'; +import { ContextExternalToolService } from './context-external-tool.service'; + +@Injectable() +export class ToolReferenceService { + constructor( + private readonly externalToolService: ExternalToolService, + private readonly schoolExternalToolService: SchoolExternalToolService, + private readonly contextExternalToolService: ContextExternalToolService, + private readonly commonToolService: CommonToolService, + private readonly externalToolLogoService: ExternalToolLogoService + ) {} + + async getToolReference(contextExternalToolId: EntityId): Promise { + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( + contextExternalToolId + ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( + contextExternalTool.schoolToolRef.schoolToolId + ); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); + + const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + const toolReference: ToolReference = ToolReferenceMapper.mapToToolReference( + externalTool, + contextExternalTool, + status + ); + toolReference.logoUrl = this.externalToolLogoService.buildLogoUrl( + '/v3/tools/external-tools/{id}/logo', + externalTool + ); + + return toolReference; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts index 7dcdea9d16b..801765f80e5 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts @@ -8,10 +8,10 @@ import { LegacyLogger } from '@src/core/logger'; import { Action, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; import { ForbiddenLoggableException } from '@src/modules/authorization/errors/forbidden.loggable-exception'; import { ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalTool } from '../domain'; import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; import { ContextExternalToolUc } from './context-external-tool.uc'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; describe('ContextExternalToolUc', () => { let module: TestingModule; @@ -339,7 +339,7 @@ describe('ContextExternalToolUc', () => { const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); toolPermissionHelper.ensureContextPermissions.mockResolvedValue(); - contextExternalToolService.getContextExternalToolById.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValue(contextExternalTool); return { contextExternalTool, @@ -496,7 +496,7 @@ describe('ContextExternalToolUc', () => { }, }); - contextExternalToolService.getContextExternalToolById.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValue(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockResolvedValue(Promise.resolve()); return { @@ -524,7 +524,7 @@ describe('ContextExternalToolUc', () => { await uc.getContextExternalTool(userId, contextExternalTool.id as string); - expect(contextExternalToolService.getContextExternalToolById).toHaveBeenCalledWith(contextExternalTool.id); + expect(contextExternalToolService.findById).toHaveBeenCalledWith(contextExternalTool.id); }); }); @@ -542,7 +542,7 @@ describe('ContextExternalToolUc', () => { }, }); - contextExternalToolService.getContextExternalToolById.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValue(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockRejectedValue( new ForbiddenLoggableException( userId, diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts index 903b8197251..04002cf9fc6 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission, User } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; import { LegacyLogger } from '@src/core/logger'; -import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; -import { ContextExternalToolDto } from './dto/context-external-tool.types'; -import { ContextExternalTool, ContextRef } from '../domain'; +import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; import { ToolContextType } from '../../common/enum'; import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool, ContextRef } from '../domain'; +import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; +import { ContextExternalToolDto } from './dto/context-external-tool.types'; @Injectable() export class ContextExternalToolUc { @@ -62,9 +62,7 @@ export class ContextExternalToolUc { } async deleteContextExternalTool(userId: EntityId, contextExternalToolId: EntityId): Promise { - const tool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById( - contextExternalToolId - ); + const tool: ContextExternalTool = await this.contextExternalToolService.findById(contextExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]); await this.toolPermissionHelper.ensureContextPermissions(userId, tool, context); @@ -85,7 +83,7 @@ export class ContextExternalToolUc { } async getContextExternalTool(userId: EntityId, contextToolId: EntityId) { - const tool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById(contextToolId); + const tool: ContextExternalTool = await this.contextExternalToolService.findById(contextToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_ADMIN]); await this.toolPermissionHelper.ensureContextPermissions(userId, tool, context); diff --git a/apps/server/src/modules/tool/context-external-tool/uc/index.ts b/apps/server/src/modules/tool/context-external-tool/uc/index.ts index cd34b162bad..12f2a82a9f1 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/index.ts @@ -1 +1,2 @@ export * from './context-external-tool.uc'; +export * from './tool-reference.uc'; diff --git a/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts new file mode 100644 index 00000000000..e0fbdcf6a6a --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts @@ -0,0 +1,215 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission } from '@shared/domain'; +import { contextExternalToolFactory, externalToolFactory } from '@shared/testing'; +import { AuthorizationContextBuilder } from '@src/modules/authorization'; +import { ToolConfigurationStatus, ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ExternalTool } from '../../external-tool/domain'; +import { ContextExternalTool, ToolReference } from '../domain'; +import { ContextExternalToolService, ToolReferenceService } from '../service'; +import { ToolReferenceUc } from './tool-reference.uc'; + +describe('ToolReferenceUc', () => { + let module: TestingModule; + let uc: ToolReferenceUc; + + let contextExternalToolService: DeepMocked; + let toolReferenceService: DeepMocked; + let toolPermissionHelper: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ToolReferenceUc, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: ToolReferenceService, + useValue: createMock(), + }, + { + provide: ToolPermissionHelper, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(ToolReferenceUc); + + contextExternalToolService = module.get(ContextExternalToolService); + toolReferenceService = module.get(ToolReferenceService); + toolPermissionHelper = module.get(ToolPermissionHelper); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getToolReferencesForContext', () => { + describe('when called with a context type and id', () => { + const setup = () => { + const userId = 'userId'; + + const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); + const toolReference: ToolReference = new ToolReference({ + logoUrl: externalTool.logoUrl, + contextToolId: contextExternalTool.id as string, + displayName: contextExternalTool.displayName as string, + status: ToolConfigurationStatus.LATEST, + openInNewTab: externalTool.openNewTab, + }); + + const contextType: ToolContextType = ToolContextType.COURSE; + const contextId = 'contextId'; + + contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); + toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); + toolReferenceService.getToolReference.mockResolvedValue(toolReference); + + return { + userId, + contextType, + contextId, + contextExternalTool, + externalTool, + toolReference, + }; + }; + + it('should call toolPermissionHelper.ensureContextPermissions', async () => { + const { userId, contextType, contextId, contextExternalTool } = setup(); + + await uc.getToolReferencesForContext(userId, contextType, contextId); + + expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( + userId, + contextExternalTool, + AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]) + ); + }); + + it('should return a list of tool references', async () => { + const { userId, contextType, contextId, toolReference } = setup(); + + const result: ToolReference[] = await uc.getToolReferencesForContext(userId, contextType, contextId); + + expect(result).toEqual([toolReference]); + }); + }); + + describe('when user does not have permission to a tool', () => { + const setup = () => { + const userId = 'userId'; + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); + + const contextType: ToolContextType = ToolContextType.COURSE; + const contextId = 'contextId'; + + contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); + toolPermissionHelper.ensureContextPermissions.mockRejectedValueOnce(new ForbiddenException()); + + return { + userId, + contextType, + contextId, + }; + }; + + it('should filter out tool references if a ForbiddenException is thrown', async () => { + const { userId, contextType, contextId } = setup(); + + const result: ToolReference[] = await uc.getToolReferencesForContext(userId, contextType, contextId); + + expect(result).toEqual([]); + }); + }); + }); + + describe('getToolReference', () => { + describe('when called with a context type and id', () => { + const setup = () => { + const userId = 'userId'; + const contextExternalToolId = 'contextExternalToolId'; + + const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( + undefined, + contextExternalToolId + ); + const toolReference: ToolReference = new ToolReference({ + logoUrl: externalTool.logoUrl, + contextToolId: contextExternalTool.id as string, + displayName: contextExternalTool.displayName as string, + status: ToolConfigurationStatus.LATEST, + openInNewTab: externalTool.openNewTab, + }); + + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); + toolReferenceService.getToolReference.mockResolvedValue(toolReference); + + return { + userId, + contextExternalTool, + externalTool, + toolReference, + contextExternalToolId, + }; + }; + + it('should call toolPermissionHelper.ensureContextPermissions', async () => { + const { userId, contextExternalToolId, contextExternalTool } = setup(); + + await uc.getToolReference(userId, contextExternalToolId); + + expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( + userId, + contextExternalTool, + AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]) + ); + }); + + it('should return a list of tool references', async () => { + const { userId, contextExternalToolId, toolReference } = setup(); + + const result: ToolReference = await uc.getToolReference(userId, contextExternalToolId); + + expect(result).toEqual(toolReference); + }); + }); + + describe('when user does not have permission to a tool', () => { + const setup = () => { + const userId = 'userId'; + const contextExternalToolId = 'contextExternalToolId'; + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( + undefined, + contextExternalToolId + ); + const error = new ForbiddenException(); + + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + toolPermissionHelper.ensureContextPermissions.mockRejectedValueOnce(error); + + return { + userId, + contextExternalToolId, + error, + }; + }; + + it('should filter out tool references if a ForbiddenException is thrown', async () => { + const { userId, contextExternalToolId, error } = setup(); + + await expect(uc.getToolReference(userId, contextExternalToolId)).rejects.toThrow(error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts new file mode 100644 index 00000000000..c044e01dfeb --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId, Permission } from '@shared/domain'; +import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; +import { ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool, ContextRef, ToolReference } from '../domain'; +import { ContextExternalToolService, ToolReferenceService } from '../service'; + +@Injectable() +export class ToolReferenceUc { + constructor( + private readonly contextExternalToolService: ContextExternalToolService, + private readonly toolReferenceService: ToolReferenceService, + private readonly toolPermissionHelper: ToolPermissionHelper + ) {} + + async getToolReferencesForContext( + userId: EntityId, + contextType: ToolContextType, + contextId: EntityId + ): Promise { + const contextRef = new ContextRef({ type: contextType, id: contextId }); + + const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext( + contextRef + ); + + const toolReferencesPromises: Promise[] = contextExternalTools.map( + async (contextExternalTool: ContextExternalTool) => this.tryBuildToolReference(userId, contextExternalTool) + ); + + const toolReferencesWithNull: (ToolReference | null)[] = await Promise.all(toolReferencesPromises); + const filteredToolReferences: ToolReference[] = toolReferencesWithNull.filter( + (toolReference: ToolReference | null): toolReference is ToolReference => toolReference !== null + ); + + return filteredToolReferences; + } + + private async tryBuildToolReference( + userId: EntityId, + contextExternalTool: ContextExternalTool + ): Promise { + try { + await this.ensureToolPermissions(userId, contextExternalTool); + + const toolReference: ToolReference = await this.toolReferenceService.getToolReference( + contextExternalTool.id as string + ); + + return toolReference; + } catch (e: unknown) { + return null; + } + } + + async getToolReference(userId: EntityId, contextExternalToolId: EntityId): Promise { + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( + contextExternalToolId + ); + + await this.ensureToolPermissions(userId, contextExternalTool); + + const toolReference: ToolReference = await this.toolReferenceService.getToolReference( + contextExternalTool.id as string + ); + + return toolReference; + } + + private async ensureToolPermissions(userId: EntityId, contextExternalTool: ContextExternalTool): Promise { + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + + const promise: Promise = this.toolPermissionHelper.ensureContextPermissions( + userId, + contextExternalTool, + context + ); + + return promise; + } +} diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index 6498f66fc1f..0d0c3fb08dc 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -1,41 +1,27 @@ +import { Loaded } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Course, Permission, SchoolEntity } from '@shared/domain'; +import { Permission } from '@shared/domain'; import { cleanupCollections, - contextExternalToolEntityFactory, - courseFactory, externalToolEntityFactory, externalToolFactory, - schoolExternalToolEntityFactory, - schoolFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; +import { ServerTestModule } from '@src/modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Response } from 'supertest'; -import { Loaded } from '@mikro-orm/core'; -import { ServerTestModule } from '@src/modules/server'; import { CustomParameterLocationParams, CustomParameterScopeTypeParams, CustomParameterTypeParams, ToolConfigType, - ToolContextType, } from '../../../common/enum'; -import { - ExternalToolCreateParams, - ExternalToolResponse, - ExternalToolSearchListResponse, - ToolConfigurationStatusResponse, - ToolReferenceListResponse, -} from '../dto'; -import { ContextExternalToolContextParams } from '../../../context-external-tool/controller/dto'; import { ExternalToolEntity } from '../../entity'; -import { ContextExternalToolEntity, ContextExternalToolType } from '../../../context-external-tool/entity'; -import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; +import { ExternalToolCreateParams, ExternalToolResponse, ExternalToolSearchListResponse } from '../dto'; describe('ToolController (API)', () => { let app: INestApplication; @@ -597,126 +583,6 @@ describe('ToolController (API)', () => { }); }); - describe('[GET] tools/external-tools/:contextType/:contextId/references', () => { - describe('when user is not authenticated', () => { - it('should return unauthorized', async () => { - const response: Response = await testApiClient.get(`contextType/${new ObjectId().toHexString()}/references`); - - expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); - }); - }); - - describe('when user has no access to a tool', () => { - const setup = async () => { - const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); - const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); - const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - school, - tool: externalToolEntity, - }); - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ - schoolTool: schoolExternalToolEntity, - contextId: course.id, - contextType: ContextExternalToolType.COURSE, - }); - - await em.persistAndFlush([ - school, - adminAccount, - adminUser, - course, - externalToolEntity, - schoolExternalToolEntity, - contextExternalToolEntity, - ]); - em.clear(); - - const params: ContextExternalToolContextParams = { - contextId: course.id, - contextType: ToolContextType.COURSE, - }; - - const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); - - return { loggedInClient, params }; - }; - - it('should filter out the tool', async () => { - const { loggedInClient, params } = await setup(); - - const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}/references`); - - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.body).toEqual({ data: [] }); - }); - }); - - describe('when user has access for a tool', () => { - const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.CONTEXT_TOOL_USER, - ]); - const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ logoUrl: undefined }); - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - school, - tool: externalToolEntity, - toolVersion: externalToolEntity.version, - }); - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ - schoolTool: schoolExternalToolEntity, - contextId: course.id, - contextType: ContextExternalToolType.COURSE, - displayName: 'This is a test tool', - toolVersion: schoolExternalToolEntity.toolVersion, - }); - - await em.persistAndFlush([ - school, - adminAccount, - adminUser, - course, - externalToolEntity, - schoolExternalToolEntity, - contextExternalToolEntity, - ]); - em.clear(); - - const params: ContextExternalToolContextParams = { - contextId: course.id, - contextType: ToolContextType.COURSE, - }; - - const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); - - return { loggedInClient, params, contextExternalToolEntity, externalToolEntity }; - }; - - it('should return an ToolReferenceListResponse with data', async () => { - const { loggedInClient, params, contextExternalToolEntity, externalToolEntity } = await setup(); - - const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}/references`); - - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.body).toEqual({ - data: [ - { - contextToolId: contextExternalToolEntity.id, - displayName: contextExternalToolEntity.displayName as string, - status: ToolConfigurationStatusResponse.LATEST, - logoUrl: externalToolEntity.logoUrl, - openInNewTab: externalToolEntity.openNewTab, - }, - ], - }); - }); - }); - }); - describe('[GET] tools/external-tools/:externalToolId/logo', () => { const setup = async () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.withBase64Logo().buildWithId(); diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts index fbae39a8b33..e9e5fafa376 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts @@ -1,10 +1,7 @@ export * from './config'; export * from './external-tool.response'; -export * from './tool-reference.response'; export * from './custom-parameter.response'; -export * from './tool-reference-list.response'; export * from './external-tool-search-list.response'; -export * from './tool-configuration-status.response'; export * from './context-external-tool-configuration-template.response'; export * from './context-external-tool-configuration-template-list.response'; export * from './school-external-tool-configuration-template.response'; diff --git a/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts b/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts index 3e6ac38fedc..4c3658d8025 100644 --- a/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts +++ b/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts @@ -18,23 +18,20 @@ import { ICurrentUser } from '@src/modules/authentication'; import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { Response } from 'express'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { ContextExternalToolContextParams } from '../../context-external-tool/controller/dto'; -import { ExternalTool, ToolReference } from '../domain'; +import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { ExternalToolRequestMapper, ExternalToolResponseMapper } from '../mapper'; -import { ExternalToolCreate, ExternalToolUc, ExternalToolUpdate, ToolReferenceUc } from '../uc'; +import { ExternalToolLogoService } from '../service'; +import { ExternalToolCreate, ExternalToolUc, ExternalToolUpdate } from '../uc'; import { ExternalToolCreateParams, + ExternalToolIdParams, ExternalToolResponse, ExternalToolSearchListResponse, ExternalToolSearchParams, ExternalToolUpdateParams, SortExternalToolParams, - ExternalToolIdParams, - ToolReferenceListResponse, - ToolReferenceResponse, } from './dto'; -import { ExternalToolLogoService } from '../service'; @ApiTags('Tool') @Authenticate('jwt') @@ -43,7 +40,6 @@ export class ToolController { constructor( private readonly externalToolUc: ExternalToolUc, private readonly externalToolDOMapper: ExternalToolRequestMapper, - private readonly toolReferenceUc: ToolReferenceUc, private readonly logger: LegacyLogger, private readonly externalToolLogoService: ExternalToolLogoService ) {} @@ -156,32 +152,6 @@ export class ToolController { return promise; } - @Get('/:contextType/:contextId/references') - @ApiOperation({ summary: 'Get ExternalTool References for a given context' }) - @ApiOkResponse({ - description: 'The Tool References has been successfully fetched.', - type: ToolReferenceListResponse, - }) - @ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' }) - @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) - async getToolReferences( - @CurrentUser() currentUser: ICurrentUser, - @Param() params: ContextExternalToolContextParams - ): Promise { - const toolReferences: ToolReference[] = await this.toolReferenceUc.getToolReferences( - currentUser.userId, - params.contextType, - params.contextId, - '/v3/tools/external-tools/{id}/logo' - ); - - const toolReferenceResponses: ToolReferenceResponse[] = - ExternalToolResponseMapper.mapToToolReferenceResponses(toolReferences); - const toolReferenceListResponse = new ToolReferenceListResponse(toolReferenceResponses); - - return toolReferenceListResponse; - } - @Get('/:externalToolId/logo') @ApiOperation({ summary: 'Gets the logo of an external tool.' }) @ApiOkResponse({ diff --git a/apps/server/src/modules/tool/external-tool/domain/index.ts b/apps/server/src/modules/tool/external-tool/domain/index.ts index 9eaf1f03cbb..e5a1dab735d 100644 --- a/apps/server/src/modules/tool/external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/external-tool/domain/index.ts @@ -1,3 +1,2 @@ export * from './external-tool.do'; export * from './config'; -export * from './tool-reference'; diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts index 2885c2ea0c0..b2035e66477 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts @@ -8,16 +8,14 @@ import { CustomParameterType, CustomParameterTypeParams, } from '../../common/enum'; -import { statusMapping } from '../../school-external-tool/mapper'; import { BasicToolConfigResponse, CustomParameterResponse, ExternalToolResponse, Lti11ToolConfigResponse, Oauth2ToolConfigResponse, - ToolReferenceResponse, } from '../controller/dto'; -import { BasicToolConfig, ExternalTool, Lti11ToolConfig, Oauth2ToolConfig, ToolReference } from '../domain'; +import { BasicToolConfig, ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '../domain'; const scopeMapping: Record = { [CustomParameterScope.GLOBAL]: CustomParameterScopeTypeParams.GLOBAL, @@ -98,24 +96,4 @@ export class ExternalToolResponseMapper { }; }); } - - static mapToToolReferenceResponses(toolReferences: ToolReference[]): ToolReferenceResponse[] { - const toolReferenceResponses: ToolReferenceResponse[] = toolReferences.map((toolReference: ToolReference) => - this.mapToToolReferenceResponse(toolReference) - ); - - return toolReferenceResponses; - } - - private static mapToToolReferenceResponse(toolReference: ToolReference): ToolReferenceResponse { - const response = new ToolReferenceResponse({ - contextToolId: toolReference.contextToolId, - displayName: toolReference.displayName, - logoUrl: toolReference.logoUrl, - openInNewTab: toolReference.openInNewTab, - status: statusMapping[toolReference.status], - }); - - return response; - } } diff --git a/apps/server/src/modules/tool/external-tool/mapper/index.ts b/apps/server/src/modules/tool/external-tool/mapper/index.ts index 92aff5e73c9..4149a17a519 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/index.ts @@ -1,3 +1,2 @@ -export * from './tool-reference.mapper'; export * from './external-tool-request.mapper'; export * from './external-tool-response.mapper'; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts index 57acd50122f..c53154098a5 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts @@ -1,4 +1,4 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -9,8 +9,8 @@ import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { - ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoFetchedLoggable, + ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoNotFoundLoggableException, ExternalToolLogoSizeExceededLoggableException, ExternalToolLogoWrongFileTypeLoggableException, @@ -329,7 +329,7 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); return { externalToolId: externalTool.id as string, @@ -355,7 +355,7 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.buildWithId({ logo: 'notAValidBase64File' }); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); return { externalToolId: externalTool.id as string, @@ -375,7 +375,7 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); return { externalToolId: externalTool.id as string, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts index f2518e65a3a..b39684fbd1b 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts @@ -1,19 +1,19 @@ +import { HttpService } from '@nestjs/axios'; import { HttpException, Inject } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { Logger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import { lastValueFrom } from 'rxjs'; -import { HttpService } from '@nestjs/axios'; -import { Logger } from '@src/core/logger'; -import { EntityId } from '@shared/domain'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; +import { ExternalToolLogo } from '../domain/external-tool-logo'; import { ExternalToolLogoFetchedLoggable, + ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoNotFoundLoggableException, ExternalToolLogoSizeExceededLoggableException, ExternalToolLogoWrongFileTypeLoggableException, - ExternalToolLogoFetchFailedLoggableException, } from '../loggable'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; -import { ExternalToolLogo } from '../domain/external-tool-logo'; import { ExternalToolService } from './external-tool.service'; const contentTypeDetector: Record = { @@ -95,7 +95,7 @@ export class ExternalToolLogoService { } async getExternalToolBinaryLogo(toolId: EntityId): Promise { - const tool: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const tool: ExternalTool = await this.externalToolService.findById(toolId); if (!tool.logo) { throw new ExternalToolLogoNotFoundLoggableException(toolId); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts index 8f5f1607df6..42efcd6559a 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts @@ -4,10 +4,10 @@ import { ValidationError } from '@shared/common'; import { externalToolFactory } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; +import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolParameterValidationService } from './external-tool-parameter-validation.service'; import { ExternalToolValidationService } from './external-tool-validation.service'; import { ExternalToolService } from './external-tool.service'; -import { ExternalToolLogoService } from './external-tool-logo.service'; describe('ExternalToolValidationService', () => { let module: TestingModule; @@ -232,7 +232,7 @@ describe('ExternalToolValidationService', () => { .buildWithId(); externalOauthTool.id = 'toolId'; - externalToolService.findExternalToolById.mockResolvedValue(externalOauthTool); + externalToolService.findById.mockResolvedValue(externalOauthTool); return { externalOauthTool, @@ -266,7 +266,7 @@ describe('ExternalToolValidationService', () => { .withOauth2Config({ clientId: 'ClientId', clientSecret: 'secret' }) .buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(existingExternalOauthTool); + externalToolService.findById.mockResolvedValue(existingExternalOauthTool); const newExternalTool: ExternalTool = externalToolFactory.buildWithId(); @@ -296,7 +296,7 @@ describe('ExternalToolValidationService', () => { .withOauth2Config({ clientId: 'ClientId', clientSecret: 'secret' }) .buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalOauthTool); + externalToolService.findById.mockResolvedValue(externalOauthTool); return { externalOauthTool }; }; @@ -318,7 +318,7 @@ describe('ExternalToolValidationService', () => { const existingExternalOauthToolDOWithDifferentClientId: ExternalTool = externalToolFactory .withOauth2Config({ clientId: 'DifferentClientId', clientSecret: 'secret' }) .buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(existingExternalOauthToolDOWithDifferentClientId); + externalToolService.findById.mockResolvedValue(existingExternalOauthToolDOWithDifferentClientId); return { externalOauthTool, @@ -344,7 +344,7 @@ describe('ExternalToolValidationService', () => { const externalLtiToolDO: ExternalTool = externalToolFactory.withLti11Config().buildWithId(); externalLtiToolDO.id = 'toolId'; - externalToolService.findExternalToolById.mockResolvedValue(externalLtiToolDO); + externalToolService.findById.mockResolvedValue(externalLtiToolDO); return { externalLtiToolDO, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts index 90b8307dc7e..434e7fac86e 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts @@ -2,9 +2,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { ValidationError } from '@shared/common'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; +import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolParameterValidationService } from './external-tool-parameter-validation.service'; import { ExternalToolService } from './external-tool.service'; -import { ExternalToolLogoService } from './external-tool-logo.service'; @Injectable() export class ExternalToolValidationService { @@ -32,7 +32,7 @@ export class ExternalToolValidationService { await this.externalToolParameterValidationService.validateCommon(externalTool); - const loadedTool: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const loadedTool: ExternalTool = await this.externalToolService.findById(toolId); if ( ExternalTool.isOauth2Config(loadedTool.config) && externalTool.config && diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts index d2913e5401a..4db2a5be0b0 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts @@ -308,7 +308,7 @@ describe('ExternalToolService', () => { }); }); - describe('findExternalToolById', () => { + describe('findById', () => { describe('when external tool id is set', () => { const setup = () => { const { externalTool } = createTools(); @@ -320,7 +320,7 @@ describe('ExternalToolService', () => { it('should get domain object', async () => { const { externalTool } = setup(); - const result: ExternalTool = await service.findExternalToolById('toolId'); + const result: ExternalTool = await service.findById('toolId'); expect(result).toEqual(externalTool); }); @@ -340,7 +340,7 @@ describe('ExternalToolService', () => { it('should get domain object and add external oauth2 data', async () => { const { externalTool, oauth2ToolConfig } = setup(); - const result: ExternalTool = await service.findExternalToolById('toolId'); + const result: ExternalTool = await service.findById('toolId'); expect(result).toEqual({ ...externalTool, config: oauth2ToolConfig }); }); @@ -362,7 +362,7 @@ describe('ExternalToolService', () => { it('should throw UnprocessableEntityException ', async () => { const { externalTool } = setup(); - const func = () => service.findExternalToolById('toolId'); + const func = () => service.findById('toolId'); await expect(func()).rejects.toThrow(`Could not resolve oauth2Config of tool ${externalTool.name}.`); }); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index 2a53f8aae45..fcc1a7e2d5c 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -75,7 +75,7 @@ export class ExternalToolService { return tools; } - async findExternalToolById(id: EntityId): Promise { + async findById(id: EntityId): Promise { const tool: ExternalTool = await this.externalToolRepo.findById(id); if (ExternalTool.isOauth2Config(tool.config)) { try { diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts index 5490f0c546b..0ed3a3317f8 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts @@ -12,14 +12,14 @@ import { } from '@shared/testing'; import { AuthorizationContextBuilder } from '@src/modules/authorization'; import { CustomParameterScope, ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { ContextExternalToolService } from '../../context-external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ExternalTool } from '../domain'; -import { ExternalToolLogoService, ExternalToolService, ExternalToolConfigurationService } from '../service'; +import { ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolService } from '../service'; import { ExternalToolConfigurationUc } from './external-tool-configuration.uc'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; describe('ExternalToolConfigurationUc', () => { let module: TestingModule; @@ -439,8 +439,8 @@ describe('ExternalToolConfigurationUc', () => { schoolExternalToolId ); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { externalTool, @@ -478,7 +478,7 @@ describe('ExternalToolConfigurationUc', () => { schoolExternalToolId ); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); toolPermissionHelper.ensureSchoolPermissions.mockImplementation(() => { throw new UnauthorizedException(); }); @@ -512,8 +512,8 @@ describe('ExternalToolConfigurationUc', () => { schoolExternalToolId ); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { schoolExternalToolId, @@ -553,9 +553,9 @@ describe('ExternalToolConfigurationUc', () => { contextExternalToolId ); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { externalTool, @@ -593,7 +593,7 @@ describe('ExternalToolConfigurationUc', () => { contextExternalToolId ); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockImplementation(() => { throw new UnauthorizedException(); }); @@ -632,9 +632,9 @@ describe('ExternalToolConfigurationUc', () => { contextExternalToolId ); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { contextExternalToolId, diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts index 9607beb84df..16dd9626d0c 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts @@ -4,14 +4,14 @@ import { EntityId, Permission } from '@shared/domain'; import { Page } from '@shared/domain/domainobject/page'; import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; import { CustomParameterScope, ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { ContextExternalToolService } from '../../context-external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ExternalTool } from '../domain'; -import { ExternalToolLogoService, ExternalToolService, ExternalToolConfigurationService } from '../service'; +import { ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolService } from '../service'; import { ContextExternalToolTemplateInfo } from './dto'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; @Injectable() export class ExternalToolConfigurationUc { @@ -117,14 +117,12 @@ export class ExternalToolConfigurationUc { userId: EntityId, schoolExternalToolId: EntityId ): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(schoolExternalTool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); if (externalTool.isHidden) { throw new NotFoundException('Could not find the Tool Template'); @@ -139,18 +137,18 @@ export class ExternalToolConfigurationUc { userId: EntityId, contextExternalToolId: EntityId ): Promise { - const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById( + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( contextExternalToolId ); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_ADMIN]); await this.toolPermissionHelper.ensureContextPermissions(userId, contextExternalTool, context); - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(schoolExternalTool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); if (externalTool.isHidden) { throw new NotFoundException('Could not find the Tool Template'); diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts index d0b02f1e4f3..f2baadd885c 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts @@ -301,7 +301,7 @@ describe('ExternalToolUc', () => { it('should fetch a tool', async () => { const { currentUser } = setupAuthorization(); const { externalTool, toolId } = setup(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const result: ExternalTool = await uc.getExternalTool(currentUser.userId, toolId); @@ -327,7 +327,7 @@ describe('ExternalToolUc', () => { }); externalToolService.updateExternalTool.mockResolvedValue(updatedExternalToolDO); - externalToolService.findExternalToolById.mockResolvedValue(new ExternalTool(externalToolDOtoUpdate)); + externalToolService.findById.mockResolvedValue(new ExternalTool(externalToolDOtoUpdate)); return { externalTool, diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts index 240977b2b38..3fb81e4f74a 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts @@ -35,7 +35,7 @@ export class ExternalToolUc { await this.toolValidationService.validateUpdate(toolId, externalTool); - const loaded: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const loaded: ExternalTool = await this.externalToolService.findById(toolId); const configToUpdate: ExternalToolConfig = { ...loaded.config, ...externalTool.config }; const toUpdate: ExternalTool = new ExternalTool({ ...loaded, @@ -63,7 +63,7 @@ export class ExternalToolUc { async getExternalTool(userId: EntityId, toolId: EntityId): Promise { await this.ensurePermission(userId, Permission.TOOL_ADMIN); - const tool: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const tool: ExternalTool = await this.externalToolService.findById(toolId); return tool; } diff --git a/apps/server/src/modules/tool/external-tool/uc/index.ts b/apps/server/src/modules/tool/external-tool/uc/index.ts index 46f3a860080..0a61273b29b 100644 --- a/apps/server/src/modules/tool/external-tool/uc/index.ts +++ b/apps/server/src/modules/tool/external-tool/uc/index.ts @@ -1,4 +1,3 @@ export * from './dto'; export * from './external-tool.uc'; -export * from './tool-reference.uc'; export * from './external-tool-configuration.uc'; diff --git a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts deleted file mode 100644 index e06c34e5e8b..00000000000 --- a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { ForbiddenException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain'; -import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { AuthorizationContextBuilder } from '@src/modules/authorization'; -import { ToolReferenceUc } from './tool-reference.uc'; -import { ToolConfigurationStatus, ToolContextType } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ExternalTool, ToolReference } from '../domain'; -import { ExternalToolLogoService, ExternalToolService } from '../service'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; - -describe('ToolReferenceUc', () => { - let module: TestingModule; - let uc: ToolReferenceUc; - - let externalToolService: DeepMocked; - let schoolExternalToolService: DeepMocked; - let contextExternalToolService: DeepMocked; - let toolPermissionHelper: DeepMocked; - let commonToolService: DeepMocked; - let logoService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - ToolReferenceUc, - { - provide: ExternalToolService, - useValue: createMock(), - }, - { - provide: SchoolExternalToolService, - useValue: createMock(), - }, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, - { - provide: CommonToolService, - useValue: createMock(), - }, - { - provide: ExternalToolLogoService, - useValue: createMock(), - }, - { - provide: ToolPermissionHelper, - useValue: createMock(), - }, - ], - }).compile(); - - uc = module.get(ToolReferenceUc); - - externalToolService = module.get(ExternalToolService); - schoolExternalToolService = module.get(SchoolExternalToolService); - contextExternalToolService = module.get(ContextExternalToolService); - toolPermissionHelper = module.get(ToolPermissionHelper); - commonToolService = module.get(CommonToolService); - logoService = module.get(ExternalToolLogoService); - }); - - afterAll(async () => { - await module.close(); - }); - - describe('getToolReferences', () => { - describe('when called with a context type and id', () => { - const setup = () => { - const userId = 'userId'; - - const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - toolId: externalTool.id, - }); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef('schoolToolId', 'schoolId') - .buildWithId(); - - const contextType: ToolContextType = ToolContextType.COURSE; - const contextId = 'contextId'; - - contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); - toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); - commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); - - return { - userId, - contextType, - contextId, - contextExternalTool, - schoolExternalTool, - externalTool, - externalToolId: externalTool.id as string, - }; - }; - - it('should call toolPermissionHelper.ensureContextPermissions', async () => { - const { userId, contextType, contextId, contextExternalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( - userId, - contextExternalTool, - AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]) - ); - }); - - it('should call contextExternalToolService.findAllByContext', async () => { - const { userId, contextType, contextId } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(contextExternalToolService.findAllByContext).toHaveBeenCalledWith({ - type: contextType, - id: contextId, - }); - }); - - it('should call schoolExternalToolService.findByExternalToolId', async () => { - const { userId, contextType, contextId, contextExternalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(schoolExternalToolService.getSchoolExternalToolById).toHaveBeenCalledWith( - contextExternalTool.schoolToolRef.schoolToolId - ); - }); - - it('should call externalToolService.findById', async () => { - const { userId, contextType, contextId, externalToolId } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(externalToolId); - }); - - it('should call commonToolService.determineToolConfigurationStatus', async () => { - const { userId, contextType, contextId, contextExternalTool, schoolExternalTool, externalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(commonToolService.determineToolConfigurationStatus).toHaveBeenCalledWith( - externalTool, - schoolExternalTool, - contextExternalTool - ); - }); - - it('should call externalToolLogoService.buildLogoUrl', async () => { - const { userId, contextType, contextId, externalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(logoService.buildLogoUrl).toHaveBeenCalledWith('/v3/tools/external-tools/{id}/logo', externalTool); - }); - - it('should return a list of tool references', async () => { - const { userId, contextType, contextId, contextExternalTool, externalTool } = setup(); - - const result: ToolReference[] = await uc.getToolReferences( - userId, - contextType, - contextId, - '/v3/tools/external-tools/{id}/logo' - ); - - expect(result).toEqual([ - { - logoUrl: `${Configuration.get('PUBLIC_BACKEND_URL') as string}/v3/tools/external-tools/${ - externalTool.id as string - }/logo`, - openInNewTab: externalTool.openNewTab, - contextToolId: contextExternalTool.id as string, - displayName: contextExternalTool.displayName as string, - status: ToolConfigurationStatus.LATEST, - }, - ]); - }); - }); - - describe('when user does not have permission to a tool', () => { - const setup = () => { - const userId = 'userId'; - - const externalTool: ExternalTool = externalToolFactory.buildWithId(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - toolId: externalTool.id, - }); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef('schoolToolId', 'schoolId') - .buildWithId(); - - const contextType: ToolContextType = ToolContextType.COURSE; - const contextId = 'contextId'; - - contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); - toolPermissionHelper.ensureContextPermissions.mockRejectedValueOnce(new ForbiddenException()); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); - - return { - userId, - contextType, - contextId, - }; - }; - - it('should filter out tool references if a ForbiddenException is thrown', async () => { - const { userId, contextType, contextId } = setup(); - - const result: ToolReference[] = await uc.getToolReferences( - userId, - contextType, - contextId, - '/v3/tools/external-tools/{id}/logo' - ); - - expect(result).toEqual([]); - }); - }); - }); -}); diff --git a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts b/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts deleted file mode 100644 index 5ddf0e467c6..00000000000 --- a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ForbiddenException, Injectable } from '@nestjs/common'; -import { EntityId, Permission } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; -import { ExternalTool, ToolReference } from '../domain'; -import { ToolConfigurationStatus, ToolContextType } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; -import { ContextExternalTool, ContextRef } from '../../context-external-tool/domain'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ToolReferenceMapper } from '../mapper/tool-reference.mapper'; -import { ExternalToolLogoService, ExternalToolService } from '../service'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; - -@Injectable() -export class ToolReferenceUc { - constructor( - private readonly externalToolService: ExternalToolService, - private readonly schoolExternalToolService: SchoolExternalToolService, - private readonly contextExternalToolService: ContextExternalToolService, - private readonly toolPermissionHelper: ToolPermissionHelper, - private readonly commonToolService: CommonToolService, - private readonly externalToolLogoService: ExternalToolLogoService - ) {} - - async getToolReferences( - userId: EntityId, - contextType: ToolContextType, - contextId: string, - logoUrlTemplate: string - ): Promise { - const contextRef = new ContextRef({ type: contextType, id: contextId }); - - const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext( - contextRef - ); - - const toolReferencesPromises: Promise[] = contextExternalTools.map( - (contextExternalTool: ContextExternalTool) => - this.buildToolReference(userId, contextExternalTool, logoUrlTemplate) - ); - - const toolReferencesWithNull: (ToolReference | null)[] = await Promise.all(toolReferencesPromises); - const filteredToolReferences: ToolReference[] = toolReferencesWithNull.filter( - (toolReference: ToolReference | null): toolReference is ToolReference => toolReference !== null - ); - - return filteredToolReferences; - } - - private async buildToolReference( - userId: EntityId, - contextExternalTool: ContextExternalTool, - logoUrlTemplate: string - ): Promise { - try { - await this.ensureToolPermissions(userId, contextExternalTool); - } catch (e: unknown) { - if (e instanceof ForbiddenException) { - return null; - } - } - - const schoolExternalTool: SchoolExternalTool = await this.fetchSchoolExternalTool(contextExternalTool); - const externalTool: ExternalTool = await this.fetchExternalTool(schoolExternalTool); - - const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( - externalTool, - schoolExternalTool, - contextExternalTool - ); - - const toolReference: ToolReference = ToolReferenceMapper.mapToToolReference( - externalTool, - contextExternalTool, - status - ); - toolReference.logoUrl = this.externalToolLogoService.buildLogoUrl(logoUrlTemplate, externalTool); - - return toolReference; - } - - private async ensureToolPermissions(userId: EntityId, contextExternalTool: ContextExternalTool): Promise { - const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); - - const promise: Promise = this.toolPermissionHelper.ensureContextPermissions( - userId, - contextExternalTool, - context - ); - - return promise; - } - - private async fetchSchoolExternalTool(contextExternalTool: ContextExternalTool): Promise { - return this.schoolExternalToolService.getSchoolExternalToolById(contextExternalTool.schoolToolRef.schoolToolId); - } - - private async fetchExternalTool(schoolExternalTool: SchoolExternalTool): Promise { - return this.externalToolService.findExternalToolById(schoolExternalTool.toolId); - } -} diff --git a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts index 89bbe0c2cb7..c2504b42de2 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts @@ -12,6 +12,9 @@ import { userFactory, } from '@shared/testing'; import { ServerTestModule } from '@src/modules/server'; +import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto/tool-configuration-status.response'; +import { ExternalToolEntity } from '../../../external-tool/entity'; +import { SchoolExternalToolEntity } from '../../entity'; import { CustomParameterEntryParam, SchoolExternalToolPostParams, @@ -19,9 +22,6 @@ import { SchoolExternalToolSearchListResponse, SchoolExternalToolSearchParams, } from '../dto'; -import { ToolConfigurationStatusResponse } from '../../../external-tool/controller/dto'; -import { SchoolExternalToolEntity } from '../../entity'; -import { ExternalToolEntity } from '../../../external-tool/entity'; describe('ToolSchoolController (API)', () => { let app: INestApplication; diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts index 62ad203fb02..32dd35f10bd 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto/tool-configuration-status.response'; import { CustomParameterEntryResponse } from './custom-parameter-entry.response'; -import { ToolConfigurationStatusResponse } from '../../../external-tool/controller/dto'; export class SchoolExternalToolResponse { @ApiProperty() diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts index 916445770e3..ca2296e6df7 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts @@ -1,5 +1,5 @@ import { schoolExternalToolFactory } from '@shared/testing/factory'; -import { ToolConfigurationStatusResponse } from '../../external-tool/controller/dto'; +import { ToolConfigurationStatusResponse } from '../../context-external-tool/controller/dto'; import { SchoolExternalToolResponse, SchoolExternalToolSearchListResponse } from '../controller/dto'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolResponseMapper } from './school-external-tool-response.mapper'; diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts index 10ee706dd81..7388b1a6a41 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { CustomParameterEntry } from '../../common/domain'; -import { ToolConfigurationStatus } from '../../common/enum'; -import { ToolConfigurationStatusResponse } from '../../external-tool/controller/dto'; +import { ToolStatusResponseMapper } from '../../common/mapper/tool-status-response.mapper'; +import { ToolConfigurationStatusResponse } from '../../context-external-tool/controller/dto'; import { CustomParameterEntryResponse, SchoolExternalToolResponse, @@ -9,12 +9,6 @@ import { } from '../controller/dto'; import { SchoolExternalTool } from '../domain'; -export const statusMapping: Record = { - [ToolConfigurationStatus.LATEST]: ToolConfigurationStatusResponse.LATEST, - [ToolConfigurationStatus.OUTDATED]: ToolConfigurationStatusResponse.OUTDATED, - [ToolConfigurationStatus.UNKNOWN]: ToolConfigurationStatusResponse.UNKNOWN, -}; - @Injectable() export class SchoolExternalToolResponseMapper { mapToSearchListResponse(externalTools: SchoolExternalTool[]): SchoolExternalToolSearchListResponse { @@ -33,7 +27,7 @@ export class SchoolExternalToolResponseMapper { parameters: this.mapToCustomParameterEntryResponse(schoolExternalTool.parameters), toolVersion: schoolExternalTool.toolVersion, status: schoolExternalTool.status - ? statusMapping[schoolExternalTool.status] + ? ToolStatusResponseMapper.mapToResponse(schoolExternalTool.status) : ToolConfigurationStatusResponse.UNKNOWN, }; } diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts index 7ca001675b0..e43bdeb42e0 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts @@ -51,7 +51,7 @@ describe('SchoolExternalToolValidationService', () => { ...externalToolFactory.buildWithId(), ...externalToolDoMock, }); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolId = schoolExternalTool.id as string; return { schoolExternalTool, @@ -66,7 +66,7 @@ describe('SchoolExternalToolValidationService', () => { await service.validate(schoolExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); it('should call commonToolValidationService.checkForDuplicateParameters', async () => { diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts index 8cc50097d5f..315d738ca64 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts @@ -15,9 +15,7 @@ export class SchoolExternalToolValidationService { async validate(schoolExternalTool: SchoolExternalTool): Promise { this.commonToolValidationService.checkForDuplicateParameters(schoolExternalTool); - const loadedExternalTool: ExternalTool = await this.externalToolService.findExternalToolById( - schoolExternalTool.toolId - ); + const loadedExternalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); this.checkVersionMatch(schoolExternalTool.toolVersion, loadedExternalTool.version); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts index 7c4031ef0b7..52f9b0a4c02 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts @@ -3,11 +3,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchoolExternalToolRepo } from '@shared/repo'; import { externalToolFactory } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; import { schoolExternalToolFactory } from '@shared/testing/factory/domainobject/tool/school-external-tool.factory'; -import { ExternalToolService } from '../../external-tool/service'; -import { SchoolExternalToolService } from './school-external-tool.service'; +import { ToolConfigurationStatus } from '../../common/enum'; import { ExternalTool } from '../../external-tool/domain'; +import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; -import { ToolConfigurationStatus } from '../../common/enum'; +import { SchoolExternalToolService } from './school-external-tool.service'; describe('SchoolExternalToolService', () => { let module: TestingModule; @@ -77,7 +77,7 @@ describe('SchoolExternalToolService', () => { await service.findSchoolExternalTools(schoolExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); describe('when determine status', () => { @@ -86,7 +86,7 @@ describe('SchoolExternalToolService', () => { const { schoolExternalTool, externalTool } = setup(); externalTool.version = 1337; schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -100,7 +100,7 @@ describe('SchoolExternalToolService', () => { schoolExternalTool.toolVersion = 1; externalTool.version = 0; schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -114,7 +114,7 @@ describe('SchoolExternalToolService', () => { schoolExternalTool.toolVersion = 1; externalTool.version = 1; schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -136,12 +136,12 @@ describe('SchoolExternalToolService', () => { }); }); - describe('getSchoolExternalToolById', () => { + describe('findById', () => { describe('when schoolExternalToolId is given', () => { it('should call schoolExternalToolRepo.findById', async () => { const { schoolExternalToolId } = setup(); - await service.getSchoolExternalToolById(schoolExternalToolId); + await service.findById(schoolExternalToolId); expect(schoolExternalToolRepo.findById).toHaveBeenCalledWith(schoolExternalToolId); }); @@ -163,7 +163,7 @@ describe('SchoolExternalToolService', () => { await service.saveSchoolExternalTool(schoolExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); }); }); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts index 9ee30d70db6..2f011560f6a 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { SchoolExternalToolRepo } from '@shared/repo'; import { EntityId } from '@shared/domain'; -import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; +import { SchoolExternalToolRepo } from '@shared/repo'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; -import { ExternalTool } from '../../external-tool/domain'; -import { ToolConfigurationStatus } from '../../common/enum'; +import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; @Injectable() export class SchoolExternalToolService { @@ -14,7 +14,7 @@ export class SchoolExternalToolService { private readonly externalToolService: ExternalToolService ) {} - async getSchoolExternalToolById(schoolExternalToolId: EntityId): Promise { + async findById(schoolExternalToolId: EntityId): Promise { const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolRepo.findById(schoolExternalToolId); return schoolExternalTool; } @@ -38,7 +38,7 @@ export class SchoolExternalToolService { } private async enrichDataFromExternalTool(tool: SchoolExternalTool): Promise { - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(tool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(tool.toolId); const status: ToolConfigurationStatus = this.determineStatus(tool, externalTool); const schoolExternalTool: SchoolExternalTool = new SchoolExternalTool({ ...tool, diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts index c0daab13cff..85f26f83679 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts @@ -3,12 +3,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, Permission, User } from '@shared/domain'; import { schoolExternalToolFactory, setupEntities, userFactory } from '@shared/testing'; import { AuthorizationContextBuilder } from '@src/modules/authorization'; -import { SchoolExternalToolUc } from './school-external-tool.uc'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; import { SchoolExternalTool } from '../domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; +import { SchoolExternalToolUc } from './school-external-tool.uc'; describe('SchoolExternalToolUc', () => { let module: TestingModule; @@ -259,7 +259,7 @@ describe('SchoolExternalToolUc', () => { const tool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const user: User = userFactory.buildWithId(); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(tool); + schoolExternalToolService.findById.mockResolvedValue(tool); return { user, @@ -285,7 +285,7 @@ describe('SchoolExternalToolUc', () => { const setup = () => { const tool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const user: User = userFactory.buildWithId(); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(tool); + schoolExternalToolService.findById.mockResolvedValue(tool); return { user, diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts index 63067c234d7..2640def12d5 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalToolDto, SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; import { SchoolExternalTool } from '../domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { SchoolExternalToolDto, SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; @Injectable() export class SchoolExternalToolUc { @@ -57,9 +57,7 @@ export class SchoolExternalToolUc { } async deleteSchoolExternalTool(userId: EntityId, schoolExternalToolId: EntityId): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); @@ -71,9 +69,7 @@ export class SchoolExternalToolUc { } async getSchoolExternalTool(userId: EntityId, schoolExternalToolId: EntityId): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index fe775e01fd3..dea3405801d 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -4,11 +4,14 @@ import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '@src/modules/authorization'; import { LegacySchoolModule } from '@src/modules/legacy-school'; import { UserModule } from '@src/modules/user'; +import { CommonToolModule } from './common'; import { ToolContextController } from './context-external-tool/controller'; -import { ContextExternalToolUc } from './context-external-tool/uc'; +import { ToolReferenceController } from './context-external-tool/controller/tool-reference.controller'; +import { ContextExternalToolUc, ToolReferenceUc } from './context-external-tool/uc'; import { ToolConfigurationController, ToolController } from './external-tool/controller'; import { ExternalToolRequestMapper, ExternalToolResponseMapper } from './external-tool/mapper'; -import { ExternalToolConfigurationUc, ExternalToolUc, ToolReferenceUc } from './external-tool/uc'; +import { ExternalToolConfigurationService } from './external-tool/service'; +import { ExternalToolConfigurationUc, ExternalToolUc } from './external-tool/uc'; import { ToolSchoolController } from './school-external-tool/controller'; import { SchoolExternalToolRequestMapper, SchoolExternalToolResponseMapper } from './school-external-tool/mapper'; import { SchoolExternalToolUc } from './school-external-tool/uc'; @@ -16,8 +19,6 @@ import { ToolConfigModule } from './tool-config.module'; import { ToolLaunchController } from './tool-launch/controller/tool-launch.controller'; import { ToolLaunchUc } from './tool-launch/uc'; import { ToolModule } from './tool.module'; -import { ExternalToolConfigurationService } from './external-tool/service'; -import { CommonToolModule } from './common'; @Module({ imports: [ @@ -34,6 +35,7 @@ import { CommonToolModule } from './common'; ToolConfigurationController, ToolSchoolController, ToolContextController, + ToolReferenceController, ToolController, ], providers: [ 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/strategy/abstract-launch.strategy.spec.ts index 7dba13fd2f5..12f8716b5b3 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/strategy/abstract-launch.strategy.spec.ts @@ -133,18 +133,6 @@ describe('AbstractLaunchStrategy', () => { name: 'autoSchoolIdParam', type: CustomParameterType.AUTO_SCHOOLID, }); - const autoCourseIdCustomParameter = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoCourseIdParam', - type: CustomParameterType.AUTO_CONTEXTID, - }); - const autoCourseNameCustomParameter = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoCourseNameParam', - type: CustomParameterType.AUTO_CONTEXTNAME, - }); const autoSchoolNumberCustomParameter = customParameterFactory.build({ scope: CustomParameterScope.GLOBAL, location: CustomParameterLocation.BODY, @@ -158,8 +146,6 @@ describe('AbstractLaunchStrategy', () => { schoolCustomParameter, contextCustomParameter, autoSchoolIdCustomParameter, - autoCourseIdCustomParameter, - autoCourseNameCustomParameter, autoSchoolNumberCustomParameter, ], }); @@ -191,15 +177,7 @@ describe('AbstractLaunchStrategy', () => { schoolId ); - const course: Course = courseFactory.buildWithId( - { - name: 'testName', - }, - contextExternalTool.contextRef.id - ); - schoolService.getSchoolById.mockResolvedValue(school); - courseService.findById.mockResolvedValue(course); const sortFn = (a: PropertyData, b: PropertyData) => { if (a.name < b.name) { @@ -215,15 +193,12 @@ describe('AbstractLaunchStrategy', () => { globalCustomParameter, schoolCustomParameter, autoSchoolIdCustomParameter, - autoCourseIdCustomParameter, - autoCourseNameCustomParameter, autoSchoolNumberCustomParameter, schoolParameterEntry, contextParameterEntry, externalTool, schoolExternalTool, contextExternalTool, - course, school, sortFn, }; @@ -235,14 +210,11 @@ describe('AbstractLaunchStrategy', () => { schoolCustomParameter, contextParameterEntry, autoSchoolIdCustomParameter, - autoCourseIdCustomParameter, - autoCourseNameCustomParameter, autoSchoolNumberCustomParameter, schoolParameterEntry, externalTool, schoolExternalTool, contextExternalTool, - course, school, sortFn, } = setup(); @@ -280,18 +252,131 @@ describe('AbstractLaunchStrategy', () => { location: PropertyLocation.BODY, }, { - name: autoCourseIdCustomParameter.name, - value: course.id, + name: autoSchoolNumberCustomParameter.name, + value: school.officialSchoolNumber as string, 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, location: PropertyLocation.BODY, }, { - name: autoSchoolNumberCustomParameter.name, - value: school.officialSchoolNumber as string, + 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, location: PropertyLocation.BODY, }, { @@ -299,7 +384,7 @@ describe('AbstractLaunchStrategy', () => { value: concreteConfigParameter.value, location: concreteConfigParameter.location, }, - ].sort(sortFn), + ], }); }); }); 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/strategy/abstract-launch.strategy.ts index 9004e461ae2..644105a3c2b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts @@ -215,15 +215,18 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { return contextExternalTool.contextRef.id; } case CustomParameterType.AUTO_CONTEXTNAME: { - if (contextExternalTool.contextRef.type === ToolContextType.COURSE) { - const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id); - - return course.name; + 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}` + ); + } } - - throw new ParameterTypeNotImplementedLoggableException( - `${customParameter.type}/${contextExternalTool.contextRef.type as string}` - ); } case CustomParameterType.AUTO_SCHOOLNUMBER: { const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); 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/strategy/lti11-tool-launch.strategy.spec.ts index a0db37651be..1c31aac36b6 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/strategy/lti11-tool-launch.strategy.spec.ts @@ -9,12 +9,12 @@ import { userDoFactory, } from '@shared/testing'; import { pseudonymFactory } from '@shared/testing/factory/domainobject/pseudonym.factory'; -import { PseudonymService } from '@src/modules/pseudonym/service'; +import { CourseService } from '@src/modules/learnroom/service'; import { LegacySchoolService } from '@src/modules/legacy-school'; +import { PseudonymService } from '@src/modules/pseudonym/service'; import { UserService } from '@src/modules/user'; import { ObjectId } from 'bson'; import { Authorization } from 'oauth-1.0a'; -import { CourseService } from '@src/modules/learnroom/service'; import { LtiMessageType, LtiPrivacyPermission, LtiRole, ToolContextType } from '../../../common/enum'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; 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/strategy/lti11-tool-launch.strategy.ts index 8c957ca9421..654e14b45d9 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/strategy/lti11-tool-launch.strategy.ts @@ -9,7 +9,7 @@ import { Authorization } from 'oauth-1.0a'; import { LtiRole } from '../../../common/enum'; import { ExternalTool } from '../../../external-tool/domain'; import { LtiRoleMapper } from '../../mapper'; -import { LaunchRequestMethod, PropertyData, PropertyLocation, AuthenticationValues } from '../../types'; +import { AuthenticationValues, LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; import { Lti11EncryptionService } from '../lti11-encryption.service'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; 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 02bb484093f..3330b0c9f0e 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 @@ -7,7 +7,14 @@ import { externalToolFactory, schoolExternalToolFactory, } from '@shared/testing'; +import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; import { ContextExternalTool } from '../../context-external-tool/domain'; +import { BasicToolConfig, ExternalTool } from '../../external-tool/domain'; +import { ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ToolStatusOutdatedLoggableException } from '../error'; import { LaunchRequestMethod, ToolLaunchData, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { BasicToolLaunchStrategy, @@ -16,13 +23,6 @@ import { OAuth2ToolLaunchStrategy, } from './strategy'; import { ToolLaunchService } from './tool-launch.service'; -import { ToolStatusOutdatedLoggableException } from '../error'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ExternalToolService } from '../../external-tool/service'; -import { CommonToolService } from '../../common/service'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { BasicToolConfig, ExternalTool } from '../../external-tool/domain'; -import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; describe('ToolLaunchService', () => { let module: TestingModule; @@ -104,8 +104,8 @@ describe('ToolLaunchService', () => { contextExternalTool, }; - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); @@ -136,9 +136,7 @@ describe('ToolLaunchService', () => { await service.getLaunchData('userId', launchParams.contextExternalTool); - expect(schoolExternalToolService.getSchoolExternalToolById).toHaveBeenCalledWith( - launchParams.schoolExternalTool.id - ); + expect(schoolExternalToolService.findById).toHaveBeenCalledWith(launchParams.schoolExternalTool.id); }); it('should call findExternalToolById', async () => { @@ -146,7 +144,7 @@ describe('ToolLaunchService', () => { await service.getLaunchData('userId', launchParams.contextExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(launchParams.schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(launchParams.schoolExternalTool.toolId); }); }); @@ -165,8 +163,8 @@ describe('ToolLaunchService', () => { contextExternalTool, }; - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); return { @@ -209,8 +207,8 @@ describe('ToolLaunchService', () => { const userId = 'userId'; - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.OUTDATED); 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 3321e782f09..46d2efdeb70 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 @@ -1,6 +1,13 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; +import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; import { CommonToolService } from '../../common/service'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { ExternalTool } from '../../external-tool/domain'; +import { ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ToolStatusOutdatedLoggableException } from '../error'; import { ToolLaunchMapper } from '../mapper'; import { ToolLaunchData, ToolLaunchRequest } from '../types'; import { @@ -9,13 +16,6 @@ import { Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, } from './strategy'; -import { ToolStatusOutdatedLoggableException } from '../error'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ExternalToolService } from '../../external-tool/service'; -import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ExternalTool } from '../../external-tool/domain'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; @Injectable() export class ToolLaunchService { @@ -73,11 +73,9 @@ export class ToolLaunchService { private async loadToolHierarchy( schoolExternalToolId: string ): Promise<{ schoolExternalTool: SchoolExternalTool; externalTool: ExternalTool }> { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(schoolExternalTool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); return { schoolExternalTool, diff --git a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts index 62424d8b8aa..e9b7311e06a 100644 --- a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts @@ -2,12 +2,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory } from '@shared/testing'; import { ObjectId } from 'bson'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { ContextExternalToolService } from '../../context-external-tool/service'; import { ToolLaunchService } from '../service'; import { ToolLaunchData, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { ToolLaunchUc } from './tool-launch.uc'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; describe('ToolLaunchUc', () => { let module: TestingModule; @@ -66,7 +66,7 @@ describe('ToolLaunchUc', () => { const userId: string = new ObjectId().toHexString(); toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); return { @@ -82,7 +82,7 @@ describe('ToolLaunchUc', () => { await uc.getToolLaunchRequest(userId, contextExternalToolId); - expect(contextExternalToolService.getContextExternalToolById).toHaveBeenCalledWith(contextExternalToolId); + expect(contextExternalToolService.findById).toHaveBeenCalledWith(contextExternalToolId); }); it('should call service to get data', async () => { diff --git a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts index c397ae1d1af..fed27fa9aad 100644 --- a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts +++ b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { ContextExternalToolService } from '../../context-external-tool/service'; import { ToolLaunchService } from '../service'; import { ToolLaunchData, ToolLaunchRequest } from '../types'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; @Injectable() export class ToolLaunchUc { @@ -16,7 +16,7 @@ export class ToolLaunchUc { ) {} async getToolLaunchRequest(userId: EntityId, contextExternalToolId: EntityId): Promise { - const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById( + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( contextExternalToolId ); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts index 7c02cbc8a75..6806aeb3f71 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts @@ -127,7 +127,7 @@ describe('ContextExternalToolRepo', () => { }); describe('save', () => { - describe('when context is known', () => { + describe('when context is course', () => { function setup() { const domainObject: ContextExternalTool = contextExternalToolFactory.build({ displayName: 'displayName', @@ -159,6 +159,38 @@ describe('ContextExternalToolRepo', () => { }); }); + describe('when context is board card', () => { + function setup() { + const domainObject: ContextExternalTool = contextExternalToolFactory.build({ + displayName: 'displayName', + contextRef: { + id: new ObjectId().toHexString(), + type: ToolContextType.BOARD_ELEMENT, + }, + parameters: [new CustomParameterEntry({ name: 'param', value: 'value' })], + schoolToolRef: { + schoolToolId: new ObjectId().toHexString(), + schoolId: undefined, + }, + toolVersion: 1, + }); + + return { + domainObject, + }; + } + + it('should save a ContextExternalToolDO', async () => { + const { domainObject } = setup(); + const { id, ...expected } = domainObject; + + const result: ContextExternalTool = await repo.save(domainObject); + + expect(result).toMatchObject(expected); + expect(result.id).toBeDefined(); + }); + }); + describe('when context is unknown', () => { const setup = () => { const domainObject: ContextExternalTool = contextExternalToolFactory.build({ diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts index b766828beff..084adb4b727 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts @@ -115,6 +115,8 @@ export class ContextExternalToolRepo extends BaseDORepo< switch (type) { case ToolContextType.COURSE: return ContextExternalToolType.COURSE; + case ToolContextType.BOARD_ELEMENT: + return ContextExternalToolType.BOARD_ELEMENT; default: throw new Error('Unknown ToolContextType'); } @@ -124,6 +126,8 @@ export class ContextExternalToolRepo extends BaseDORepo< switch (type) { case ContextExternalToolType.COURSE: return ToolContextType.COURSE; + case ContextExternalToolType.BOARD_ELEMENT: + return ToolContextType.BOARD_ELEMENT; default: throw new Error('Unknown ContextExternalToolType'); }