diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts index 9249683024a..8d08d2fa56f 100644 --- a/apps/server/src/infra/schulconnex-client/index.ts +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -17,5 +17,5 @@ export { SchulconnexPersonenkontextResponse, SchulconnexSonstigeGruppenzugehoerigeResponse, } from './response'; -export { schulconnexResponseFactory } from './testing/schulconnex-response-factory'; +export { schulconnexResponseFactory, schulconnexLizenzInfoResponseFactory } from './testing'; export { SchulconnexClientConfig } from './schulconnex-client-config'; diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts index 35d871c8ab6..4c4884a21cd 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts @@ -200,7 +200,7 @@ describe(SchulconnexRestClient.name, () => { describe('when requesting lizenz-info', () => { const setup = () => { const accessToken = 'accessToken'; - const response: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.build(); + const response: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1); httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); @@ -235,7 +235,7 @@ describe(SchulconnexRestClient.name, () => { const setup = () => { const accessToken = 'accessToken'; const customUrl = 'https://override.url/lizenz-info'; - const response: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.build(); + const response: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1); httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); diff --git a/apps/server/src/infra/schulconnex-client/testing/index.ts b/apps/server/src/infra/schulconnex-client/testing/index.ts index 673fc651a51..23b700123f1 100644 --- a/apps/server/src/infra/schulconnex-client/testing/index.ts +++ b/apps/server/src/infra/schulconnex-client/testing/index.ts @@ -1 +1,2 @@ export { schulconnexResponseFactory } from './schulconnex-response-factory'; +export { schulconnexLizenzInfoResponseFactory } from './schulconnex-lizenz-info-response-factory'; 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 16e9d355428..c89989a180e 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 @@ -1,8 +1,8 @@ import { Factory } from 'fishery'; import { SchulconnexLizenzInfoActionType, SchulconnexLizenzInfoResponse } from '../response'; -export const schulconnexLizenzInfoResponseFactory = Factory.define(() => [ - { +export const schulconnexLizenzInfoResponseFactory = Factory.define(() => { + return { target: { uid: 'bildungscloud', partOf: '', @@ -12,5 +12,5 @@ export const schulconnexLizenzInfoResponseFactory = Factory.define { + const uniqueSourceIds: unknown[] = await this.getCollection('user-licenses').distinct('mediaSourceId'); + + const validSourceIds: string[] = uniqueSourceIds.filter( + (sourceId): sourceId is string => !!sourceId && typeof sourceId === 'string' + ); + + if (!validSourceIds.length) { + console.info(`no media sources for migration found`); + return; + } + + const sourceObjects: MediaSourceEntityProps[] = validSourceIds.map((sourceId: string): MediaSourceEntityProps => { + return { + sourceId, + }; + }); + + const mediaSourcesInsertResult = await this.driver.nativeInsertMany('media-sources', sourceObjects); + + console.info(`${mediaSourcesInsertResult.affectedRows} media sources were added`); + + const mediaSources = await this.driver.find<{ _id: ObjectId } & MediaSourceEntityProps>('media-sources', {}); + + let total = 0; + for await (const mediaSource of mediaSources) { + const userLicenses = await this.driver.nativeUpdate( + 'user-licenses', + { + mediaSourceId: mediaSource.sourceId, + }, + { + mediaSourceId: mediaSource._id, + } + ); + + total += userLicenses.affectedRows; + console.info( + `${userLicenses.affectedRows} user-licenses are now referenced to ${mediaSource.sourceId ?? 'unknown'}` + ); + } + console.info(`${total} user-licenses are now referenced to media-sources`); + + const userLicensesNameChange = await this.driver.nativeUpdate( + 'user-licenses', + { + mediaSourceId: { $exists: true }, + }, + { + $rename: { mediaSourceId: 'mediaSource' }, + } + ); + console.info(`mediaSourceId was renamed to mediaSource in ${userLicensesNameChange.affectedRows} user licenses`); + } + + async down(): Promise { + await this.driver.aggregate('user-licenses', [ + { $match: { mediaSource: { $ne: null } } }, + { $lookup: { from: 'media-sources', localField: 'mediaSource', foreignField: '_id', as: 'mediaSourceId' } }, // Joins media-sources as array in mediaSourceId + { $unwind: { path: '$mediaSourceId', preserveNullAndEmptyArrays: true } }, // Transforms mediaSourceId array to individual documents with the object + { $set: { mediaSourceId: '$mediaSourceId.sourceId' } }, // Replaces the mediaSourceId object with the contained mediaSourceId + { $unset: 'mediaSource' }, + { $merge: { into: 'user-licenses', on: '_id', whenMatched: 'replace', whenNotMatched: 'fail' } }, + ]); + + await this.getCollection('media-sources').drop(); + console.info(`media-sources was dropped`); + } +} diff --git a/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts b/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts index 756fcddc685..30d8ae2287c 100644 --- a/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts +++ b/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts @@ -4,7 +4,7 @@ import { contextExternalToolEntityFactory } from '@modules/tool/context-external import { externalToolEntityFactory } from '@modules/tool/external-tool/testing'; import { schoolExternalToolEntityFactory } from '@modules/tool/school-external-tool/testing'; import { MediaUserLicenseEntity } from '@modules/user-license/entity'; -import { mediaUserLicenseEntityFactory } from '@modules/user-license/testing'; +import { mediaSourceEntityFactory, mediaUserLicenseEntityFactory } from '@modules/user-license/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { TestApiClient, UserAndAccountTestFactory, type DatesToStrings } from '@shared/testing'; @@ -498,8 +498,14 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + const mediaSource = mediaSourceEntityFactory.build(); const externalTool = externalToolEntityFactory.build(); - const licensedUnusedExternalTool = externalToolEntityFactory.withMedium().build(); + const licensedUnusedExternalTool = externalToolEntityFactory + .withMedium({ + mediumId: 'mediumId', + mediaSourceId: mediaSource.sourceId, + }) + .build(); const unusedExternalTool = externalToolEntityFactory.build({ medium: { mediumId: 'notLicensedByUser' } }); const schoolExternalTool = schoolExternalToolEntityFactory.build({ tool: externalTool, @@ -531,7 +537,7 @@ describe('Media Board (API)', () => { const userLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ user: studentUser, mediumId: 'mediumId', - mediaSourceId: 'mediaSourceId', + mediaSource, }); await em.persistAndFlush([ diff --git a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts index 1a361ff9547..534e5b133c0 100644 --- a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts +++ b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts @@ -4,8 +4,7 @@ import { Action, AuthorizationService } from '@modules/authorization'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; -import { MediaUserLicense, mediaUserLicenseFactory, UserLicenseService } from '@modules/user-license'; -import { MediaUserLicenseService } from '@modules/user-license/service'; +import { MediaUserLicense, mediaUserLicenseFactory, MediaUserLicenseService } from '@modules/user-license'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; @@ -45,7 +44,6 @@ describe(MediaAvailableLineUc.name, () => { let mediaAvailableLineService: DeepMocked; let configService: DeepMocked>; let mediaBoardService: DeepMocked; - let userLicenseService: DeepMocked; let mediaUserLicenseService: DeepMocked; beforeAll(async () => { @@ -78,10 +76,6 @@ describe(MediaAvailableLineUc.name, () => { provide: MediaBoardService, useValue: createMock(), }, - { - provide: UserLicenseService, - useValue: createMock(), - }, { provide: MediaUserLicenseService, useValue: createMock(), @@ -96,7 +90,6 @@ describe(MediaAvailableLineUc.name, () => { mediaAvailableLineService = module.get(MediaAvailableLineService); configService = module.get(ConfigService); mediaBoardService = module.get(MediaBoardService); - userLicenseService = module.get(UserLicenseService); mediaUserLicenseService = module.get(MediaUserLicenseService); }); @@ -256,7 +249,7 @@ describe(MediaAvailableLineUc.name, () => { const schoolExternalTool1: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool1.id }); const schoolExternalTool2: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool2.id }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([]); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValue([]); boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); mediaAvailableLineService.getUnusedAvailableSchoolExternalTools.mockResolvedValueOnce([ @@ -326,7 +319,7 @@ describe(MediaAvailableLineUc.name, () => { const mediaUserlicense: MediaUserLicense = mediaUserLicenseFactory.build(); mediaUserlicense.mediumId = 'mediumId'; - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValue(true); boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); @@ -392,7 +385,7 @@ describe(MediaAvailableLineUc.name, () => { const mediaUserlicense: MediaUserLicense = mediaUserLicenseFactory.build(); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValue(false); boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); 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 b2dc2dd0716..c17a23afb7e 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 @@ -1,13 +1,13 @@ import { Action, AuthorizationService } from '@modules/authorization'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; -import { MediaUserLicense, UserLicenseService } from '@modules/user-license'; -import { MediaUserLicenseService } from '@modules/user-license/service'; +import { MediaUserLicense, MediaUserLicenseService } from '@modules/user-license'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; import { EntityId } from '@shared/domain/types'; import { MediaAvailableLine, MediaBoard } from '../../domain'; +import { MediaBoardColors } from '../../domain/media-board/types'; import type { MediaBoardConfig } from '../../media-board.config'; import { BoardNodePermissionService, @@ -15,7 +15,6 @@ import { MediaAvailableLineService, MediaBoardService, } from '../../service'; -import { MediaBoardColors } from '../../domain/media-board/types'; @Injectable() export class MediaAvailableLineUc { @@ -26,7 +25,6 @@ export class MediaAvailableLineUc { private readonly mediaAvailableLineService: MediaAvailableLineService, private readonly mediaBoardService: MediaBoardService, private readonly configService: ConfigService, - private readonly userLicenseService: UserLicenseService, private readonly mediaUserLicenseService: MediaUserLicenseService ) {} @@ -95,7 +93,9 @@ export class MediaAvailableLineUc { userId: EntityId, matchedTools: [ExternalTool, SchoolExternalTool][] ): Promise<[ExternalTool, SchoolExternalTool][]> { - const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser(userId); + const mediaUserLicenses: MediaUserLicense[] = await this.mediaUserLicenseService.getMediaUserLicensesForUser( + userId + ); matchedTools = matchedTools.filter((tool: [ExternalTool, SchoolExternalTool]): boolean => { const externalToolMedium = tool[0]?.medium; diff --git a/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.spec.ts index 04d3ef13c91..8cfbe2a5677 100644 --- a/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.spec.ts +++ b/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.spec.ts @@ -28,7 +28,7 @@ describe('SchoolExternalToolCreatedLoggable', () => { userId: license.userId, schoolId: schoolExternalTool.schoolId, mediumId: license.mediumId, - mediaSourceId: license.mediaSourceId, + mediaSourceId: license.mediaSource?.sourceId, schoolExternalToolId: schoolExternalTool.id, }, }); diff --git a/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.ts b/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.ts index 1a3d5de9150..5525312a845 100644 --- a/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.ts +++ b/apps/server/src/modules/provisioning/loggable/school-external-tool-created.loggable.ts @@ -12,7 +12,7 @@ export class SchoolExternalToolCreatedLoggable implements Loggable { userId: this.license.userId, schoolId: this.schoolExternalTool.schoolId, mediumId: this.license.mediumId, - mediaSourceId: this.license.mediaSourceId, + mediaSourceId: this.license.mediaSource?.sourceId, schoolExternalToolId: this.schoolExternalTool.id, }, }; diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.spec.ts index 760703f6aba..74f5d254ecb 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.spec.ts @@ -1,5 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { MediaUserLicense, mediaUserLicenseFactory, UserLicenseService, UserLicenseType } from '@modules/user-license'; +import { + MediaSource, + mediaSourceFactory, + MediaSourceService, + MediaUserLicense, + mediaUserLicenseFactory, + MediaUserLicenseService, + UserLicenseType, +} from '@modules/user-license'; import { Test, TestingModule } from '@nestjs/testing'; import { User as UserEntity } from '@shared/domain/entity'; import { setupEntities, userFactory } from '@shared/testing'; @@ -10,7 +18,8 @@ describe(SchulconnexLicenseProvisioningService.name, () => { let module: TestingModule; let service: SchulconnexLicenseProvisioningService; - let userLicenseService: DeepMocked; + let mediaUserLicenseService: DeepMocked; + let mediaSourceService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -18,14 +27,19 @@ describe(SchulconnexLicenseProvisioningService.name, () => { providers: [ SchulconnexLicenseProvisioningService, { - provide: UserLicenseService, - useValue: createMock(), + provide: MediaUserLicenseService, + useValue: createMock(), + }, + { + provide: MediaSourceService, + useValue: createMock(), }, ], }).compile(); service = module.get(SchulconnexLicenseProvisioningService); - userLicenseService = module.get(UserLicenseService); + mediaUserLicenseService = module.get(MediaUserLicenseService); + mediaSourceService = module.get(MediaSourceService); }); afterAll(async () => { @@ -41,9 +55,9 @@ describe(SchulconnexLicenseProvisioningService.name, () => { it('should not call services', async () => { await service.provisionExternalLicenses('userId', undefined); - expect(userLicenseService.getMediaUserLicensesForUser).not.toHaveBeenCalled(); - expect(userLicenseService.saveUserLicense).not.toHaveBeenCalled(); - expect(userLicenseService.deleteUserLicense).not.toHaveBeenCalled(); + expect(mediaUserLicenseService.getMediaUserLicensesForUser).not.toHaveBeenCalled(); + expect(mediaUserLicenseService.saveUserLicense).not.toHaveBeenCalled(); + expect(mediaUserLicenseService.deleteUserLicense).not.toHaveBeenCalled(); }); }); @@ -51,38 +65,67 @@ describe(SchulconnexLicenseProvisioningService.name, () => { const setup = () => { const user: UserEntity = userFactory.build(); - const newExternalLicense: ExternalLicenseDto = { + const newExternalLicense1: ExternalLicenseDto = { + mediumId: 'newMediumId', + mediaSourceId: 'newMediaSourceId', + }; + const newExternalLicense2: ExternalLicenseDto = { mediumId: 'newMediumId', - mediaSourceId: 'nMediaSourceId', }; const existingExternalLicense: ExternalLicenseDto = { mediumId: 'existingMediumId', mediaSourceId: 'existingMediaSourceId', }; - const externalLicenses: ExternalLicenseDto[] = [newExternalLicense]; - - const existingMediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); - existingMediaUserLicense.mediumId = existingExternalLicense.mediumId; - existingMediaUserLicense.mediaSourceId = existingExternalLicense.mediaSourceId; - existingMediaUserLicense.userId = user.id; + const externalLicenses: ExternalLicenseDto[] = [ + newExternalLicense1, + newExternalLicense2, + existingExternalLicense, + ]; + + const mediaSource: MediaSource = mediaSourceFactory.build({ + sourceId: 'existingMediaSourceId', + }); + const existingMediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build({ + mediumId: existingExternalLicense.mediumId, + mediaSource, + userId: user.id, + }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([existingMediaUserLicense]); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([existingMediaUserLicense]); + mediaSourceService.findBySourceId.mockResolvedValueOnce(null); - return { user, externalLicenses, newExternalLicense }; + return { + user, + externalLicenses, + newExternalLicense1, + newExternalLicense2, + }; }; it('should provision new licenses', async () => { - const { user, externalLicenses, newExternalLicense } = setup(); + const { user, externalLicenses, newExternalLicense1, newExternalLicense2 } = setup(); await service.provisionExternalLicenses(user.id, externalLicenses); - expect(userLicenseService.saveUserLicense).toHaveBeenCalledWith( - expect.objectContaining({ + expect(mediaUserLicenseService.saveUserLicense).toHaveBeenCalledWith( + new MediaUserLicense({ + id: expect.any(String), + type: UserLicenseType.MEDIA_LICENSE, + userId: user.id, + mediumId: newExternalLicense1.mediumId, + mediaSource: new MediaSource({ + id: expect.any(String), + sourceId: newExternalLicense1.mediaSourceId as string, + }), + }) + ); + expect(mediaUserLicenseService.saveUserLicense).toHaveBeenCalledWith( + new MediaUserLicense({ id: expect.any(String), type: UserLicenseType.MEDIA_LICENSE, userId: user.id, - mediumId: newExternalLicense.mediumId, - mediaSourceId: newExternalLicense.mediaSourceId, + mediumId: newExternalLicense2.mediumId, + mediaSource: undefined, }) ); }); @@ -94,47 +137,50 @@ describe(SchulconnexLicenseProvisioningService.name, () => { const activeExternalLicense: ExternalLicenseDto = { mediumId: 'activeMediumId', - mediaSourceId: 'activeMediaSourceId', + mediaSourceId: 'mediaSourceId', }; - const existingMediaUserLicenses: MediaUserLicense[] = mediaUserLicenseFactory.buildList(1, { + const mediaSource: MediaSource = mediaSourceFactory.build({ + sourceId: 'mediaSourceId', + }); + const expiredMediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build({ userId: user.id, - mediumId: 'toDeleteMediumId', - mediaSourceId: 'toDeleteMediaSourceId', + mediumId: 'expiredMediumId', + mediaSource, + }); + const activeMediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build({ + userId: user.id, + mediumId: 'activeMediumId', + mediaSource, }); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValue(existingMediaUserLicenses); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([ + activeMediaUserLicense, + expiredMediaUserLicense, + ]); - return { user, existingMediaUserLicense: existingMediaUserLicenses[0], activeExternalLicense }; + return { + user, + expiredMediaUserLicense, + activeExternalLicense, + activeMediaUserLicense, + }; }; it('should delete the expired license', async () => { - const { user, existingMediaUserLicense } = setup(); + const { user, expiredMediaUserLicense, activeExternalLicense } = setup(); - await service.provisionExternalLicenses(user.id, []); + await service.provisionExternalLicenses(user.id, [activeExternalLicense]); - expect(userLicenseService.deleteUserLicense).toHaveBeenCalledWith( - expect.objectContaining({ - id: existingMediaUserLicense.id, - type: existingMediaUserLicense.type, - userId: existingMediaUserLicense.userId, - mediumId: existingMediaUserLicense.mediumId, - mediaSourceId: existingMediaUserLicense.mediaSourceId, - }) - ); + expect(mediaUserLicenseService.deleteUserLicense).toHaveBeenCalledWith(expiredMediaUserLicense); }); it('should not delete active licenses', async () => { - const { user, activeExternalLicense } = setup(); + const { user, activeExternalLicense, activeMediaUserLicense } = setup(); await service.provisionExternalLicenses(user.id, [activeExternalLicense]); - expect(userLicenseService.deleteUserLicense).not.toHaveBeenCalledWith( - expect.objectContaining({ - mediumId: activeExternalLicense.mediumId, - mediaSourceId: activeExternalLicense.mediaSourceId, - }) - ); + expect(mediaUserLicenseService.deleteUserLicense).not.toHaveBeenCalledWith(activeMediaUserLicense); }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.ts index 4646fff786b..a345cbf5fde 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.ts @@ -1,21 +1,29 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaUserLicense, UserLicenseService, UserLicenseType } from '@modules/user-license'; +import { + MediaSource, + MediaSourceService, + MediaUserLicense, + MediaUserLicenseService, + UserLicenseType, +} from '@modules/user-license'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { ExternalLicenseDto } from '../../../dto'; @Injectable() export class SchulconnexLicenseProvisioningService { - constructor(private readonly userLicenseService: UserLicenseService) {} + constructor( + private readonly mediaUserLicenseService: MediaUserLicenseService, + private readonly mediaSourceService: MediaSourceService + ) {} public async provisionExternalLicenses(userId: EntityId, externalLicenses?: ExternalLicenseDto[]): Promise { if (!externalLicenses) { return; } - const existingMediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser( - userId - ); + const existingMediaUserLicenses: MediaUserLicense[] = + await this.mediaUserLicenseService.getMediaUserLicensesForUser(userId); await this.provisionNewLicenses(externalLicenses, existingMediaUserLicenses, userId); @@ -31,12 +39,13 @@ export class SchulconnexLicenseProvisioningService { externalLicenses.map(async (externalLicense: ExternalLicenseDto): Promise => { const existingLicense: MediaUserLicense | undefined = mediaUserLicenses.find( (license: MediaUserLicense) => - license.mediumId === externalLicense.mediumId && license.mediaSourceId === externalLicense.mediaSourceId + license.mediumId === externalLicense.mediumId && + license.mediaSource?.sourceId === externalLicense.mediaSourceId ); if (!existingLicense) { - const newLicense: MediaUserLicense = this.buildLicense(externalLicense, userId); - await this.userLicenseService.saveUserLicense(newLicense); + const newLicense: MediaUserLicense = await this.buildLicense(externalLicense, userId); + await this.mediaUserLicenseService.saveUserLicense(newLicense); } }) ); @@ -51,22 +60,35 @@ export class SchulconnexLicenseProvisioningService { const existingExternalLicense: ExternalLicenseDto | undefined = externalLicenses.find( (externalLicense: ExternalLicenseDto) => mediaUserLicense.mediumId === externalLicense.mediumId && - mediaUserLicense.mediaSourceId === externalLicense.mediaSourceId + mediaUserLicense.mediaSource?.sourceId === externalLicense.mediaSourceId ); if (!existingExternalLicense) { - await this.userLicenseService.deleteUserLicense(mediaUserLicense); + await this.mediaUserLicenseService.deleteUserLicense(mediaUserLicense); } }) ); } - private buildLicense(externalLicense: ExternalLicenseDto, userId: EntityId): MediaUserLicense { + private async buildLicense(externalLicense: ExternalLicenseDto, userId: EntityId): Promise { + let mediaSource: MediaSource | null = null; + + if (externalLicense.mediaSourceId) { + mediaSource = await this.mediaSourceService.findBySourceId(externalLicense.mediaSourceId); + + if (!mediaSource) { + mediaSource = new MediaSource({ + id: new ObjectId().toHexString(), + sourceId: externalLicense.mediaSourceId, + }); + } + } + const mediaUserLicense: MediaUserLicense = new MediaUserLicense({ id: new ObjectId().toHexString(), type: UserLicenseType.MEDIA_LICENSE, userId, - mediaSourceId: externalLicense.mediaSourceId, + mediaSource: mediaSource ?? undefined, mediumId: externalLicense.mediumId, }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts index c91ce1c59b7..9601b32ab9a 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts @@ -8,7 +8,7 @@ import { customParameterFactory, externalToolFactory } from '@modules/tool/exter import { SchoolExternalToolService } from '@modules/tool/school-external-tool'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; -import { MediaUserLicense, mediaUserLicenseFactory, UserLicenseService } from '@modules/user-license'; +import { MediaUserLicense, mediaUserLicenseFactory, MediaUserLicenseService } from '@modules/user-license'; import { Test, TestingModule } from '@nestjs/testing'; import { schoolSystemOptionsFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; @@ -19,7 +19,7 @@ describe(SchulconnexToolProvisioningService.name, () => { let externalToolService: DeepMocked; let schoolExternalToolService: DeepMocked; - let userLicenseService: DeepMocked; + let mediaUserLicenseService: DeepMocked; let schoolSystemOptionsService: DeepMocked; beforeAll(async () => { @@ -35,8 +35,8 @@ describe(SchulconnexToolProvisioningService.name, () => { useValue: createMock(), }, { - provide: UserLicenseService, - useValue: createMock(), + provide: MediaUserLicenseService, + useValue: createMock(), }, { provide: SchoolSystemOptionsService, @@ -52,7 +52,7 @@ describe(SchulconnexToolProvisioningService.name, () => { service = module.get(SchulconnexToolProvisioningService); externalToolService = module.get(ExternalToolService); schoolExternalToolService = module.get(SchoolExternalToolService); - userLicenseService = module.get(UserLicenseService); + mediaUserLicenseService = module.get(MediaUserLicenseService); schoolSystemOptionsService = module.get(SchoolSystemOptionsService); }); @@ -75,7 +75,10 @@ describe(SchulconnexToolProvisioningService.name, () => { }); const mediaUserLicenses: MediaUserLicense[] = [mediaUserLicenseFactory.build({ userId })]; const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: mediaUserLicenses[0].mediumId, mediaSourceId: mediaUserLicenses[0].mediaSourceId }, + medium: { + mediumId: mediaUserLicenses[0].mediumId, + mediaSourceId: mediaUserLicenses[0].mediaSource?.sourceId, + }, }); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id, @@ -83,7 +86,7 @@ describe(SchulconnexToolProvisioningService.name, () => { }); schoolSystemOptionsService.getProvisioningOptions.mockResolvedValueOnce(provisioningOptions); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); externalToolService.findExternalToolByMedium.mockResolvedValueOnce(externalTool); schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([]); @@ -114,7 +117,7 @@ describe(SchulconnexToolProvisioningService.name, () => { await service.provisionSchoolExternalTools(userId, schoolId, systemId); - expect(userLicenseService.getMediaUserLicensesForUser).toHaveBeenCalledWith(userId); + expect(mediaUserLicenseService.getMediaUserLicensesForUser).toHaveBeenCalledWith(userId); }); it('should get external tool', async () => { @@ -124,7 +127,7 @@ describe(SchulconnexToolProvisioningService.name, () => { expect(externalToolService.findExternalToolByMedium).toHaveBeenCalledWith( mediaUserLicenses[0].mediumId, - mediaUserLicenses[0].mediaSourceId + mediaUserLicenses[0].mediaSource?.sourceId ); }); @@ -163,7 +166,10 @@ describe(SchulconnexToolProvisioningService.name, () => { const { provisioningOptions } = schoolSystemOptionsFactory.build({}); const mediaUserLicenses: MediaUserLicense[] = [mediaUserLicenseFactory.build({ userId })]; const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: mediaUserLicenses[0].mediumId, mediaSourceId: mediaUserLicenses[0].mediaSourceId }, + medium: { + mediumId: mediaUserLicenses[0].mediumId, + mediaSourceId: mediaUserLicenses[0].mediaSource?.sourceId, + }, }); schoolSystemOptionsService.getProvisioningOptions.mockResolvedValueOnce(provisioningOptions); @@ -197,7 +203,7 @@ describe(SchulconnexToolProvisioningService.name, () => { const externalTool: ExternalTool = externalToolFactory.build({}); schoolSystemOptionsService.getProvisioningOptions.mockResolvedValueOnce(provisioningOptions); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([]); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([]); externalToolService.findExternalToolByMedium.mockResolvedValueOnce(externalTool); schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([]); @@ -228,7 +234,7 @@ describe(SchulconnexToolProvisioningService.name, () => { const mediaUserLicenses: MediaUserLicense[] = [mediaUserLicenseFactory.build({ userId })]; schoolSystemOptionsService.getProvisioningOptions.mockResolvedValueOnce(provisioningOptions); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); externalToolService.findExternalToolByMedium.mockResolvedValueOnce(null); schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([]); @@ -258,12 +264,15 @@ describe(SchulconnexToolProvisioningService.name, () => { }); const mediaUserLicenses: MediaUserLicense[] = [mediaUserLicenseFactory.build({ userId })]; const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: mediaUserLicenses[0].mediumId, mediaSourceId: mediaUserLicenses[0].mediaSourceId }, + medium: { + mediumId: mediaUserLicenses[0].mediumId, + mediaSourceId: mediaUserLicenses[0].mediaSource?.sourceId, + }, parameters: [customParameterFactory.build()], }); schoolSystemOptionsService.getProvisioningOptions.mockResolvedValueOnce(provisioningOptions); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); externalToolService.findExternalToolByMedium.mockResolvedValueOnce(externalTool); schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([]); @@ -293,7 +302,10 @@ describe(SchulconnexToolProvisioningService.name, () => { }); const mediaUserLicenses: MediaUserLicense[] = [mediaUserLicenseFactory.build({ userId })]; const externalTool: ExternalTool = externalToolFactory.build({ - medium: { mediumId: mediaUserLicenses[0].mediumId, mediaSourceId: mediaUserLicenses[0].mediaSourceId }, + medium: { + mediumId: mediaUserLicenses[0].mediumId, + mediaSourceId: mediaUserLicenses[0].mediaSource?.sourceId, + }, }); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id, @@ -301,7 +313,7 @@ describe(SchulconnexToolProvisioningService.name, () => { }); schoolSystemOptionsService.getProvisioningOptions.mockResolvedValueOnce(provisioningOptions); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce(mediaUserLicenses); externalToolService.findExternalToolByMedium.mockResolvedValueOnce(externalTool); schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([schoolExternalTool]); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts index 148e3b7e594..75505abe65f 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts @@ -6,7 +6,7 @@ import { CustomParameterScope } from '@modules/tool/common/enum'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { SchoolExternalToolService } from '@modules/tool/school-external-tool'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; -import { MediaUserLicense, UserLicenseService } from '@modules/user-license'; +import { MediaUserLicense, MediaUserLicenseService } from '@modules/user-license'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; @@ -17,7 +17,7 @@ export class SchulconnexToolProvisioningService { constructor( private readonly externalToolService: ExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, - private readonly userLicenseService: UserLicenseService, + private readonly mediaUserLicenseService: MediaUserLicenseService, private readonly schoolSystemOptionsService: SchoolSystemOptionsService, private readonly logger: Logger ) {} @@ -30,13 +30,15 @@ export class SchulconnexToolProvisioningService { return; } - const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseService.getMediaUserLicensesForUser(userId); + const mediaUserLicenses: MediaUserLicense[] = await this.mediaUserLicenseService.getMediaUserLicensesForUser( + userId + ); await Promise.all( mediaUserLicenses.map(async (license) => { const externalTool: ExternalTool | null = await this.externalToolService.findExternalToolByMedium( license.mediumId, - license.mediaSourceId + license.mediaSource?.sourceId ); if (!externalTool || !this.hasOnlyGlobalParamters(externalTool)) { 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 1ff135e602d..5b380116894 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 @@ -173,7 +173,7 @@ describe(SanisProvisioningStrategy.name, () => { }), ]; const schulconnexLizenzInfoResponses: SchulconnexLizenzInfoResponse[] = - schulconnexLizenzInfoResponseFactory.build(); + schulconnexLizenzInfoResponseFactory.buildList(1); const schulconnexLizenzInfoResponse = schulconnexLizenzInfoResponses[0]; const licenses: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses([ schulconnexLizenzInfoResponse, diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts index 72507971a21..86a91085ec6 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts @@ -3,13 +3,13 @@ import { SchulconnexGroupRole, SchulconnexGroupType, SchulconnexGruppenResponse, + schulconnexLizenzInfoResponseFactory, SchulconnexPersonenkontextResponse, SchulconnexResponse, schulconnexResponseFactory, SchulconnexSonstigeGruppenzugehoerigeResponse, } 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'; @@ -43,25 +43,21 @@ describe(SchulconnexResponseMapper.name, () => { provisioningFeatures = module.get(ProvisioningFeatures); }); - const setupSchulconnexResponse = () => { - const externalUserId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; - const externalSchoolId = 'df66c8e6-cfac-40f7-b35b-0da5d8ee680e'; + describe('mapToExternalSchoolDto', () => { + describe('when a schulconnex response is provided', () => { + const setup = () => { + const externalSchoolId = 'df66c8e6-cfac-40f7-b35b-0da5d8ee680e'; - const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); - const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.build(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); - return { - externalUserId, - externalSchoolId, - schulconnexResponse, - licenseResponse, - }; - }; + return { + externalSchoolId, + schulconnexResponse, + }; + }; - describe('mapToExternalSchoolDto', () => { - describe('when a schulconnex response is provided', () => { it('should map the response to an ExternalSchoolDto', () => { - const { schulconnexResponse, externalSchoolId } = setupSchulconnexResponse(); + const { schulconnexResponse, externalSchoolId } = setup(); const result: ExternalSchoolDto = mapper.mapToExternalSchoolDto(schulconnexResponse); @@ -77,8 +73,19 @@ describe(SchulconnexResponseMapper.name, () => { describe('mapToExternalUserDto', () => { describe('when a schulconnex response is provided', () => { + const setup = () => { + const externalUserId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; + + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + + return { + externalUserId, + schulconnexResponse, + }; + }; + it('should map the response to an ExternalUserDto', () => { - const { schulconnexResponse, externalUserId } = setupSchulconnexResponse(); + const { schulconnexResponse, externalUserId } = setup(); const result: ExternalUserDto = mapper.mapToExternalUserDto(schulconnexResponse); @@ -97,7 +104,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('mapToExternalGroupDtos', () => { describe('when no group is given', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen = undefined; return { @@ -116,10 +123,8 @@ describe(SchulconnexResponseMapper.name, () => { describe('when unknown group type is given', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - schulconnexResponse.personenkontexte[0].gruppen?.[0].gruppe.typ = 'unknown'; + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + schulconnexResponse.personenkontexte[0].gruppen![0].gruppe.typ = 'unknown' as SchulconnexGroupType; return { schulconnexResponse, @@ -141,7 +146,7 @@ describe(SchulconnexResponseMapper.name, () => { schulconnexOtherGroupusersEnabled: true, }); - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); const personenkontext: SchulconnexPersonenkontextResponse = schulconnexResponse.personenkontexte[0]; const group: SchulconnexGruppenResponse = personenkontext.gruppen![0]; const otherParticipant: SchulconnexSonstigeGruppenzugehoerigeResponse = group.sonstige_gruppenzugehoerige![0]; @@ -179,7 +184,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when group type other is provided', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.typ = SchulconnexGroupType.OTHER; return { @@ -202,7 +207,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when group type course is provided', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppe.typ = SchulconnexGroupType.COURSE; return { @@ -225,7 +230,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when the group role mapping for the user is missing', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppenzugehoerigkeit.rollen = [ SchulconnexGroupRole.SCHOOL_SUPPORT, ]; @@ -246,7 +251,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when the user has no role in the group', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0]!.gruppenzugehoerigkeit.rollen = []; return { @@ -268,7 +273,7 @@ describe(SchulconnexResponseMapper.name, () => { Object.assign>(provisioningFeatures, { schulconnexOtherGroupusersEnabled: false, }); - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0].sonstige_gruppenzugehoerige = undefined; return { @@ -291,7 +296,7 @@ describe(SchulconnexResponseMapper.name, () => { schulconnexOtherGroupusersEnabled: true, }); - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0].sonstige_gruppenzugehoerige = undefined; return { @@ -310,7 +315,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when other participants have unknown roles', () => { const setup = () => { - const { schulconnexResponse } = setupSchulconnexResponse(); + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0]!.sonstige_gruppenzugehoerige = [ { ktid: 'ktid', @@ -334,9 +339,19 @@ describe(SchulconnexResponseMapper.name, () => { }); describe('mapToExternalLicenses', () => { - describe('when a sanis response with license is provided', () => { + describe('when a license response has a medium id and no media source', () => { + const setup = () => { + const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1, { + target: { uid: 'bildungscloud', partOf: '' }, + }); + + return { + licenseResponse, + }; + }; + it('should map the response to an ExternalLicenseDto', () => { - const { licenseResponse } = setupSchulconnexResponse(); + const { licenseResponse } = setup(); const result: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); @@ -348,5 +363,50 @@ describe(SchulconnexResponseMapper.name, () => { ]); }); }); + + describe('when a license response has a medium id and a media source', () => { + const setup = () => { + const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1, { + target: { uid: 'bildungscloud', partOf: 'bildungscloud-source' }, + }); + + return { + licenseResponse, + }; + }; + + it('should map the response to an ExternalLicenseDto', () => { + const { licenseResponse } = setup(); + + const result: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); + + expect(result).toEqual([ + { + mediumId: 'bildungscloud', + mediaSourceId: 'bildungscloud-source', + }, + ]); + }); + }); + + describe('when a license response has no medium id', () => { + const setup = () => { + const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1, { + target: { uid: '', partOf: 'bildungscloud-source' }, + }); + + return { + licenseResponse, + }; + }; + + it('should should be filtered out', () => { + const { licenseResponse } = setup(); + + const result: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); + + expect(result).toEqual([]); + }); + }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts index ebfb786a18b..aef7f09366d 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts @@ -173,18 +173,20 @@ export class SchulconnexResponseMapper { } public static mapToExternalLicenses(licenseInfos: SchulconnexLizenzInfoResponse[]): ExternalLicenseDto[] { - 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; - }); + 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; + }) + .filter((license: ExternalLicenseDto) => license.mediumId !== ''); return externalLicenseDtos; } 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 76b8b7fc7d9..1dc112369d3 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,8 +1,7 @@ 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 { MediaUserLicense, mediaUserLicenseFactory, MediaUserLicenseService } from '@modules/user-license'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; @@ -23,7 +22,6 @@ describe(ToolConfigurationStatusService.name, () => { let service: ToolConfigurationStatusService; let commonToolValidationService: DeepMocked; - let userLicenseService: DeepMocked; let mediaUserLicenseService: DeepMocked; let configService: DeepMocked>; @@ -35,10 +33,6 @@ describe(ToolConfigurationStatusService.name, () => { provide: CommonToolValidationService, useValue: createMock(), }, - { - provide: UserLicenseService, - useValue: createMock(), - }, { provide: MediaUserLicenseService, useValue: createMock(), @@ -52,7 +46,6 @@ describe(ToolConfigurationStatusService.name, () => { service = module.get(ToolConfigurationStatusService); commonToolValidationService = module.get(CommonToolValidationService); - userLicenseService = module.get(UserLicenseService); mediaUserLicenseService = module.get(MediaUserLicenseService); configService = module.get(ConfigService); }); @@ -547,7 +540,7 @@ describe(ToolConfigurationStatusService.name, () => { commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); commonToolValidationService.validateParameters.mockReturnValueOnce([new ValidationError('')]); - userLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([mediaUserLicense]); + mediaUserLicenseService.getMediaUserLicensesForUser.mockResolvedValueOnce([mediaUserLicense]); return { externalTool, @@ -563,7 +556,7 @@ describe(ToolConfigurationStatusService.name, () => { await service.determineToolConfigurationStatus(externalTool, schoolExternalTool, contextExternalTool, userId); - expect(userLicenseService.getMediaUserLicensesForUser).toHaveBeenCalledWith(userId); + expect(mediaUserLicenseService.getMediaUserLicensesForUser).toHaveBeenCalledWith(userId); }); it('should check if user has license for external tool', async () => { 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 c89bf0847af..b5464fbac89 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,6 +1,5 @@ import { MediaBoardConfig } from '@modules/board/media-board.config'; -import { MediaUserLicense, UserLicenseService } from '@modules/user-license'; -import { MediaUserLicenseService } from '@modules/user-license/service'; +import { MediaUserLicense, MediaUserLicenseService } from '@modules/user-license'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { ConfigService } from '@nestjs/config'; import { ValidationError } from '@shared/common'; @@ -19,7 +18,6 @@ import { ContextExternalToolLaunchable } from '../domain'; export class ToolConfigurationStatusService { constructor( private readonly commonToolValidationService: CommonToolValidationService, - private readonly userLicenseService: UserLicenseService, private readonly mediaUserLicenseService: MediaUserLicenseService, private readonly configService: ConfigService ) {} @@ -79,7 +77,9 @@ export class ToolConfigurationStatusService { 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 mediaUserLicenses: MediaUserLicense[] = await this.mediaUserLicenseService.getMediaUserLicensesForUser( + userId + ); const externalToolMedium = externalTool.medium; if (externalToolMedium) { 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 3e2357cdfdd..909fa436b8d 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 @@ -76,10 +76,10 @@ export class ExternalToolEntityFactory extends BaseFactory = { medium: new ExternalToolMediumEntity({ - ...medium, mediumId: 'mediumId', publisher: 'publisher', mediaSourceId: 'mediaSourceId', + ...medium, }), }; diff --git a/apps/server/src/modules/user-license/domain/index.ts b/apps/server/src/modules/user-license/domain/index.ts index 09646422117..f665ca644c2 100644 --- a/apps/server/src/modules/user-license/domain/index.ts +++ b/apps/server/src/modules/user-license/domain/index.ts @@ -1,2 +1,3 @@ -export { MediaUserLicense } from './media-user-license'; +export { MediaUserLicense, MediaUserLicenseProps } from './media-user-license'; export { AnyUserLicense } from './any-user-license.type'; +export { MediaSource, MediaSourceProps } from './media-source'; diff --git a/apps/server/src/modules/user-license/domain/media-source.ts b/apps/server/src/modules/user-license/domain/media-source.ts new file mode 100644 index 00000000000..7759d36bb4e --- /dev/null +++ b/apps/server/src/modules/user-license/domain/media-source.ts @@ -0,0 +1,19 @@ +import { DomainObject } from '@shared/domain/domain-object'; + +export interface MediaSourceProps { + id: string; + + name?: string; + + sourceId: string; +} + +export class MediaSource extends DomainObject { + get name(): string | undefined { + return this.props.name; + } + + get sourceId(): string { + return this.props.sourceId; + } +} diff --git a/apps/server/src/modules/user-license/domain/media-user-license.ts b/apps/server/src/modules/user-license/domain/media-user-license.ts index 607cc6eb687..37338a8d2c5 100644 --- a/apps/server/src/modules/user-license/domain/media-user-license.ts +++ b/apps/server/src/modules/user-license/domain/media-user-license.ts @@ -1,9 +1,10 @@ +import { MediaSource } from './media-source'; import { UserLicense, UserLicenseProps } from './user-license'; export interface MediaUserLicenseProps extends UserLicenseProps { mediumId: string; - mediaSourceId?: string; + mediaSource?: MediaSource; } export class MediaUserLicense extends UserLicense { @@ -15,11 +16,7 @@ export class MediaUserLicense extends UserLicense { this.props.mediumId = value; } - get mediaSourceId(): string | undefined { - return this.props.mediaSourceId; - } - - set mediaSourceId(value: string | undefined) { - this.props.mediaSourceId = value; + get mediaSource(): MediaSource | undefined { + return this.props.mediaSource; } } diff --git a/apps/server/src/modules/user-license/entity/index.ts b/apps/server/src/modules/user-license/entity/index.ts index 8fa435c99c7..7b7925040ed 100644 --- a/apps/server/src/modules/user-license/entity/index.ts +++ b/apps/server/src/modules/user-license/entity/index.ts @@ -1,3 +1,4 @@ export { UserLicenseType } from './user-license-type'; export { MediaUserLicenseEntity, MediaUserLicenseEntityProps } from './media-user-license.entity'; export { UserLicenseEntity } from './user-license.entity'; +export { MediaSourceEntity, MediaSourceEntityProps } from './media-source.entity'; diff --git a/apps/server/src/modules/user-license/entity/media-source.entity.ts b/apps/server/src/modules/user-license/entity/media-source.entity.ts new file mode 100644 index 00000000000..57ee3b55cfa --- /dev/null +++ b/apps/server/src/modules/user-license/entity/media-source.entity.ts @@ -0,0 +1,30 @@ +import { Entity, Index, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain/types'; + +export interface MediaSourceEntityProps { + id?: EntityId; + + name?: string; + + sourceId: string; +} + +@Entity({ tableName: 'media-sources' }) +export class MediaSourceEntity extends BaseEntityWithTimestamps { + constructor(props: MediaSourceEntityProps) { + super(); + if (props.id != null) { + this.id = props.id; + } + this.name = props.name; + this.sourceId = props.sourceId; + } + + @Property({ nullable: true }) + name?: string; + + @Index() + @Property() + sourceId: string; +} diff --git a/apps/server/src/modules/user-license/entity/media-user-license.entity.ts b/apps/server/src/modules/user-license/entity/media-user-license.entity.ts index 487c950b460..4cc501c4975 100644 --- a/apps/server/src/modules/user-license/entity/media-user-license.entity.ts +++ b/apps/server/src/modules/user-license/entity/media-user-license.entity.ts @@ -1,10 +1,11 @@ -import { Entity, Property } from '@mikro-orm/core'; +import { Entity, ManyToOne, Property } from '@mikro-orm/core'; +import { MediaSourceEntity } from './media-source.entity'; import { UserLicenseType } from './user-license-type'; import { UserLicenseEntity, UserLicenseProps } from './user-license.entity'; export interface MediaUserLicenseEntityProps extends UserLicenseProps { mediumId: string; - mediaSourceId?: string; + mediaSource?: MediaSourceEntity; } @Entity({ discriminatorValue: UserLicenseType.MEDIA_LICENSE }) @@ -13,12 +14,12 @@ export class MediaUserLicenseEntity extends UserLicenseEntity { super(props); this.type = UserLicenseType.MEDIA_LICENSE; this.mediumId = props.mediumId; - this.mediaSourceId = props.mediaSourceId; + this.mediaSource = props.mediaSource; } @Property() mediumId: string; - @Property({ nullable: true }) - mediaSourceId?: string; + @ManyToOne(() => MediaSourceEntity, { nullable: true }) + mediaSource?: MediaSourceEntity; } diff --git a/apps/server/src/modules/user-license/index.ts b/apps/server/src/modules/user-license/index.ts index fbea18a1136..ebb28ca5a46 100644 --- a/apps/server/src/modules/user-license/index.ts +++ b/apps/server/src/modules/user-license/index.ts @@ -1,5 +1,10 @@ export { UserLicenseModule } from './user-license.module'; -export { UserLicenseService } from './service/user-license.service'; -export { MediaUserLicense } from './domain/media-user-license'; +export { MediaUserLicenseService, MediaSourceService } from './service'; +export { MediaUserLicense, MediaSource, MediaUserLicenseProps, MediaSourceProps, AnyUserLicense } from './domain'; export { UserLicenseType } from './entity/user-license-type'; -export { mediaUserLicenseFactory } from './testing'; +export { + mediaUserLicenseFactory, + mediaSourceFactory, + mediaSourceEntityFactory, + mediaUserLicenseEntityFactory, +} from './testing'; diff --git a/apps/server/src/modules/user-license/repo/index.ts b/apps/server/src/modules/user-license/repo/index.ts index d3c73f90597..102b731f497 100644 --- a/apps/server/src/modules/user-license/repo/index.ts +++ b/apps/server/src/modules/user-license/repo/index.ts @@ -1,3 +1,3 @@ -export { UserLicenseRepo } from './user-license.repo'; -export { UserLicenseScope } from './user-license.scope'; +export { MediaUserLicenseRepo } from './media-user-license.repo'; export { UserLicenseQuery } from './user-license-query'; +export { MediaSourceRepo } from './media-source.repo'; diff --git a/apps/server/src/modules/user-license/repo/media-source.mapper.ts b/apps/server/src/modules/user-license/repo/media-source.mapper.ts new file mode 100644 index 00000000000..f1ae887a3b0 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-source.mapper.ts @@ -0,0 +1,24 @@ +import { EntityData } from '@mikro-orm/core'; +import { MediaSource } from '../domain'; +import { MediaSourceEntity } from '../entity'; + +export class MediaSourceMapper { + public static mapToEntityProperties(entityDO: MediaSource): EntityData { + const entityProps: EntityData = { + name: entityDO.name, + sourceId: entityDO.sourceId, + }; + + return entityProps; + } + + public static mapEntityToDo(entity: MediaSourceEntity): MediaSource { + const domainObject = new MediaSource({ + id: entity.id, + name: entity.name, + sourceId: entity.sourceId, + }); + + return domainObject; + } +} diff --git a/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts b/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts new file mode 100644 index 00000000000..12708211687 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts @@ -0,0 +1,98 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections } from '@shared/testing'; +import { MediaSource } from '../domain'; +import { MediaSourceEntity } from '../entity'; +import { mediaSourceEntityFactory, mediaSourceFactory } from '../testing'; +import { MediaSourceRepo } from './media-source.repo'; + +describe(MediaSourceRepo.name, () => { + let module: TestingModule; + let repo: MediaSourceRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [MediaSourceRepo], + }).compile(); + + repo = module.get(MediaSourceRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('findBySourceId', () => { + describe('when a media source exists', () => { + const setup = async () => { + const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build(); + + await em.persistAndFlush([mediaSource]); + + em.clear(); + + return { + mediaSource, + }; + }; + + it('should return user licenses for user', async () => { + const { mediaSource } = await setup(); + + const result = await repo.findBySourceId(mediaSource.sourceId); + + expect(result).toEqual( + new MediaSource({ + id: mediaSource.id, + name: mediaSource.name, + sourceId: mediaSource.sourceId, + }) + ); + }); + }); + + describe('when no media source exists', () => { + it('should return null', async () => { + const result = await repo.findBySourceId(new ObjectId().toHexString()); + + expect(result).toBeNull(); + }); + }); + }); + + describe('save', () => { + describe('when saving a media source', () => { + const setup = () => { + const mediaSource: MediaSource = mediaSourceFactory.build(); + + return { + mediaSource, + }; + }; + + it('should return the media source', async () => { + const { mediaSource } = setup(); + + const result = await repo.save(mediaSource); + + expect(result).toEqual(mediaSource); + }); + + it('should save the media source', async () => { + const { mediaSource } = setup(); + + await repo.save(mediaSource); + + expect(await em.findOne(MediaSourceEntity, { id: mediaSource.id })).toBeDefined(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-license/repo/media-source.repo.ts b/apps/server/src/modules/user-license/repo/media-source.repo.ts new file mode 100644 index 00000000000..b886f14b6e1 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-source.repo.ts @@ -0,0 +1,31 @@ +import { EntityData, EntityName } from '@mikro-orm/core'; +import { Injectable } from '@nestjs/common'; +import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; +import { MediaSource } from '../domain'; +import { MediaSourceEntity } from '../entity'; +import { MediaSourceMapper } from './media-source.mapper'; + +@Injectable() +export class MediaSourceRepo extends BaseDomainObjectRepo { + protected get entityName(): EntityName { + return MediaSourceEntity; + } + + protected mapDOToEntityProperties(entityDO: MediaSource): EntityData { + const entityProps: EntityData = MediaSourceMapper.mapToEntityProperties(entityDO); + + return entityProps; + } + + public async findBySourceId(sourceId: string): Promise { + const entity: MediaSourceEntity | null = await this.em.findOne(MediaSourceEntity, { sourceId }); + + if (!entity) { + return null; + } + + const domainObject: MediaSource = MediaSourceMapper.mapEntityToDo(entity); + + return domainObject; + } +} diff --git a/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts b/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts new file mode 100644 index 00000000000..5940d568137 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts @@ -0,0 +1,109 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { User as UserEntity } from '@shared/domain/entity'; +import { cleanupCollections, userFactory } from '@shared/testing'; +import { MediaSource, MediaUserLicense } from '../domain'; +import { MediaSourceEntity, MediaUserLicenseEntity } from '../entity'; +import { + mediaSourceEntityFactory, + mediaSourceFactory, + mediaUserLicenseEntityFactory, + mediaUserLicenseFactory, +} from '../testing'; +import { MediaUserLicenseRepo } from './media-user-license.repo'; + +describe(MediaUserLicenseRepo.name, () => { + let module: TestingModule; + let repo: MediaUserLicenseRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [MediaUserLicenseRepo], + }).compile(); + + repo = module.get(MediaUserLicenseRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('findMediaUserLicensesForUser', () => { + describe('when searching for a users media licences', () => { + const setup = async () => { + const user: UserEntity = userFactory.build(); + const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build(); + const mediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ user, mediaSource }); + const otherMediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build(); + + await em.persistAndFlush([user, mediaUserLicense, otherMediaUserLicense]); + + em.clear(); + + return { + user, + mediaUserLicense, + mediaSource, + }; + }; + + it('should return user licenses for user', async () => { + const { user, mediaUserLicense, mediaSource } = await setup(); + + const result: MediaUserLicense[] = await repo.findMediaUserLicensesForUser(user.id); + + expect(result).toEqual([ + new MediaUserLicense({ + id: mediaUserLicense.id, + type: mediaUserLicense.type, + userId: mediaUserLicense.user.id, + mediumId: mediaUserLicense.mediumId, + mediaSource: new MediaSource({ + id: mediaSource.id, + name: mediaSource.name, + sourceId: mediaSource.sourceId, + }), + }), + ]); + }); + }); + }); + + describe('save', () => { + describe('when saving a media user license', () => { + const setup = () => { + const mediaSource: MediaSource = mediaSourceFactory.build(); + const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build({ mediaSource }); + + return { + mediaUserLicense, + mediaSource, + }; + }; + + it('should return the media user license', async () => { + const { mediaUserLicense } = setup(); + + const result = await repo.save(mediaUserLicense); + + expect(result).toEqual(mediaUserLicense); + }); + + it('should save the media user license', async () => { + const { mediaUserLicense } = setup(); + + await repo.save(mediaUserLicense); + + expect(await em.findOne(MediaUserLicenseEntity, { id: mediaUserLicense.id })).not.toBeNull(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-license/repo/media-user-license.repo.ts b/apps/server/src/modules/user-license/repo/media-user-license.repo.ts new file mode 100644 index 00000000000..19685fd8656 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-user-license.repo.ts @@ -0,0 +1,63 @@ +import { EntityData, EntityName } from '@mikro-orm/core'; +import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain/entity'; +import { EntityId } from '@shared/domain/types'; +import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; +import { MediaSource, MediaUserLicense } from '../domain'; +import { MediaSourceEntity, MediaUserLicenseEntity, UserLicenseType } from '../entity'; + +@Injectable() +export class MediaUserLicenseRepo extends BaseDomainObjectRepo { + protected get entityName(): EntityName { + return MediaUserLicenseEntity; + } + + private mapEntityToDomainObject(entity: MediaUserLicenseEntity): MediaUserLicense { + let mediaSource: MediaSource | undefined; + + if (entity.mediaSource) { + mediaSource = new MediaSource({ + id: entity.mediaSource.id, + name: entity.mediaSource.name, + sourceId: entity.mediaSource.sourceId, + }); + } + + const userLicense: MediaUserLicense = new MediaUserLicense({ + id: entity.id, + userId: entity.user.id, + mediumId: entity.mediumId, + mediaSource, + type: entity.type, + }); + + return userLicense; + } + + protected mapDOToEntityProperties(entityDO: MediaUserLicense): EntityData { + const entityProps: EntityData = { + user: this.em.getReference(User, entityDO.userId), + type: UserLicenseType.MEDIA_LICENSE, + mediumId: entityDO.mediumId, + mediaSource: entityDO.mediaSource ? this.em.getReference(MediaSourceEntity, entityDO.mediaSource.id) : undefined, + }; + + return entityProps; + } + + public async findMediaUserLicensesForUser(userId: EntityId): Promise { + const entities: MediaUserLicenseEntity[] = await this.em.find( + MediaUserLicenseEntity, + { user: userId, type: UserLicenseType.MEDIA_LICENSE }, + { + populate: ['mediaSource'], + } + ); + + const domainObjects: MediaUserLicense[] = entities.map((entity: MediaUserLicenseEntity) => + this.mapEntityToDomainObject(entity) + ); + + return domainObjects; + } +} diff --git a/apps/server/src/modules/user-license/repo/user-license-query.ts b/apps/server/src/modules/user-license/repo/user-license-query.ts index d5643cb5cf2..d10e59458a4 100644 --- a/apps/server/src/modules/user-license/repo/user-license-query.ts +++ b/apps/server/src/modules/user-license/repo/user-license-query.ts @@ -1,7 +1,5 @@ import { EntityId } from '@shared/domain/types'; -import { UserLicenseType } from '../entity'; export type UserLicenseQuery = { - type?: UserLicenseType; userId?: EntityId; }; diff --git a/apps/server/src/modules/user-license/repo/user-license.repo.spec.ts b/apps/server/src/modules/user-license/repo/user-license.repo.spec.ts deleted file mode 100644 index 016d547729f..00000000000 --- a/apps/server/src/modules/user-license/repo/user-license.repo.spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { EntityManager } from '@mikro-orm/mongodb'; -import { InternalServerErrorException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { User as UserEntity } from '@shared/domain/entity'; -import { cleanupCollections, userFactory } from '@shared/testing'; -import { MediaUserLicense } from '../domain'; -import { MediaUserLicenseEntity, UserLicenseEntity, UserLicenseType } from '../entity'; -import { mediaUserLicenseEntityFactory, mediaUserLicenseFactory } from '../testing'; -import { UserLicenseRepo } from './user-license.repo'; - -describe(UserLicenseRepo.name, () => { - let module: TestingModule; - let repo: UserLicenseRepo; - let em: EntityManager; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [MongoMemoryDatabaseModule.forRoot()], - providers: [UserLicenseRepo], - }).compile(); - - repo = module.get(UserLicenseRepo); - em = module.get(EntityManager); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(async () => { - await cleanupCollections(em); - }); - - describe('findUserLicenses', () => { - describe('when query is empty', () => { - const setup = async () => { - const user: UserEntity = userFactory.build(); - const mediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ - user, - mediumId: 'mediumId', - mediaSourceId: 'sourceId', - }); - - await em.persistAndFlush([user, mediaUserLicense]); - - em.clear(); - - return { - user, - mediaUserLicense, - }; - }; - - it('should return all user licenses', async () => { - const { mediaUserLicense } = await setup(); - - const result: MediaUserLicense[] = await repo.findUserLicenses({}); - - expect(result).toEqual([ - new MediaUserLicense({ - id: mediaUserLicense.id, - userId: mediaUserLicense.user.id, - mediumId: mediaUserLicense.mediumId, - mediaSourceId: mediaUserLicense.mediaSourceId, - type: mediaUserLicense.type, - }), - ]); - }); - }); - - describe('when query has userId', () => { - const setup = async () => { - const user: UserEntity = userFactory.build(); - const mediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ user }); - const otherMediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build(); - - await em.persistAndFlush([user, mediaUserLicense, otherMediaUserLicense]); - - em.clear(); - - return { - user, - }; - }; - - it('should return user licenses for user', async () => { - const { user } = await setup(); - - const result: MediaUserLicense[] = await repo.findUserLicenses({ userId: user.id }); - - expect(result).toEqual([ - expect.objectContaining>({ - userId: user.id, - }), - ]); - }); - }); - - describe('when query has type', () => { - const setup = async () => { - const user: UserEntity = userFactory.build(); - const mediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ user }); - - await em.persistAndFlush([user, mediaUserLicense]); - - em.clear(); - - return { - user, - }; - }; - - it('should return user licenses for type', async () => { - await setup(); - - const result: MediaUserLicense[] = await repo.findUserLicenses({ type: UserLicenseType.MEDIA_LICENSE }); - - expect(result).toEqual([ - expect.objectContaining>({ - type: UserLicenseType.MEDIA_LICENSE, - }), - ]); - }); - }); - - describe('when entity type is unknown', () => { - const setup = async () => { - const unknownEntity: UserLicenseEntity = mediaUserLicenseEntityFactory.build(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - unknownEntity.type = 'shouldNeverHappen'; - - await em.persistAndFlush([unknownEntity]); - }; - - it('should throw InternalServerErrorException', async () => { - await setup(); - - await expect(repo.findUserLicenses({})).rejects.toThrow(InternalServerErrorException); - }); - }); - }); - - describe('deleteUserLicense', () => { - const setup = async () => { - const mediaUserLicenseEntity: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build(); - const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build({ id: mediaUserLicenseEntity.id }); - - await em.persistAndFlush(mediaUserLicenseEntity); - - return { mediaUserLicense }; - }; - - it('should not find deleted user license', async () => { - const { mediaUserLicense } = await setup(); - - await repo.deleteUserLicense(mediaUserLicense); - - expect(await em.findOne(MediaUserLicenseEntity, { id: mediaUserLicense.id })).toBeNull(); - }); - }); - - describe('saveUserLicense', () => { - const setup = () => { - const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); - - return { mediaUserLicense }; - }; - - it('should find saved user license', async () => { - const { mediaUserLicense } = setup(); - - await repo.saveUserLicense(mediaUserLicense); - - expect(await em.findOne(MediaUserLicenseEntity, { id: mediaUserLicense.id })).toBeDefined(); - }); - }); -}); diff --git a/apps/server/src/modules/user-license/repo/user-license.repo.ts b/apps/server/src/modules/user-license/repo/user-license.repo.ts deleted file mode 100644 index 6b921f50b6f..00000000000 --- a/apps/server/src/modules/user-license/repo/user-license.repo.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { EntityManager } from '@mikro-orm/mongodb'; -import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { User } from '@shared/domain/entity'; -import { AnyUserLicense, MediaUserLicense } from '../domain'; -import { MediaUserLicenseEntity, UserLicenseEntity, UserLicenseType } from '../entity'; -import { UserLicenseQuery } from './user-license-query'; -import { UserLicenseScope } from './user-license.scope'; - -@Injectable() -export class UserLicenseRepo { - constructor(private readonly em: EntityManager) {} - - public async findUserLicenses(query: UserLicenseQuery): Promise { - const scope: UserLicenseScope = new UserLicenseScope(); - scope.byUserId(query.userId); - scope.byType(query.type); - scope.allowEmptyQuery(true); - - const entities: UserLicenseEntity[] = await this.em.find(UserLicenseEntity, scope.query); - - const domainObjects: AnyUserLicense[] = entities.map((entity: UserLicenseEntity) => - this.mapEntityToDomainObject(entity) - ); - - return domainObjects; - } - - public async deleteUserLicense(license: AnyUserLicense): Promise { - const entity: UserLicenseEntity = this.mapDomainObjectToEntity(license); - - await this.em.removeAndFlush(entity); - } - - public async saveUserLicense(license: AnyUserLicense): Promise { - const entity: UserLicenseEntity = this.mapDomainObjectToEntity(license); - - await this.em.persistAndFlush(entity); - } - - private mapDomainObjectToEntity(domainObject: AnyUserLicense): UserLicenseEntity { - const entity: MediaUserLicenseEntity = new MediaUserLicenseEntity({ - id: domainObject.id, - user: this.em.getReference(User, domainObject.userId), - type: UserLicenseType.MEDIA_LICENSE, - mediumId: domainObject.mediumId, - mediaSourceId: domainObject.mediaSourceId, - }); - - return entity; - } - - private mapEntityToDomainObject(entity: UserLicenseEntity): AnyUserLicense { - if (entity.type === UserLicenseType.MEDIA_LICENSE && entity instanceof MediaUserLicenseEntity) { - const userLicense: MediaUserLicense = new MediaUserLicense({ - id: entity.id, - userId: entity.user.id, - mediumId: entity.mediumId, - mediaSourceId: entity.mediaSourceId, - type: entity.type, - }); - - return userLicense; - } - throw new InternalServerErrorException(`Unknown entity type: ${entity.constructor.name}`); - } -} diff --git a/apps/server/src/modules/user-license/repo/user-license.scope.spec.ts b/apps/server/src/modules/user-license/repo/user-license.scope.spec.ts deleted file mode 100644 index dfd4ab20f67..00000000000 --- a/apps/server/src/modules/user-license/repo/user-license.scope.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { UserLicenseType } from '../entity'; -import { UserLicenseScope } from './user-license.scope'; - -describe(UserLicenseScope.name, () => { - let userLicenseScope: UserLicenseScope; - - beforeEach(() => { - userLicenseScope = new UserLicenseScope(); - }); - - describe('byType', () => { - describe('when type is undefined', () => { - it('should return scope without type', () => { - expect(userLicenseScope.byType()).toBe(userLicenseScope); - }); - }); - - describe('when type is defined', () => { - it('should add type to query', () => { - const type = UserLicenseType.MEDIA_LICENSE; - - userLicenseScope.byType(type); - - expect(userLicenseScope.query).toEqual({ type }); - }); - }); - }); - - describe('byUserId', () => { - describe('when userId is undefined', () => { - it('should return scope without userId', () => { - expect(userLicenseScope.byUserId()).toBe(userLicenseScope); - }); - }); - - describe('when userId is defined', () => { - it('should add user to query', () => { - const userId = 'userId'; - - userLicenseScope.byUserId(userId); - - expect(userLicenseScope.query).toEqual({ user: userId }); - }); - }); - }); -}); diff --git a/apps/server/src/modules/user-license/repo/user-license.scope.ts b/apps/server/src/modules/user-license/repo/user-license.scope.ts deleted file mode 100644 index 89309908d0b..00000000000 --- a/apps/server/src/modules/user-license/repo/user-license.scope.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { EntityId } from '@shared/domain/types'; -import { Scope } from '@shared/repo'; -import { UserLicenseEntity, UserLicenseType } from '../entity'; - -export class UserLicenseScope extends Scope { - public byType(type?: UserLicenseType): this { - if (!type) { - return this; - } - this.addQuery({ type }); - - return this; - } - - public byUserId(userId?: EntityId): this { - if (!userId) { - return this; - } - this.addQuery({ user: userId }); - - return this; - } -} diff --git a/apps/server/src/modules/user-license/service/index.ts b/apps/server/src/modules/user-license/service/index.ts index 0829d1f649a..66e350b1ea5 100644 --- a/apps/server/src/modules/user-license/service/index.ts +++ b/apps/server/src/modules/user-license/service/index.ts @@ -1,2 +1,2 @@ -export { UserLicenseService } from './user-license.service'; export { MediaUserLicenseService } from './media-user-license.service'; +export { MediaSourceService } from './media-source.service'; diff --git a/apps/server/src/modules/user-license/service/media-source.service.spec.ts b/apps/server/src/modules/user-license/service/media-source.service.spec.ts new file mode 100644 index 00000000000..526c7ac59ac --- /dev/null +++ b/apps/server/src/modules/user-license/service/media-source.service.spec.ts @@ -0,0 +1,87 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MediaSourceRepo } from '../repo'; +import { mediaSourceFactory } from '../testing'; +import { MediaSourceService } from './media-source.service'; + +describe(MediaSourceService.name, () => { + let module: TestingModule; + let service: MediaSourceService; + + let mediaSourceRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + MediaSourceService, + { + provide: MediaSourceRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(MediaSourceService); + mediaSourceRepo = module.get(MediaSourceRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('findBySourceId', () => { + describe('when searching for a media source', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.build(); + + mediaSourceRepo.findBySourceId.mockResolvedValueOnce(mediaSource); + + return { + mediaSource, + }; + }; + + it('should return the media source', async () => { + const { mediaSource } = setup(); + + const result = await service.findBySourceId(mediaSource.sourceId); + + expect(result).toEqual(mediaSource); + }); + }); + }); + + describe('save', () => { + describe('when saving a media source', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.build(); + + mediaSourceRepo.save.mockResolvedValueOnce(mediaSource); + + return { + mediaSource, + }; + }; + + it('should save the media source', async () => { + const { mediaSource } = setup(); + + await service.save(mediaSource); + + expect(mediaSourceRepo.save).toHaveBeenCalledWith(mediaSource); + }); + + it('should return the media source', async () => { + const { mediaSource } = setup(); + + const result = await service.save(mediaSource); + + expect(result).toEqual(mediaSource); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-license/service/media-source.service.ts b/apps/server/src/modules/user-license/service/media-source.service.ts new file mode 100644 index 00000000000..151ce483184 --- /dev/null +++ b/apps/server/src/modules/user-license/service/media-source.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { MediaSource } from '../domain'; +import { MediaSourceRepo } from '../repo'; + +@Injectable() +export class MediaSourceService { + constructor(private readonly mediaSourceRepo: MediaSourceRepo) {} + + public async findBySourceId(id: EntityId): Promise { + const domainObject: MediaSource | null = await this.mediaSourceRepo.findBySourceId(id); + + return domainObject; + } + + public async save(domainObject: MediaSource): Promise { + const savedObject: MediaSource = await this.mediaSourceRepo.save(domainObject); + + return savedObject; + } +} 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 1fcac4b22aa..5ce1007a1f7 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,18 +1,37 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; 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'; +import { MediaUserLicense } from '../domain'; +import { MediaSourceRepo, MediaUserLicenseRepo } from '../repo'; +import { mediaSourceFactory, mediaUserLicenseFactory } from '../testing'; +import { MediaUserLicenseService } from './media-user-license.service'; describe(MediaUserLicenseService.name, () => { let module: TestingModule; let service: MediaUserLicenseService; + let mediaUserLicenseRepo: DeepMocked; + let mediaSourceRepo: DeepMocked; + beforeAll(async () => { module = await Test.createTestingModule({ - providers: [MediaUserLicenseService], + providers: [ + MediaUserLicenseService, + { + provide: MediaUserLicenseRepo, + useValue: createMock(), + }, + { + provide: MediaSourceRepo, + useValue: createMock(), + }, + ], }).compile(); service = module.get(MediaUserLicenseService); + mediaUserLicenseRepo = module.get(MediaUserLicenseRepo); + mediaSourceRepo = module.get(MediaSourceRepo); }); afterAll(async () => { @@ -23,6 +42,61 @@ describe(MediaUserLicenseService.name, () => { jest.resetAllMocks(); }); + describe('getMediaUserLicensesForUser', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); + + mediaUserLicenseRepo.findMediaUserLicensesForUser.mockResolvedValue([mediaUserLicense]); + + return { userId, mediaUserLicense }; + }; + + it('should call user license repo with correct arguments', async () => { + const { userId } = setup(); + + await service.getMediaUserLicensesForUser(userId); + + expect(mediaUserLicenseRepo.findMediaUserLicensesForUser).toHaveBeenCalledWith(userId); + }); + + it('should return media user licenses for user', async () => { + const { userId, mediaUserLicense } = setup(); + + const result: MediaUserLicense[] = await service.getMediaUserLicensesForUser(userId); + + expect(result).toEqual([mediaUserLicense]); + }); + }); + + describe('saveUserLicense', () => { + it('should save the media source', async () => { + const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); + + await service.saveUserLicense(mediaUserLicense); + + expect(mediaSourceRepo.save).toHaveBeenCalledWith(mediaUserLicense.mediaSource); + }); + + it('should save the media user license', async () => { + const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); + + await service.saveUserLicense(mediaUserLicense); + + expect(mediaUserLicenseRepo.save).toHaveBeenCalledWith(mediaUserLicense); + }); + }); + + describe('deleteUserLicense', () => { + it('should call user license repo with correct arguments', async () => { + const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); + + await service.deleteUserLicense(mediaUserLicense); + + expect(mediaUserLicenseRepo.delete).toHaveBeenCalledWith(mediaUserLicense); + }); + }); + describe('hasLicenseForExternalTool', () => { describe('when user has license', () => { const setup = () => { @@ -32,7 +106,9 @@ describe(MediaUserLicenseService.name, () => { }; const medium = mediaUserLicenseFactory.build({ mediumId: toolMedium.mediumId, - mediaSourceId: toolMedium.mediaSourceId, + mediaSource: mediaSourceFactory.build({ + sourceId: toolMedium.mediaSourceId, + }), }); const unusedMedium = mediaUserLicenseFactory.build(); const mediaUserLicenses: MediaUserLicense[] = [medium, unusedMedium]; @@ -59,7 +135,7 @@ describe(MediaUserLicenseService.name, () => { }; const medium = mediaUserLicenseFactory.build({ mediumId: toolMedium.mediumId, - mediaSourceId: undefined, + mediaSource: undefined, }); const unusedMedium = mediaUserLicenseFactory.build(); const mediaUserLicenses: MediaUserLicense[] = [medium, unusedMedium]; 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 2bb808887a9..b04d5688eed 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,16 +1,42 @@ import { ExternalToolMedium } from '@modules/tool/external-tool/domain'; import { MediaUserLicense } from '@modules/user-license'; import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { MediaSourceRepo, MediaUserLicenseRepo } from '../repo'; @Injectable() export class MediaUserLicenseService { + constructor( + private readonly mediaUserLicenseRepo: MediaUserLicenseRepo, + private readonly mediaSourceRepo: MediaSourceRepo + ) {} + + public async getMediaUserLicensesForUser(userId: EntityId): Promise { + const mediaUserLicenses: MediaUserLicense[] = await this.mediaUserLicenseRepo.findMediaUserLicensesForUser(userId); + + return mediaUserLicenses; + } + + public async saveUserLicense(license: MediaUserLicense): Promise { + if (license.mediaSource) { + await this.mediaSourceRepo.save(license.mediaSource); + } + + await this.mediaUserLicenseRepo.save(license); + } + + public async deleteUserLicense(license: MediaUserLicense): Promise { + await this.mediaUserLicenseRepo.delete(license); + } + public hasLicenseForExternalTool( externalToolMedium: ExternalToolMedium, mediaUserLicenses: MediaUserLicense[] ): boolean { return mediaUserLicenses.some( (license: MediaUserLicense) => - license.mediumId === externalToolMedium.mediumId && license.mediaSourceId === externalToolMedium.mediaSourceId + license.mediumId === externalToolMedium.mediumId && + license.mediaSource?.sourceId === externalToolMedium.mediaSourceId ); } } diff --git a/apps/server/src/modules/user-license/service/user-license.service.spec.ts b/apps/server/src/modules/user-license/service/user-license.service.spec.ts deleted file mode 100644 index d311df15e13..00000000000 --- a/apps/server/src/modules/user-license/service/user-license.service.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { Test, TestingModule } from '@nestjs/testing'; -import { MediaUserLicense } from '../domain'; -import { UserLicenseType } from '../entity'; -import { UserLicenseRepo } from '../repo'; -import { mediaUserLicenseFactory } from '../testing'; -import { UserLicenseService } from './user-license.service'; - -describe(UserLicenseService.name, () => { - let module: TestingModule; - let service: UserLicenseService; - - let userLicenseRepo: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - UserLicenseService, - { - provide: UserLicenseRepo, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(UserLicenseService); - userLicenseRepo = module.get(UserLicenseRepo); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('getMediaUserLicensesForUser', () => { - const setup = () => { - const userId = new ObjectId().toHexString(); - const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); - - userLicenseRepo.findUserLicenses.mockResolvedValue([mediaUserLicense]); - - return { userId, mediaUserLicense }; - }; - - it('should call user license repo with correct arguments', async () => { - const { userId } = setup(); - - await service.getMediaUserLicensesForUser(userId); - - expect(userLicenseRepo.findUserLicenses).toHaveBeenCalledWith({ - userId, - type: UserLicenseType.MEDIA_LICENSE, - }); - }); - - it('should return media user licenses for user', async () => { - const { userId, mediaUserLicense } = setup(); - - const result: MediaUserLicense[] = await service.getMediaUserLicensesForUser(userId); - - expect(result).toEqual([mediaUserLicense]); - }); - }); - - describe('saveUserLicense', () => { - it('should call user license repo with correct arguments', async () => { - const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); - - await service.saveUserLicense(mediaUserLicense); - - expect(userLicenseRepo.saveUserLicense).toHaveBeenCalledWith(mediaUserLicense); - }); - }); - - describe('deleteUserLicense', () => { - it('should call user license repo with correct arguments', async () => { - const mediaUserLicense: MediaUserLicense = mediaUserLicenseFactory.build(); - - await service.deleteUserLicense(mediaUserLicense); - - expect(userLicenseRepo.deleteUserLicense).toHaveBeenCalledWith(mediaUserLicense); - }); - }); -}); diff --git a/apps/server/src/modules/user-license/service/user-license.service.ts b/apps/server/src/modules/user-license/service/user-license.service.ts deleted file mode 100644 index d5c5ae7c619..00000000000 --- a/apps/server/src/modules/user-license/service/user-license.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; -import { MediaUserLicense } from '../domain'; -import { UserLicenseType } from '../entity'; -import { UserLicenseRepo } from '../repo'; - -@Injectable() -export class UserLicenseService { - constructor(private readonly userLicenseRepo: UserLicenseRepo) {} - - public async getMediaUserLicensesForUser(userId: EntityId): Promise { - const mediaUserLicenses: MediaUserLicense[] = await this.userLicenseRepo.findUserLicenses({ - userId, - type: UserLicenseType.MEDIA_LICENSE, - }); - - return mediaUserLicenses; - } - - public async saveUserLicense(license: MediaUserLicense): Promise { - await this.userLicenseRepo.saveUserLicense(license); - } - - public async deleteUserLicense(license: MediaUserLicense): Promise { - await this.userLicenseRepo.deleteUserLicense(license); - } -} diff --git a/apps/server/src/modules/user-license/testing/index.ts b/apps/server/src/modules/user-license/testing/index.ts index 9eacb7e5afa..7123544025e 100644 --- a/apps/server/src/modules/user-license/testing/index.ts +++ b/apps/server/src/modules/user-license/testing/index.ts @@ -1,2 +1,4 @@ export { mediaUserLicenseEntityFactory } from './media-user-license-entity.factory'; export { mediaUserLicenseFactory } from './media-user-license.factory'; +export { mediaSourceEntityFactory } from './media-source-entity.factory'; +export { mediaSourceFactory } from './media-source.factory'; diff --git a/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts b/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts new file mode 100644 index 00000000000..801927dca57 --- /dev/null +++ b/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts @@ -0,0 +1,14 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { MediaSourceEntity, MediaSourceEntityProps } from '../entity'; + +export const mediaSourceEntityFactory = BaseFactory.define( + MediaSourceEntity, + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + name: `media-source-${sequence}`, + sourceId: `source-id-${sequence}`, + }; + } +); diff --git a/apps/server/src/modules/user-license/testing/media-source.factory.ts b/apps/server/src/modules/user-license/testing/media-source.factory.ts new file mode 100644 index 00000000000..a7f14a14b23 --- /dev/null +++ b/apps/server/src/modules/user-license/testing/media-source.factory.ts @@ -0,0 +1,11 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { MediaSource, MediaSourceProps } from '../domain'; + +export const mediaSourceFactory = BaseFactory.define(MediaSource, ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + name: `media-source-${sequence}`, + sourceId: `source-id-${sequence}`, + }; +}); diff --git a/apps/server/src/modules/user-license/testing/media-user-license-entity.factory.ts b/apps/server/src/modules/user-license/testing/media-user-license-entity.factory.ts index bf46daee6b6..6c46f7e5981 100644 --- a/apps/server/src/modules/user-license/testing/media-user-license-entity.factory.ts +++ b/apps/server/src/modules/user-license/testing/media-user-license-entity.factory.ts @@ -1,18 +1,15 @@ -import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory, userFactory } from '@shared/testing'; import { MediaUserLicenseEntity, MediaUserLicenseEntityProps, UserLicenseType } from '../entity'; +import { mediaSourceEntityFactory } from './media-source-entity.factory'; export const mediaUserLicenseEntityFactory = BaseFactory.define( MediaUserLicenseEntity, ({ sequence }) => { return { - id: new ObjectId().toHexString(), user: userFactory.build(), type: UserLicenseType.MEDIA_LICENSE, mediumId: `medium-${sequence}`, - mediaSourceId: `media-source-${sequence}`, - createdAt: new Date(), - updatedAt: new Date(), + mediaSource: mediaSourceEntityFactory.buildWithId(), }; } ); diff --git a/apps/server/src/modules/user-license/testing/media-user-license.factory.ts b/apps/server/src/modules/user-license/testing/media-user-license.factory.ts index 71d6cd00383..80c2dfe0269 100644 --- a/apps/server/src/modules/user-license/testing/media-user-license.factory.ts +++ b/apps/server/src/modules/user-license/testing/media-user-license.factory.ts @@ -1,8 +1,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { MediaUserLicense } from '../domain'; -import { MediaUserLicenseProps } from '../domain/media-user-license'; +import { MediaUserLicense, MediaUserLicenseProps } from '../domain'; import { UserLicenseType } from '../entity'; +import { mediaSourceFactory } from './media-source.factory'; export const mediaUserLicenseFactory = BaseFactory.define( MediaUserLicense, @@ -12,7 +12,7 @@ export const mediaUserLicenseFactory = BaseFactory.define