diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts index 3d07455440d..16e9d355428 100644 --- a/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts +++ b/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts @@ -5,6 +5,7 @@ export const schulconnexLizenzInfoResponseFactory = Factory.define { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const externalTool = externalToolEntityFactory.build(); - const licensedUnusedExternalTool = externalToolEntityFactory.build({ medium: { mediumId: 'mediumId' } }); + const licensedUnusedExternalTool = externalToolEntityFactory.withMedium().build(); const unusedExternalTool = externalToolEntityFactory.build({ medium: { mediumId: 'notLicensedByUser' } }); const schoolExternalTool = schoolExternalToolEntityFactory.build({ tool: externalTool, @@ -532,6 +532,7 @@ describe('Media Board (API)', () => { const userLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ user: studentUser, mediumId: 'mediumId', + mediaSourceId: 'mediaSourceId', }); await em.persistAndFlush([ diff --git a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts index 6047ac25ed6..275fd859134 100644 --- a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts +++ b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts @@ -100,9 +100,9 @@ export class MediaAvailableLineUc { const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser(userId); matchedTools = matchedTools.filter((tool: [ExternalTool, SchoolExternalTool]): boolean => { - const externalToolMediumId = tool[0]?.medium?.mediumId; - if (externalToolMediumId) { - return this.mediaUserLicenseService.hasLicenseForExternalTool(externalToolMediumId, mediaUserLicenses); + const externalToolMedium = tool[0]?.medium; + if (externalToolMedium) { + return this.mediaUserLicenseService.hasLicenseForExternalTool(externalToolMedium, mediaUserLicenses); } return true; }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts index 1bea7896289..4698d751599 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts @@ -8,12 +8,14 @@ import { SanisSonstigeGruppenzugehoerigeResponse, schulconnexResponseFactory, } from '@infra/schulconnex-client'; +import { SchulconnexLizenzInfoResponse } from '@infra/schulconnex-client/response'; +import { schulconnexLizenzInfoResponseFactory } from '@infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory'; import { GroupTypes } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; -import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; +import { ExternalGroupDto, ExternalLicenseDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { SanisResponseMapper } from './sanis-response.mapper'; describe('SanisResponseMapper', () => { @@ -46,11 +48,13 @@ describe('SanisResponseMapper', () => { const externalSchoolId = 'df66c8e6-cfac-40f7-b35b-0da5d8ee680e'; const sanisResponse: SanisResponse = schulconnexResponseFactory.build(); + const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.build(); return { externalUserId, externalSchoolId, sanisResponse, + licenseResponse, }; }; @@ -305,4 +309,21 @@ describe('SanisResponseMapper', () => { }); }); }); + + describe('mapToExternalLicenses', () => { + describe('when a sanis response with license is provided', () => { + it('should map the response to an ExternalLicenseDto', () => { + const { licenseResponse } = setupSanisResponse(); + + const result: ExternalLicenseDto[] = SanisResponseMapper.mapToExternalLicenses(licenseResponse); + + expect(result).toEqual([ + { + mediumId: 'bildungscloud', + mediaSourceId: undefined, + }, + ]); + }); + }); + }); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts index ba072041846..8937b5bec79 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts @@ -172,13 +172,18 @@ export class SanisResponseMapper { } public static mapToExternalLicenses(licenseInfos: SchulconnexLizenzInfoResponse[]): ExternalLicenseDto[] { - const externalLicenseDtos: ExternalLicenseDto[] = licenseInfos.map( - (license: SchulconnexLizenzInfoResponse) => - new ExternalLicenseDto({ - mediumId: license.target.uid, - mediaSourceId: license.target.partOf, - }) - ); + const externalLicenseDtos: ExternalLicenseDto[] = licenseInfos.map((license: SchulconnexLizenzInfoResponse) => { + if (license.target.partOf === '') { + license.target.partOf = undefined; + } + + const externalLicenseDto: ExternalLicenseDto = new ExternalLicenseDto({ + mediumId: license.target.uid, + mediaSourceId: license.target.partOf, + }); + + return externalLicenseDto; + }); return externalLicenseDtos; } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts index 4fc0c390535..ba2ad32e6ce 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -183,12 +183,9 @@ describe(SanisProvisioningStrategy.name, () => { const schulconnexLizenzInfoResponses: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.build(); const schulconnexLizenzInfoResponse = schulconnexLizenzInfoResponses[0]; - const licenses: ExternalLicenseDto[] = [ - new ExternalLicenseDto({ - mediumId: schulconnexLizenzInfoResponse.target.uid, - mediaSourceId: schulconnexLizenzInfoResponse.target.partOf, - }), - ]; + const licenses: ExternalLicenseDto[] = SanisResponseMapper.mapToExternalLicenses([ + schulconnexLizenzInfoResponse, + ]); httpService.get.mockReturnValue(of(createAxiosResponse(sanisResponse))); mapper.mapToExternalUserDto.mockReturnValue(user); diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-medium.params.ts b/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-medium.params.ts index d097b2f5aa1..bf0400e23d7 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-medium.params.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-medium.params.ts @@ -12,4 +12,10 @@ export class ExternalToolMediumParams { @IsNotEmpty() @ApiPropertyOptional({ type: String, description: 'Publisher of the medium' }) publisher?: string; + + @IsString() + @IsOptional() + @IsNotEmpty() + @ApiPropertyOptional({ type: String, description: 'The id of the media source' }) + mediaSourceId?: string; } diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-medium.response.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-medium.response.ts index a56f8291daf..84ee950498b 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-medium.response.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-medium.response.ts @@ -7,8 +7,12 @@ export class ExternalToolMediumResponse { @ApiPropertyOptional({ type: String, description: 'Publisher of the medium' }) publisher?: string; + @ApiPropertyOptional({ type: String, description: 'The id of the media source' }) + mediaSourceId?: string; + constructor(props: ExternalToolMediumResponse) { this.mediumId = props.mediumId; this.publisher = props.publisher; + this.mediaSourceId = props.mediaSourceId; } } diff --git a/apps/server/src/modules/tool/external-tool/domain/external-tool-medium.do.ts b/apps/server/src/modules/tool/external-tool/domain/external-tool-medium.do.ts index 8bfeeab4abc..0c99e8dd1dd 100644 --- a/apps/server/src/modules/tool/external-tool/domain/external-tool-medium.do.ts +++ b/apps/server/src/modules/tool/external-tool/domain/external-tool-medium.do.ts @@ -2,6 +2,8 @@ export interface ExternalToolMediumProps { mediumId: string; publisher?: string; + + mediaSourceId?: string; } export class ExternalToolMedium { @@ -9,8 +11,11 @@ export class ExternalToolMedium { publisher?: string; + mediaSourceId?: string; + constructor(props: ExternalToolMediumProps) { this.mediumId = props.mediumId; this.publisher = props.publisher; + this.mediaSourceId = props.mediaSourceId; } } diff --git a/apps/server/src/modules/tool/external-tool/entity/external-tool-medium.entity.ts b/apps/server/src/modules/tool/external-tool/entity/external-tool-medium.entity.ts index acb2094cf8e..f02ae36a58d 100644 --- a/apps/server/src/modules/tool/external-tool/entity/external-tool-medium.entity.ts +++ b/apps/server/src/modules/tool/external-tool/entity/external-tool-medium.entity.ts @@ -8,8 +8,12 @@ export class ExternalToolMediumEntity { @Property({ nullable: true }) publisher?: string; + @Property({ nullable: true }) + mediaSourceId?: string; + constructor(props: ExternalToolMediumEntity) { this.mediumId = props.mediumId; this.publisher = props.publisher; + this.mediaSourceId = props.mediaSourceId; } } diff --git a/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts b/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts index 5da47740fab..3e2357cdfdd 100644 --- a/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts @@ -79,6 +79,7 @@ export class ExternalToolEntityFactory extends BaseFactory medium: { mediumId: 'mediumId', publisher: 'publisher', + mediaSourceId: 'mediaSourceId', ...externalToolMedium, }, }; 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 0e787b93853..5135cf210f4 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 @@ -506,7 +506,7 @@ describe('ToolLaunchUc', () => { const user: User = userFactory.build(); const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: mediaUserLicense.mediumId }, + medium: { mediumId: mediaUserLicense.mediumId, mediaSourceId: mediaUserLicense.mediaSourceId }, }); const schoolExternalToolId = new ObjectId().toHexString(); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ id: schoolExternalToolId }); @@ -543,6 +543,7 @@ describe('ToolLaunchUc', () => { return { user, + externalTool, schoolExternalTool, contextExternalTool, toolLaunchData, @@ -552,11 +553,11 @@ describe('ToolLaunchUc', () => { }; it('should check license', async () => { - const { user, contextExternalTool, mediaUserLicense } = setup(); + const { user, contextExternalTool, mediaUserLicense, externalTool } = setup(); await uc.getSchoolExternalToolLaunchRequest(user.id, contextExternalTool); - expect(mediaUserLicenseService.hasLicenseForExternalTool).toHaveBeenCalledWith(mediaUserLicense.mediumId, [ + expect(mediaUserLicenseService.hasLicenseForExternalTool).toHaveBeenCalledWith(externalTool.medium, [ mediaUserLicense, ]); }); 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 cee7aa74877..4962760cdd6 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 @@ -102,8 +102,8 @@ export class ToolLaunchUc { const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser(userId); if ( - externalTool.medium?.mediumId && - !this.mediaUserLicenseService.hasLicenseForExternalTool(externalTool.medium.mediumId, mediaUserLicenses) + externalTool.medium && + !this.mediaUserLicenseService.hasLicenseForExternalTool(externalTool.medium, mediaUserLicenses) ) { throw new MissingMediaLicenseLoggableException(externalTool.medium, userId, contextExternalTool); } diff --git a/apps/server/src/modules/user-license/service/media-user-license.service.spec.ts b/apps/server/src/modules/user-license/service/media-user-license.service.spec.ts index 12f84280635..1fcac4b22aa 100644 --- a/apps/server/src/modules/user-license/service/media-user-license.service.spec.ts +++ b/apps/server/src/modules/user-license/service/media-user-license.service.spec.ts @@ -1,3 +1,4 @@ +import { ExternalToolMedium } from '@modules/tool/external-tool/domain'; import { MediaUserLicense, mediaUserLicenseFactory } from '@modules/user-license'; import { MediaUserLicenseService } from '@modules/user-license/service/media-user-license.service'; import { Test, TestingModule } from '@nestjs/testing'; @@ -25,20 +26,54 @@ describe(MediaUserLicenseService.name, () => { describe('hasLicenseForExternalTool', () => { describe('when user has license', () => { const setup = () => { - const mediumId = 'mediumId'; - const mediaUserLicenses: MediaUserLicense[] = mediaUserLicenseFactory.buildList(2); - mediaUserLicenses[0].mediumId = 'mediumId'; + const toolMedium: ExternalToolMedium = { + mediumId: 'mediumId', + mediaSourceId: 'mediaSourceId', + }; + const medium = mediaUserLicenseFactory.build({ + mediumId: toolMedium.mediumId, + mediaSourceId: toolMedium.mediaSourceId, + }); + const unusedMedium = mediaUserLicenseFactory.build(); + const mediaUserLicenses: MediaUserLicense[] = [medium, unusedMedium]; + + return { + toolMedium, + mediaUserLicenses, + }; + }; + + it('should return true', () => { + const { toolMedium, mediaUserLicenses } = setup(); + + const result = service.hasLicenseForExternalTool(toolMedium, mediaUserLicenses); + + expect(result).toEqual(true); + }); + }); + + describe('when user has license without sourceId', () => { + const setup = () => { + const toolMedium: ExternalToolMedium = { + mediumId: 'mediumId', + }; + const medium = mediaUserLicenseFactory.build({ + mediumId: toolMedium.mediumId, + mediaSourceId: undefined, + }); + const unusedMedium = mediaUserLicenseFactory.build(); + const mediaUserLicenses: MediaUserLicense[] = [medium, unusedMedium]; return { - mediumId, + toolMedium, mediaUserLicenses, }; }; it('should return true', () => { - const { mediumId, mediaUserLicenses } = setup(); + const { toolMedium, mediaUserLicenses } = setup(); - const result = service.hasLicenseForExternalTool(mediumId, mediaUserLicenses); + const result = service.hasLicenseForExternalTool(toolMedium, mediaUserLicenses); expect(result).toEqual(true); }); @@ -46,19 +81,19 @@ describe(MediaUserLicenseService.name, () => { describe('when user has not the correct license', () => { const setup = () => { - const mediumId = 'mediumId'; + const medium: ExternalToolMedium = { mediumId: 'mediumId' }; const mediaUserLicenses: MediaUserLicense[] = mediaUserLicenseFactory.buildList(2); return { - mediumId, + medium, mediaUserLicenses, }; }; it('should return false', () => { - const { mediumId, mediaUserLicenses } = setup(); + const { medium, mediaUserLicenses } = setup(); - const result = service.hasLicenseForExternalTool(mediumId, mediaUserLicenses); + const result = service.hasLicenseForExternalTool(medium, mediaUserLicenses); expect(result).toEqual(false); }); @@ -66,19 +101,19 @@ describe(MediaUserLicenseService.name, () => { describe('when user has no licenses', () => { const setup = () => { - const mediumId = 'mediumId'; + const medium: ExternalToolMedium = { mediumId: 'mediumId' }; const mediaUserLicenses: MediaUserLicense[] = []; return { - mediumId, + medium, mediaUserLicenses, }; }; it('should return false', () => { - const { mediumId, mediaUserLicenses } = setup(); + const { medium, mediaUserLicenses } = setup(); - const result = service.hasLicenseForExternalTool(mediumId, mediaUserLicenses); + const result = service.hasLicenseForExternalTool(medium, mediaUserLicenses); expect(result).toEqual(false); }); diff --git a/apps/server/src/modules/user-license/service/media-user-license.service.ts b/apps/server/src/modules/user-license/service/media-user-license.service.ts index dc6d0bc86e9..2bb808887a9 100644 --- a/apps/server/src/modules/user-license/service/media-user-license.service.ts +++ b/apps/server/src/modules/user-license/service/media-user-license.service.ts @@ -1,9 +1,16 @@ +import { ExternalToolMedium } from '@modules/tool/external-tool/domain'; import { MediaUserLicense } from '@modules/user-license'; import { Injectable } from '@nestjs/common'; @Injectable() export class MediaUserLicenseService { - public hasLicenseForExternalTool(externalToolMediumId: string, mediaUserLicenses: MediaUserLicense[]): boolean { - return mediaUserLicenses.some((license: MediaUserLicense) => license.mediumId === externalToolMediumId); + public hasLicenseForExternalTool( + externalToolMedium: ExternalToolMedium, + mediaUserLicenses: MediaUserLicense[] + ): boolean { + return mediaUserLicenses.some( + (license: MediaUserLicense) => + license.mediumId === externalToolMedium.mediumId && license.mediaSourceId === externalToolMedium.mediaSourceId + ); } } diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts index f4fa71957eb..83fc394fd44 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts @@ -64,6 +64,7 @@ export class ExternalToolRepoMapper { return new ExternalToolMedium({ mediumId: entity.mediumId, publisher: entity.publisher, + mediaSourceId: entity.mediaSourceId, }); } @@ -137,6 +138,7 @@ export class ExternalToolRepoMapper { return new ExternalToolMediumEntity({ mediumId: medium.mediumId, publisher: medium.publisher, + mediaSourceId: medium.mediaSourceId, }); }