diff --git a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts index 96b5adf31c3..34326a769de 100644 --- a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts +++ b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts @@ -34,11 +34,18 @@ export class ContextExternalToolConfigurationStatusResponse { }) isDeactivated: boolean; + @ApiProperty({ + type: Boolean, + description: 'True if the tool is not licensed for user', + }) + isNotLicensed: boolean; + constructor(props: ContextExternalToolConfigurationStatusResponse) { this.isOutdatedOnScopeSchool = props.isOutdatedOnScopeSchool; this.isOutdatedOnScopeContext = props.isOutdatedOnScopeContext; this.isIncompleteOnScopeContext = props.isIncompleteOnScopeContext; this.isIncompleteOperationalOnScopeContext = props.isIncompleteOperationalOnScopeContext; this.isDeactivated = props.isDeactivated; + this.isNotLicensed = props.isNotLicensed; } } diff --git a/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts b/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts index b46506f0399..cad99903c9c 100644 --- a/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts +++ b/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts @@ -7,6 +7,8 @@ export class ContextExternalToolConfigurationStatus { isDeactivated: boolean; + isNotLicensed: boolean; + isIncompleteOperationalOnScopeContext: boolean; constructor(props: ContextExternalToolConfigurationStatus) { @@ -15,5 +17,6 @@ export class ContextExternalToolConfigurationStatus { this.isIncompleteOnScopeContext = props.isIncompleteOnScopeContext; this.isIncompleteOperationalOnScopeContext = props.isIncompleteOperationalOnScopeContext; this.isDeactivated = props.isDeactivated; + this.isNotLicensed = props.isNotLicensed; } } diff --git a/apps/server/src/modules/tool/common/interface/index.ts b/apps/server/src/modules/tool/common/interface/index.ts index b41fc78539c..2a8834ff859 100644 --- a/apps/server/src/modules/tool/common/interface/index.ts +++ b/apps/server/src/modules/tool/common/interface/index.ts @@ -1,2 +1 @@ export * from './external-tool-search-query'; -export * from './tool-version.interface'; diff --git a/apps/server/src/modules/tool/common/interface/tool-version.interface.ts b/apps/server/src/modules/tool/common/interface/tool-version.interface.ts deleted file mode 100644 index 5b7183dfbdd..00000000000 --- a/apps/server/src/modules/tool/common/interface/tool-version.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ToolVersion { - getVersion(): number; -} 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 index 8822b2ad1a0..ee95eaabe27 100644 --- 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 @@ -10,6 +10,7 @@ export class ToolStatusResponseMapper { isIncompleteOnScopeContext: status.isIncompleteOnScopeContext, isIncompleteOperationalOnScopeContext: status.isIncompleteOperationalOnScopeContext, isDeactivated: status.isDeactivated, + isNotLicensed: status.isNotLicensed, }); return configurationStatus; 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 da76ff8544e..563118a175d 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,4 +1,5 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { UserLicenseModule } from '@modules/user-license'; +import { forwardRef, Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; @@ -15,6 +16,7 @@ import { ToolConfigurationStatusService } from './service/tool-configuration-sta SchoolExternalToolModule, LoggerModule, ToolConfigModule, + UserLicenseModule, ], providers: [ ContextExternalToolService, diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts index 5f3fb6cd096..76b8b7fc7d9 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts @@ -1,4 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { MediaBoardConfig } from '@modules/board/media-board.config'; +import { MediaUserLicense, mediaUserLicenseFactory, UserLicenseService } from '@modules/user-license'; +import { MediaUserLicenseService } from '@modules/user-license/service'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; import { @@ -18,6 +23,9 @@ describe(ToolConfigurationStatusService.name, () => { let service: ToolConfigurationStatusService; let commonToolValidationService: DeepMocked; + let userLicenseService: DeepMocked; + let mediaUserLicenseService: DeepMocked; + let configService: DeepMocked>; beforeAll(async () => { module = await Test.createTestingModule({ @@ -27,11 +35,26 @@ describe(ToolConfigurationStatusService.name, () => { provide: CommonToolValidationService, useValue: createMock(), }, + { + provide: UserLicenseService, + useValue: createMock(), + }, + { + provide: MediaUserLicenseService, + useValue: createMock(), + }, + { + provide: ConfigService, + useValue: createMock(), + }, ], }).compile(); service = module.get(ToolConfigurationStatusService); commonToolValidationService = module.get(CommonToolValidationService); + userLicenseService = module.get(UserLicenseService); + mediaUserLicenseService = module.get(MediaUserLicenseService); + configService = module.get(ConfigService); }); afterAll(async () => { @@ -45,6 +68,7 @@ describe(ToolConfigurationStatusService.name, () => { describe('determineToolConfigurationStatus', () => { describe('when validation runs through', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, @@ -59,16 +83,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return latest tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return latest tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -77,21 +103,22 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }); }); - it('should validate the school external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the school external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, schoolExternalTool); }); - it('should validate the context external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the context external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, contextExternalTool); }); @@ -99,6 +126,7 @@ describe(ToolConfigurationStatusService.name, () => { describe('when validation of SchoolExternalTool throws an error', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, @@ -114,16 +142,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return outdated tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return outdated tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -132,21 +162,22 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }); }); - it('should validate the school external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the school external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, schoolExternalTool); }); - it('should validate the context external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the context external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, contextExternalTool); }); @@ -154,6 +185,7 @@ describe(ToolConfigurationStatusService.name, () => { describe('when validation of ContextExternalTool throws an error', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, @@ -169,16 +201,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return outdated tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return outdated tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -187,28 +221,30 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }); }); - it('should validate the school external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the school external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, schoolExternalTool); }); - it('should validate the context external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the context external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, contextExternalTool); }); }); - describe('when validation of SchoolExternalTool and ContextExternalTool throws an error', () => { + describe('when validation of SchoolExternalTool and ContextExternalTool throws an error', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, @@ -224,16 +260,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return outdated tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return outdated tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -242,21 +280,22 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }); }); - it('should validate the school external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the school external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, schoolExternalTool); }); - it('should validate the context external tool', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should validate the context external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool); + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); expect(commonToolValidationService.validateParameters).toHaveBeenCalledWith(externalTool, contextExternalTool); }); @@ -264,6 +303,7 @@ describe(ToolConfigurationStatusService.name, () => { describe('when validation of ContextExternalTool throws at least 1 missing value on mandatory parameter errors', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const customParameter = customParameterFactory.build(); const externalTool = externalToolFactory.buildWithId({ parameters: [customParameter] }); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ @@ -284,16 +324,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return incomplete as tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return incomplete as tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -302,12 +344,14 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: true, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }); }); }); describe('when validation of ContextExternalTool throws at least 1 missing value on optional parameter errors', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const customParameter = customParameterFactory.build(); const externalTool = externalToolFactory.buildWithId({ parameters: [customParameter] }); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ @@ -327,16 +371,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return incomplete operational as tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return incomplete operational as tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -345,12 +391,14 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: true, isDeactivated: false, + isNotLicensed: false, }); }); }); describe('when validation of ContextExternalTool throws only missing value on optional parameter errors', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const customParameter = customParameterFactory.build(); const externalTool = externalToolFactory.buildWithId({ parameters: [customParameter] }); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ @@ -371,16 +419,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return incomplete operational as tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return incomplete operational as tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -389,12 +439,14 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: true, isDeactivated: false, + isNotLicensed: false, }); }); }); describe('when SchoolExternalTool is deactivated', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, @@ -411,16 +463,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return status is deactivated', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return status is deactivated', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -429,12 +483,14 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: true, + isNotLicensed: false, }); }); }); describe('when externalTool is deactivated', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId({ isDeactivated: true }); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, @@ -450,16 +506,18 @@ describe(ToolConfigurationStatusService.name, () => { externalTool, schoolExternalTool, contextExternalTool, + userId, }; }; - it('should return deactivated tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should return deactivated tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -468,37 +526,64 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: true, + isNotLicensed: false, }); }); }); - describe('when externalTool and schoolExternalTool are not deactivated', () => { + describe('when license feature is enabled and user has no license for externalTool', () => { const setup = () => { - const externalTool = externalToolFactory.buildWithId(); + configService.get.mockReturnValueOnce(true); + + const userId: string = new ObjectId().toHexString(); + const externalTool = externalToolFactory.withMedium().buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, }); const contextExternalTool = contextExternalToolFactory .withSchoolExternalToolRef(schoolExternalTool.id) .buildWithId(); + const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); + userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([mediaUserLicense]); return { externalTool, schoolExternalTool, contextExternalTool, + userId, + mediaUserLicense, }; }; - it('should return deactivated tool status', () => { - const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + it('should get the mediaUserLicenses for user', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); + + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); + + expect(userLicenseService.getMediaUserLicensesForUser).toHaveBeenCalledWith(userId); + }); + + it('should check if user has license for external tool', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId, mediaUserLicense } = setup(); - const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); + + expect(mediaUserLicenseService.hasLicenseForExternalTool).toHaveBeenCalledWith(externalTool.medium, [ + mediaUserLicense, + ]); + }); + + it('should return not licensed tool status', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); + + const status: ContextExternalToolConfigurationStatus = await service.determineToolConfigurationStatus( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); expect(status).toEqual({ @@ -507,6 +592,7 @@ describe(ToolConfigurationStatusService.name, () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: true, }); }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts index 6bced502c86..c89bf0847af 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts @@ -1,5 +1,10 @@ +import { MediaBoardConfig } from '@modules/board/media-board.config'; +import { MediaUserLicense, UserLicenseService } from '@modules/user-license'; +import { MediaUserLicenseService } from '@modules/user-license/service'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; +import { ConfigService } from '@nestjs/config'; import { ValidationError } from '@shared/common'; +import { EntityId } from '@shared/domain/types'; import { ContextExternalToolConfigurationStatus, ToolParameterMandatoryValueMissingLoggableException, @@ -12,19 +17,26 @@ import { ContextExternalToolLaunchable } from '../domain'; @Injectable() export class ToolConfigurationStatusService { - constructor(private readonly commonToolValidationService: CommonToolValidationService) {} + constructor( + private readonly commonToolValidationService: CommonToolValidationService, + private readonly userLicenseService: UserLicenseService, + private readonly mediaUserLicenseService: MediaUserLicenseService, + private readonly configService: ConfigService + ) {} - public determineToolConfigurationStatus( + public async determineToolConfigurationStatus( externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool, - contextExternalTool: ContextExternalToolLaunchable - ): ContextExternalToolConfigurationStatus { + contextExternalTool: ContextExternalToolLaunchable, + userId: EntityId + ): Promise { const configurationStatus: ContextExternalToolConfigurationStatus = new ContextExternalToolConfigurationStatus({ isOutdatedOnScopeContext: false, isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isOutdatedOnScopeSchool: false, isDeactivated: this.isToolDeactivated(externalTool, schoolExternalTool), + isNotLicensed: !(await this.isToolLicensed(externalTool, userId)), }); const schoolParameterErrors: ValidationError[] = this.commonToolValidationService.validateParameters( @@ -61,11 +73,23 @@ export class ToolConfigurationStatusService { return configurationStatus; } - private isToolDeactivated(externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool) { + private isToolDeactivated(externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool): boolean { return !!(externalTool.isDeactivated || (schoolExternalTool.status && schoolExternalTool.status.isDeactivated)); } - private isIncompleteOperational(errors: ValidationError[]) { + private async isToolLicensed(externalTool: ExternalTool, userId: EntityId): Promise { + if (this.configService.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED')) { + const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser(userId); + + const externalToolMedium = externalTool.medium; + if (externalToolMedium) { + return this.mediaUserLicenseService.hasLicenseForExternalTool(externalToolMedium, mediaUserLicenses); + } + } + return true; + } + + private isIncompleteOperational(errors: ValidationError[]): boolean { return errors.some((error: ValidationError) => error instanceof ToolParameterOptionalValueMissingLoggableException); } 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 index 3e60c4eb44d..e62888c22d8 100644 --- 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 @@ -18,7 +18,7 @@ describe('ToolReferenceService', () => { let externalToolService: DeepMocked; let schoolExternalToolService: DeepMocked; let contextExternalToolService: DeepMocked; - let toolVersionService: DeepMocked; + let toolConfigurationStatusService: DeepMocked; let externalToolLogoService: DeepMocked; beforeAll(async () => { @@ -52,7 +52,7 @@ describe('ToolReferenceService', () => { externalToolService = module.get(ExternalToolService); schoolExternalToolService = module.get(SchoolExternalToolService); contextExternalToolService = module.get(ContextExternalToolService); - toolVersionService = module.get(ToolConfigurationStatusService); + toolConfigurationStatusService = module.get(ToolConfigurationStatusService); externalToolLogoService = module.get(ExternalToolLogoService); }); @@ -67,6 +67,7 @@ describe('ToolReferenceService', () => { describe('getToolReference', () => { describe('when a context external tool id is provided', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); const contextExternalToolId = new ObjectId().toHexString(); const externalTool = externalToolFactory.buildWithId(); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ @@ -80,7 +81,7 @@ describe('ToolReferenceService', () => { contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); externalToolService.findById.mockResolvedValueOnce(externalTool); - toolVersionService.determineToolConfigurationStatus.mockReturnValue( + toolConfigurationStatusService.determineToolConfigurationStatus.mockResolvedValue( toolConfigurationStatusFactory.build({ isOutdatedOnScopeSchool: true, isOutdatedOnScopeContext: false, @@ -94,33 +95,35 @@ describe('ToolReferenceService', () => { schoolExternalTool, contextExternalTool, logoUrl, + userId, }; }; it('should determine the tool status', async () => { - const { contextExternalToolId, externalTool, schoolExternalTool, contextExternalTool } = setup(); + const { contextExternalToolId, externalTool, schoolExternalTool, contextExternalTool, userId } = setup(); - await service.getToolReference(contextExternalToolId); + await service.getToolReference(contextExternalToolId, userId); - expect(toolVersionService.determineToolConfigurationStatus).toHaveBeenCalledWith( + expect(toolConfigurationStatusService.determineToolConfigurationStatus).toHaveBeenCalledWith( externalTool, schoolExternalTool, - contextExternalTool + contextExternalTool, + userId ); }); it('should build the logo url', async () => { - const { contextExternalToolId, externalTool } = setup(); + const { contextExternalToolId, externalTool, userId } = setup(); - await service.getToolReference(contextExternalToolId); + await service.getToolReference(contextExternalToolId, userId); expect(externalToolLogoService.buildLogoUrl).toHaveBeenCalledWith(externalTool); }); it('should return the tool reference', async () => { - const { contextExternalToolId, logoUrl, contextExternalTool, externalTool } = setup(); + const { contextExternalToolId, logoUrl, contextExternalTool, externalTool, userId } = setup(); - const result: ToolReference = await service.getToolReference(contextExternalToolId); + const result: ToolReference = await service.getToolReference(contextExternalToolId, userId); expect(result).toEqual({ logoUrl, 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 index ae3e8fdb08c..f39c8bfc3c5 100644 --- 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 @@ -17,10 +17,10 @@ export class ToolReferenceService { private readonly schoolExternalToolService: SchoolExternalToolService, private readonly contextExternalToolService: ContextExternalToolService, private readonly externalToolLogoService: ExternalToolLogoService, - private readonly toolVersionService: ToolConfigurationStatusService + private readonly toolConfigurationStatusService: ToolConfigurationStatusService ) {} - async getToolReference(contextExternalToolId: EntityId): Promise { + async getToolReference(contextExternalToolId: EntityId, userId: EntityId): Promise { const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findByIdOrFail( contextExternalToolId ); @@ -29,11 +29,13 @@ export class ToolReferenceService { ); const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); - const status: ContextExternalToolConfigurationStatus = this.toolVersionService.determineToolConfigurationStatus( - externalTool, - schoolExternalTool, - contextExternalTool - ); + const status: ContextExternalToolConfigurationStatus = + await this.toolConfigurationStatusService.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool, + userId + ); const toolReference: ToolReference = ToolReferenceMapper.mapToToolReference( externalTool, diff --git a/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool-configuration-status-response.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool-configuration-status-response.factory.ts index c501b4119a9..feb5c5f9b15 100644 --- a/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool-configuration-status-response.factory.ts +++ b/apps/server/src/modules/tool/context-external-tool/testing/context-external-tool-configuration-status-response.factory.ts @@ -9,5 +9,6 @@ export const contextExternalToolConfigurationStatusResponseFactory = isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }; }); 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 index d9f99e0311f..55760080042 100644 --- 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 @@ -48,7 +48,10 @@ export class ToolReferenceUc { try { await this.ensureToolPermissions(user, contextExternalTool); - const toolReference: ToolReference = await this.toolReferenceService.getToolReference(contextExternalTool.id); + const toolReference: ToolReference = await this.toolReferenceService.getToolReference( + contextExternalTool.id, + user.id + ); return toolReference; } catch (e: unknown) { @@ -64,7 +67,10 @@ export class ToolReferenceUc { const user: User = await this.authorizationService.getUserWithPermissions(userId); await this.ensureToolPermissions(user, contextExternalTool); - const toolReference: ToolReference = await this.toolReferenceService.getToolReference(contextExternalTool.id); + const toolReference: ToolReference = await this.toolReferenceService.getToolReference( + contextExternalTool.id, + userId + ); return toolReference; } diff --git a/apps/server/src/modules/tool/external-tool/testing/tool-configuration-status.factory.ts b/apps/server/src/modules/tool/external-tool/testing/tool-configuration-status.factory.ts index 64fed88854c..6a040358ce3 100644 --- a/apps/server/src/modules/tool/external-tool/testing/tool-configuration-status.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/tool-configuration-status.factory.ts @@ -8,5 +8,6 @@ export const toolConfigurationStatusFactory = Factory.define { toolConfigStatus.isOutdatedOnScopeContext, toolConfigStatus.isIncompleteOnScopeContext, toolConfigStatus.isIncompleteOperationalOnScopeContext, - toolConfigStatus.isDeactivated + toolConfigStatus.isDeactivated, + toolConfigStatus.isNotLicensed ); return { @@ -40,6 +41,7 @@ describe('ToolStatusNotLaunchableLoggableException', () => { isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: false, + isNotLicensed: false, }, }); }); diff --git a/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts b/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts index a0fca0f2c1b..c8d1c7d49c3 100644 --- a/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts +++ b/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts @@ -10,7 +10,8 @@ export class ToolStatusNotLaunchableLoggableException extends UnprocessableEntit private readonly isOutdatedOnScopeContext: boolean, private readonly isIncompleteOnScopeContext: boolean, private readonly isIncompleteOperationalOnScopeContext: boolean, - private readonly isDeactivated: boolean + private readonly isDeactivated: boolean, + private readonly isNotLicensed: boolean ) { super(); } @@ -28,6 +29,7 @@ export class ToolStatusNotLaunchableLoggableException extends UnprocessableEntit isIncompleteOnScopeContext: this.isIncompleteOnScopeContext, isIncompleteOperationalOnScopeContext: this.isIncompleteOperationalOnScopeContext, isDeactivated: this.isDeactivated, + isNotLicensed: this.isNotLicensed, }, }; } 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 5b5a6e8f7c5..54eea73c539 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 @@ -31,7 +31,7 @@ describe('ToolLaunchService', () => { let schoolExternalToolService: DeepMocked; let externalToolService: DeepMocked; let basicToolLaunchStrategy: DeepMocked; - let toolVersionService: DeepMocked; + let toolConfigurationStatusService: DeepMocked; beforeEach(async () => { module = await Test.createTestingModule({ @@ -68,7 +68,7 @@ describe('ToolLaunchService', () => { schoolExternalToolService = module.get(SchoolExternalToolService); externalToolService = module.get(ExternalToolService); basicToolLaunchStrategy = module.get(BasicToolLaunchStrategy); - toolVersionService = module.get(ToolConfigurationStatusService); + toolConfigurationStatusService = module.get(ToolConfigurationStatusService); }); afterAll(async () => { @@ -107,7 +107,7 @@ describe('ToolLaunchService', () => { schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); - toolVersionService.determineToolConfigurationStatus.mockReturnValueOnce( + toolConfigurationStatusService.determineToolConfigurationStatus.mockResolvedValueOnce( toolConfigurationStatusFactory.build({ isOutdatedOnScopeContext: false, isOutdatedOnScopeSchool: false, @@ -153,12 +153,12 @@ describe('ToolLaunchService', () => { expect(externalToolService.findById).toHaveBeenCalledWith(launchParams.schoolExternalTool.toolId); }); - it('should call toolVersionService', async () => { + it('should call toolConfigurationStatusService', async () => { const { launchParams } = setup(); await service.getLaunchData('userId', launchParams.contextExternalTool); - expect(toolVersionService.determineToolConfigurationStatus).toHaveBeenCalled(); + expect(toolConfigurationStatusService.determineToolConfigurationStatus).toHaveBeenCalled(); }); }); @@ -179,7 +179,7 @@ describe('ToolLaunchService', () => { schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); - toolVersionService.determineToolConfigurationStatus.mockReturnValueOnce( + toolConfigurationStatusService.determineToolConfigurationStatus.mockResolvedValueOnce( toolConfigurationStatusFactory.build({ isOutdatedOnScopeContext: false, isOutdatedOnScopeSchool: false, @@ -230,13 +230,14 @@ describe('ToolLaunchService', () => { schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); - toolVersionService.determineToolConfigurationStatus.mockReturnValueOnce( + toolConfigurationStatusService.determineToolConfigurationStatus.mockResolvedValueOnce( toolConfigurationStatusFactory.build({ isOutdatedOnScopeContext: true, isOutdatedOnScopeSchool: true, isIncompleteOnScopeContext: false, isIncompleteOperationalOnScopeContext: false, isDeactivated: true, + isNotLicensed: true, }) ); @@ -253,7 +254,16 @@ describe('ToolLaunchService', () => { const func = () => service.getLaunchData(userId, launchParams.contextExternalTool); await expect(func).rejects.toThrow( - new ToolStatusNotLaunchableLoggableException(userId, contextExternalToolId, true, true, false, false, true) + new ToolStatusNotLaunchableLoggableException( + userId, + contextExternalToolId, + true, + true, + false, + false, + true, + true + ) ); }); }); 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 a5fad4d9bc3..4466ac4c12f 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 @@ -28,7 +28,7 @@ export class ToolLaunchService { private readonly basicToolLaunchStrategy: BasicToolLaunchStrategy, private readonly lti11ToolLaunchStrategy: Lti11ToolLaunchStrategy, private readonly oauth2ToolLaunchStrategy: OAuth2ToolLaunchStrategy, - private readonly toolVersionService: ToolConfigurationStatusService + private readonly toolConfigurationStatusService: ToolConfigurationStatusService ) { this.strategies = new Map(); this.strategies.set(ToolConfigType.BASIC, basicToolLaunchStrategy); @@ -36,7 +36,7 @@ export class ToolLaunchService { this.strategies.set(ToolConfigType.OAUTH2, oauth2ToolLaunchStrategy); } - generateLaunchRequest(toolLaunchData: ToolLaunchData): ToolLaunchRequest { + public generateLaunchRequest(toolLaunchData: ToolLaunchData): ToolLaunchRequest { const toolConfigType: ToolConfigType = ToolLaunchMapper.mapToToolConfigType(toolLaunchData.type); const strategy: ToolLaunchStrategy | undefined = this.strategies.get(toolConfigType); @@ -49,12 +49,15 @@ export class ToolLaunchService { return launchRequest; } - async getLaunchData(userId: EntityId, contextExternalTool: ContextExternalToolLaunchable): Promise { + public async getLaunchData( + userId: EntityId, + contextExternalTool: ContextExternalToolLaunchable + ): Promise { const schoolExternalToolId: EntityId = contextExternalTool.schoolToolRef.schoolToolId; const { externalTool, schoolExternalTool } = await this.loadToolHierarchy(schoolExternalToolId); - this.isToolStatusLaunchableOrThrow(userId, externalTool, schoolExternalTool, contextExternalTool); + await this.isToolStatusLaunchableOrThrow(userId, externalTool, schoolExternalTool, contextExternalTool); const strategy: ToolLaunchStrategy | undefined = this.strategies.get(externalTool.config.type); @@ -84,22 +87,25 @@ export class ToolLaunchService { }; } - private isToolStatusLaunchableOrThrow( + private async isToolStatusLaunchableOrThrow( userId: EntityId, externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool, contextExternalTool: ContextExternalToolLaunchable - ): void { - const status: ContextExternalToolConfigurationStatus = this.toolVersionService.determineToolConfigurationStatus( - externalTool, - schoolExternalTool, - contextExternalTool - ); + ): Promise { + const status: ContextExternalToolConfigurationStatus = + await this.toolConfigurationStatusService.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool, + userId + ); if ( status.isOutdatedOnScopeSchool || status.isOutdatedOnScopeContext || status.isDeactivated || + status.isNotLicensed || status.isIncompleteOnScopeContext ) { throw new ToolStatusNotLaunchableLoggableException( @@ -109,7 +115,8 @@ export class ToolLaunchService { status.isOutdatedOnScopeContext, status.isIncompleteOnScopeContext, status.isIncompleteOperationalOnScopeContext, - status.isDeactivated + status.isDeactivated, + status.isNotLicensed ); } } 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 5135cf210f4..1274f3fb69b 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 @@ -1,15 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaBoardConfig } from '@modules/board/media-board.config'; -import { ExternalTool } from '@modules/tool/external-tool/domain'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; -import { - LaunchContextUnavailableLoggableException, - MissingMediaLicenseLoggableException, -} from '@modules/tool/tool-launch/error'; -import { MediaUserLicense, mediaUserLicenseFactory, UserLicenseService } from '@modules/user-license'; -import { MediaUserLicenseService } from '@modules/user-license/service'; -import { ConfigService } from '@nestjs/config'; +import { LaunchContextUnavailableLoggableException } from '@modules/tool/tool-launch/error'; import { Test, TestingModule } from '@nestjs/testing'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; @@ -20,7 +12,6 @@ import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalTool, ContextExternalToolLaunchable } from '../../context-external-tool/domain'; import { ContextExternalToolService } from '../../context-external-tool/service'; import { contextExternalToolFactory } from '../../context-external-tool/testing'; -import { externalToolFactory } from '../../external-tool/testing'; import { SchoolExternalToolService } from '../../school-external-tool'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; import { ToolLaunchService } from '../service'; @@ -36,9 +27,6 @@ describe('ToolLaunchUc', () => { let schoolExternalToolService: DeepMocked; let toolPermissionHelper: DeepMocked; let authorizationService: DeepMocked; - let userLicenseService: DeepMocked; - let mediaUserLicenseService: DeepMocked; - let configService: DeepMocked>; beforeEach(async () => { module = await Test.createTestingModule({ @@ -64,18 +52,6 @@ describe('ToolLaunchUc', () => { provide: AuthorizationService, useValue: createMock(), }, - { - provide: UserLicenseService, - useValue: createMock(), - }, - { - provide: MediaUserLicenseService, - useValue: createMock(), - }, - { - provide: ConfigService, - useValue: createMock(), - }, ], }).compile(); @@ -85,9 +61,6 @@ describe('ToolLaunchUc', () => { schoolExternalToolService = module.get(SchoolExternalToolService); toolPermissionHelper = module.get(ToolPermissionHelper); authorizationService = module.get(AuthorizationService); - userLicenseService = module.get(UserLicenseService); - mediaUserLicenseService = module.get(MediaUserLicenseService); - configService = module.get(ConfigService); }); beforeAll(async () => { @@ -103,10 +76,8 @@ describe('ToolLaunchUc', () => { }); describe('getContextExternalToolLaunchRequest', () => { - describe('when licensing feature is disabled', () => { + describe('when tool exists', () => { const setup = () => { - configService.get.mockReturnValueOnce(false); - const user: User = userFactory.build(); const contextExternalToolId = 'contextExternalToolId'; const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ @@ -181,170 +152,10 @@ describe('ToolLaunchUc', () => { expect(toolLaunchRequest).toBeDefined(); }); }); - - describe('when licensing feature flag is enabled', () => { - describe('when tool has no mediumId', () => { - const setup = () => { - configService.get.mockReturnValueOnce(true); - - const user: User = userFactory.build(); - const contextExternalToolId = 'contextExternalToolId'; - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - id: contextExternalToolId, - }); - const externalTool: ExternalTool = externalToolFactory.build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const toolLaunchData: ToolLaunchData = new ToolLaunchData({ - baseUrl: 'baseUrl', - type: ToolLaunchDataType.BASIC, - openNewTab: true, - properties: [], - }); - - toolLaunchService.loadToolHierarchy.mockResolvedValue({ externalTool, schoolExternalTool }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([]); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); - toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); - - return { - user, - contextExternalToolId, - contextExternalTool, - toolLaunchData, - }; - }; - - it('should not check license', async () => { - const { user, contextExternalToolId } = setup(); - - await uc.getContextExternalToolLaunchRequest(user.id, contextExternalToolId); - - expect(mediaUserLicenseService.hasLicenseForExternalTool).not.toHaveBeenCalled(); - }); - - it('should return launch request', async () => { - const { user, contextExternalToolId } = setup(); - - const toolLaunchRequest: ToolLaunchRequest = await uc.getContextExternalToolLaunchRequest( - user.id, - contextExternalToolId - ); - - expect(toolLaunchRequest).toBeDefined(); - }); - }); - - describe('when license exist', () => { - const setup = () => { - configService.get.mockReturnValueOnce(true); - - const user: User = userFactory.build(); - const contextExternalToolId = 'contextExternalToolId'; - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - id: contextExternalToolId, - }); - const externalTool: ExternalTool = externalToolFactory.build({ medium: { mediumId: 'mediumId' } }); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const toolLaunchData: ToolLaunchData = new ToolLaunchData({ - baseUrl: 'baseUrl', - type: ToolLaunchDataType.BASIC, - openNewTab: true, - properties: [], - }); - const mediaUserlicense: MediaUserLicense = mediaUserLicenseFactory.build(); - - toolLaunchService.loadToolHierarchy.mockResolvedValue({ externalTool, schoolExternalTool }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); - mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValue(true); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); - toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); - - return { - user, - contextExternalToolId, - contextExternalTool, - toolLaunchData, - }; - }; - - it('should check license', async () => { - const { user, contextExternalToolId } = setup(); - - await uc.getContextExternalToolLaunchRequest(user.id, contextExternalToolId); - - expect(mediaUserLicenseService.hasLicenseForExternalTool).toHaveBeenCalled(); - }); - - it('should return launch request', async () => { - const { user, contextExternalToolId } = setup(); - - const toolLaunchRequest: ToolLaunchRequest = await uc.getContextExternalToolLaunchRequest( - user.id, - contextExternalToolId - ); - - expect(toolLaunchRequest).toBeDefined(); - }); - }); - - describe('when license does not exist', () => { - const setup = () => { - configService.get.mockReturnValueOnce(true); - - const user: User = userFactory.build(); - const contextExternalToolId = 'contextExternalToolId'; - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ - id: contextExternalToolId, - }); - const externalTool: ExternalTool = externalToolFactory.build({ medium: { mediumId: 'mediumId' } }); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const toolLaunchData: ToolLaunchData = new ToolLaunchData({ - baseUrl: 'baseUrl', - type: ToolLaunchDataType.BASIC, - openNewTab: true, - properties: [], - }); - const mediaUserlicense: MediaUserLicense = mediaUserLicenseFactory.build(); - - toolLaunchService.loadToolHierarchy.mockResolvedValue({ externalTool, schoolExternalTool }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); - mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValue(false); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); - toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); - - return { - user, - contextExternalToolId, - contextExternalTool, - toolLaunchData, - }; - }; - - it('should throw MissingMediaLicenseLoggableException', async () => { - const { user, contextExternalToolId } = setup(); - - const toolLaunchRequest: Promise = uc.getContextExternalToolLaunchRequest( - user.id, - contextExternalToolId - ); - - await expect(toolLaunchRequest).rejects.toThrow(MissingMediaLicenseLoggableException); - }); - }); - }); }); describe('getSchoolExternalToolLaunchRequest', () => { - describe('when licensing feature is disabled', () => { + describe('when tool exists', () => { const setup = () => { const user: User = userFactory.build(); const schoolExternalToolId = new ObjectId().toHexString(); @@ -373,7 +184,6 @@ describe('ToolLaunchUc', () => { schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - configService.get.mockReturnValueOnce(false); toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); toolLaunchService.generateLaunchRequest.mockReturnValueOnce(toolLaunchRequest); @@ -435,200 +245,6 @@ describe('ToolLaunchUc', () => { }); }); - describe('when licensing feature flag is enabled', () => { - describe('when the tool has no medium id', () => { - const setup = () => { - const user: User = userFactory.build(); - const externalTool: ExternalTool = externalToolFactory.build(); - const schoolExternalToolId = new ObjectId().toHexString(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ id: schoolExternalToolId }); - const contextExternalTool: ContextExternalToolLaunchable = { - schoolToolRef: { - schoolToolId: schoolExternalToolId, - }, - contextRef: { - type: ToolContextType.MEDIA_BOARD, - id: new ObjectId().toHexString(), - }, - parameters: [], - }; - const toolLaunchData: ToolLaunchData = new ToolLaunchData({ - baseUrl: 'baseUrl', - type: ToolLaunchDataType.BASIC, - openNewTab: true, - properties: [], - }); - const toolLaunchRequest = new ToolLaunchRequest({ - openNewTab: true, - method: LaunchRequestMethod.GET, - url: 'https://mock.com/', - }); - - schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - configService.get.mockReturnValueOnce(true); - toolLaunchService.loadToolHierarchy.mockResolvedValue({ externalTool, schoolExternalTool }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([]); - toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); - toolLaunchService.generateLaunchRequest.mockReturnValueOnce(toolLaunchRequest); - - return { - user, - schoolExternalTool, - contextExternalTool, - toolLaunchData, - toolLaunchRequest, - }; - }; - - it('should not check license', async () => { - const { user, contextExternalTool } = setup(); - - await uc.getSchoolExternalToolLaunchRequest(user.id, contextExternalTool); - - expect(mediaUserLicenseService.hasLicenseForExternalTool).not.toHaveBeenCalled(); - }); - - it('should return launch request', async () => { - const { user, contextExternalTool } = setup(); - - const toolLaunchRequest: ToolLaunchRequest = await uc.getSchoolExternalToolLaunchRequest( - user.id, - contextExternalTool - ); - - expect(toolLaunchRequest).toBeDefined(); - }); - }); - - describe('when the tool has a medium id and a license exists', () => { - const setup = () => { - const user: User = userFactory.build(); - const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); - const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: mediaUserLicense.mediumId, mediaSourceId: mediaUserLicense.mediaSourceId }, - }); - const schoolExternalToolId = new ObjectId().toHexString(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ id: schoolExternalToolId }); - const contextExternalTool: ContextExternalToolLaunchable = { - schoolToolRef: { - schoolToolId: schoolExternalToolId, - }, - contextRef: { - type: ToolContextType.MEDIA_BOARD, - id: new ObjectId().toHexString(), - }, - parameters: [], - }; - const toolLaunchData: ToolLaunchData = new ToolLaunchData({ - baseUrl: 'baseUrl', - type: ToolLaunchDataType.BASIC, - openNewTab: true, - properties: [], - }); - const toolLaunchRequest = new ToolLaunchRequest({ - openNewTab: true, - method: LaunchRequestMethod.GET, - url: 'https://mock.com/', - }); - - schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - configService.get.mockReturnValueOnce(true); - toolLaunchService.loadToolHierarchy.mockResolvedValue({ externalTool, schoolExternalTool }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserLicense]); - mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValueOnce(true); - toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); - toolLaunchService.generateLaunchRequest.mockReturnValueOnce(toolLaunchRequest); - - return { - user, - externalTool, - schoolExternalTool, - contextExternalTool, - toolLaunchData, - toolLaunchRequest, - mediaUserLicense, - }; - }; - - it('should check license', async () => { - const { user, contextExternalTool, mediaUserLicense, externalTool } = setup(); - - await uc.getSchoolExternalToolLaunchRequest(user.id, contextExternalTool); - - expect(mediaUserLicenseService.hasLicenseForExternalTool).toHaveBeenCalledWith(externalTool.medium, [ - mediaUserLicense, - ]); - }); - - it('should return launch request', async () => { - const { user, contextExternalTool, toolLaunchRequest } = setup(); - - const result = await uc.getSchoolExternalToolLaunchRequest(user.id, contextExternalTool); - - expect(result).toEqual(toolLaunchRequest); - }); - }); - - describe('when the tool has a medium id and no license exists', () => { - const setup = () => { - const user: User = userFactory.build(); - const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: 'mediumId' }, - }); - const schoolExternalToolId = new ObjectId().toHexString(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ id: schoolExternalToolId }); - const contextExternalTool: ContextExternalToolLaunchable = { - schoolToolRef: { - schoolToolId: schoolExternalToolId, - }, - contextRef: { - type: ToolContextType.MEDIA_BOARD, - id: new ObjectId().toHexString(), - }, - parameters: [], - }; - const toolLaunchData: ToolLaunchData = new ToolLaunchData({ - baseUrl: 'baseUrl', - type: ToolLaunchDataType.BASIC, - openNewTab: true, - properties: [], - }); - const toolLaunchRequest = new ToolLaunchRequest({ - openNewTab: true, - method: LaunchRequestMethod.GET, - url: 'https://mock.com/', - }); - - schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - configService.get.mockReturnValueOnce(true); - toolLaunchService.loadToolHierarchy.mockResolvedValue({ externalTool, schoolExternalTool }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([]); - mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValueOnce(false); - toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); - toolLaunchService.generateLaunchRequest.mockReturnValueOnce(toolLaunchRequest); - - return { - user, - schoolExternalTool, - contextExternalTool, - toolLaunchData, - toolLaunchRequest, - }; - }; - - it('should throw MissingMediaLicenseLoggableException', async () => { - const { user, contextExternalTool } = setup(); - - await expect(uc.getSchoolExternalToolLaunchRequest(user.id, contextExternalTool)).rejects.toThrow( - MissingMediaLicenseLoggableException - ); - }); - }); - }); - describe('when launching a context that is not available', () => { const setup = () => { const user: User = userFactory.build(); 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 4962760cdd6..4f96c380554 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,10 +1,5 @@ import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { ProvisioningConfig } from '@modules/provisioning'; -import { MissingMediaLicenseLoggableException } from '@modules/tool/tool-launch/error'; -import { MediaUserLicense, UserLicenseService } from '@modules/user-license'; -import { MediaUserLicenseService } from '@modules/user-license/service'; import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -25,10 +20,7 @@ export class ToolLaunchUc { private readonly contextExternalToolService: ContextExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, private readonly toolPermissionHelper: ToolPermissionHelper, - private readonly authorizationService: AuthorizationService, - private readonly userLicenseService: UserLicenseService, - private readonly mediaUserLicenseService: MediaUserLicenseService, - private readonly configService: ConfigService + private readonly authorizationService: AuthorizationService ) {} async getContextExternalToolLaunchRequest( @@ -43,10 +35,6 @@ export class ToolLaunchUc { const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); await this.toolPermissionHelper.ensureContextPermissions(user, contextExternalTool, context); - if (this.configService.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED')) { - await this.checkUserHasLicenseForExternalTool(contextExternalTool, userId); - } - const toolLaunchData: ToolLaunchData = await this.toolLaunchService.getLaunchData(userId, contextExternalTool); const launchRequest: ToolLaunchRequest = this.toolLaunchService.generateLaunchRequest(toolLaunchData); @@ -78,10 +66,6 @@ export class ToolLaunchUc { await this.contextExternalToolService.checkContextRestrictions(pseudoContextExternalTool); - if (this.configService.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED')) { - await this.checkUserHasLicenseForExternalTool(pseudoContextExternalTool, userId); - } - const toolLaunchData: ToolLaunchData = await this.toolLaunchService.getLaunchData( userId, pseudoContextExternalTool @@ -90,22 +74,4 @@ export class ToolLaunchUc { return launchRequest; } - - private async checkUserHasLicenseForExternalTool( - contextExternalTool: ContextExternalToolLaunchable, - userId: EntityId - ): Promise { - const schoolExternalToolId: EntityId = contextExternalTool.schoolToolRef.schoolToolId; - - const { externalTool } = await this.toolLaunchService.loadToolHierarchy(schoolExternalToolId); - - const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser(userId); - - if ( - externalTool.medium && - !this.mediaUserLicenseService.hasLicenseForExternalTool(externalTool.medium, mediaUserLicenses) - ) { - throw new MissingMediaLicenseLoggableException(externalTool.medium, userId, contextExternalTool); - } - } }