From 80a3d13d9e99de74f0687a9ffc7beff6d7bc7a0e Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 11:29:56 +0100 Subject: [PATCH 01/55] EW-1057: made barrel exports changes and changed the imports --- apps/server/src/core/error/loggable/index.ts | 4 +- apps/server/src/core/logger/index.ts | 21 +++-- .../src/core/logger/interfaces/index.ts | 4 +- apps/server/src/core/logger/types/index.ts | 9 +- apps/server/src/core/validation/index.ts | 2 +- apps/server/src/infra/database/index.ts | 4 +- .../src/infra/database/management/index.ts | 4 +- .../database/mongo-memory-database/index.ts | 4 +- apps/server/src/infra/sync/index.ts | 2 +- apps/server/src/infra/sync/sync.module.ts | 20 ++--- .../src/infra/sync/tsp/loggable/index.ts | 16 ++++ .../tsp/loggable/tsp-data-fetched.loggable.ts | 2 +- .../tsp-legacy-migration-start.loggable.ts | 2 +- ...egacy-migration-system-missing.loggable.ts | 2 +- ...-legacy-school-migration-count.loggable.ts | 2 +- ...egacy-school-migration-success.loggable.ts | 2 +- .../tsp-missing-external-id.loggable.ts | 2 +- .../loggable/tsp-schools-fetched.loggable.ts | 2 +- .../loggable/tsp-schools-synced.loggable.ts | 2 +- .../tsp-schulnummer-missing.loggable.ts | 2 +- .../loggable/tsp-students-fetched.loggable.ts | 2 +- .../tsp-students-migrated.loggable.ts | 2 +- .../tsp/loggable/tsp-synced-users.loggable.ts | 2 +- .../loggable/tsp-syncing-users.loggable.ts | 2 +- ...tsp-system-not-found.loggable-exception.ts | 4 +- .../loggable/tsp-teachers-fetched.loggable.ts | 2 +- .../tsp-teachers-migrated.loggable.ts | 2 +- .../loggable/tsp-users-migrated.loggable.ts | 2 +- .../infra/sync/tsp/tsp-fetch.service.spec.ts | 12 +-- .../src/infra/sync/tsp/tsp-fetch.service.ts | 10 +-- ...gacy-migration.service.integration.spec.ts | 14 +-- .../sync/tsp/tsp-legacy-migration.service.ts | 4 +- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 18 ++-- .../infra/sync/tsp/tsp-oauth-data.mapper.ts | 18 ++-- .../infra/sync/tsp/tsp-sync.service.spec.ts | 24 ++--- .../src/infra/sync/tsp/tsp-sync.service.ts | 29 +++--- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 33 ++++--- apps/server/src/infra/tsp-client/index.ts | 24 +++-- .../tsp-client/tsp-client-factory.spec.ts | 8 +- .../school/repo/mikro-orm/mapper/index.ts | 6 +- .../src/modules/synchronization/repo/index.ts | 4 +- .../synchronization/repo/mapper/index.ts | 2 +- apps/server/src/shared/common/error/index.ts | 16 ++-- .../src/shared/domain/domainobject/index.ts | 17 ++-- apps/server/src/shared/domain/entity/index.ts | 90 ++++++++++++++----- .../domain/entity/legacy-board/index.ts | 15 ++-- .../src/shared/domain/interface/index.ts | 17 ++-- apps/server/src/shared/testing/index.ts | 80 +++++++++++++++-- 48 files changed, 365 insertions(+), 202 deletions(-) create mode 100644 apps/server/src/infra/sync/tsp/loggable/index.ts diff --git a/apps/server/src/core/error/loggable/index.ts b/apps/server/src/core/error/loggable/index.ts index 0470cbee690..108f319b561 100644 --- a/apps/server/src/core/error/loggable/index.ts +++ b/apps/server/src/core/error/loggable/index.ts @@ -1,2 +1,2 @@ -export * from './error.loggable'; -export * from './axios-error.loggable'; +export { ErrorLoggable } from './error.loggable'; +export { AxiosErrorLoggable } from './axios-error.loggable'; diff --git a/apps/server/src/core/logger/index.ts b/apps/server/src/core/logger/index.ts index 1037edaaa34..005753c7b4d 100644 --- a/apps/server/src/core/logger/index.ts +++ b/apps/server/src/core/logger/index.ts @@ -1,8 +1,15 @@ -export * from './interfaces'; -export * from './logger.module'; -export * from './legacy-logger.service'; -export * from './logger'; -export * from './error-logger'; -export * from './types'; -export * from './logging.utils'; +export { Loggable, ILegacyLogger } from './interfaces'; +export { LoggerModule } from './logger.module'; +export { LegacyLogger } from './legacy-logger.service'; +export { Logger } from './logger'; +export { ErrorLogger } from './error-logger'; +export { + LogMessage, + LogMessageData, + LogMessageDataObject, + LogMessageWithContext, + ErrorLogMessage, + ValidationErrorLogMessage, +} from './types'; +export { LoggingUtils } from './logging.utils'; export { LoggerConfig } from './logger.config'; diff --git a/apps/server/src/core/logger/interfaces/index.ts b/apps/server/src/core/logger/interfaces/index.ts index 77c1c08176d..2c4736d6656 100644 --- a/apps/server/src/core/logger/interfaces/index.ts +++ b/apps/server/src/core/logger/interfaces/index.ts @@ -1,2 +1,2 @@ -export * from './legacy-logger.interface'; -export * from './loggable'; +export { ILegacyLogger } from './legacy-logger.interface'; +export { Loggable } from './loggable'; diff --git a/apps/server/src/core/logger/types/index.ts b/apps/server/src/core/logger/types/index.ts index 71e27b34575..0b9dc3b409f 100644 --- a/apps/server/src/core/logger/types/index.ts +++ b/apps/server/src/core/logger/types/index.ts @@ -1 +1,8 @@ -export * from './logging.types'; +export { + LogMessage, + ErrorLogMessage, + ValidationErrorLogMessage, + LogMessageWithContext, + LogMessageData, + LogMessageDataObject, +} from './logging.types'; diff --git a/apps/server/src/core/validation/index.ts b/apps/server/src/core/validation/index.ts index 28dc72dd458..177ecd810c2 100644 --- a/apps/server/src/core/validation/index.ts +++ b/apps/server/src/core/validation/index.ts @@ -1 +1 @@ -export * from './validation.module'; +export { ValidationModule } from './validation.module'; diff --git a/apps/server/src/infra/database/index.ts b/apps/server/src/infra/database/index.ts index 618339f9c52..29ed7f7282c 100644 --- a/apps/server/src/infra/database/index.ts +++ b/apps/server/src/infra/database/index.ts @@ -1,2 +1,2 @@ -export * from './mongo-memory-database'; -export * from './management'; +export { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from './mongo-memory-database'; +export { DatabaseManagementModule, DatabaseManagementService } from './management'; diff --git a/apps/server/src/infra/database/management/index.ts b/apps/server/src/infra/database/management/index.ts index 8f89890cd94..5bc56f7264a 100644 --- a/apps/server/src/infra/database/management/index.ts +++ b/apps/server/src/infra/database/management/index.ts @@ -1,2 +1,2 @@ -export * from './database-management.service'; -export * from './database-management.module'; +export { DatabaseManagementService } from './database-management.service'; +export { DatabaseManagementModule } from './database-management.module'; diff --git a/apps/server/src/infra/database/mongo-memory-database/index.ts b/apps/server/src/infra/database/mongo-memory-database/index.ts index cce731df885..9f9e928232e 100644 --- a/apps/server/src/infra/database/mongo-memory-database/index.ts +++ b/apps/server/src/infra/database/mongo-memory-database/index.ts @@ -1,2 +1,2 @@ -export * from './mongo-memory-database.module'; -export * from './types'; +export { MongoMemoryDatabaseModule } from './mongo-memory-database.module'; +export { MongoDatabaseModuleOptions } from './types'; diff --git a/apps/server/src/infra/sync/index.ts b/apps/server/src/infra/sync/index.ts index 0a3165f6f52..ad799139a2e 100644 --- a/apps/server/src/infra/sync/index.ts +++ b/apps/server/src/infra/sync/index.ts @@ -1 +1 @@ -export * from './tsp'; +export { TspSyncConfig, TspSyncStrategy } from './tsp'; diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 0bf0c2dc0ec..63d0472ec07 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -1,15 +1,15 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { ConsoleWriterModule } from '@infra/console'; -import { RabbitMQWrapperModule } from '@infra/rabbitmq'; -import { TspClientModule } from '@infra/tsp-client/tsp-client.module'; -import { AccountModule } from '@modules/account'; -import { LegacySchoolModule } from '@modules/legacy-school'; -import { SchoolModule } from '@modules/school'; -import { SystemModule } from '@modules/system'; -import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; -import { LoggerModule } from '@src/core/logger'; -import { ProvisioningModule } from '@src/modules/provisioning'; +import { RabbitMQWrapperModule } from '../rabbitmq'; +import { ConsoleWriterModule } from '../console'; +import { TspClientModule } from '../tsp-client'; +import { AccountModule } from '../../modules/account'; +import { LegacySchoolModule } from '../../modules/legacy-school'; +import { SchoolModule } from '../../modules/school'; +import { SystemModule } from '../../modules/system'; +import { UserModule } from '../../modules/user'; +import { LoggerModule } from '../../core/logger'; +import { ProvisioningModule } from '../../modules/provisioning'; import { SyncConsole } from './console/sync.console'; import { SyncService } from './service/sync.service'; import { TspLegacyMigrationService } from './tsp/tsp-legacy-migration.service'; diff --git a/apps/server/src/infra/sync/tsp/loggable/index.ts b/apps/server/src/infra/sync/tsp/loggable/index.ts new file mode 100644 index 00000000000..cfae9e5c457 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/index.ts @@ -0,0 +1,16 @@ +export { TspDataFetchedLoggable } from './tsp-data-fetched.loggable'; +export { TspLegacyMigrationStartLoggable } from './tsp-legacy-migration-start.loggable'; +export { TspLegacySchoolMigrationCountLoggable } from './tsp-legacy-school-migration-count.loggable'; +export { TspLegacySchoolMigrationSuccessLoggable } from './tsp-legacy-school-migration-success.loggable'; +export { TspMissingExternalIdLoggable } from './tsp-missing-external-id.loggable'; +export { TspSchoolsFetchedLoggable } from './tsp-schools-fetched.loggable'; +export { TspSchoolsSyncedLoggable } from './tsp-schools-synced.loggable'; +export { TspSchulnummerMissingLoggable } from './tsp-schulnummer-missing.loggable'; +export { TspStudentsFetchedLoggable } from './tsp-students-fetched.loggable'; +export { TspStudentsMigratedLoggable } from './tsp-students-migrated.loggable'; +export { TspSyncedUsersLoggable } from './tsp-synced-users.loggable'; +export { TspSyncingUsersLoggable } from './tsp-syncing-users.loggable'; +export { TspSystemNotFoundLoggableException } from './tsp-system-not-found.loggable-exception'; +export { TspTeachersFetchedLoggable } from './tsp-teachers-fetched.loggable'; +export { TspTeachersMigratedLoggable } from './tsp-teachers-migrated.loggable'; +export { TspUsersMigratedLoggable } from './tsp-users-migrated.loggable'; diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts index e096993c328..3606755d851 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspDataFetchedLoggable implements Loggable { constructor( diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts index c3af20dc0c7..3277bace76d 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspLegacyMigrationStartLoggable implements Loggable { getLogMessage(): LogMessage { diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts index fcdf3b26d0a..07a555c93f0 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspLegacyMigrationSystemMissingLoggable implements Loggable { getLogMessage(): LogMessage { diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts index c04fc6b5a53..bb9679433fc 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspLegacySchoolMigrationCountLoggable implements Loggable { constructor(private readonly total: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts index b6ac7b247e2..a81a62f549a 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspLegacySchoolMigrationSuccessLoggable implements Loggable { constructor(private readonly total: number, private readonly migrated: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts index 1ef7d8114b2..262c6d97703 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspMissingExternalIdLoggable implements Loggable { constructor(private readonly objectType: string) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts index f2f5bf512a7..5e7f777a917 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSchoolsFetchedLoggable implements Loggable { constructor(private readonly tspSchoolCount: number, private readonly daysFetched: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts index 1068270a151..126942ef5e5 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSchoolsSyncedLoggable implements Loggable { constructor( diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts index 3be67a149ee..2b7245862d1 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSchulnummerMissingLoggable implements Loggable { constructor(private readonly schulName?: string) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts index 5a8af5758b3..db4d2f67e3c 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspStudentsFetchedLoggable implements Loggable { constructor(private readonly tspStudentCount: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts index 5937433ea6e..0465556da9c 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspStudentsMigratedLoggable implements Loggable { constructor(private readonly migratedStudents: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts index 58a15f94cfa..29b59ecbeb3 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSyncedUsersLoggable implements Loggable { constructor(private readonly syncedUsers: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts index 58efd8e41c8..73c3cacc775 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSyncingUsersLoggable implements Loggable { constructor(private readonly syncingUsers: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts index c65fd7ad992..bbf6e9d3471 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts @@ -1,6 +1,6 @@ import { HttpStatus } from '@nestjs/common'; -import { BusinessError, ErrorLogMessage } from '@shared/common'; -import { Loggable, LogMessage } from '@src/core/logger'; +import { BusinessError, ErrorLogMessage } from '../../../../shared/common/error'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSystemNotFoundLoggableException extends BusinessError implements Loggable { constructor() { diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts index 476327462bc..9896045abaa 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspTeachersFetchedLoggable implements Loggable { constructor(private readonly tspTeacherCount: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts index ebbe515d06c..c3fe90566b4 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspTeachersMigratedLoggable implements Loggable { constructor(private readonly migratedTeachers: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts index 9000de6b9bc..b93e3e2549b 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '@src/core/logger'; +import { Loggable, LogMessage } from '../../../../core/logger'; export class TspUsersMigratedLoggable implements Loggable { constructor(private readonly migratedUsers: number) {} diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts index 4c7c76c8ea5..f49e2c37d8c 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts @@ -1,8 +1,7 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; -import { Logger } from '@src/core/logger'; +import { AxiosError, AxiosResponse } from 'axios'; import { ExportApiInterface, RobjExportKlasse, @@ -12,10 +11,11 @@ import { RobjExportSchuelerMigration, RobjExportSchule, TspClientFactory, -} from '@src/infra/tsp-client'; -import { OauthConfigMissingLoggableException } from '@src/modules/oauth/loggable'; -import { systemFactory } from '@src/modules/system/testing'; -import { AxiosError, AxiosResponse } from 'axios'; +} from '../../tsp-client'; +import { systemFactory } from '../../../modules/system/testing'; +import { OauthConfigMissingLoggableException } from '../../../modules/oauth/loggable'; +import { Logger } from '../../../core/logger'; +import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; import { TspFetchService } from './tsp-fetch.service'; describe(TspFetchService.name, () => { diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts index 9285fbfd9a8..94a86d389ff 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; -import { Logger } from '@src/core/logger'; -import { ExportApiInterface, TspClientFactory } from '@src/infra/tsp-client'; -import { OauthConfigMissingLoggableException } from '@src/modules/oauth/loggable'; -import { System } from '@src/modules/system'; import { AxiosError, AxiosResponse } from 'axios'; import moment from 'moment'; +import { System } from '../../../modules/system'; +import { OauthConfigMissingLoggableException } from '../../../modules/oauth/loggable'; +import { ExportApiInterface, TspClientFactory } from '../../tsp-client'; +import { Logger } from '../../../core/logger'; +import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; @Injectable() export class TspFetchService { diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts index ee52feb75cf..4f91858498d 100644 --- a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity } from '@shared/domain/entity'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SchoolFeature } from '@shared/domain/types'; -import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '@shared/testing'; -import { Logger } from '@src/core/logger'; -import { MongoMemoryDatabaseModule } from '@src/infra/database'; -import { SystemType } from '@src/modules/system'; +import { SystemType } from '../../../modules/system'; +import { MongoMemoryDatabaseModule } from '../../database'; +import { Logger } from '../../../core/logger'; +import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '../../../shared/testing'; +import { SchoolFeature } from '../../../shared/domain/types'; +import { SystemProvisioningStrategy } from '../../../shared/domain/interface'; +import { SchoolEntity } from '../../../shared/domain/entity'; import { TspLegacyMigrationSystemMissingLoggable } from './loggable/tsp-legacy-migration-system-missing.loggable'; import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts index 1bff95ef2b5..cfe099a65fc 100644 --- a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { EntityId, SchoolFeature } from '@shared/domain/types'; -import { Logger } from '@src/core/logger'; +import { Logger } from '../../../core/logger'; +import { EntityId, SchoolFeature } from '../../../shared/domain/types'; import { TspLegacyMigrationStartLoggable } from './loggable/tsp-legacy-migration-start.loggable'; import { TspLegacyMigrationSystemMissingLoggable } from './loggable/tsp-legacy-migration-system-missing.loggable'; import { TspLegacySchoolMigrationCountLoggable } from './loggable/tsp-legacy-school-migration-count.loggable'; diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index fda468415cb..2f64794d8bf 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -1,20 +1,20 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { schoolFactory } from '../../../modules/school/testing'; +import { systemFactory } from '../../../modules/system/testing'; +import { BadDataLoggableException } from '../../../modules/provisioning/loggable'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; +import { Logger } from '../../../core/logger'; +import { SystemProvisioningStrategy, RoleName } from '../../../shared/domain/interface'; + import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto, -} from '@modules/provisioning'; -import { Test, TestingModule } from '@nestjs/testing'; -import { RoleName } from '@shared/domain/interface'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { Logger } from '@src/core/logger'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; -import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; -import { schoolFactory } from '@src/modules/school/testing'; -import { systemFactory } from '@src/modules/system/testing'; +} from '../../../modules/provisioning'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index 31904871123..7d27752cce6 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -1,18 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { BadDataLoggableException } from '../../../modules/provisioning/loggable'; +import { System } from '../../../modules/system'; +import { RoleName, SystemProvisioningStrategy } from '../../../shared/domain/interface'; + +import { Logger } from '../../../core/logger'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; +import { School } from '../../../modules/school'; import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto, -} from '@modules/provisioning'; -import { School } from '@modules/school'; -import { System } from '@modules/system'; -import { Injectable } from '@nestjs/common'; -import { RoleName } from '@shared/domain/interface'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { Logger } from '@src/core/logger'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; -import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; +} from '../../../modules/provisioning'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; @Injectable() diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts index b86d44430f7..91b86cd5142 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts @@ -1,18 +1,18 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AccountService } from '@modules/account'; -import { School, SchoolService } from '@modules/school'; -import { SystemService, SystemType } from '@modules/system'; -import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { federalStateFactory, schoolYearFactory, userDoFactory } from '@shared/testing'; -import { accountDoFactory } from '@src/modules/account/testing'; -import { FederalStateService, SchoolYearService } from '@src/modules/legacy-school'; -import { FileStorageType, SchoolProps } from '@src/modules/school/domain'; -import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@src/modules/school/repo/mikro-orm/mapper'; -import { schoolFactory } from '@src/modules/school/testing'; -import { systemFactory } from '@src/modules/system/testing'; +import { AccountService } from '../../../modules/account'; +import { School, SchoolService } from '../../../modules/school'; +import { SystemService, SystemType } from '../../../modules/system'; +import { UserService } from '../../../modules/user'; +import { SystemProvisioningStrategy } from '../../../shared/domain/interface/system-provisioning.strategy'; +import { federalStateFactory, schoolYearFactory, userDoFactory } from '../../../shared/testing'; +import { accountDoFactory } from '../../../modules/account/testing'; +import { FederalStateService, SchoolYearService } from '../../../modules/legacy-school'; +import { FileStorageType, SchoolProps } from '../../../modules/school/domain'; +import { FederalStateEntityMapper, SchoolYearEntityMapper } from '../../../modules/school/repo/mikro-orm/mapper'; +import { schoolFactory } from '../../../modules/school/testing'; +import { systemFactory } from '../../../modules/system/testing'; import { TspSyncService } from './tsp-sync.service'; describe(TspSyncService.name, () => { diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts index c79e1165343..362fe1607ac 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts @@ -1,20 +1,19 @@ -import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; -import { School, SchoolService } from '@modules/school'; -import { System, SystemService, SystemType } from '@modules/system'; import { Injectable } from '@nestjs/common'; -import { UserDO } from '@shared/domain/domainobject'; -import { UserSourceOptions } from '@shared/domain/domainobject/user-source-options.do'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { EntityId, SchoolFeature } from '@shared/domain/types'; -import { Account, AccountService } from '@src/modules/account'; -import { FederalStateNames } from '@src/modules/legacy-school/types'; -import { FederalState, FileStorageType } from '@src/modules/school/domain'; -import { SchoolFactory } from '@src/modules/school/domain/factory'; -import { SchoolPermissions } from '@src/modules/school/domain/type'; -import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@src/modules/school/repo/mikro-orm/mapper'; -import { UserService } from '@src/modules/user'; import { ObjectId } from 'bson'; -import { TspSystemNotFoundLoggableException } from './loggable/tsp-system-not-found.loggable-exception'; +import { FederalStateService, SchoolYearService } from '../../../modules/legacy-school'; +import { School, SchoolService } from '../../../modules/school'; +import { System, SystemService, SystemType } from '../../../modules/system'; +import { UserSourceOptions, UserDO } from '../../../shared/domain/domainobject'; +import { SystemProvisioningStrategy } from '../../../shared/domain/interface/system-provisioning.strategy'; +import { EntityId, SchoolFeature } from '../../../shared/domain/types'; +import { Account, AccountService } from '../../../modules/account'; +import { FederalStateNames } from '../../../modules/legacy-school/types'; +import { FederalState, FileStorageType } from '../../../modules/school/domain'; +import { SchoolFactory } from '../../../modules/school/domain/factory'; +import { SchoolPermissions } from '../../../modules/school/domain/type'; +import { FederalStateEntityMapper, SchoolYearEntityMapper } from '../../../modules/school/repo/mikro-orm/mapper'; +import { UserService } from '../../../modules/user'; +import { TspSystemNotFoundLoggableException } from './loggable'; @Injectable() export class TspSyncService { diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index b4d8a3b8a52..fdbd6ae0655 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -1,5 +1,23 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Account } from '../../../modules/account'; +import { accountDoFactory } from '../../../modules/account/testing'; +import { + ExternalUserDto, + OauthDataDto, + ProvisioningService, + ProvisioningSystemDto, +} from '../../../modules/provisioning'; +import { School } from '../../../modules/school'; +import { schoolFactory } from '../../../modules/school/testing'; +import { System } from '../../../modules/system'; +import { systemFactory } from '../../../modules/system/testing'; +import { Logger } from '../../../core/logger'; +import { userDoFactory } from '../../../shared/testing'; +import { SystemProvisioningStrategy } from '../../../shared/domain/interface'; +import { UserDO } from '../../../shared/domain/domainobject'; import { RobjExportKlasse, RobjExportLehrer, @@ -7,20 +25,7 @@ import { RobjExportSchueler, RobjExportSchuelerMigration, RobjExportSchule, -} from '@infra/tsp-client'; -import { ConfigService } from '@nestjs/config'; -import { Test, TestingModule } from '@nestjs/testing'; -import { UserDO } from '@shared/domain/domainobject'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { userDoFactory } from '@shared/testing'; -import { Logger } from '@src/core/logger'; -import { Account } from '@src/modules/account'; -import { accountDoFactory } from '@src/modules/account/testing'; -import { ExternalUserDto, OauthDataDto, ProvisioningService, ProvisioningSystemDto } from '@src/modules/provisioning'; -import { School } from '@src/modules/school'; -import { schoolFactory } from '@src/modules/school/testing'; -import { System } from '@src/modules/system'; -import { systemFactory } from '@src/modules/system/testing'; +} from '../../tsp-client'; import { SyncStrategyTarget } from '../sync-strategy.types'; import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspFetchService } from './tsp-fetch.service'; diff --git a/apps/server/src/infra/tsp-client/index.ts b/apps/server/src/infra/tsp-client/index.ts index 54d121063e1..d8adc320885 100644 --- a/apps/server/src/infra/tsp-client/index.ts +++ b/apps/server/src/infra/tsp-client/index.ts @@ -1,5 +1,19 @@ -export * from './generated/api'; -export * from './generated/models'; -export * from './tsp-client-config'; -export * from './tsp-client-factory'; -export * from './tsp-client.module'; +export { + ExportApi, + ExportApiAxiosParamCreator, + ExportApiFactory, + ExportApiFp, + ExportApiInterface, +} from './generated/api'; +export { + RobjExportKlasse, + RobjExportLehrer, + RobjExportLehrerMigration, + RobjExportSchueler, + RobjExportSchuelerMigration, + RobjExportSchule, + VersionResponse, +} from './generated/models'; +export { TspClientConfig } from './tsp-client-config'; +export { TspClientFactory } from './tsp-client-factory'; +export { TspClientModule } from './tsp-client.module'; diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index b8123894961..663a0b4303b 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -1,12 +1,12 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { OauthAdapterService } from '@modules/oauth'; -import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; -import { Logger } from '@src/core/logger'; import axios, { AxiosError } from 'axios'; +import { AxiosErrorLoggable, ErrorLoggable } from '../../core/error/loggable'; +import { Logger } from '../../core/logger'; +import { ServerConfig } from '../../modules/server'; +import { OauthAdapterService } from '../../modules/oauth'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; diff --git a/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts b/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts index 545b7213ea7..0b07f6c24f9 100644 --- a/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts +++ b/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts @@ -1,3 +1,3 @@ -export * from './federal-state.entity.mapper'; -export * from './school-year.entity.mapper'; -export * from './school.entity.mapper'; +export { FederalStateEntityMapper } from './federal-state.entity.mapper'; +export { SchoolYearEntityMapper } from './school-year.entity.mapper'; +export { SchoolEntityMapper } from './school.entity.mapper'; diff --git a/apps/server/src/modules/synchronization/repo/index.ts b/apps/server/src/modules/synchronization/repo/index.ts index d9e439d5871..026819e3d40 100644 --- a/apps/server/src/modules/synchronization/repo/index.ts +++ b/apps/server/src/modules/synchronization/repo/index.ts @@ -1,2 +1,2 @@ -export * from './entity'; -export * from './synchronization.repo'; +export { SynchronizationEntity, SynchronizationEntityProps } from './entity'; +export { SynchronizationRepo } from './synchronization.repo'; diff --git a/apps/server/src/modules/synchronization/repo/mapper/index.ts b/apps/server/src/modules/synchronization/repo/mapper/index.ts index 5b1ccd6a474..a27eb43fb06 100644 --- a/apps/server/src/modules/synchronization/repo/mapper/index.ts +++ b/apps/server/src/modules/synchronization/repo/mapper/index.ts @@ -1 +1 @@ -export * from './synchronization.mapper'; +export { SynchronizationMapper } from './synchronization.mapper'; diff --git a/apps/server/src/shared/common/error/index.ts b/apps/server/src/shared/common/error/index.ts index 58446694939..a157fd4a7c0 100644 --- a/apps/server/src/shared/common/error/index.ts +++ b/apps/server/src/shared/common/error/index.ts @@ -1,10 +1,10 @@ -export * from './api-validation.error'; -export * from './authorization.error'; -export * from './business.error'; -export * from './entity-not-found.error'; -export * from './forbidden-operation.error'; -export * from './validation.error'; -export * from './interfaces'; +export { ApiValidationError } from './api-validation.error'; +export { AuthorizationError } from './authorization.error'; +export { BusinessError } from './business.error'; +export { EntityNotFoundError } from './entity-not-found.error'; +export { ForbiddenOperationError } from './forbidden-operation.error'; +export { ValidationError } from './validation.error'; +export { ValidationErrorLogMessage, ErrorLogMessage } from './interfaces'; // business errors -export * from './user-already-assigned-to-import-user.business-error'; +export { UserAlreadyAssignedToImportUserError } from './user-already-assigned-to-import-user.business-error'; diff --git a/apps/server/src/shared/domain/domainobject/index.ts b/apps/server/src/shared/domain/domainobject/index.ts index 51fbc5000a2..083ceaf0dae 100644 --- a/apps/server/src/shared/domain/domainobject/index.ts +++ b/apps/server/src/shared/domain/domainobject/index.ts @@ -1,10 +1,11 @@ export * from './base.do'; -export * from './pseudonym.do'; -export * from './video-conference.do'; -export * from './user-login-migration.do'; +export { Pseudonym, PseudonymProps } from './pseudonym.do'; +export { VideoConferenceDO, VideoConferenceOptionsDO } from './video-conference.do'; +export { UserLoginMigrationDO } from './user-login-migration.do'; export * from './legacy-school.do'; -export * from './user.do'; -export * from './page'; -export * from './role-reference'; -export * from './ltitool.do'; -export * from './external-source'; +export { SecondarySchoolReference, UserDO } from './user.do'; +export { Page } from './page'; +export { RoleReference } from './role-reference'; +export { CustomLtiPropertyDO, LtiToolDO } from './ltitool.do'; +export { ExternalSource } from './external-source'; +export { UserSourceOptions, UserSourceOptionsProps } from './user-source-options.do'; diff --git a/apps/server/src/shared/domain/entity/index.ts b/apps/server/src/shared/domain/entity/index.ts index 0925cb5d2d9..4fc8de3ab7d 100644 --- a/apps/server/src/shared/domain/entity/index.ts +++ b/apps/server/src/shared/domain/entity/index.ts @@ -1,24 +1,66 @@ -export * from './all-entities'; -export * from './base.entity'; -export * from './column-board-node.entity'; -export * from './course.entity'; -export * from './coursegroup.entity'; -export * from './dashboard.entity'; -export * from './dashboard.model.entity'; -export * from './federal-state.entity'; -export * from './legacy-board'; -export * from './lesson.entity'; -export * from './ltitool.entity'; -export * from './materials.entity'; -export * from './news.entity'; -export * from './role.entity'; -export * from './school.entity'; -export * from './schoolyear.entity'; -export * from './storageprovider.entity'; -export * from './submission.entity'; -export * from './task.entity'; -export * from './team.entity'; -export * from './user-login-migration.entity'; -export * from './user.entity'; -export * from './video-conference.entity'; -export * from './consent'; +export { ALL_ENTITIES } from './all-entities'; +export { BaseEntity, BaseEntityReference, BaseEntityWithTimestamps } from './base.entity'; +export { ColumnBoardNodeProps } from './column-board-node.entity'; +export { Course, CourseFeatures, CourseProperties, SyncAttribute, UsersList } from './course.entity'; +export { CourseGroup, CourseGroupProperties } from './coursegroup.entity'; +export { + DashboardEntity, + DashboardProps, + GridElement, + GridElementContent, + GridElementWithPosition, + GridPosition, + GridPositionWithGroupIndex, + IGridElement, +} from './dashboard.entity'; +export { + DashboardGridElementModel, + DashboardGridElementModelProperties, + DashboardModelEntity, + DashboardModelProperties, +} from './dashboard.model.entity'; +export { CountyEmbeddable, FederalStateEntity, FederalStateProperties } from './federal-state.entity'; +export { + BoardProps, + ColumnboardBoardElement, + LegacyBoard, + LegacyBoardElement, + LegacyBoardElementProps, + LegacyBoardElementReference, + LegacyBoardElementType, + LessonBoardElement, + TaskBoardElement, +} from './legacy-board'; +export { + ComponentEtherpadProperties, + ComponentGeogebraProperties, + ComponentInternalProperties, + ComponentLernstoreProperties, + ComponentNexboardProperties, + ComponentProperties, + ComponentTextProperties, + ComponentType, + LessonEntity, + LessonParent, + LessonProperties, + isLesson, +} from './lesson.entity'; +export { CustomLtiProperty, ILtiToolProperties, LtiPrivacyPermission, LtiRoleType, LtiTool } from './ltitool.entity'; +export { Material, MaterialProperties, RelatedResourceProperties, TargetGroupProperties } from './materials.entity'; +export { CourseNews, News, NewsProperties, SchoolNews, TeamNews } from './news.entity'; +export { Role, RoleProperties } from './role.entity'; +export { SchoolEntity, SchoolProperties, SchoolRolePermission, SchoolRoles } from './school.entity'; +export { SchoolYearEntity, SchoolYearProperties } from './schoolyear.entity'; +export { StorageProviderEntity, StorageProviderProperties } from './storageprovider.entity'; +export { Submission, SubmissionProperties } from './submission.entity'; +export { Task, TaskParent, TaskParentDescriptions, TaskWithStatusVo, isTask } from './task.entity'; +export { TeamEntity, TeamProperties, TeamUserEntity, TeamUserProperties } from './team.entity'; +export { IUserLoginMigration, UserLoginMigrationEntity } from './user-login-migration.entity'; +export { User, UserProperties, UserSchoolEmbeddable } from './user.entity'; +export { + IVideoConferenceProperties, + TargetModels, + VideoConference, + VideoConferenceOptions, +} from './video-conference.entity'; +export { ConsentEntity, ParentConsentEntity, UserConsentEntity } from './consent'; diff --git a/apps/server/src/shared/domain/entity/legacy-board/index.ts b/apps/server/src/shared/domain/entity/legacy-board/index.ts index bf6b64bda35..ba9072ecc1b 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/index.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/index.ts @@ -1,5 +1,10 @@ -export * from './legacy-board.entity'; -export * from './legacy-boardelement.entity'; -export * from './column-board-boardelement'; -export * from './lesson-boardelement.entity'; -export * from './task-boardelement.entity'; +export { BoardProps, LegacyBoard } from './legacy-board.entity'; +export { + LegacyBoardElement, + LegacyBoardElementProps, + LegacyBoardElementReference, + LegacyBoardElementType, +} from './legacy-boardelement.entity'; +export { ColumnboardBoardElement } from './column-board-boardelement'; +export { LessonBoardElement } from './lesson-boardelement.entity'; +export { TaskBoardElement } from './task-boardelement.entity'; diff --git a/apps/server/src/shared/domain/interface/index.ts b/apps/server/src/shared/domain/interface/index.ts index e5eb9ca8da5..5fb0a74d7f4 100644 --- a/apps/server/src/shared/domain/interface/index.ts +++ b/apps/server/src/shared/domain/interface/index.ts @@ -1,8 +1,9 @@ -export * from './account'; -export * from './entity'; -export * from './find-options'; -export * from './language-type.enum'; -export * from './learnroom'; -export * from './permission.enum'; -export * from './rolename.enum'; -export * from './video-conference-scope.enum'; +export { IdmAccount, IdmAccountUpdate } from './account'; +export { EntityWithSchool, IEntity, IEntityWithTimestamps } from './entity'; +export { IFindOptions, Pagination, SortOrder, SortOrderMap, SortOrderNumberType } from './find-options'; +export { LanguageType } from './language-type.enum'; +export { Learnroom, LearnroomElement } from './learnroom'; +export { Permission } from './permission.enum'; +export { GuestRole, GuestRoleArray, IUserRoleName, RoleName, RoomRole, RoomRoleArray } from './rolename.enum'; +export { VideoConferenceScope } from './video-conference-scope.enum'; +export { SystemProvisioningStrategy } from './system-provisioning.strategy'; diff --git a/apps/server/src/shared/testing/index.ts b/apps/server/src/shared/testing/index.ts index 5615f70b563..1ab2378f8be 100644 --- a/apps/server/src/shared/testing/index.ts +++ b/apps/server/src/shared/testing/index.ts @@ -1,8 +1,74 @@ -export * from './factory'; -export * from './setup-entities'; -export * from './create-collections'; -export * from './cleanup-collections'; -export * from './map-user-to-current-user'; -export * from './test-api-client'; -export * from './web-socket-ready-state-enum'; +export { + AxiosHeadersKeyValue, + BaseFactory, + CurrentUserFactory, + DoBaseFactory, + DomainObjectFactory, + EntityFactory, + JwtPayloadFactory, + JwtTestFactory, + SystemEntityFactory, + UserAndAccountParams, + UserAndAccountTestFactory, + axiosErrorFactory, + axiosResponseFactory, + boardFactory, + columnBoardNodeFactory, + columnboardBoardElementFactory, + countyEmbeddableFactory, + courseFactory, + courseGroupFactory, + courseNewsFactory, + courseUnpublishedNewsFactory, + currentUserFactory, + externalGroupDtoFactory, + externalSchoolDtoFactory, + externalToolPseudonymEntityFactory, + federalStateFactory, + fileRecordFactory, + groupEntityFactory, + groupFactory, + h5pContentFactory, + importUserFactory, + jwtPayloadFactory, + legacyFileEntityMockFactory, + legacySchoolDoFactory, + lessonBoardElementFactory, + lessonFactory, + ltiToolDOFactory, + materialFactory, + pseudonymFactory, + roleDtoFactory, + roleFactory, + schoolEntityFactory, + schoolNewsFactory, + schoolSystemOptionsEntityFactory, + schoolSystemOptionsFactory, + schoolUnpublishedNewsFactory, + schoolYearFactory, + shareTokenFactory, + storageProviderFactory, + submissionFactory, + systemEntityFactory, + systemLdapConfigEntityFactory, + systemOauthConfigEntityFactory, + systemOidcConfigEntityFactory, + taskBoardElementFactory, + taskFactory, + teamFactory, + teamNewsFactory, + teamUnpublishedNewsFactory, + teamUserFactory, + tldrawFileDtoFactory, + userDoFactory, + userFactory, + userLoginMigrationDOFactory, + userLoginMigrationFactory, +} from './factory'; +export { setupEntities } from './setup-entities'; +export { createCollections } from './create-collections'; +export { cleanupCollections } from './cleanup-collections'; +export { mapUserToCurrentUser } from './map-user-to-current-user'; +export { TestApiClient } from './test-api-client'; +export { WebSocketReadyStateEnum } from './web-socket-ready-state-enum'; export { DatesToStrings } from './dates-to-strings'; From 61725443a3c9cfbe8457ab44f16ea4d5cb51d7c8 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 11:49:22 +0100 Subject: [PATCH 02/55] EW-1057: fixing an export --- apps/server/src/shared/domain/domainobject/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/shared/domain/domainobject/index.ts b/apps/server/src/shared/domain/domainobject/index.ts index 083ceaf0dae..f1e15d4290a 100644 --- a/apps/server/src/shared/domain/domainobject/index.ts +++ b/apps/server/src/shared/domain/domainobject/index.ts @@ -3,7 +3,7 @@ export { Pseudonym, PseudonymProps } from './pseudonym.do'; export { VideoConferenceDO, VideoConferenceOptionsDO } from './video-conference.do'; export { UserLoginMigrationDO } from './user-login-migration.do'; export * from './legacy-school.do'; -export { SecondarySchoolReference, UserDO } from './user.do'; +export { UserDO } from './user.do'; export { Page } from './page'; export { RoleReference } from './role-reference'; export { CustomLtiPropertyDO, LtiToolDO } from './ltitool.do'; From 47a9878c7d453a02805e8e285a59bd1bdf83fc65 Mon Sep 17 00:00:00 2001 From: Martin Schuhmacher <55735359+MartinSchuhmacher@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:19:58 +0100 Subject: [PATCH 03/55] BC-8213 - validate room dates (#5347) * fixing room date validation on create and update --- .../modules/room/domain/do/room.do.spec.ts | 52 ------------------- .../src/modules/room/domain/do/room.do.ts | 14 ----- .../room/domain/service/room.service.spec.ts | 17 ++++++ .../room/domain/service/room.service.ts | 11 ++++ 4 files changed, 28 insertions(+), 66 deletions(-) diff --git a/apps/server/src/modules/room/domain/do/room.do.spec.ts b/apps/server/src/modules/room/domain/do/room.do.spec.ts index bd213e52259..465fc4b95e8 100644 --- a/apps/server/src/modules/room/domain/do/room.do.spec.ts +++ b/apps/server/src/modules/room/domain/do/room.do.spec.ts @@ -1,4 +1,3 @@ -import { ValidationError } from '@shared/common'; import { EntityId } from '@shared/domain/types'; import { roomFactory } from '../../testing'; import { RoomColor } from '../type'; @@ -71,55 +70,4 @@ describe('Room', () => { const expectedUpdatedAt = new Date('2024-01-01'); expect(room.updatedAt).toEqual(expectedUpdatedAt); }); - - describe('time frame validation', () => { - const setup = () => { - const props: RoomProps = { - id: roomId, - name: 'Conference Room', - color: RoomColor.BLUE, - startDate: new Date('2024-01-01'), - endDate: new Date('2024-12-31'), - createdAt: new Date('2024-01-01'), - updatedAt: new Date('2024-01-01'), - }; - - return { props }; - }; - - describe('when costructor is called with invalid time frame', () => { - it('should throw validation error', () => { - const buildInvalid = () => { - const { props } = setup(); - props.startDate = new Date('2024-12-31'); - props.endDate = new Date('2024-01-01'); - // eslint-disable-next-line no-new - new Room(props); - }; - expect(buildInvalid).toThrowError(ValidationError); - }); - }); - - describe('when setting start date after end date', () => { - it('should throw validation error', () => { - const setInvalidStartDate = () => { - const { props } = setup(); - const inValidRoom = new Room(props); - inValidRoom.startDate = new Date('2025-01-01'); - }; - expect(setInvalidStartDate).toThrowError(ValidationError); - }); - }); - - describe('when setting end date before start date', () => { - it('should throw validation error', () => { - const setInvalidEndDate = () => { - const { props } = setup(); - const inValidRoom = new Room(props); - inValidRoom.endDate = new Date('2023-12-31'); - }; - expect(setInvalidEndDate).toThrowError(ValidationError); - }); - }); - }); }); diff --git a/apps/server/src/modules/room/domain/do/room.do.ts b/apps/server/src/modules/room/domain/do/room.do.ts index 05feb9d07d4..1cdd5093a63 100644 --- a/apps/server/src/modules/room/domain/do/room.do.ts +++ b/apps/server/src/modules/room/domain/do/room.do.ts @@ -1,4 +1,3 @@ -import { ValidationError } from '@shared/common'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { EntityId } from '@shared/domain/types'; import { RoomColor } from '../type'; @@ -19,7 +18,6 @@ export type RoomUpdateProps = RoomCreateProps; // will probably change in the fu export class Room extends DomainObject { public constructor(props: RoomProps) { super(props); - this.validateTimeSpan(); } public getProps(): RoomProps { @@ -54,7 +52,6 @@ export class Room extends DomainObject { public set startDate(value: Date) { this.props.startDate = value; - this.validateTimeSpan(); } public get endDate(): Date | undefined { @@ -63,7 +60,6 @@ export class Room extends DomainObject { public set endDate(value: Date) { this.props.endDate = value; - this.validateTimeSpan(); } public get createdAt(): Date { @@ -73,14 +69,4 @@ export class Room extends DomainObject { public get updatedAt(): Date { return this.props.updatedAt; } - - private validateTimeSpan() { - if (this.props.startDate != null && this.props.endDate != null && this.props.startDate > this.props.endDate) { - throw new ValidationError( - `Invalid room timespan. Start date '${this.props.startDate.toISOString()}' has to be before end date: '${this.props.endDate.toISOString()}'. Room id='${ - this.id - }'` - ); - } - } } diff --git a/apps/server/src/modules/room/domain/service/room.service.spec.ts b/apps/server/src/modules/room/domain/service/room.service.spec.ts index 5edb5710a03..2eb114a4f1f 100644 --- a/apps/server/src/modules/room/domain/service/room.service.spec.ts +++ b/apps/server/src/modules/room/domain/service/room.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { Page } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; +import { ValidationError } from '@shared/common'; import { RoomRepo } from '../../repo'; import { roomFactory } from '../../testing'; import { Room, RoomCreateProps, RoomUpdateProps } from '../do'; @@ -80,6 +81,14 @@ describe('RoomService', () => { expect(roomRepo.save).toHaveBeenCalledWith(expect.objectContaining(props)); }); + + it('should throw validation error if start date is after end date', async () => { + const { props } = setup(); + props.startDate = new Date('2024-12-31'); + props.endDate = new Date('2024-01-01'); + + await expect(service.createRoom(props)).rejects.toThrowError(ValidationError); + }); }); describe('getSingleRoom', () => { @@ -137,6 +146,14 @@ describe('RoomService', () => { expect(roomRepo.save).toHaveBeenCalledWith(room); }); + + it('should throw validation error if start date is after end date', async () => { + const { props, room } = setup(); + props.startDate = new Date('2024-12-31'); + props.endDate = new Date('2024-01-01'); + + await expect(service.updateRoom(room, props)).rejects.toThrowError(ValidationError); + }); }); describe('deleteRoom', () => { diff --git a/apps/server/src/modules/room/domain/service/room.service.ts b/apps/server/src/modules/room/domain/service/room.service.ts index 9f0a974ae5b..4ec2c62c779 100644 --- a/apps/server/src/modules/room/domain/service/room.service.ts +++ b/apps/server/src/modules/room/domain/service/room.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { ValidationError } from '@shared/common'; import { RoomRepo } from '../../repo'; import { Room, RoomCreateProps, RoomProps, RoomUpdateProps } from '../do'; @@ -29,6 +30,7 @@ export class RoomService { createdAt: new Date(), updatedAt: new Date(), }; + this.validateTimeSpan(props, roomProps.id); const room = new Room(roomProps); await this.roomRepo.save(room); @@ -43,6 +45,7 @@ export class RoomService { } public async updateRoom(room: Room, props: RoomUpdateProps): Promise { + this.validateTimeSpan(props, room.id); Object.assign(room, props); await this.roomRepo.save(room); @@ -51,4 +54,12 @@ export class RoomService { public async deleteRoom(room: Room): Promise { await this.roomRepo.delete(room); } + + private validateTimeSpan(props: RoomCreateProps | RoomUpdateProps, roomId: string): void { + if (props.startDate != null && props.endDate != null && props.startDate > props.endDate) { + throw new ValidationError( + `Invalid room timespan. Start date '${props.startDate.toISOString()}' has to be before end date: '${props.endDate.toISOString()}'. Room id='${roomId}'` + ); + } + } } From 6701a1d1ab278b0e5e61ce408115c87326fe08d4 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 12:14:05 +0100 Subject: [PATCH 04/55] EW-1057: fixed more imports --- apps/server/src/core/logger/interfaces/index.ts | 2 +- .../modules/learnroom/service/board-copy.service.ts | 10 +++++----- .../common/interceptor/request-logging.interceptor.ts | 3 ++- apps/server/src/shared/domain/entity/index.ts | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/server/src/core/logger/interfaces/index.ts b/apps/server/src/core/logger/interfaces/index.ts index 2c4736d6656..871f806743d 100644 --- a/apps/server/src/core/logger/interfaces/index.ts +++ b/apps/server/src/core/logger/interfaces/index.ts @@ -1,2 +1,2 @@ -export { ILegacyLogger } from './legacy-logger.interface'; +export { ILegacyLogger, RequestLoggingBody } from './legacy-logger.interface'; export { Loggable } from './loggable'; diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.ts b/apps/server/src/modules/learnroom/service/board-copy.service.ts index b77dd910e6f..8b09df5f1a7 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.ts @@ -4,6 +4,10 @@ import { LessonCopyService } from '@modules/lesson'; import { TaskCopyService } from '@modules/task'; import { Injectable } from '@nestjs/common'; import { getResolvedValues } from '@shared/common/utils/promise'; +import { EntityId } from '@shared/domain/types'; +import { LegacyBoardRepo } from '@shared/repo'; +import { LegacyLogger } from '@src/core/logger'; +import { sortBy } from 'lodash'; import { ColumnboardBoardElement, ColumnBoardNode, @@ -18,11 +22,7 @@ import { Task, TaskBoardElement, User, -} from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { LegacyBoardRepo } from '@shared/repo'; -import { LegacyLogger } from '@src/core/logger'; -import { sortBy } from 'lodash'; +} from '../../../shared/domain/entity'; import { ColumnBoardNodeRepo } from '../repo'; type BoardCopyParams = { diff --git a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts index b1fab899762..abde0bc30c3 100644 --- a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts +++ b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts @@ -1,6 +1,7 @@ import { ICurrentUser } from '@infra/auth-guard'; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { LegacyLogger, RequestLoggingBody } from '@src/core/logger'; +import { LegacyLogger } from '@src/core/logger'; +import { RequestLoggingBody } from '@src/core/logger/interfaces'; import { Request } from 'express'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; diff --git a/apps/server/src/shared/domain/entity/index.ts b/apps/server/src/shared/domain/entity/index.ts index 4fc8de3ab7d..8e3be771d10 100644 --- a/apps/server/src/shared/domain/entity/index.ts +++ b/apps/server/src/shared/domain/entity/index.ts @@ -1,7 +1,7 @@ export { ALL_ENTITIES } from './all-entities'; -export { BaseEntity, BaseEntityReference, BaseEntityWithTimestamps } from './base.entity'; -export { ColumnBoardNodeProps } from './column-board-node.entity'; -export { Course, CourseFeatures, CourseProperties, SyncAttribute, UsersList } from './course.entity'; +export { BaseEntity, BaseEntityReference, BaseEntityWithTimestamps, baseEntityProperties } from './base.entity'; +export { ColumnBoardNodeProps, ColumnBoardNode } from './column-board-node.entity'; +export { Course, CourseFeatures, CourseProperties, UsersList, SyncAttribute } from './course.entity'; export { CourseGroup, CourseGroupProperties } from './coursegroup.entity'; export { DashboardEntity, From a77e5fe2a80aff18a5ab5fc4ddbc4bee92346970 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 12:59:35 +0100 Subject: [PATCH 05/55] EW-1057: some imports modified --- .../src/infra/schulconnex-client/index.ts | 30 ++++++++++++++++++- apps/server/src/modules/provisioning/index.ts | 26 +++++++++++++--- .../provisioning/provisioning.module.ts | 24 +++++++-------- .../provisioning/strategy/base.strategy.ts | 2 +- .../strategy/tsp/tsp.strategy.spec.ts | 9 +++--- .../provisioning/strategy/tsp/tsp.strategy.ts | 6 ++-- 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts index ef1cc753c41..daf5cf5eba0 100644 --- a/apps/server/src/infra/schulconnex-client/index.ts +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -1,4 +1,32 @@ export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; export { SchulconnexRestClient } from './schulconnex-rest-client'; -export * from './response'; +export { + SchulconnexAnschriftResponse, + SchulconnexCommunicationType, + SchulconnexErreichbarkeitenResponse, + SchulconnexGroupRole, + SchulconnexGroupType, + SchulconnexGruppeResponse, + SchulconnexGruppenResponse, + SchulconnexGruppenzugehoerigkeitResponse, + SchulconnexLaufzeitResponse, + SchulconnexNameResponse, + SchulconnexOrganisationResponse, + SchulconnexPersonResponse, + SchulconnexPersonenkontextResponse, + SchulconnexPoliciesInfoAccessControlResponse, + SchulconnexPoliciesInfoActionType, + SchulconnexPoliciesInfoErrorDescriptionResponse, + SchulconnexPoliciesInfoErrorResponse, + SchulconnexPoliciesInfoLicenseResponse, + SchulconnexPoliciesInfoPermissionResponse, + SchulconnexPoliciesInfoResponse, + SchulconnexPoliciesInfoTargetResponse, + SchulconnexResponse, + SchulconnexResponseValidationGroups, + SchulconnexRole, + SchulconnexSonstigeGruppenzugehoerigeResponse, + lernperiodeFormat, +} from './response'; export { SchulconnexClientConfig } from './schulconnex-client-config'; +export { SchulconnexClientModule } from './schulconnex-client.module'; diff --git a/apps/server/src/modules/provisioning/index.ts b/apps/server/src/modules/provisioning/index.ts index caded3ec616..e3d0496183d 100644 --- a/apps/server/src/modules/provisioning/index.ts +++ b/apps/server/src/modules/provisioning/index.ts @@ -1,5 +1,23 @@ -export * from './provisioning.module'; -export * from './dto'; -export * from './service/provisioning.service'; -export * from './strategy'; +export { ProvisioningModule } from './provisioning.module'; +export { + ExternalClassDto, + ExternalGroupDto, + ExternalGroupUserDto, + ExternalLicenseDto, + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + OauthDataStrategyInputDto, + ProvisioningDto, + ProvisioningSystemDto, +} from './dto'; +export { ProvisioningService } from './service/provisioning.service'; +export { + IservProvisioningStrategy, + OidcMockProvisioningStrategy, + ProvisioningStrategy, + SanisProvisioningStrategy, + SchulconnexProvisioningStrategy, + SchulconnexResponseMapper, +} from './strategy'; export { ProvisioningConfig } from './provisioning.config'; diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 6474efc61ce..6613e10e259 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -1,16 +1,16 @@ -import { AccountModule } from '@modules/account'; -import { GroupModule } from '@modules/group'; -import { LearnroomModule } from '@modules/learnroom'; -import { LegacySchoolModule } from '@modules/legacy-school'; -import { RoleModule } from '@modules/role'; -import { SchoolModule } from '@modules/school'; -import { SystemModule } from '@modules/system/system.module'; -import { ExternalToolModule } from '@modules/tool'; -import { SchoolExternalToolModule } from '@modules/tool/school-external-tool'; -import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; -import { LoggerModule } from '@src/core/logger'; -import { SchulconnexClientModule } from '@src/infra/schulconnex-client/schulconnex-client.module'; +import { GroupModule } from '../group'; +import { LearnroomModule } from '../learnroom'; +import { LegacySchoolModule } from '../legacy-school'; +import { RoleModule } from '../role'; +import { SchoolModule } from '../school'; +import { SystemModule } from '../system/system.module'; +import { ExternalToolModule } from '../tool'; +import { SchoolExternalToolModule } from '../tool/school-external-tool'; +import { UserModule } from '../user'; +import { LoggerModule } from '../../core/logger'; +import { SchulconnexClientModule } from '../../infra/schulconnex-client'; +import { AccountModule } from '../account'; import { ClassModule } from '../class'; import { UserLicenseModule } from '../user-license'; import { ProvisioningService } from './service/provisioning.service'; diff --git a/apps/server/src/modules/provisioning/strategy/base.strategy.ts b/apps/server/src/modules/provisioning/strategy/base.strategy.ts index 991f8dcfabd..b107034a73b 100644 --- a/apps/server/src/modules/provisioning/strategy/base.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/base.strategy.ts @@ -1,4 +1,4 @@ -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { SystemProvisioningStrategy } from '../../../shared/domain/interface/system-provisioning.strategy'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto } from '../dto'; export abstract class ProvisioningStrategy { diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts index 444261b6236..a2eae5dae60 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts @@ -1,12 +1,11 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { schoolFactory } from '@modules/school/testing'; import { Test, TestingModule } from '@nestjs/testing'; -import { RoleName } from '@shared/domain/interface'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { userDoFactory } from '@shared/testing'; -import { IdTokenExtractionFailureLoggableException } from '@src/modules/oauth/loggable'; import jwt from 'jsonwebtoken'; +import { IdTokenExtractionFailureLoggableException } from '../../../oauth/loggable'; +import { userDoFactory } from '../../../../shared/testing'; +import { RoleName, SystemProvisioningStrategy } from '../../../../shared/domain/interface'; +import { schoolFactory } from '../../../school/testing'; import { ExternalClassDto, ExternalSchoolDto, diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts index f9f8ca71c43..c6af6fc92bd 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { RoleName } from '@shared/domain/interface'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { IdTokenExtractionFailureLoggableException } from '@src/modules/oauth/loggable'; import { validate } from 'class-validator'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { RoleName } from '../../../../shared/domain/interface'; +import { SystemProvisioningStrategy } from '../../../../shared/domain/interface/system-provisioning.strategy'; +import { IdTokenExtractionFailureLoggableException } from '../../../oauth/loggable'; import { ExternalClassDto, ExternalSchoolDto, From d798d449041840b2007c038467558309c57c726d Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 13:41:26 +0100 Subject: [PATCH 06/55] EW-1057: modified an import causing tests to break --- .../modules/provisioning/service/provisioning.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 37790f6cadd..2c5d2385b7e 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -11,7 +11,7 @@ import { ProvisioningDto, ProvisioningSystemDto, } from '../dto'; -import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; +import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '..'; import { ProvisioningService } from './provisioning.service'; import { TspProvisioningStrategy } from '../strategy/tsp/tsp.strategy'; From f840ab4f0fa89226320f58b3ef4c92458510eaff Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Fri, 29 Nov 2024 16:17:03 +0100 Subject: [PATCH 07/55] Add and use Test Factories --- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 68 +++++++------- .../service/provisioning.service.spec.ts | 13 +-- .../service/tsp-provisioning.service.spec.ts | 89 ++++++++----------- .../factory/external-class-dto.factory.ts | 11 +++ .../factory/external-school-dto.factory.ts | 15 ++-- .../factory/external-user-dto.factory.ts | 13 +++ .../testing/factory/oauth-data-dto.factory.ts | 15 ++++ .../factory/provisioning-dto.factory.ts | 10 +++ .../provisioning-system-dto.factory.ts | 12 +++ 9 files changed, 149 insertions(+), 97 deletions(-) create mode 100644 apps/server/src/shared/testing/factory/external-class-dto.factory.ts create mode 100644 apps/server/src/shared/testing/factory/external-user-dto.factory.ts create mode 100644 apps/server/src/shared/testing/factory/oauth-data-dto.factory.ts create mode 100644 apps/server/src/shared/testing/factory/provisioning-dto.factory.ts create mode 100644 apps/server/src/shared/testing/factory/provisioning-system-dto.factory.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index fda468415cb..c0f83f20820 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -1,15 +1,14 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { - ExternalClassDto, - ExternalSchoolDto, - ExternalUserDto, - OauthDataDto, - ProvisioningSystemDto, -} from '@modules/provisioning'; +import { ExternalClassDto, ExternalSchoolDto, ProvisioningSystemDto } from '@modules/provisioning'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { externalSchoolDtoFactory } from '@shared/testing'; +import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; +import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; import { Logger } from '@src/core/logger'; import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; @@ -93,42 +92,49 @@ describe(TspOauthDataMapper.name, () => { }, ]; - const provisioningSystemDto = new ProvisioningSystemDto({ + const provisioningSystemDto: ProvisioningSystemDto = provisioningSystemDtoFactory.build({ systemId: system.id, provisioningStrategy: SystemProvisioningStrategy.TSP, }); - const externalSchool = new ExternalSchoolDto({ - externalId: school.externalId ?? '', + const externalSchoolDto: ExternalSchoolDto = externalSchoolDtoFactory.build({ + externalId: school.externalId, + name: undefined, }); - const externalClass = new ExternalClassDto({ - externalId: klasseId, + const externalClassDto: ExternalClassDto = externalClassDtoFactory.build({ + externalId: tspClasses[0].klasseId ?? '', name: tspClasses[0].klasseName, }); - const expected: OauthDataDto[] = [ - new OauthDataDto({ + const externalTeacherUserDto = externalUserDtoFactory.build({ + externalId: tspTeachers[0].lehrerUid ?? '', + firstName: tspTeachers[0].lehrerVorname, + lastName: tspTeachers[0].lehrerNachname, + roles: [RoleName.TEACHER], + email: undefined, + }); + + const externalStudentUserDto = externalUserDtoFactory.build({ + externalId: tspStudents[0].schuelerUid ?? '', + firstName: tspStudents[0].schuelerVorname, + lastName: tspStudents[0].schuelerNachname, + roles: [RoleName.STUDENT], + email: undefined, + }); + + const expected = [ + oauthDataDtoFactory.build({ system: provisioningSystemDto, - externalUser: new ExternalUserDto({ - externalId: tspTeachers[0].lehrerUid ?? '', - firstName: tspTeachers[0].lehrerVorname, - lastName: tspTeachers[0].lehrerNachname, - roles: [RoleName.TEACHER], - }), - externalSchool, - externalClasses: [externalClass], + externalUser: externalTeacherUserDto, + externalSchool: externalSchoolDto, + externalClasses: [externalClassDto], }), - new OauthDataDto({ + oauthDataDtoFactory.build({ system: provisioningSystemDto, - externalUser: new ExternalUserDto({ - externalId: tspStudents[0].schuelerUid ?? '', - firstName: tspStudents[0].schuelerVorname, - lastName: tspStudents[0].schuelerNachname, - roles: [RoleName.STUDENT], - }), - externalSchool, - externalClasses: [externalClass], + externalUser: externalStudentUserDto, + externalSchool: externalSchoolDto, + externalClasses: [externalClassDto], }), ]; diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 37790f6cadd..79894dd1514 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -1,9 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { System, SystemService } from '@modules/system'; -import { systemFactory } from '@modules/system/testing'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningDtoFactory } from '@shared/testing/factory/provisioning-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { systemFactory } from '@src/modules/system/testing'; import { ExternalUserDto, OauthDataDto, @@ -12,8 +15,8 @@ import { ProvisioningSystemDto, } from '../dto'; import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; -import { ProvisioningService } from './provisioning.service'; import { TspProvisioningStrategy } from '../strategy/tsp/tsp.strategy'; +import { ProvisioningService } from './provisioning.service'; describe('ProvisioningService', () => { let module: TestingModule; @@ -83,18 +86,18 @@ describe('ProvisioningService', () => { provisioningUrl: 'https://api.moin.schule/', provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const provisioningSystemDto: ProvisioningSystemDto = new ProvisioningSystemDto({ + const provisioningSystemDto: ProvisioningSystemDto = provisioningSystemDtoFactory.build({ systemId: system.id, provisioningUrl: 'https://api.moin.schule/', provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const oauthDataDto: OauthDataDto = new OauthDataDto({ + const oauthDataDto: OauthDataDto = oauthDataDtoFactory.build({ system: provisioningSystemDto, externalUser: new ExternalUserDto({ externalId: 'externalUserId', }), }); - const provisioningDto: ProvisioningDto = new ProvisioningDto({ + const provisioningDto: ProvisioningDto = provisioningDtoFactory.build({ externalUserId: 'externalUserId', }); diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index da637580400..c2ed303cd63 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -3,8 +3,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; +import { externalSchoolDtoFactory, roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; +import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; +import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; import { AccountService } from '@src/modules/account'; import { ClassService } from '@src/modules/class'; import { classFactory } from '@src/modules/class/domain/testing'; @@ -12,7 +15,6 @@ import { RoleService } from '@src/modules/role'; import { SchoolService } from '@src/modules/school'; import { schoolFactory } from '@src/modules/school/testing'; import { UserService } from '@src/modules/user'; -import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '../dto'; import { BadDataLoggableException } from '../loggable'; import { TspProvisioningService } from './tsp-provisioning.service'; @@ -25,27 +27,6 @@ describe('TspProvisioningService', () => { let userServiceMock: DeepMocked; let accountServiceMock: DeepMocked; - const setupExternalSystem = (props?: Partial) => { - const baseProps = { systemId: faker.string.uuid(), provisioningStrategy: SystemProvisioningStrategy.TSP }; - - return new ProvisioningSystemDto({ ...baseProps, ...props }); - }; - const setupExternalSchool = (props?: Partial) => { - const baseProps = { externalId: faker.string.uuid(), name: faker.string.sample() }; - - return new ExternalSchoolDto({ ...baseProps, ...props }); - }; - const setupExternalClass = (props?: Partial) => { - const baseProps = { externalId: faker.string.uuid(), name: faker.string.sample() }; - - return new ExternalClassDto({ ...baseProps, ...props }); - }; - const setupExternalUser = (props?: Partial) => { - const baseProps = { externalId: faker.string.uuid(), username: faker.internet.userName() }; - - return new ExternalUserDto({ ...baseProps, ...props }); - }; - beforeAll(async () => { module = await Test.createTestingModule({ providers: [ @@ -96,8 +77,8 @@ describe('TspProvisioningService', () => { describe('findSchoolOrFail', () => { describe('when school is found', () => { const setup = () => { - const system = setupExternalSystem(); - const externalSchool = setupExternalSchool(); + const system = provisioningSystemDtoFactory.build(); + const externalSchool = externalSchoolDtoFactory.build(); const school = schoolFactory.build(); schoolServiceMock.getSchools.mockResolvedValueOnce([school]); @@ -116,8 +97,8 @@ describe('TspProvisioningService', () => { describe('when school is not found', () => { const setup = () => { - const system = setupExternalSystem(); - const externalSchool = setupExternalSchool(); + const system = provisioningSystemDtoFactory.build(); + const externalSchool = externalSchoolDtoFactory.build(); schoolServiceMock.getSchools.mockResolvedValueOnce([]); @@ -136,7 +117,7 @@ describe('TspProvisioningService', () => { describe('when user ID is missing', () => { const setup = () => { const school = schoolFactory.build(); - const classes = [setupExternalClass()]; + const classes = [externalClassDtoFactory.build()]; const user = userDoFactory.build(); return { school, classes, user }; @@ -152,7 +133,7 @@ describe('TspProvisioningService', () => { describe('when class exists', () => { const setup = () => { const school = schoolFactory.build(); - const classes = [setupExternalClass()]; + const classes = [externalClassDtoFactory.build()]; const clazz = classFactory.build(); const user = userDoFactory.buildWithId({ roles: [roleFactory.build({ name: RoleName.TEACHER }), roleFactory.build({ name: RoleName.STUDENT })], @@ -175,7 +156,7 @@ describe('TspProvisioningService', () => { describe('when class does not exist', () => { const setup = () => { const school = schoolFactory.build(); - const classes = [setupExternalClass()]; + const classes = [externalClassDtoFactory.build()]; const user = userDoFactory.buildWithId({ roles: [roleFactory.build({ name: RoleName.TEACHER }), roleFactory.build({ name: RoleName.STUDENT })], }); @@ -198,9 +179,9 @@ describe('TspProvisioningService', () => { describe('provisionUser', () => { describe('when external school is missing', () => { const setup = () => { - const data = new OauthDataDto({ - system: setupExternalSystem(), - externalUser: setupExternalUser(), + const data = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build(), + externalSchool: undefined, }); const school = schoolFactory.build(); @@ -217,10 +198,10 @@ describe('TspProvisioningService', () => { describe('when user exists and school is the same', () => { const setup = () => { const school = schoolFactory.build(); - const data = new OauthDataDto({ - system: setupExternalSystem(), - externalUser: setupExternalUser(), - externalSchool: setupExternalSchool({ + const data = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build(), + externalUser: externalUserDtoFactory.build(), + externalSchool: externalSchoolDtoFactory.build({ externalId: school.externalId, }), }); @@ -246,10 +227,10 @@ describe('TspProvisioningService', () => { describe('when user exists and school is different', () => { const setup = () => { const school = schoolFactory.build(); - const data = new OauthDataDto({ - system: setupExternalSystem(), - externalUser: setupExternalUser(), - externalSchool: setupExternalSchool(), + const data = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build(), + externalUser: externalUserDtoFactory.build(), + externalSchool: externalSchoolDtoFactory.build(), }); const user = userDoFactory.build({ id: faker.string.uuid() }); const roles = [ @@ -278,14 +259,14 @@ describe('TspProvisioningService', () => { describe('when user does not exist and has no firstname, lastname and email', () => { const setup = (withFirstname: boolean, withLastname: boolean, withEmail: boolean) => { const school = schoolFactory.build(); - const data = new OauthDataDto({ - system: setupExternalSystem(), - externalUser: setupExternalUser({ + const data = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build(), + externalUser: externalUserDtoFactory.build({ firstName: withFirstname ? faker.person.firstName() : undefined, lastName: withLastname ? faker.person.lastName() : undefined, email: withEmail ? faker.internet.email() : undefined, }), - externalSchool: setupExternalSchool(), + externalSchool: externalSchoolDtoFactory.build(), }); userServiceMock.findByExternalId.mockResolvedValue(null); @@ -311,14 +292,14 @@ describe('TspProvisioningService', () => { describe('when user does not exist', () => { const setup = () => { const school = schoolFactory.build(); - const data = new OauthDataDto({ - system: setupExternalSystem(), - externalUser: setupExternalUser({ + const data = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build(), + externalUser: externalUserDtoFactory.build({ firstName: faker.person.firstName(), lastName: faker.person.lastName(), email: faker.internet.email(), }), - externalSchool: setupExternalSchool(), + externalSchool: externalSchoolDtoFactory.build(), }); const user = userDoFactory.build({ id: faker.string.uuid(), roles: [] }); @@ -343,14 +324,14 @@ describe('TspProvisioningService', () => { describe('when user id is not set after create', () => { const setup = () => { const school = schoolFactory.build(); - const data = new OauthDataDto({ - system: setupExternalSystem(), - externalUser: setupExternalUser({ + const data = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build(), + externalUser: externalUserDtoFactory.build({ firstName: faker.person.firstName(), lastName: faker.person.lastName(), email: faker.internet.email(), }), - externalSchool: setupExternalSchool(), + externalSchool: externalSchoolDtoFactory.build(), }); const user = userDoFactory.build({ id: undefined, roles: [] }); diff --git a/apps/server/src/shared/testing/factory/external-class-dto.factory.ts b/apps/server/src/shared/testing/factory/external-class-dto.factory.ts new file mode 100644 index 00000000000..7bc8d9f3f55 --- /dev/null +++ b/apps/server/src/shared/testing/factory/external-class-dto.factory.ts @@ -0,0 +1,11 @@ +import { ExternalClassDto } from '@modules/provisioning'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Factory } from 'fishery'; + +export const externalClassDtoFactory = Factory.define( + ({ sequence }) => + new ExternalClassDto({ + externalId: new ObjectId().toHexString(), + name: `external Class ${sequence}`, + }) +); diff --git a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts b/apps/server/src/shared/testing/factory/external-school-dto.factory.ts index e56e64d9243..f559e5613f4 100644 --- a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/external-school-dto.factory.ts @@ -1,10 +1,11 @@ -import { ExternalSchoolDto } from '@modules/provisioning/dto'; +import { ExternalSchoolDto } from '@modules/provisioning'; import { ObjectId } from '@mikro-orm/mongodb'; import { Factory } from 'fishery'; -export const externalSchoolDtoFactory = Factory.define(({ sequence }) => { - return { - externalId: new ObjectId().toHexString(), - name: `External School ${sequence}`, - }; -}); +export const externalSchoolDtoFactory = Factory.define( + ({ sequence }) => + new ExternalSchoolDto({ + externalId: new ObjectId().toHexString(), + name: `External School ${sequence}`, + }) +); diff --git a/apps/server/src/shared/testing/factory/external-user-dto.factory.ts b/apps/server/src/shared/testing/factory/external-user-dto.factory.ts new file mode 100644 index 00000000000..19ea7fb2119 --- /dev/null +++ b/apps/server/src/shared/testing/factory/external-user-dto.factory.ts @@ -0,0 +1,13 @@ +import { ExternalUserDto } from '@src/modules/provisioning'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Factory } from 'fishery'; + +export const externalUserDtoFactory = Factory.define( + ({ sequence }) => + new ExternalUserDto({ + externalId: new ObjectId().toHexString(), + firstName: `Firstname ${sequence}`, + lastName: `Lastname ${sequence}`, + email: `Email ${sequence}`, + }) +); diff --git a/apps/server/src/shared/testing/factory/oauth-data-dto.factory.ts b/apps/server/src/shared/testing/factory/oauth-data-dto.factory.ts new file mode 100644 index 00000000000..b8a11d639f7 --- /dev/null +++ b/apps/server/src/shared/testing/factory/oauth-data-dto.factory.ts @@ -0,0 +1,15 @@ +import { OauthDataDto } from '@modules/provisioning'; +import { Factory } from 'fishery'; +import { externalUserDtoFactory } from './external-user-dto.factory'; +import { provisioningSystemDtoFactory } from './provisioning-system-dto.factory'; + +export const oauthDataDtoFactory = Factory.define(() => { + const system = provisioningSystemDtoFactory.build(); + const externalUser = externalUserDtoFactory.build(); + const oauthDataDto = new OauthDataDto({ + system, + externalUser, + }); + + return oauthDataDto; +}); diff --git a/apps/server/src/shared/testing/factory/provisioning-dto.factory.ts b/apps/server/src/shared/testing/factory/provisioning-dto.factory.ts new file mode 100644 index 00000000000..5262232bfa9 --- /dev/null +++ b/apps/server/src/shared/testing/factory/provisioning-dto.factory.ts @@ -0,0 +1,10 @@ +import { ProvisioningDto } from '@modules/provisioning'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Factory } from 'fishery'; + +export const provisioningDtoFactory = Factory.define( + () => + new ProvisioningDto({ + externalUserId: new ObjectId().toHexString(), + }) +); diff --git a/apps/server/src/shared/testing/factory/provisioning-system-dto.factory.ts b/apps/server/src/shared/testing/factory/provisioning-system-dto.factory.ts new file mode 100644 index 00000000000..131c5c72364 --- /dev/null +++ b/apps/server/src/shared/testing/factory/provisioning-system-dto.factory.ts @@ -0,0 +1,12 @@ +import { ProvisioningSystemDto } from '@modules/provisioning'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Factory } from 'fishery'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; + +export const provisioningSystemDtoFactory = Factory.define( + () => + new ProvisioningSystemDto({ + systemId: new ObjectId().toHexString(), + provisioningStrategy: SystemProvisioningStrategy.TSP, + }) +); From 4e0f4e297360fa41b6522d55fe40b5f5a66be7f3 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Mon, 2 Dec 2024 15:02:43 +0100 Subject: [PATCH 08/55] revert ExternalSchoolDto changes --- .../infra/sync/tsp/tsp-oauth-data.mapper.spec.ts | 14 +++----------- .../factory/external-school-dto.factory.ts | 15 +++++++-------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index c0f83f20820..8dca6da47d1 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -1,10 +1,9 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ExternalClassDto, ExternalSchoolDto, ProvisioningSystemDto } from '@modules/provisioning'; +import { ExternalClassDto, ProvisioningSystemDto } from '@modules/provisioning'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { externalSchoolDtoFactory } from '@shared/testing'; import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; @@ -97,11 +96,6 @@ describe(TspOauthDataMapper.name, () => { provisioningStrategy: SystemProvisioningStrategy.TSP, }); - const externalSchoolDto: ExternalSchoolDto = externalSchoolDtoFactory.build({ - externalId: school.externalId, - name: undefined, - }); - const externalClassDto: ExternalClassDto = externalClassDtoFactory.build({ externalId: tspClasses[0].klasseId ?? '', name: tspClasses[0].klasseName, @@ -127,13 +121,11 @@ describe(TspOauthDataMapper.name, () => { oauthDataDtoFactory.build({ system: provisioningSystemDto, externalUser: externalTeacherUserDto, - externalSchool: externalSchoolDto, externalClasses: [externalClassDto], }), oauthDataDtoFactory.build({ system: provisioningSystemDto, externalUser: externalStudentUserDto, - externalSchool: externalSchoolDto, externalClasses: [externalClassDto], }), ]; @@ -142,9 +134,9 @@ describe(TspOauthDataMapper.name, () => { }; it('should return an array of oauth data dtos', () => { - const { system, school, tspTeachers, tspStudents, tspClasses, expected } = setup(); + const { system, tspTeachers, tspStudents, tspClasses, expected } = setup(); - const result = sut.mapTspDataToOauthData(system, [school], tspTeachers, tspStudents, tspClasses); + const result = sut.mapTspDataToOauthData(system, [], tspTeachers, tspStudents, tspClasses); expect(result).toStrictEqual(expected); }); diff --git a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts b/apps/server/src/shared/testing/factory/external-school-dto.factory.ts index f559e5613f4..e56e64d9243 100644 --- a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/external-school-dto.factory.ts @@ -1,11 +1,10 @@ -import { ExternalSchoolDto } from '@modules/provisioning'; +import { ExternalSchoolDto } from '@modules/provisioning/dto'; import { ObjectId } from '@mikro-orm/mongodb'; import { Factory } from 'fishery'; -export const externalSchoolDtoFactory = Factory.define( - ({ sequence }) => - new ExternalSchoolDto({ - externalId: new ObjectId().toHexString(), - name: `External School ${sequence}`, - }) -); +export const externalSchoolDtoFactory = Factory.define(({ sequence }) => { + return { + externalId: new ObjectId().toHexString(), + name: `External School ${sequence}`, + }; +}); From ba8417f39817d0d9ce1072dda23e1771c190686d Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Mon, 2 Dec 2024 17:54:42 +0100 Subject: [PATCH 09/55] EW-1057: reverted the changes excluding @src --- apps/server/src/infra/sync/sync.module.ts | 18 ++++++------- ...tsp-system-not-found.loggable-exception.ts | 2 +- .../src/infra/sync/tsp/tsp-fetch.service.ts | 4 +-- ...gacy-migration.service.integration.spec.ts | 12 ++++----- .../sync/tsp/tsp-legacy-migration.service.ts | 2 +- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 16 ++++++------ .../infra/sync/tsp/tsp-oauth-data.mapper.ts | 14 +++++----- .../infra/sync/tsp/tsp-sync.service.spec.ts | 24 ++++++++--------- .../src/infra/sync/tsp/tsp-sync.service.ts | 26 +++++++++---------- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 25 +++++++----------- .../tsp-client/tsp-client-factory.spec.ts | 4 +-- .../learnroom/service/board-copy.service.ts | 2 +- .../provisioning/provisioning.module.ts | 20 +++++++------- .../provisioning/strategy/base.strategy.ts | 2 +- .../strategy/tsp/tsp.strategy.spec.ts | 8 +++--- .../provisioning/strategy/tsp/tsp.strategy.ts | 6 ++--- .../tool/external-tool/entity/index.ts | 17 +++++++++--- 17 files changed, 104 insertions(+), 98 deletions(-) diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 63d0472ec07..ea4bddffbb8 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -1,15 +1,15 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Module } from '@nestjs/common'; -import { RabbitMQWrapperModule } from '../rabbitmq'; -import { ConsoleWriterModule } from '../console'; -import { TspClientModule } from '../tsp-client'; -import { AccountModule } from '../../modules/account'; -import { LegacySchoolModule } from '../../modules/legacy-school'; -import { SchoolModule } from '../../modules/school'; -import { SystemModule } from '../../modules/system'; -import { UserModule } from '../../modules/user'; +import { AccountModule } from '@modules/account'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SchoolModule } from '@modules/school'; +import { SystemModule } from '@modules/system'; +import { UserModule } from '@modules/user'; +import { ProvisioningModule } from '@modules/provisioning'; +import { TspClientModule } from '@infra/tsp-client'; +import { ConsoleWriterModule } from '@infra/console'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; import { LoggerModule } from '../../core/logger'; -import { ProvisioningModule } from '../../modules/provisioning'; import { SyncConsole } from './console/sync.console'; import { SyncService } from './service/sync.service'; import { TspLegacyMigrationService } from './tsp/tsp-legacy-migration.service'; diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts index bbf6e9d3471..6394a258cb6 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts @@ -1,5 +1,5 @@ import { HttpStatus } from '@nestjs/common'; -import { BusinessError, ErrorLogMessage } from '../../../../shared/common/error'; +import { BusinessError, ErrorLogMessage } from '@shared/common'; import { Loggable, LogMessage } from '../../../../core/logger'; export class TspSystemNotFoundLoggableException extends BusinessError implements Loggable { diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts index 94a86d389ff..93f45386b96 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { AxiosError, AxiosResponse } from 'axios'; import moment from 'moment'; -import { System } from '../../../modules/system'; -import { OauthConfigMissingLoggableException } from '../../../modules/oauth/loggable'; +import { System } from '@modules/system'; +import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; import { ExportApiInterface, TspClientFactory } from '../../tsp-client'; import { Logger } from '../../../core/logger'; import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts index 4f91858498d..01d09432ec1 100644 --- a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { SystemType } from '../../../modules/system'; -import { MongoMemoryDatabaseModule } from '../../database'; +import { SystemType } from '@modules/system'; +import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '@shared/testing'; +import { SchoolFeature } from '@shared/domain/types'; +import { SystemProvisioningStrategy } from '@shared/domain/interface'; +import { SchoolEntity } from '@shared/domain/entity'; import { Logger } from '../../../core/logger'; -import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '../../../shared/testing'; -import { SchoolFeature } from '../../../shared/domain/types'; -import { SystemProvisioningStrategy } from '../../../shared/domain/interface'; -import { SchoolEntity } from '../../../shared/domain/entity'; +import { MongoMemoryDatabaseModule } from '../../database'; import { TspLegacyMigrationSystemMissingLoggable } from './loggable/tsp-legacy-migration-system-missing.loggable'; import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts index cfe099a65fc..f01ed36cb84 100644 --- a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; +import { EntityId, SchoolFeature } from '@shared/domain/types'; import { Logger } from '../../../core/logger'; -import { EntityId, SchoolFeature } from '../../../shared/domain/types'; import { TspLegacyMigrationStartLoggable } from './loggable/tsp-legacy-migration-start.loggable'; import { TspLegacyMigrationSystemMissingLoggable } from './loggable/tsp-legacy-migration-system-missing.loggable'; import { TspLegacySchoolMigrationCountLoggable } from './loggable/tsp-legacy-school-migration-count.loggable'; diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index 2f64794d8bf..6e43463af91 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -1,20 +1,20 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { schoolFactory } from '../../../modules/school/testing'; -import { systemFactory } from '../../../modules/system/testing'; -import { BadDataLoggableException } from '../../../modules/provisioning/loggable'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; -import { Logger } from '../../../core/logger'; -import { SystemProvisioningStrategy, RoleName } from '../../../shared/domain/interface'; - +import { schoolFactory } from '@modules/school/testing'; +import { systemFactory } from '@modules/system/testing'; +import { BadDataLoggableException } from '@modules/provisioning/loggable'; +import { SystemProvisioningStrategy, RoleName } from '@shared/domain/interface'; import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto, -} from '../../../modules/provisioning'; +} from '@modules/provisioning'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; +import { Logger } from '../../../core/logger'; + import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index 7d27752cce6..91984b046d4 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -1,18 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { BadDataLoggableException } from '../../../modules/provisioning/loggable'; -import { System } from '../../../modules/system'; -import { RoleName, SystemProvisioningStrategy } from '../../../shared/domain/interface'; +import { BadDataLoggableException } from '@modules/provisioning/loggable'; +import { System } from '@modules/system'; +import { RoleName, SystemProvisioningStrategy } from '@shared/domain/interface'; -import { Logger } from '../../../core/logger'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; -import { School } from '../../../modules/school'; +import { School } from '@modules/school'; import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto, -} from '../../../modules/provisioning'; +} from '@modules/provisioning'; +import { Logger } from '../../../core/logger'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; @Injectable() diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts index 91b86cd5142..b5c2bd87b20 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts @@ -1,18 +1,18 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { AccountService } from '../../../modules/account'; -import { School, SchoolService } from '../../../modules/school'; -import { SystemService, SystemType } from '../../../modules/system'; -import { UserService } from '../../../modules/user'; -import { SystemProvisioningStrategy } from '../../../shared/domain/interface/system-provisioning.strategy'; -import { federalStateFactory, schoolYearFactory, userDoFactory } from '../../../shared/testing'; -import { accountDoFactory } from '../../../modules/account/testing'; -import { FederalStateService, SchoolYearService } from '../../../modules/legacy-school'; -import { FileStorageType, SchoolProps } from '../../../modules/school/domain'; -import { FederalStateEntityMapper, SchoolYearEntityMapper } from '../../../modules/school/repo/mikro-orm/mapper'; -import { schoolFactory } from '../../../modules/school/testing'; -import { systemFactory } from '../../../modules/system/testing'; +import { AccountService } from '@modules/account'; +import { School, SchoolService } from '@modules/school'; +import { SystemService, SystemType } from '@modules/system'; +import { UserService } from '@modules/user'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { federalStateFactory, schoolYearFactory, userDoFactory } from '@shared/testing'; +import { accountDoFactory } from '@modules/account/testing'; +import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; +import { FileStorageType, SchoolProps } from '@modules/school/domain'; +import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@modules/school/repo/mikro-orm/mapper'; +import { schoolFactory } from '@modules/school/testing'; +import { systemFactory } from '@modules/system/testing'; import { TspSyncService } from './tsp-sync.service'; describe(TspSyncService.name, () => { diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts index 362fe1607ac..9cd679c57a8 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts @@ -1,18 +1,18 @@ import { Injectable } from '@nestjs/common'; import { ObjectId } from 'bson'; -import { FederalStateService, SchoolYearService } from '../../../modules/legacy-school'; -import { School, SchoolService } from '../../../modules/school'; -import { System, SystemService, SystemType } from '../../../modules/system'; -import { UserSourceOptions, UserDO } from '../../../shared/domain/domainobject'; -import { SystemProvisioningStrategy } from '../../../shared/domain/interface/system-provisioning.strategy'; -import { EntityId, SchoolFeature } from '../../../shared/domain/types'; -import { Account, AccountService } from '../../../modules/account'; -import { FederalStateNames } from '../../../modules/legacy-school/types'; -import { FederalState, FileStorageType } from '../../../modules/school/domain'; -import { SchoolFactory } from '../../../modules/school/domain/factory'; -import { SchoolPermissions } from '../../../modules/school/domain/type'; -import { FederalStateEntityMapper, SchoolYearEntityMapper } from '../../../modules/school/repo/mikro-orm/mapper'; -import { UserService } from '../../../modules/user'; +import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; +import { School, SchoolService } from '@modules/school'; +import { System, SystemService, SystemType } from '@modules/system'; +import { UserSourceOptions, UserDO } from '@shared/domain/domainobject'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { EntityId, SchoolFeature } from '@shared/domain/types'; +import { Account, AccountService } from '@modules/account'; +import { FederalStateNames } from '@modules/legacy-school/types'; +import { FederalState, FileStorageType } from '@modules/school/domain'; +import { SchoolFactory } from '@modules/school/domain/factory'; +import { SchoolPermissions } from '@modules/school/domain/type'; +import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@modules/school/repo/mikro-orm/mapper'; +import { UserService } from '@modules/user'; import { TspSystemNotFoundLoggableException } from './loggable'; @Injectable() diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index fdbd6ae0655..de780853688 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -2,22 +2,17 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { Account } from '../../../modules/account'; -import { accountDoFactory } from '../../../modules/account/testing'; -import { - ExternalUserDto, - OauthDataDto, - ProvisioningService, - ProvisioningSystemDto, -} from '../../../modules/provisioning'; -import { School } from '../../../modules/school'; -import { schoolFactory } from '../../../modules/school/testing'; -import { System } from '../../../modules/system'; -import { systemFactory } from '../../../modules/system/testing'; +import { Account } from '@modules/account'; +import { accountDoFactory } from '@modules/account/testing'; +import { ExternalUserDto, OauthDataDto, ProvisioningService, ProvisioningSystemDto } from '@modules/provisioning'; +import { School } from '@modules/school'; +import { schoolFactory } from '@modules/school/testing'; +import { System } from '@modules/system'; +import { systemFactory } from '@modules/system/testing'; +import { userDoFactory } from '@shared/testing'; +import { SystemProvisioningStrategy } from '@shared/domain/interface'; +import { UserDO } from '@shared/domain/domainobject'; import { Logger } from '../../../core/logger'; -import { userDoFactory } from '../../../shared/testing'; -import { SystemProvisioningStrategy } from '../../../shared/domain/interface'; -import { UserDO } from '../../../shared/domain/domainobject'; import { RobjExportKlasse, RobjExportLehrer, diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index 663a0b4303b..bd7af8d05e8 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -3,10 +3,10 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import axios, { AxiosError } from 'axios'; +import { ServerConfig } from '@modules/server'; +import { OauthAdapterService } from '@modules/oauth'; import { AxiosErrorLoggable, ErrorLoggable } from '../../core/error/loggable'; import { Logger } from '../../core/logger'; -import { ServerConfig } from '../../modules/server'; -import { OauthAdapterService } from '../../modules/oauth'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.ts b/apps/server/src/modules/learnroom/service/board-copy.service.ts index 8b09df5f1a7..67ace1924ee 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.ts @@ -22,7 +22,7 @@ import { Task, TaskBoardElement, User, -} from '../../../shared/domain/entity'; +} from '@shared/domain/entity'; import { ColumnBoardNodeRepo } from '../repo'; type BoardCopyParams = { diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 6613e10e259..3dce3da8a1b 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -1,16 +1,16 @@ import { Module } from '@nestjs/common'; -import { GroupModule } from '../group'; -import { LearnroomModule } from '../learnroom'; -import { LegacySchoolModule } from '../legacy-school'; -import { RoleModule } from '../role'; -import { SchoolModule } from '../school'; -import { SystemModule } from '../system/system.module'; -import { ExternalToolModule } from '../tool'; -import { SchoolExternalToolModule } from '../tool/school-external-tool'; -import { UserModule } from '../user'; +import { GroupModule } from '@modules/group'; +import { LearnroomModule } from '@modules/learnroom'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { RoleModule } from '@modules/role'; +import { SchoolModule } from '@modules/school'; +import { SystemModule } from '@modules/system/system.module'; +import { ExternalToolModule } from '@modules/tool'; +import { SchoolExternalToolModule } from '@modules/tool/school-external-tool'; +import { UserModule } from '@modules/user'; +import { AccountModule } from '@modules/account'; import { LoggerModule } from '../../core/logger'; import { SchulconnexClientModule } from '../../infra/schulconnex-client'; -import { AccountModule } from '../account'; import { ClassModule } from '../class'; import { UserLicenseModule } from '../user-license'; import { ProvisioningService } from './service/provisioning.service'; diff --git a/apps/server/src/modules/provisioning/strategy/base.strategy.ts b/apps/server/src/modules/provisioning/strategy/base.strategy.ts index b107034a73b..991f8dcfabd 100644 --- a/apps/server/src/modules/provisioning/strategy/base.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/base.strategy.ts @@ -1,4 +1,4 @@ -import { SystemProvisioningStrategy } from '../../../shared/domain/interface/system-provisioning.strategy'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto } from '../dto'; export abstract class ProvisioningStrategy { diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts index a2eae5dae60..82b2c409aef 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts @@ -2,10 +2,10 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import jwt from 'jsonwebtoken'; -import { IdTokenExtractionFailureLoggableException } from '../../../oauth/loggable'; -import { userDoFactory } from '../../../../shared/testing'; -import { RoleName, SystemProvisioningStrategy } from '../../../../shared/domain/interface'; -import { schoolFactory } from '../../../school/testing'; +import { IdTokenExtractionFailureLoggableException } from '@modules/oauth/loggable'; +import { userDoFactory } from '@shared/testing'; +import { RoleName, SystemProvisioningStrategy } from '@shared/domain/interface'; +import { schoolFactory } from '@modules/school/testing'; import { ExternalClassDto, ExternalSchoolDto, diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts index c6af6fc92bd..d888d76ba82 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { validate } from 'class-validator'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { RoleName } from '../../../../shared/domain/interface'; -import { SystemProvisioningStrategy } from '../../../../shared/domain/interface/system-provisioning.strategy'; -import { IdTokenExtractionFailureLoggableException } from '../../../oauth/loggable'; +import { RoleName } from '@shared/domain/interface'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { IdTokenExtractionFailureLoggableException } from '@modules/oauth/loggable'; import { ExternalClassDto, ExternalSchoolDto, diff --git a/apps/server/src/modules/tool/external-tool/entity/index.ts b/apps/server/src/modules/tool/external-tool/entity/index.ts index 07ade6ea541..772848e2f9d 100644 --- a/apps/server/src/modules/tool/external-tool/entity/index.ts +++ b/apps/server/src/modules/tool/external-tool/entity/index.ts @@ -1,5 +1,16 @@ -export * from './external-tool.entity'; -export * from './config'; -export * from './custom-parameter'; +export { ExternalToolEntity, ExternalToolEntityProps } from './external-tool.entity'; +export { + BasicToolConfigEntity, + ExternalToolConfigEntity, + Lti11ToolConfigEntity, + Oauth2ToolConfigEntity, +} from './config'; +export { + CustomParameterEntity, + CustomParameterLocation, + CustomParameterScope, + CustomParameterType, + autoParameters, +} from './custom-parameter'; export { ExternalToolMediumEntity } from './external-tool-medium.entity'; export { FileRecordRefEmbeddable } from './file-record-ref.embeddable'; From 64b5ffb5f3d8930085fc430993e9f33a03443437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:26:58 +0100 Subject: [PATCH 10/55] N21-2264 Remove unused tools from courses (#5361) --- .../mikro-orm/Migration20241127134513.ts | 29 +++++++++++++++++++ backup/setup/migrations.json | 9 ++++++ 2 files changed, 38 insertions(+) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20241127134513.ts diff --git a/apps/server/src/migrations/mikro-orm/Migration20241127134513.ts b/apps/server/src/migrations/mikro-orm/Migration20241127134513.ts new file mode 100644 index 00000000000..18369aadf4a --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241127134513.ts @@ -0,0 +1,29 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ContextExternalToolType } from '@modules/tool/context-external-tool/entity/context-external-tool-type.enum'; +import type { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity/context-external-tool.entity'; + +export class Migration20241127134513 extends Migration { + async up(): Promise { + let deleteCount = 0; + const cursor = this.getCollection('context-external-tools').find({ + contextType: ContextExternalToolType.COURSE, + }); + + for await (const doc of cursor) { + const course = await this.getCollection('courses').findOne({ _id: new ObjectId(doc.contextId) }); + + if (course === null) { + await this.getCollection('context-external-tools').deleteOne({ _id: doc._id }); + deleteCount += 1; + } + } + + console.info(`Deleted ${deleteCount} context-external-tools without a reference to an existing course context.`); + } + + // eslint-disable-next-line @typescript-eslint/require-await + async down(): Promise { + console.info('This migration cannot be rolled back.'); + } +} diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 762b54a8e40..f31be452adb 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -260,6 +260,15 @@ "$date": "2024-10-30T17:03:31.473Z" } }, + { + "_id": { + "$oid": "674847ac3c76d17b0c01c155" + }, + "name": "Migration20241127134513", + "created_at": { + "$date": "2024-11-28T10:37:51.515Z" + } + }, { "_id": { "$oid": "674444262ba8186272dc8abd" From b4e81d2f6f8e773194940c5aef8ea5cf098f1240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:44:20 +0100 Subject: [PATCH 11/55] N21-2309 Fix validation and add support for unknown schulconnex group roles (#5367) --- .../response/schulconnex-gruppenzugehoerigkeit-response.ts | 7 +++---- .../schulconnex-sonstige-gruppenzugehoerige-response.ts | 7 +++---- .../strategy/sanis/schulconnex-response-mapper.ts | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppenzugehoerigkeit-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppenzugehoerigkeit-response.ts index 653201ade6a..a641be93220 100644 --- a/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppenzugehoerigkeit-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppenzugehoerigkeit-response.ts @@ -1,9 +1,8 @@ -import { IsArray, IsEnum, IsOptional } from 'class-validator'; -import { SchulconnexGroupRole } from './schulconnex-group-role'; +import { IsArray, IsOptional, IsString } from 'class-validator'; export class SchulconnexGruppenzugehoerigkeitResponse { @IsOptional() @IsArray() - @IsEnum(SchulconnexGroupRole, { each: true }) - rollen?: SchulconnexGroupRole[]; + @IsString({ each: true }) + rollen?: string[]; } diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-sonstige-gruppenzugehoerige-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-sonstige-gruppenzugehoerige-response.ts index e9c297d8c30..6c90101f9dc 100644 --- a/apps/server/src/infra/schulconnex-client/response/schulconnex-sonstige-gruppenzugehoerige-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-sonstige-gruppenzugehoerige-response.ts @@ -1,5 +1,4 @@ -import { IsArray, IsEnum, IsOptional, IsString } from 'class-validator'; -import { SchulconnexGroupRole } from './schulconnex-group-role'; +import { IsArray, IsOptional, IsString } from 'class-validator'; export class SchulconnexSonstigeGruppenzugehoerigeResponse { @IsString() @@ -7,6 +6,6 @@ export class SchulconnexSonstigeGruppenzugehoerigeResponse { @IsOptional() @IsArray() - @IsEnum(SchulconnexGroupRole, { each: true }) - rollen?: SchulconnexGroupRole[]; + @IsString({ each: true }) + rollen?: string[]; } 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 5961ae19917..cd15c272342 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 @@ -34,7 +34,7 @@ const RoleMapping: Record = { [SchulconnexRole.ORGADMIN]: RoleName.ADMINISTRATOR, }; -const GroupRoleMapping: Partial> = { +const GroupRoleMapping: Partial> = { [SchulconnexGroupRole.TEACHER]: RoleName.TEACHER, [SchulconnexGroupRole.STUDENT]: RoleName.STUDENT, }; @@ -146,7 +146,7 @@ export class SchulconnexResponseMapper { otherUsers = group.sonstige_gruppenzugehoerige ? group.sonstige_gruppenzugehoerige .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) - .filter((otherUser: ExternalGroupUserDto | null) => otherUser !== null) + .filter((otherUser: ExternalGroupUserDto | null): otherUser is ExternalGroupUserDto => otherUser !== null) : []; } From 2cc1559f1e49f2f1fe48365952f3f8feecfeb450 Mon Sep 17 00:00:00 2001 From: Phillip Date: Mon, 2 Dec 2024 13:17:18 +0100 Subject: [PATCH 12/55] cleanup ancient and false information (#5357) --- .codacy.yml | 10 - .github/PULL_REQUEST_TEMPLATE.md | 38 +-- .github/autolabeler.yml | 1 - SECURITY.md | 9 - scripts/copy-legacy-tool-to-ctl.js | 451 ----------------------------- scripts/migrate-legacy-bbb.js | 109 ------- 6 files changed, 4 insertions(+), 614 deletions(-) delete mode 100644 .codacy.yml delete mode 100644 .github/autolabeler.yml delete mode 100644 SECURITY.md delete mode 100644 scripts/copy-legacy-tool-to-ctl.js delete mode 100644 scripts/migrate-legacy-bbb.js diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index ece54c71579..00000000000 --- a/.codacy.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -exclude_paths: - - ".idea" - - ".vscode" - - "**/backup/**" - - "**/migrations/**" - - "**/tests/**" - - "**.js" - - "**.spec.js" - - "**.spec.ts" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7b19d3a17e7..0f19bc7837e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,13 @@ # Description @@ -19,45 +18,16 @@ Base links to copy - https://ticketsystem.dbildungscloud.de/browse/BC-???? --> -## Changes -## Datasecurity - - -## Deployment - - -## New Repos, NPM pakages or vendor scripts - - ## Approval for review - [ ] DEV: If api was changed - `generate-client:server` was executed in vue frontend and changes were tested and put in a PR with the same branch name. - [ ] QA: In addition to review, the code has been manually tested (if manual testing is possible) - [ ] All points were discussed with the ticket creator, support-team or product owner. The code upholds all quality guidelines from the PR-template. -> Notice: Please remove the WIP label if the PR is ready to review, otherwise nobody will review it. diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml deleted file mode 100644 index ebc2bbbfc48..00000000000 --- a/.github/autolabeler.yml +++ /dev/null @@ -1 +0,0 @@ -wip: * \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 43cbf08dcab..00000000000 --- a/SECURITY.md +++ /dev/null @@ -1,9 +0,0 @@ -# Security Policy - -## Supported Versions - -Please always use the latest release. - -## Reporting a Vulnerability - -Plase check https://dbildungscloud.de/security on how to report security issues. Thanks for your support. diff --git a/scripts/copy-legacy-tool-to-ctl.js b/scripts/copy-legacy-tool-to-ctl.js deleted file mode 100644 index 4f2455bd955..00000000000 --- a/scripts/copy-legacy-tool-to-ctl.js +++ /dev/null @@ -1,451 +0,0 @@ -/* eslint-disable no-await-in-loop */ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; -const { program } = require('commander'); -const { v4: uuidv4 } = require('uuid'); - -program.requiredOption('-u, --url ', '(Required) URL of the MongoDB instance'); -program.parse(); - -const options = program.opts(); -const mongodbUrl = options.url; - -const close = async () => mongoose.connection.close(); - -const connect = async () => { - const mongooseOptions = { - useNewUrlParser: true, - useUnifiedTopology: true, - }; - - return mongoose.connect(mongodbUrl, mongooseOptions); -}; - -const customParameterEntrySchema = new Schema( - { - name: String, - value: String, - }, - { _id: false } -); - -const LtiTool = mongoose.model( - 'ltiTool0906202311481', - new Schema( - { - name: { type: String }, - url: { type: String, required: true }, - key: { type: String }, - secret: { type: String, required: true }, - logo_url: { type: String }, - lti_message_type: { type: String }, - lti_version: { type: String }, - resource_link_id: { type: String }, - roles: { - type: [ - { - type: String, - enum: ['Learner', 'Instructor', 'ContentDeveloper', 'Administrator', 'Mentor', 'TeachingAssistant'], - }, - ], - }, - privacy_permission: { - type: String, - enum: ['anonymous', 'e-mail', 'name', 'public', 'pseudonymous'], - default: 'anonymous', - }, - customs: { type: [{ key: { type: String }, value: { type: String } }] }, - isTemplate: { type: Boolean }, - isLocal: { type: Boolean }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - originTool: { type: Schema.Types.ObjectId, ref: 'ltiTool0906202311481' }, - oAuthClientId: { type: String }, - friendlyUrl: { type: String, unique: true, sparse: true }, - skipConsent: { type: Boolean }, - openNewTab: { type: Boolean, default: false }, - frontchannel_logout_uri: { type: String }, - isHidden: { type: Boolean, default: false }, - }, - { - timestamps: true, - } - ), - 'ltitools' -); - -const ExternalTool = mongoose.model( - 'external_tool0906202311482', - new Schema( - { - name: { type: String, unique: true }, - url: String, - logoUrl: String, - config_type: String, - config_baseUrl: String, - config_clientId: String, - config_skipConsent: Boolean, - config_key: String, - config_secret: String, - config_lti_message_type: { - type: String, - enum: ['basic-lti-launch-request'], - }, - config_privacy_permission: { - type: String, - enum: ['anonymous', 'e-mail', 'name', 'public', 'pseudonymous'], - }, - parameters: [ - { - type: { - name: String, - displayName: String, - description: String, - default: String, - regex: String, - regexComment: String, - scope: { - type: String, - enum: ['global', 'school', 'context'], - }, - location: { - type: String, - enum: ['path', 'body', 'query'], - }, - type: { - type: String, - enum: ['string', 'number', 'boolean', 'auto_contextid', 'auto_contextname', 'auto_schoolid'], - }, - isOptional: Boolean, - isProtected: Boolean, - }, - }, - ], - - isHidden: Boolean, - openNewTab: Boolean, - version: Number, - isDeactivated: Boolean, - restrictToContexts: [], - }, - { - timestamps: true, - } - ), - 'external-tools' -); - -const SchoolExternalTool = mongoose.model( - 'school_external_tool0906202311483', - new Schema( - { - tool: { type: Schema.Types.ObjectId, ref: 'external_tool0906202311482' }, - school: { type: Schema.Types.ObjectId }, - schoolParameters: [customParameterEntrySchema], - toolVersion: Number, - isDeactivated: Boolean, - }, - { - timestamps: true, - } - ), - 'school-external-tools' -); - -const ContextExternalTool = mongoose.model( - 'context_external_tool0906202311484', - new Schema( - { - schoolTool: { type: Schema.Types.ObjectId, ref: 'school_external_tool0906202311483' }, - contextId: String, - contextType: { type: String, enum: ['course'] }, - displayName: String, - parameters: [customParameterEntrySchema], - toolVersion: Number, - }, - { - timestamps: true, - } - ), - 'context-external-tools' -); - -const Course = mongoose.model( - 'course0906202311485', - new mongoose.Schema( - { - school: Schema.Types.ObjectId, - ltiToolIds: [{ type: Schema.Types.ObjectId, ref: 'ltiTool0906202311481' }], - }, - { - timestamps: true, - } - ), - 'courses' -); - -const Pseudonym = mongoose.model( - 'pseudonym0906202311486', - new Schema( - { - userId: { type: Schema.Types.ObjectId }, - toolId: { type: Schema.Types.ObjectId, ref: 'ltiTool0906202311481' }, - pseudonym: { - type: String, - required: true, - unique: true, - default: uuidv4, - }, - }, - { - timestamps: true, - } - ), - 'pseudonyms' -); - -const Pseudonym2 = mongoose.model( - 'external-tool-pseudonyms0906202311486', - new Schema( - { - userId: { type: Schema.Types.ObjectId }, - toolId: { type: Schema.Types.ObjectId, ref: 'external_tool0906202311482' }, - pseudonym: { - type: String, - required: true, - unique: true, - default: uuidv4, - }, - }, - { - timestamps: true, - } - ), - 'external-tool-pseudonyms' -); - -function toolConfigMapper(ltiToolTemplate) { - let toolConfig = { - config_baseUrl: ltiToolTemplate.url, - config_type: 'basic', - }; - - if (ltiToolTemplate.oAuthClientId) { - toolConfig = { - ...toolConfig, - config_type: 'oauth2', - config_clientId: ltiToolTemplate.oAuthClientId, - config_skipConsent: ltiToolTemplate.skipConsent, - }; - } else if (ltiToolTemplate.key && ltiToolTemplate.key !== 'none') { - toolConfig = { - ...toolConfig, - config_type: 'lti11', - config_key: ltiToolTemplate.key, - config_secret: ltiToolTemplate.secret, - config_lti_message_type: ltiToolTemplate.lti_message_type, - config_privacy_permission: ltiToolTemplate.privacy_permission || 'anonymous', - }; - } - - return toolConfig; -} - -function mapToExternalTool(ltiToolTemplate) { - return { - name: ltiToolTemplate.name, - url: ltiToolTemplate.url, - logoUrl: ltiToolTemplate.logo_url, - parameters: [], - isHidden: ltiToolTemplate.isHidden, - openNewTab: ltiToolTemplate.openNewTab, - version: 1, - restrictToContexts: [], - isDeactivated: false, - ...toolConfigMapper(ltiToolTemplate), - }; -} - -function mapToSchoolExternalTool(externalTool, course) { - return { - tool: externalTool._id, - school: course.schoolId, - schoolParameters: [], - toolVersion: externalTool.version, - isDeactivated: false, - }; -} - -function mapToContextExternalTool(schoolExternalTool, course, externalToolName) { - return { - schoolTool: schoolExternalTool._id, - contextId: course._id, - contextType: 'course', - parameters: [], - toolVersion: schoolExternalTool.toolVersion, - displayName: externalToolName, - }; -} - -function mapPseudonyms(pseudonym, externalTool) { - return { - pseudonym: pseudonym.pseudonym, - toolId: externalTool._id, - userId: pseudonym.userId, - }; -} - -async function createPseudonyms(toolTemplate, externalTool) { - const pseudonymsLegacyTools = await Pseudonym.find({ - toolId: toolTemplate._id, - }) - .lean() - .exec(); - - const newPseudonyms = []; - for (const legacyPseudonym of pseudonymsLegacyTools) { - const existingPseudonym = await Pseudonym2.findOne({ - pseudonym: legacyPseudonym.pseudonym, - }) - .lean() - .exec(); - - if (!existingPseudonym) { - newPseudonyms.push({ - userId: legacyPseudonym.userId, - toolId: externalTool._id, - pseudonym: legacyPseudonym.pseudonym, - }); - } - } - await Pseudonym2.insertMany(newPseudonyms); -} - -async function createExternalTool(toolTemplate) { - let externalTool = await ExternalTool.findOne({ - name: { $regex: `${toolTemplate.name}` }, - }) - .lean() - .exec(); - - if (!externalTool) { - externalTool = mapToExternalTool(toolTemplate); - externalTool = (await ExternalTool.insertMany(externalTool))[0]; - } - - createPseudonyms(toolTemplate, externalTool); - - return externalTool; -} - -async function createSchoolExternalTool(externalTool, course) { - let schoolExternalTool = await SchoolExternalTool.findOne({ - school: course.schoolId, - tool: externalTool._id, - }) - .lean() - .exec(); - - // CHECK IF SCHOOLEXTERNALTOOL EXISTS - if (!schoolExternalTool) { - schoolExternalTool = mapToSchoolExternalTool(externalTool, course); - schoolExternalTool = (await SchoolExternalTool.insertMany(schoolExternalTool))[0]; - } - - return schoolExternalTool; -} - -async function createContextExternalTool(schoolExternalTool, course, externalToolName) { - const contextExternalTools = await ContextExternalTool.find({ - schoolTool: schoolExternalTool._id, - contextId: course._id, - contextType: 'course', - }) - .lean() - .exec(); - - // CHECK IF CONTEXTEXTERNALTOOL EXISTS - if ((contextExternalTools || []).length === 0) { - const contextExternalTool = mapToContextExternalTool(schoolExternalTool, course, externalToolName); - await ContextExternalTool.insertMany(contextExternalTool); - } -} - -const up = async () => { - await connect(); - - // FIND ALL LTI TOOL TEMPLATES - const ltiToolTemplates = await LtiTool.find({ - $or: [{ name: { $regex: /Bettermarks/i } }, { name: { $regex: /Nextcloud/i } }], - isTemplate: true, - }) - .lean() - .exec(); - - if ((ltiToolTemplates || []).length === 0) { - return Promise.reject(new Error('No LtiTool Template found.')); - } - - const ltiToolExternalToolIdTupelList = []; - - // FIND EXTERNAL TOOLS - const externalTools = await Promise.all( - ltiToolTemplates.map(async (toolTemplate) => { - const externalTool = await createExternalTool(toolTemplate); - ltiToolExternalToolIdTupelList.push({ - templateId: toolTemplate._id, - externalToolId: externalTool._id, - }); - - return externalTool; - }) - ); - - // FIND ALL LEGACY TOOLS - const ltiTools = await LtiTool.find({ - $or: [{ name: { $regex: /Bettermarks/i } }, { name: { $regex: /Nextcloud/i } }], - isTemplate: false, - }) - .lean() - .exec(); - - for (const ltiTool of ltiTools) { - // GET COURSE - const course = await Course.findOne({ - ltiToolIds: { $in: [ltiTool._id] }, - }) - .lean() - .exec(); - - if (!course) { - console.info(`No course found with LtiToolId: ${ltiTool._id}.`); - // eslint-disable-next-line no-continue - continue; - } - - // GET EXTERNALTOOL - const ltiToolExternalToolIdTupel = ltiToolExternalToolIdTupelList.find( - (idTupel) => idTupel.templateId.toString() === ltiTool.originTool.toString() - ); - const externalTool = externalTools.find((tool) => tool._id === ltiToolExternalToolIdTupel.externalToolId); - - // GET SCHOOLEXTERNALTOOL - const schoolExternalTool = await createSchoolExternalTool(externalTool, course); - - // CREATE CONTEXTEXTERNALTOOL - await createContextExternalTool(schoolExternalTool, course, externalTool.name); - } - - await close(); - return Promise.resolve(); -}; - -(async () => { - try { - await up(); - } catch (e) { - console.error(e); - process.exit(1); - } -})(); diff --git a/scripts/migrate-legacy-bbb.js b/scripts/migrate-legacy-bbb.js deleted file mode 100644 index 32d5a0c888e..00000000000 --- a/scripts/migrate-legacy-bbb.js +++ /dev/null @@ -1,109 +0,0 @@ -const mongoose = require('mongoose'); -// eslint-disable-next-line no-unused-vars - -const { Schema } = mongoose; - -const { program } = require('commander'); - -program.requiredOption('-u, --url ', '(Required) URL of the MongoDB instance'); -program.parse(); - -const options = program.opts(); -const mongodbUrl = options.url; - -const close = async () => mongoose.connection.close(); - -const connect = async () => { - const mongooseOptions = { - useNewUrlParser: true, - useUnifiedTopology: true, - }; - - return mongoose.connect(mongodbUrl, mongooseOptions); -}; - -const COURSE_FEATURES = { - VIDEOCONFERENCE: 'videoconference', -}; - -const LtiTool = mongoose.model( - 'ltiTools1688028372783', - new mongoose.Schema( - { - isTemplate: { type: Boolean }, - name: { type: String }, - }, - { - timestamps: true, - } - ), - 'ltitools' -); - -const Course = mongoose.model( - 'course1688028372783', - new mongoose.Schema( - { - ltiToolIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'ltiTools1688028372783' }], - features: [{ type: String, enum: Object.values(COURSE_FEATURES) }], - }, - { - timestamps: true, - } - ), - 'courses' -); - -const up = async () => { - await connect(); - - // find all non-template bbb tools - const bbbTools = await LtiTool.find({ - $and: [{ name: 'Video-Konferenz mit BigBlueButton' }, { isTemplate: false }], - }) - .lean() - .exec(); - - if ((bbbTools || []).length === 0) { - console.error('No non-template videoconferences found. Nothing to migrate.'); - return; - } - - console.log(`Found ${bbbTools.length} tool(s) to migrate.`); - - // find all courses that use BBB - const coursesWithBbb = await Course.find({ ltiToolIds: { $in: bbbTools } }) - .lean() - .exec(); - - if ((coursesWithBbb || []).length === 0) { - console.error('No courses with BBB found. Nothing to migrate.'); - return; - } - - console.log(`Found ${coursesWithBbb.length} course(s) to update.`); - - // add videoconference feature to courses that use bbb - const addFeature = async () => { - for (const course of coursesWithBbb) { - await Course.updateOne( - { _id: course._id }, - { $addToSet: { features: { $each: [COURSE_FEATURES.VIDEOCONFERENCE] } } } - ).exec(); - } - }; - await addFeature(); - - console.log(`Updated ${coursesWithBbb.length} courses.`); - - await close(); -}; - -(async () => { - try { - await up(); - } catch (e) { - console.error(e); - process.exit(1); - } -})(); From 17471bad8010327e7b81dece0e1de940f2eb5d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:37:27 +0100 Subject: [PATCH 13/55] N21-2248 Create LTI 1.1 deep links with external tools (#5349) --- .../api-test/media-board.api.spec.ts | 6 +- .../board-node-copy-specific.service.spec.ts | 1 + .../oauth-provider.controller.api.spec.ts | 2 +- .../modules/server/admin-api-server.config.ts | 1 + .../src/modules/server/server.config.ts | 1 + .../modules/tool/common/common-tool.module.ts | 9 +- .../src/modules/tool/common/service/index.ts | 1 + .../service/lti11-encryption.service.spec.ts | 117 ++ .../service/lti11-encryption.service.ts | 13 +- .../context-external-tool.module.ts | 19 +- .../api-test/tool-deep-link.api.spec.ts | 132 ++ .../api-test/tool-reference.api.spec.ts | 68 +- .../dto/context-external-tool.response.ts | 4 - .../controller/dto/index.ts | 1 + .../controller/dto/lti11-deep-link/index.ts | 6 + .../lti11-deep-link/lti-deep-link.response.ts | 34 + .../lti11-content-item-type.ts | 5 + ...-deep-link-content-item-duration.params.ts | 14 + ...ti11-deep-link-content-item-list.params.ts | 17 + .../lti11-deep-link-content-item.params.ts | 50 + .../lti11-deep-link-raw.params.ts | 51 + .../lti11-deep-link/lti11-deep-link.params.ts | 56 + .../controller/dto/tool-reference.response.ts | 21 +- .../context-external-tool/controller/index.ts | 1 + .../controller/tool-deep-link.controller.ts | 26 + .../domain/context-external-tool.do.ts | 15 + .../domain/error/index.ts | 4 + ...oauth-signature.loggable-exception.spec.ts | 25 + ...alid-oauth-signature.loggable-exception.ts | 14 + ...valid-tool-type.loggable-exception.spec.ts | 35 + .../invalid-tool-type.loggable-exception.ts | 23 + ...k-token-missing.loggable-exception.spec.ts | 36 + ...p-link-token-missing.loggable-exception.ts | 23 + ...not-implemented.loggable-exception.spec.ts | 31 + ...type-not-implemented.loggable-exception.ts | 21 + .../context-external-tool/domain/index.ts | 4 +- .../domain/lti-deep-link-token.ts | 24 + .../domain/lti-deep-link.ts | 33 + .../domain/tool-reference.ts | 7 + .../entity/context-external-tool.entity.ts | 7 + .../context-external-tool/entity/index.ts | 2 + .../entity/lti-deep-link-token.entity.ts | 38 + .../entity/lti-deep-link.embeddable.ts | 44 + .../context-external-tool-response.mapper.ts | 19 +- .../context-external-tool/mapper/index.ts | 1 + .../mapper/lti-deep-link-request.mapper.ts | 32 + .../mapper/tool-reference.mapper.ts | 2 + .../tool/context-external-tool/repo/index.ts | 2 + .../lti-deep-link-token.repo.interface.ts | 11 + .../repo/mikro-orm/index.ts | 1 + .../lti-deep-link-token.repo.spec.ts | 133 ++ .../mikro-orm/lti-deep-link-token.repo.ts | 33 + .../repo/mikro-orm/mapper/index.ts | 1 + .../lti-deep-link-token-entity.mapper.ts | 31 + ...t-external-tool-validation.service.spec.ts | 116 +- ...ontext-external-tool-validation.service.ts | 22 - .../context-external-tool/service/index.ts | 2 + .../lti-deep-link-token.service.spec.ts | 108 ++ .../service/lti-deep-link-token.service.ts | 37 + .../service/lti-deep-linking.service.spec.ts | 62 + .../service/lti-deep-linking.service.ts | 19 + .../service/tool-reference.service.spec.ts | 23 +- .../context-external-tool/testing/index.ts | 5 + .../lti-deep-link-embeddable.factory.ts | 20 + .../lti-deep-link-token-entity.factory.ts | 18 + .../testing/lti-deep-link-token.factory.ts | 18 + .../testing/lti-deep-link.factory.ts | 17 + .../testing/lti11-deep-link-params.factory.ts | 88 + .../uc/context-external-tool.uc.spec.ts | 277 ++- .../uc/context-external-tool.uc.ts | 88 +- .../uc/tool-reference.uc.spec.ts | 2 + .../external-tool/domain/external-tool.do.ts | 9 +- ...al-tool-datasheet-template-data.factory.ts | 4 +- .../testing/external-tool-entity.factory.ts | 75 +- .../testing/external-tool.factory.ts | 4 +- .../testing/file-record-ref.factory.ts | 21 +- .../tool/external-tool/testing/index.ts | 8 +- .../src/modules/tool/tool-api.module.ts | 3 +- apps/server/src/modules/tool/tool-config.ts | 1 + .../tool-launch.controller.api.spec.ts | 6 +- .../dto/tool-launch-request.response.ts | 10 +- .../mapper/tool-launch.mapper.spec.ts | 6 +- .../modules/tool/tool-launch/service/index.ts | 1 - .../abstract-launch.strategy.spec.ts | 10 +- .../abstract-launch.strategy.ts | 25 +- .../basic-tool-launch.strategy.spec.ts | 31 +- .../basic-tool-launch.strategy.ts | 6 +- .../lti11-tool-launch.strategy.spec.ts | 1725 +++++++++++------ .../lti11-tool-launch.strategy.ts | 138 +- .../oauth2-tool-launch.strategy.spec.ts | 14 +- .../oauth2-tool-launch.strategy.ts | 6 +- .../service/lti11-encryption.service.spec.ts | 56 - .../service/tool-launch.service.spec.ts | 27 +- .../modules/tool/tool-launch/testing/index.ts | 1 + .../testing/tool-launch-request.factory.ts | 13 + .../tool/tool-launch/tool-launch.module.ts | 3 +- .../modules/tool/tool-launch/types/index.ts | 1 + .../tool-launch/types/launch-type.enum.ts | 6 + .../tool-launch/types/tool-launch-request.ts | 5 +- .../tool-launch/uc/tool-launch.uc.spec.ts | 19 +- .../shared/controller/transformer/index.ts | 1 + .../string-to-object.transformer.spec.ts | 75 + .../string-to-object.transformer.ts | 15 + .../src/shared/controller/validator/index.ts | 1 + .../validate-record.validator.spec.ts | 96 + .../validator/validate-record.validator.ts | 23 + .../src/shared/domain/entity/all-entities.ts | 5 +- .../context-external-tool.repo.spec.ts | 22 +- .../context-external-tool.repo.ts | 36 +- .../externaltool/external-tool.repo.spec.ts | 8 +- .../src/shared/testing/date-to-string.ts | 1 + .../src/shared/testing/dates-to-strings.ts | 3 - apps/server/src/shared/testing/index.ts | 2 +- backup/setup/external-tools.json | 4 +- 114 files changed, 3779 insertions(+), 977 deletions(-) create mode 100644 apps/server/src/modules/tool/common/service/lti11-encryption.service.spec.ts rename apps/server/src/modules/tool/{tool-launch => common}/service/lti11-encryption.service.ts (62%) create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-deep-link.api.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/index.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti-deep-link.response.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-content-item-type.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-duration.params.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-list.params.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item.params.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-raw.params.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link.params.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/controller/tool-deep-link.controller.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link-token.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link-token.entity.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link.embeddable.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/mapper/lti-deep-link-request.mapper.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/index.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/lti-deep-link-token.repo.interface.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/index.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/index.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/lti-deep-link-token-entity.mapper.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.spec.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-embeddable.factory.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token-entity.factory.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token.factory.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link.factory.ts create mode 100644 apps/server/src/modules/tool/context-external-tool/testing/lti11-deep-link-params.factory.ts delete mode 100644 apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.spec.ts create mode 100644 apps/server/src/modules/tool/tool-launch/testing/index.ts create mode 100644 apps/server/src/modules/tool/tool-launch/testing/tool-launch-request.factory.ts create mode 100644 apps/server/src/modules/tool/tool-launch/types/launch-type.enum.ts create mode 100644 apps/server/src/shared/controller/transformer/string-to-object.transformer.spec.ts create mode 100644 apps/server/src/shared/controller/transformer/string-to-object.transformer.ts create mode 100644 apps/server/src/shared/controller/validator/validate-record.validator.spec.ts create mode 100644 apps/server/src/shared/controller/validator/validate-record.validator.ts create mode 100644 apps/server/src/shared/testing/date-to-string.ts delete mode 100644 apps/server/src/shared/testing/dates-to-strings.ts 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 9c047992067..99d45ad89ee 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 @@ -7,7 +7,7 @@ import { MediaUserLicenseEntity } from '@modules/user-license/entity'; import { mediaSourceEntityFactory, mediaUserLicenseEntityFactory } from '@modules/user-license/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { type DatesToStrings, fileRecordFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { DateToString, fileRecordFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { BoardExternalReferenceType, BoardLayout, MediaBoardColors } from '../../../domain'; import { BoardNodeEntity } from '../../../repo'; import { @@ -85,7 +85,7 @@ describe('Media Board (API)', () => { const response = await studentClient.get('me'); - expect(response.body).toEqual>({ + expect(response.body).toEqual>({ id: mediaBoard.id, timestamps: { createdAt: mediaBoard.createdAt.toISOString(), @@ -205,7 +205,7 @@ describe('Media Board (API)', () => { const response = await studentClient.post(`${mediaBoard.id}/media-lines`); - expect(response.body).toEqual>({ + expect(response.body).toEqual>({ id: expect.any(String), timestamps: { createdAt: expect.any(String), diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts index 1a82f545a59..5c05465ad7e 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts @@ -56,6 +56,7 @@ describe(BoardNodeCopyService.name, () => { FILES_STORAGE__SERVICE_BASE_URL: '', CTL_TOOLS__PREFERRED_TOOLS_LIMIT: 10, FEATURE_PREFERRED_CTL_TOOLS_ENABLED: false, + PUBLIC_BACKEND_URL: '', }; let contextExternalToolService: DeepMocked; let copyHelperService: DeepMocked; diff --git a/apps/server/src/modules/oauth-provider/api/test/oauth-provider.controller.api.spec.ts b/apps/server/src/modules/oauth-provider/api/test/oauth-provider.controller.api.spec.ts index 5e8b9099488..911864a7622 100644 --- a/apps/server/src/modules/oauth-provider/api/test/oauth-provider.controller.api.spec.ts +++ b/apps/server/src/modules/oauth-provider/api/test/oauth-provider.controller.api.spec.ts @@ -536,7 +536,7 @@ describe(OauthProviderController.name, () => { const setup = async () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const consentListResponse: ProviderConsentSessionResponse = providerConsentSessionResponseFactory.build(); - const externalTool = externalToolEntityFactory.withOauth2Config('clientId').buildWithId(); + const externalTool = externalToolEntityFactory.withOauth2Config().buildWithId(); const pseudonym = externalToolPseudonymEntityFactory.buildWithId({ toolId: externalTool.id, userId: studentUser.id, diff --git a/apps/server/src/modules/server/admin-api-server.config.ts b/apps/server/src/modules/server/admin-api-server.config.ts index d0656f53483..4e889b24015 100644 --- a/apps/server/src/modules/server/admin-api-server.config.ts +++ b/apps/server/src/modules/server/admin-api-server.config.ts @@ -67,6 +67,7 @@ const config: AdminApiServerConfig = { TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION: Configuration.get( 'TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION' ) as string, + PUBLIC_BACKEND_URL: Configuration.get('PUBLIC_BACKEND_URL') as string, }; export const adminApiServerConfig = () => config; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index f4e5eb6ec5d..3c8f9d89708 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -323,6 +323,7 @@ const config: ServerConfig = { AES_KEY: Configuration.get('AES_KEY') as string, FEATURE_OAUTH_LOGIN: Configuration.get('FEATURE_OAUTH_LOGIN') as boolean, FEATURE_EXTERNAL_SYSTEM_LOGOUT_ENABLED: Configuration.get('FEATURE_EXTERNAL_SYSTEM_LOGOUT_ENABLED') as boolean, + PUBLIC_BACKEND_URL: Configuration.get('PUBLIC_BACKEND_URL') as string, }; export const serverConfig = () => config; diff --git a/apps/server/src/modules/tool/common/common-tool.module.ts b/apps/server/src/modules/tool/common/common-tool.module.ts index a48b814900f..cb6b7e20ff5 100644 --- a/apps/server/src/modules/tool/common/common-tool.module.ts +++ b/apps/server/src/modules/tool/common/common-tool.module.ts @@ -4,7 +4,12 @@ import { CqrsModule } from '@nestjs/cqrs'; import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { SchoolModule } from '@src/modules/school'; -import { CommonToolDeleteService, CommonToolService, CommonToolValidationService } from './service'; +import { + CommonToolDeleteService, + CommonToolService, + CommonToolValidationService, + Lti11EncryptionService, +} from './service'; import { CommonToolMetadataService } from './service/common-tool-metadata.service'; @Module({ @@ -18,6 +23,7 @@ import { CommonToolMetadataService } from './service/common-tool-metadata.servic ContextExternalToolRepo, CommonToolMetadataService, CommonToolDeleteService, + Lti11EncryptionService, ], exports: [ CommonToolService, @@ -27,6 +33,7 @@ import { CommonToolMetadataService } from './service/common-tool-metadata.servic ContextExternalToolRepo, CommonToolMetadataService, CommonToolDeleteService, + Lti11EncryptionService, ], }) export class CommonToolModule {} diff --git a/apps/server/src/modules/tool/common/service/index.ts b/apps/server/src/modules/tool/common/service/index.ts index 9a6567dbbcf..43595c76ba7 100644 --- a/apps/server/src/modules/tool/common/service/index.ts +++ b/apps/server/src/modules/tool/common/service/index.ts @@ -1,3 +1,4 @@ export * from './common-tool.service'; export { CommonToolValidationService, ToolParameterTypeValidationUtil } from './validation'; export { CommonToolDeleteService } from './common-tool-delete.service'; +export { Lti11EncryptionService } from './lti11-encryption.service'; diff --git a/apps/server/src/modules/tool/common/service/lti11-encryption.service.spec.ts b/apps/server/src/modules/tool/common/service/lti11-encryption.service.spec.ts new file mode 100644 index 00000000000..ef8baadfaa3 --- /dev/null +++ b/apps/server/src/modules/tool/common/service/lti11-encryption.service.spec.ts @@ -0,0 +1,117 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Authorization } from 'oauth-1.0a'; +import { Lti11EncryptionService } from './lti11-encryption.service'; + +describe(Lti11EncryptionService.name, () => { + let module: TestingModule; + let service: Lti11EncryptionService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [Lti11EncryptionService], + }).compile(); + + service = module.get(Lti11EncryptionService); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('sign', () => { + describe('when signing with OAuth1', () => { + const setup = () => { + const mockKey = 'mockKey'; + const mockSecret = 'mockSecret'; + const mockUrl = 'https://mockurl.com/'; + const testPayload: Record = { + param1: 'test1', + }; + + return { + mockKey, + mockSecret, + mockUrl, + testPayload, + }; + }; + + it('should sign the payload with OAuth1', () => { + const { mockKey, mockSecret, mockUrl, testPayload } = setup(); + + const result: Authorization = service.sign(mockKey, mockSecret, mockUrl, testPayload); + + expect(result).toEqual({ + oauth_consumer_key: mockKey, + oauth_nonce: expect.any(String), + oauth_signature: expect.any(String), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: expect.any(Number), + oauth_version: '1.0', + ...testPayload, + }); + }); + }); + }); + + describe('verify', () => { + describe('when the OAuth1 signature is valid', () => { + const setup = () => { + const mockKey = 'mockKey'; + const mockSecret = 'mockSecret'; + const mockUrl = 'https://mockurl.com/'; + const testPayload: Record = { + param1: 'test1', + }; + + const signedPayload: Authorization = service.sign(mockKey, mockSecret, mockUrl, testPayload); + + return { + mockKey, + mockSecret, + mockUrl, + testPayload, + signedPayload, + }; + }; + + it('should return true', () => { + const { mockKey, mockSecret, mockUrl, signedPayload } = setup(); + + const result = service.verify(mockKey, mockSecret, mockUrl, signedPayload); + + expect(result).toEqual(true); + }); + }); + + describe('when the OAuth1 signature is invalid', () => { + const setup = () => { + const mockKey = 'mockKey'; + const mockSecret = 'mockSecret'; + const mockUrl = 'https://mockurl.com/'; + const testPayload: Record = { + param1: 'test1', + }; + + const signedPayload: Authorization = service.sign(mockKey, mockSecret, mockUrl, testPayload); + const tamperedPayload = { ...signedPayload, param1: 'test2' }; + + return { + mockKey, + mockSecret, + mockUrl, + testPayload, + tamperedPayload, + }; + }; + + it('should return false', () => { + const { mockKey, mockSecret, mockUrl, tamperedPayload } = setup(); + + const result = service.verify(mockKey, mockSecret, mockUrl, tamperedPayload); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.ts b/apps/server/src/modules/tool/common/service/lti11-encryption.service.ts similarity index 62% rename from apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.ts rename to apps/server/src/modules/tool/common/service/lti11-encryption.service.ts index 22f3bb9bee6..41e3b0c2581 100644 --- a/apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.ts +++ b/apps/server/src/modules/tool/common/service/lti11-encryption.service.ts @@ -4,7 +4,7 @@ import OAuth, { Authorization, RequestOptions } from 'oauth-1.0a'; @Injectable() export class Lti11EncryptionService { - public sign(key: string, secret: string, url: string, payload: Record): Authorization { + public sign(key: string, secret: string, url: string, payload: unknown): Authorization { const requestData: RequestOptions = { url, method: 'POST', @@ -25,4 +25,15 @@ export class Lti11EncryptionService { return authorization; } + + public verify(key: string, secret: string, url: string, payload: Authorization): boolean { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { oauth_signature, ...validationPayload } = payload; + + const authorization: Authorization = this.sign(key, secret, url, validationPayload); + + const isValid = oauth_signature === authorization.oauth_signature; + + return isValid; + } } diff --git a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts index 79e6cc63adc..9e5d4547989 100644 --- a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts +++ b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts @@ -6,9 +6,16 @@ import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; import { ContextExternalToolRule } from './authorisation/context-external-tool.rule'; -import { ContextExternalToolAuthorizableService, ContextExternalToolService, ToolReferenceService } from './service'; +import { LTI_DEEP_LINK_TOKEN_REPO, LtiDeepLinkTokenMikroOrmRepo } from './repo'; +import { + ContextExternalToolAuthorizableService, + ContextExternalToolService, + LtiDeepLinkingService, + LtiDeepLinkTokenService, + ToolConfigurationStatusService, + ToolReferenceService, +} from './service'; import { ContextExternalToolValidationService } from './service/context-external-tool-validation.service'; -import { ToolConfigurationStatusService } from './service/tool-configuration-status.service'; @Module({ imports: [ @@ -26,12 +33,20 @@ import { ToolConfigurationStatusService } from './service/tool-configuration-sta ToolReferenceService, ToolConfigurationStatusService, ContextExternalToolRule, + LtiDeepLinkTokenService, + LtiDeepLinkingService, + { + provide: LTI_DEEP_LINK_TOKEN_REPO, + useClass: LtiDeepLinkTokenMikroOrmRepo, + }, ], exports: [ ContextExternalToolService, ContextExternalToolValidationService, ToolReferenceService, ToolConfigurationStatusService, + LtiDeepLinkTokenService, + LtiDeepLinkingService, ], }) export class ContextExternalToolModule {} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-deep-link.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-deep-link.api.spec.ts new file mode 100644 index 00000000000..bb71215cf33 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-deep-link.api.spec.ts @@ -0,0 +1,132 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { ServerTestModule } from '@modules/server'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import crypto from 'crypto-js'; +import { externalToolEntityFactory, lti11ToolConfigEntityFactory } from '../../../external-tool/testing'; +import { schoolExternalToolEntityFactory } from '../../../school-external-tool/testing'; +import { ContextExternalToolEntity, ContextExternalToolType, LtiDeepLinkEmbeddable } from '../../entity'; +import { + contextExternalToolEntityFactory, + Lti11DeepLinkParamsFactory, + ltiDeepLinkTokenEntityFactory, +} from '../../testing'; +import { Lti11DeepLinkContentItemParams } from '../dto'; + +describe('ToolDeepLinkController (API)', () => { + let app: INestApplication; + let em: EntityManager; + let orm: MikroORM; + let testApiClient: TestApiClient; + + const basePath = '/tools/context-external-tools'; + const decryptedSecret = 'secret'; + const encryptedSecret = crypto.AES.encrypt(decryptedSecret, Configuration.get('AES_KEY') as string).toString(); + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + orm = app.get(MikroORM); + testApiClient = new TestApiClient(app, basePath); + }); + + afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { + await orm.getSchemaGenerator().clearDatabase(); + }); + + describe('[POST] tools/context-external-tools/:contextExternalToolId/lti11-deep-link-callback', () => { + describe('when the lti deep linking callback is successfully', () => { + const setup = async () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const ltiDeepLinkToken = ltiDeepLinkTokenEntityFactory.build({ user: teacherUser }); + const course = courseFactory.buildWithId({ + teachers: [teacherUser], + }); + + const lti11Config = lti11ToolConfigEntityFactory.build({ + secret: encryptedSecret, + }); + const externalTool = externalToolEntityFactory.buildWithId({ config: lti11Config }); + const schoolExternalTool = schoolExternalToolEntityFactory.buildWithId({ + tool: externalTool, + school: teacherUser.school, + }); + const contextExternalTool = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalTool, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + }); + + const publicBackendUrl = Configuration.get('PUBLIC_BACKEND_URL') as string; + const callbackUrl = `${publicBackendUrl}/v3${basePath}/${contextExternalTool.id}/lti11-deep-link-callback`; + const requestFactory = new Lti11DeepLinkParamsFactory(callbackUrl, lti11Config.key, decryptedSecret); + const postParams = requestFactory.buildRaw({ + data: ltiDeepLinkToken.state, + }); + + await em.persistAndFlush([ + teacherAccount, + teacherUser, + ltiDeepLinkToken, + course, + externalTool, + schoolExternalTool, + contextExternalTool, + ]); + em.clear(); + + const targetContent = requestFactory.build({ + data: ltiDeepLinkToken.state, + }).content_items?.['@graph'][0] as Lti11DeepLinkContentItemParams; + + return { + postParams, + contextExternalTool, + targetContent, + }; + }; + + it('should create a lti deep link with the context external tool', async () => { + const { postParams, contextExternalTool, targetContent } = await setup(); + + const response = await testApiClient + .post(`/${contextExternalTool.id}/lti11-deep-link-callback`) + .send(postParams); + + expect(response.statusCode).toEqual(HttpStatus.CREATED); + expect(response.text).toEqual( + 'Window can be closedThis window can be closed' + ); + const dbContextExternalTool = await em.findOneOrFail(ContextExternalToolEntity, contextExternalTool.id); + expect(dbContextExternalTool.ltiDeepLink).toMatchObject({ + mediaType: targetContent.mediaType, + title: targetContent.title, + url: targetContent.url, + text: targetContent.text, + parameters: [ + { + name: 'dl_param', + value: targetContent.custom?.dl_param, + }, + ], + availableFrom: targetContent.available?.startDatetime, + availableUntil: targetContent.available?.endDatetime, + submissionFrom: targetContent.submission?.startDatetime, + submissionUntil: targetContent.submission?.endDatetime, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts index 5803971a64b..180fcab6715 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts @@ -7,19 +7,24 @@ import { Permission } from '@shared/domain/interface'; import { cleanupCollections, courseFactory, + DateToString, fileRecordFactory, schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; import { Response } from 'supertest'; -import { CustomParameterLocation, CustomParameterScope, ToolContextType } from '../../../common/enum'; +import { CustomParameterLocation, CustomParameterScope, LtiMessageType, ToolContextType } from '../../../common/enum'; import { ExternalToolEntity } from '../../../external-tool/entity'; import { customParameterFactory, externalToolEntityFactory } from '../../../external-tool/testing'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; import { schoolExternalToolEntityFactory } from '../../../school-external-tool/testing'; import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; -import { contextExternalToolConfigurationStatusResponseFactory, contextExternalToolEntityFactory } from '../../testing'; +import { + contextExternalToolConfigurationStatusResponseFactory, + contextExternalToolEntityFactory, + ltiDeepLinkEmbeddableFactory, +} from '../../testing'; import { ContextExternalToolContextParams, ToolReferenceListResponse, ToolReferenceResponse } from '../dto'; describe('ToolReferenceController (API)', () => { @@ -188,6 +193,7 @@ describe('ToolReferenceController (API)', () => { thumbnailUrl: `/api/v3/file/preview/${thumbnailFileRecord.id}/${encodeURIComponent( thumbnailFileRecord.name )}`, + isLtiDeepLinkingTool: false, }, ], }); @@ -257,34 +263,40 @@ describe('ToolReferenceController (API)', () => { ]); const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); const thumbnailFileRecord = fileRecordFactory.build(); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ - logoBase64: 'logoBase64', - parameters: [ - customParameterFactory.build({ - name: 'schoolMockParameter', - scope: CustomParameterScope.SCHOOL, - location: CustomParameterLocation.PATH, - }), - customParameterFactory.build({ - name: 'contextMockParameter', - scope: CustomParameterScope.CONTEXT, - location: CustomParameterLocation.PATH, - }), - ], - thumbnail: { - uploadUrl: 'https://uploadurl.com', - fileRecord: thumbnailFileRecord, - }, - }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory + .withLti11Config({ + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + }) + .buildWithId({ + logoBase64: 'logoBase64', + parameters: [ + customParameterFactory.build({ + name: 'schoolMockParameter', + scope: CustomParameterScope.SCHOOL, + location: CustomParameterLocation.PATH, + }), + customParameterFactory.build({ + name: 'contextMockParameter', + scope: CustomParameterScope.CONTEXT, + location: CustomParameterLocation.PATH, + }), + ], + thumbnail: { + uploadUrl: 'https://uploadurl.com', + fileRecord: thumbnailFileRecord, + }, + }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ school, tool: externalToolEntity, }); + const ltiDeepLinkEmbeddable = ltiDeepLinkEmbeddableFactory.build(); const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalToolEntity, contextId: course.id, contextType: ContextExternalToolType.COURSE, displayName: 'This is a test tool', + ltiDeepLink: ltiDeepLinkEmbeddable, }); await em.persistAndFlush([ @@ -307,6 +319,7 @@ describe('ToolReferenceController (API)', () => { contextExternalToolEntity, externalToolEntity, thumbnailFileRecord, + ltiDeepLinkEmbeddable, }; }; @@ -317,12 +330,13 @@ describe('ToolReferenceController (API)', () => { contextExternalToolEntity, externalToolEntity, thumbnailFileRecord, + ltiDeepLinkEmbeddable, } = await setup(); const response: Response = await loggedInClient.get(`context-external-tools/${contextExternalToolId}`); expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.body).toEqual({ + expect(response.body).toEqual>({ contextToolId: contextExternalToolEntity.id, description: externalToolEntity.description, displayName: contextExternalToolEntity.displayName as string, @@ -335,6 +349,16 @@ describe('ToolReferenceController (API)', () => { thumbnailUrl: `/api/v3/file/preview/${thumbnailFileRecord.id}/${encodeURIComponent( thumbnailFileRecord.name )}`, + isLtiDeepLinkingTool: true, + ltiDeepLink: { + mediaType: ltiDeepLinkEmbeddable.mediaType, + title: ltiDeepLinkEmbeddable.title, + text: ltiDeepLinkEmbeddable.text, + availableFrom: ltiDeepLinkEmbeddable.availableFrom?.toISOString(), + availableUntil: ltiDeepLinkEmbeddable.availableUntil?.toISOString(), + submissionFrom: ltiDeepLinkEmbeddable.submissionFrom?.toISOString(), + submissionUntil: ltiDeepLinkEmbeddable.submissionUntil?.toISOString(), + }, }); }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool.response.ts index 60145a6d140..0584d6a981d 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool.response.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool.response.ts @@ -21,9 +21,6 @@ export class ContextExternalToolResponse { @ApiProperty({ type: [CustomParameterEntryResponse] }) parameters: CustomParameterEntryResponse[] = []; - @ApiPropertyOptional() - logoUrl?: string; - constructor(response: ContextExternalToolResponse) { this.id = response.id; this.schoolToolId = response.schoolToolId; @@ -31,6 +28,5 @@ export class ContextExternalToolResponse { this.contextType = response.contextType; this.displayName = response.displayName; this.parameters = response.parameters; - this.logoUrl = response.logoUrl; } } diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts index 89922cda255..a41423e9d72 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts @@ -5,3 +5,4 @@ export * from './context-external-tool-context.params'; export * from './context-external-tool.response'; export * from './tool-reference-list.response'; export * from './tool-reference.response'; +export * from './lti11-deep-link'; diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/index.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/index.ts new file mode 100644 index 00000000000..1053bafe92a --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/index.ts @@ -0,0 +1,6 @@ +export { Lti11DeepLinkContentItemParams } from './lti11-deep-link-content-item.params'; +export { Lti11DeepLinkContentItemListParams } from './lti11-deep-link-content-item-list.params'; +export { Lti11ContentItemType } from './lti11-content-item-type'; +export { Lti11DeepLinkParams } from './lti11-deep-link.params'; +export { LtiDeepLinkResponse } from './lti-deep-link.response'; +export { Lti11DeepLinkContentItemDurationParams } from './lti11-deep-link-content-item-duration.params'; diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti-deep-link.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti-deep-link.response.ts new file mode 100644 index 00000000000..5962dd333fb --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti-deep-link.response.ts @@ -0,0 +1,34 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class LtiDeepLinkResponse { + @ApiProperty() + mediaType: string; + + @ApiPropertyOptional() + title?: string; + + @ApiPropertyOptional() + text?: string; + + @ApiPropertyOptional() + availableFrom?: Date; + + @ApiPropertyOptional() + availableUntil?: Date; + + @ApiPropertyOptional() + submissionFrom?: Date; + + @ApiPropertyOptional() + submissionUntil?: Date; + + constructor(props: LtiDeepLinkResponse) { + this.mediaType = props.mediaType; + this.title = props.title; + this.text = props.text; + this.availableFrom = props.availableFrom; + this.availableUntil = props.availableUntil; + this.submissionFrom = props.submissionFrom; + this.submissionUntil = props.submissionUntil; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-content-item-type.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-content-item-type.ts new file mode 100644 index 00000000000..5764510b9db --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-content-item-type.ts @@ -0,0 +1,5 @@ +export enum Lti11ContentItemType { + CONTENT_ITEM = 'ContentItem', + LTI_LINK_ITEM = 'LtiLinkItem', + FILE_ITEM = 'FileItem', +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-duration.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-duration.params.ts new file mode 100644 index 00000000000..102cc5eb147 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-duration.params.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDate, IsOptional } from 'class-validator'; + +export class Lti11DeepLinkContentItemDurationParams { + @IsOptional() + @IsDate() + @ApiPropertyOptional() + startDatetime?: Date; + + @IsOptional() + @IsDate() + @ApiPropertyOptional() + endDatetime?: Date; +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-list.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-list.params.ts new file mode 100644 index 00000000000..1ebb6b7f9d8 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item-list.params.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayMaxSize, IsArray, IsString, ValidateNested } from 'class-validator'; +import { Lti11DeepLinkContentItemParams } from './lti11-deep-link-content-item.params'; + +export class Lti11DeepLinkContentItemListParams { + @IsString() + @ApiProperty() + '@context'!: string; + + @IsArray() + @ArrayMaxSize(1) + @ValidateNested({ each: true }) + @Type(() => Lti11DeepLinkContentItemParams) + @ApiProperty() + '@graph'!: Lti11DeepLinkContentItemParams[]; +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item.params.ts new file mode 100644 index 00000000000..402eb2f376c --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-content-item.params.ts @@ -0,0 +1,50 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ValidateRecord } from '@shared/controller'; +import { Type } from 'class-transformer'; +import { IsEnum, IsObject, IsOptional, isString, IsString, ValidateNested } from 'class-validator'; +import { Lti11ContentItemType } from './lti11-content-item-type'; +import { Lti11DeepLinkContentItemDurationParams } from './lti11-deep-link-content-item-duration.params'; + +export class Lti11DeepLinkContentItemParams { + @IsEnum(Lti11ContentItemType) + @ApiProperty() + '@type'!: Lti11ContentItemType; + + @IsString() + @ApiProperty() + mediaType!: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + url?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + title?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + text?: string; + + @IsOptional() + @ValidateNested() + @Type(() => Lti11DeepLinkContentItemDurationParams) + @ApiPropertyOptional() + available?: Lti11DeepLinkContentItemDurationParams; + + @IsOptional() + @ValidateNested() + @Type(() => Lti11DeepLinkContentItemDurationParams) + @ApiPropertyOptional() + submission?: Lti11DeepLinkContentItemDurationParams; + + @IsOptional() + @IsObject() + @ValidateRecord(isString) + @Type(() => Object) + @ApiPropertyOptional() + custom?: Record; +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-raw.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-raw.params.ts new file mode 100644 index 00000000000..847f151cac5 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link-raw.params.ts @@ -0,0 +1,51 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Equals, IsJSON, IsNumber, IsOptional, IsString } from 'class-validator'; +import { Authorization } from 'oauth-1.0a'; + +export class Lti11DeepLinkParamsRaw implements Authorization { + @Equals('ContentItemSelection') + @ApiProperty() + lti_message_type!: string; + + @Equals('LTI-1p0') + @ApiProperty() + lti_version!: string; + + @IsOptional() + @IsJSON() + @ApiPropertyOptional() + content_items?: string; + + @IsString() + @ApiProperty() + data!: string; + + @Equals('1.0') + @ApiProperty() + oauth_version!: string; + + @IsString() + @ApiProperty() + oauth_nonce!: string; + + @IsNumber() + @ApiProperty() + oauth_timestamp!: number; + + @Equals('HMAC-SHA1') + @ApiProperty() + oauth_signature_method!: string; + + @IsString() + @ApiProperty() + oauth_consumer_key!: string; + + @IsString() + @ApiProperty() + oauth_signature!: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + oauth_callback?: string; +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link.params.ts new file mode 100644 index 00000000000..047e44d91b5 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/lti11-deep-link/lti11-deep-link.params.ts @@ -0,0 +1,56 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { StringToObject } from '@shared/controller'; +import { Type } from 'class-transformer'; +import { Equals, IsNumber, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Lti11DeepLinkContentItemListParams } from './lti11-deep-link-content-item-list.params'; + +export class Lti11DeepLinkParams { + @Equals('ContentItemSelection') + @ApiProperty() + lti_message_type!: string; + + @Equals('LTI-1p0') + @ApiProperty() + lti_version!: string; + + @IsOptional() + @IsObject() + @ValidateNested() + @StringToObject(Lti11DeepLinkContentItemListParams) + @Type(() => Lti11DeepLinkContentItemListParams) + @ApiPropertyOptional() + content_items?: Lti11DeepLinkContentItemListParams; + + @IsString() + @ApiProperty() + data!: string; + + @Equals('1.0') + @ApiProperty() + oauth_version!: string; + + @IsString() + @ApiProperty() + oauth_nonce!: string; + + @IsNumber() + @ApiProperty() + oauth_timestamp!: number; + + @Equals('HMAC-SHA1') + @ApiProperty() + oauth_signature_method!: string; + + @IsString() + @ApiProperty() + oauth_consumer_key!: string; + + @IsString() + @ApiProperty() + oauth_signature!: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional() + oauth_callback?: string; +} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts index 9f66a020c3d..834ca4ad69a 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts @@ -1,41 +1,42 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ContextExternalToolConfigurationStatusResponse } from '../../../common/controller/dto'; +import { LtiDeepLinkResponse } from './lti11-deep-link'; export class ToolReferenceResponse { - @ApiProperty({ nullable: false, required: true, description: 'The id of the tool in the context' }) + @ApiProperty({ description: 'The id of the tool in the context' }) contextToolId: string; @ApiPropertyOptional({ description: 'The description of the tool' }) description?: string; @ApiPropertyOptional({ - nullable: false, - required: false, description: 'The url of the logo which is stored in the db', }) logoUrl?: string; @ApiPropertyOptional({ - nullable: false, - required: false, description: 'The url of the thumbnail which is stored in the db', }) thumbnailUrl?: string; - @ApiProperty({ nullable: false, required: true, description: 'The display name of the tool' }) + @ApiProperty({ description: 'The display name of the tool' }) displayName: string; - @ApiProperty({ nullable: false, required: true, description: 'Whether the tool should be opened in a new tab' }) + @ApiProperty({ description: 'Whether the tool should be opened in a new tab' }) openInNewTab: boolean; @ApiProperty({ type: ContextExternalToolConfigurationStatusResponse, - nullable: false, - required: true, description: 'The status of the tool', }) status: ContextExternalToolConfigurationStatusResponse; + @ApiProperty({ description: 'Whether the tool is a lti deep linking tool' }) + isLtiDeepLinkingTool: boolean; + + @ApiPropertyOptional({ type: LtiDeepLinkResponse }) + ltiDeepLink?: LtiDeepLinkResponse; + constructor(toolReferenceResponse: ToolReferenceResponse) { this.contextToolId = toolReferenceResponse.contextToolId; this.description = toolReferenceResponse.description; @@ -44,5 +45,7 @@ export class ToolReferenceResponse { this.displayName = toolReferenceResponse.displayName; this.openInNewTab = toolReferenceResponse.openInNewTab; this.status = toolReferenceResponse.status; + this.isLtiDeepLinkingTool = toolReferenceResponse.isLtiDeepLinkingTool; + this.ltiDeepLink = toolReferenceResponse.ltiDeepLink; } } diff --git a/apps/server/src/modules/tool/context-external-tool/controller/index.ts b/apps/server/src/modules/tool/context-external-tool/controller/index.ts index 6927a20482c..1a0addf31d9 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/index.ts @@ -1,2 +1,3 @@ export * from './tool-context.controller'; export { AdminApiContextExternalToolController } from './admin-api-context-external-tool.controller'; +export { ToolDeepLinkController } from './tool-deep-link.controller'; diff --git a/apps/server/src/modules/tool/context-external-tool/controller/tool-deep-link.controller.ts b/apps/server/src/modules/tool/context-external-tool/controller/tool-deep-link.controller.ts new file mode 100644 index 00000000000..95339a12eb9 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/tool-deep-link.controller.ts @@ -0,0 +1,26 @@ +import { Body, Controller, Param, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { LtiDeepLink } from '../domain'; +import { LtiDeepLinkRequestMapper } from '../mapper'; +import { ContextExternalToolUc } from '../uc'; +import { ContextExternalToolIdParams, Lti11DeepLinkParams } from './dto'; +import { Lti11DeepLinkParamsRaw } from './dto/lti11-deep-link/lti11-deep-link-raw.params'; + +@ApiTags('Tool') +@Controller('tools/context-external-tools') +export class ToolDeepLinkController { + constructor(private readonly contextExternalToolUc: ContextExternalToolUc) {} + + @Post(':contextExternalToolId/lti11-deep-link-callback') + public async deepLink( + @Param() params: ContextExternalToolIdParams, + @Body() rawBody: Lti11DeepLinkParamsRaw, + @Body() body: Lti11DeepLinkParams + ): Promise { + const deepLink: LtiDeepLink | undefined = LtiDeepLinkRequestMapper.mapRequestToDO(body); + + await this.contextExternalToolUc.updateLtiDeepLink(params.contextExternalToolId, rawBody, body.data, deepLink); + + return 'Window can be closedThis window can be closed'; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/context-external-tool.do.ts b/apps/server/src/modules/tool/context-external-tool/domain/context-external-tool.do.ts index 0b3a2bf7335..dbf649fc92e 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/context-external-tool.do.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/context-external-tool.do.ts @@ -2,6 +2,7 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { CustomParameterEntry } from '../../common/domain'; import { SchoolExternalToolRef } from '../../school-external-tool/domain'; import { ContextRef } from './context-ref'; +import { LtiDeepLink } from './lti-deep-link'; export interface ContextExternalToolLaunchable { id?: string; @@ -11,6 +12,8 @@ export interface ContextExternalToolLaunchable { contextRef: ContextRef; parameters: CustomParameterEntry[]; + + ltiDeepLink?: LtiDeepLink; } export interface ContextExternalToolProps extends AuthorizableObject, ContextExternalToolLaunchable { @@ -32,7 +35,19 @@ export class ContextExternalTool extends DomainObject return this.props.displayName; } + set displayName(value: string | undefined) { + this.props.displayName = value; + } + get parameters(): CustomParameterEntry[] { return this.props.parameters; } + + get ltiDeepLink(): LtiDeepLink | undefined { + return this.props.ltiDeepLink; + } + + set ltiDeepLink(value: LtiDeepLink | undefined) { + this.props.ltiDeepLink = value; + } } diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/index.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/index.ts index 75d3370f475..ca3eeb7e8e4 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/error/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/index.ts @@ -1 +1,5 @@ export { RestrictedContextMismatchLoggableException } from './restricted-context-mismatch-loggabble'; +export { LtiDeepLinkTokenMissingLoggableException } from './lti-deep-link-token-missing.loggable-exception'; +export { LtiMessageTypeNotImplementedLoggableException } from './lti-message-type-not-implemented.loggable-exception'; +export { InvalidToolTypeLoggableException } from './invalid-tool-type.loggable-exception'; +export { InvalidOauthSignatureLoggableException } from './invalid-oauth-signature.loggable-exception'; diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.spec.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.spec.ts new file mode 100644 index 00000000000..2374ee52909 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.spec.ts @@ -0,0 +1,25 @@ +import { InvalidOauthSignatureLoggableException } from './invalid-oauth-signature.loggable-exception'; + +describe(InvalidOauthSignatureLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const loggable = new InvalidOauthSignatureLoggableException(); + + return { + loggable, + }; + }; + + it('should return a loggable message', () => { + const { loggable } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'INVALID_OAUTH_SIGNATURE', + message: 'The oauth signature is invalid.', + stack: loggable.stack, + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.ts new file mode 100644 index 00000000000..0a4966999e6 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-oauth-signature.loggable-exception.ts @@ -0,0 +1,14 @@ +import { BadRequestException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class InvalidOauthSignatureLoggableException extends BadRequestException implements Loggable { + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + const message: LogMessage | ErrorLogMessage | ValidationErrorLogMessage = { + type: 'INVALID_OAUTH_SIGNATURE', + message: 'The oauth signature is invalid.', + stack: this.stack, + }; + + return message; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.spec.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.spec.ts new file mode 100644 index 00000000000..9183c772095 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.spec.ts @@ -0,0 +1,35 @@ +import { ToolConfigType } from '../../../common/enum'; +import { InvalidToolTypeLoggableException } from './invalid-tool-type.loggable-exception'; + +describe(InvalidToolTypeLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const expected = ToolConfigType.LTI11; + const received = ToolConfigType.OAUTH2; + + const loggable = new InvalidToolTypeLoggableException(expected, received); + + return { + loggable, + expected, + received, + }; + }; + + it('should return a loggable message', () => { + const { loggable, expected, received } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'INVALID_TOOL_TYPE', + message: 'The external tool has the wrong tool type.', + stack: loggable.stack, + data: { + expected, + received, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.ts new file mode 100644 index 00000000000..7974634fde2 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/invalid-tool-type.loggable-exception.ts @@ -0,0 +1,23 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ToolConfigType } from '../../../common/enum'; + +export class InvalidToolTypeLoggableException extends UnprocessableEntityException implements Loggable { + constructor(private readonly expected: ToolConfigType, private readonly received: ToolConfigType) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + const message: LogMessage | ErrorLogMessage | ValidationErrorLogMessage = { + type: 'INVALID_TOOL_TYPE', + message: 'The external tool has the wrong tool type.', + stack: this.stack, + data: { + expected: this.expected, + received: this.received, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.spec.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.spec.ts new file mode 100644 index 00000000000..0822150fe0c --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.spec.ts @@ -0,0 +1,36 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { UUID } from 'bson'; +import { LtiDeepLinkTokenMissingLoggableException } from './lti-deep-link-token-missing.loggable-exception'; + +describe(LtiDeepLinkTokenMissingLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const state = new UUID().toString(); + const contextExternalToolId = new ObjectId().toHexString(); + + const loggable = new LtiDeepLinkTokenMissingLoggableException(state, contextExternalToolId); + + return { + loggable, + state, + contextExternalToolId, + }; + }; + + it('should return a loggable message', () => { + const { loggable, state, contextExternalToolId } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'UNAUTHORIZED_EXCEPTION', + message: 'Unable to find lti deep link token for this state. It might have expired.', + stack: loggable.stack, + data: { + state, + contextExternalToolId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.ts new file mode 100644 index 00000000000..8fe180a21b6 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-deep-link-token-missing.loggable-exception.ts @@ -0,0 +1,23 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class LtiDeepLinkTokenMissingLoggableException extends UnauthorizedException implements Loggable { + constructor(private readonly state: string, private readonly contextExternalToolId: EntityId) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + const message: LogMessage | ErrorLogMessage | ValidationErrorLogMessage = { + type: 'UNAUTHORIZED_EXCEPTION', + message: 'Unable to find lti deep link token for this state. It might have expired.', + stack: this.stack, + data: { + state: this.state, + contextExternalToolId: this.contextExternalToolId, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.spec.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.spec.ts new file mode 100644 index 00000000000..1d612307fc9 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { LtiMessageTypeNotImplementedLoggableException } from './lti-message-type-not-implemented.loggable-exception'; + +describe(LtiMessageTypeNotImplementedLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const unknownMessageType = 'unknownMessageType'; + + const loggable = new LtiMessageTypeNotImplementedLoggableException(unknownMessageType); + + return { + loggable, + unknownMessageType, + }; + }; + + it('should return a loggable message', () => { + const { loggable, unknownMessageType } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'LTI_MESSAGE_TYPE_NOT_IMPLEMENTED', + message: 'The lti message type is not implemented.', + stack: loggable.stack, + data: { + lti_message_type: unknownMessageType, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.ts b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.ts new file mode 100644 index 00000000000..31202563978 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/error/lti-message-type-not-implemented.loggable-exception.ts @@ -0,0 +1,21 @@ +import { NotImplementedException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class LtiMessageTypeNotImplementedLoggableException extends NotImplementedException implements Loggable { + constructor(private readonly ltiMessageType: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + const message: LogMessage | ErrorLogMessage | ValidationErrorLogMessage = { + type: 'LTI_MESSAGE_TYPE_NOT_IMPLEMENTED', + message: 'The lti message type is not implemented.', + stack: this.stack, + data: { + lti_message_type: this.ltiMessageType, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/index.ts b/apps/server/src/modules/tool/context-external-tool/domain/index.ts index bb51be61682..e0b8c357724 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/index.ts @@ -2,4 +2,6 @@ export * from './context-external-tool.do'; export * from './context-ref'; export * from './tool-reference'; export * from './event'; -export { RestrictedContextMismatchLoggableException } from './error'; +export * from './error'; +export { LtiDeepLink } from './lti-deep-link'; +export { LtiDeepLinkToken, LtiDeepLinkTokenProps } from './lti-deep-link-token'; diff --git a/apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link-token.ts b/apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link-token.ts new file mode 100644 index 00000000000..3ad685a9f36 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link-token.ts @@ -0,0 +1,24 @@ +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { EntityId } from '@shared/domain/types'; + +export interface LtiDeepLinkTokenProps extends AuthorizableObject { + state: string; + + userId: EntityId; + + expiresAt: Date; +} + +export class LtiDeepLinkToken extends DomainObject { + get state(): string { + return this.props.state; + } + + get userId(): EntityId { + return this.props.userId; + } + + get expiresAt(): Date { + return this.props.expiresAt; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link.ts b/apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link.ts new file mode 100644 index 00000000000..f2695e92391 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/domain/lti-deep-link.ts @@ -0,0 +1,33 @@ +import { CustomParameterEntry } from '../../common/domain'; + +export class LtiDeepLink { + mediaType: string; + + url?: string; + + title?: string; + + text?: string; + + parameters: CustomParameterEntry[]; + + availableFrom?: Date; + + availableUntil?: Date; + + submissionFrom?: Date; + + submissionUntil?: Date; + + constructor(props: LtiDeepLink) { + this.mediaType = props.mediaType; + this.url = props.url; + this.title = props.title; + this.text = props.text; + this.parameters = props.parameters; + this.availableFrom = props.availableFrom; + this.availableUntil = props.availableUntil; + this.submissionFrom = props.submissionFrom; + this.submissionUntil = props.submissionUntil; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts b/apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts index 4db5fe8f120..76e857c3ece 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts @@ -1,4 +1,5 @@ import { ContextExternalToolConfigurationStatus } from '../../common/domain'; +import { LtiDeepLink } from './lti-deep-link'; export class ToolReference { contextToolId: string; @@ -15,6 +16,10 @@ export class ToolReference { status: ContextExternalToolConfigurationStatus; + isLtiDeepLinkingTool: boolean; + + ltiDeepLink?: LtiDeepLink; + constructor(toolReference: ToolReference) { this.contextToolId = toolReference.contextToolId; this.description = toolReference.description; @@ -23,5 +28,7 @@ export class ToolReference { this.displayName = toolReference.displayName; this.openInNewTab = toolReference.openInNewTab; this.status = toolReference.status; + this.isLtiDeepLinkingTool = toolReference.isLtiDeepLinkingTool; + this.ltiDeepLink = toolReference.ltiDeepLink; } } diff --git a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts index 17e415eb7d3..98581c15a41 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.ts @@ -5,6 +5,7 @@ import { EntityId } from '@shared/domain/types'; import { CustomParameterEntryEntity } from '../../common/entity'; import { SchoolExternalToolEntity } from '../../school-external-tool/entity'; import { ContextExternalToolType } from './context-external-tool-type.enum'; +import { LtiDeepLinkEmbeddable } from './lti-deep-link.embeddable'; export interface ContextExternalToolEntityProps { id?: EntityId; @@ -18,6 +19,8 @@ export interface ContextExternalToolEntityProps { displayName?: string; parameters?: CustomParameterEntryEntity[]; + + ltiDeepLink?: LtiDeepLinkEmbeddable; } @Entity({ tableName: 'context-external-tools' }) @@ -37,6 +40,9 @@ export class ContextExternalToolEntity extends BaseEntityWithTimestamps { @Embedded(() => CustomParameterEntryEntity, { array: true }) parameters: CustomParameterEntryEntity[]; + @Embedded(() => LtiDeepLinkEmbeddable, { nullable: true, object: true }) + ltiDeepLink?: LtiDeepLinkEmbeddable; + constructor(props: ContextExternalToolEntityProps) { super(); if (props.id) { @@ -47,5 +53,6 @@ export class ContextExternalToolEntity extends BaseEntityWithTimestamps { this.contextType = props.contextType; this.displayName = props.displayName; this.parameters = props.parameters ?? []; + this.ltiDeepLink = props.ltiDeepLink; } } diff --git a/apps/server/src/modules/tool/context-external-tool/entity/index.ts b/apps/server/src/modules/tool/context-external-tool/entity/index.ts index cc6609164e4..9399a382d9c 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/index.ts @@ -1,2 +1,4 @@ export * from './context-external-tool.entity'; export * from './context-external-tool-type.enum'; +export { LtiDeepLinkEmbeddable } from './lti-deep-link.embeddable'; +export { LtiDeepLinkTokenEntity, LtiDeepLinkTokenEntityProps } from './lti-deep-link-token.entity'; diff --git a/apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link-token.entity.ts b/apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link-token.entity.ts new file mode 100644 index 00000000000..ebc8396a7ca --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link-token.entity.ts @@ -0,0 +1,38 @@ +import { Entity, Index, ManyToOne, Property, Unique } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { User } from '@shared/domain/entity/user.entity'; +import { EntityId } from '@shared/domain/types'; + +export interface LtiDeepLinkTokenEntityProps { + id?: EntityId; + + state: string; + + user: User; + + expiresAt: Date; +} + +@Entity({ tableName: 'lti-deep-link-token' }) +export class LtiDeepLinkTokenEntity extends BaseEntityWithTimestamps { + @Unique() + @Property() + state: string; + + @ManyToOne(() => User) + user: User; + + @Index({ options: { expireAfterSeconds: 0 } }) + @Property() + expiresAt: Date; + + constructor(props: LtiDeepLinkTokenEntityProps) { + super(); + if (props.id) { + this.id = props.id; + } + this.state = props.state; + this.user = props.user; + this.expiresAt = props.expiresAt; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link.embeddable.ts b/apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link.embeddable.ts new file mode 100644 index 00000000000..31889a2aee1 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/entity/lti-deep-link.embeddable.ts @@ -0,0 +1,44 @@ +import { Embeddable, Embedded, Property } from '@mikro-orm/core'; +import { CustomParameterEntryEntity } from '../../common/entity'; + +@Embeddable() +export class LtiDeepLinkEmbeddable { + @Property() + mediaType: string; + + @Property({ nullable: true }) + url?: string; + + @Property({ nullable: true }) + title?: string; + + @Property({ nullable: true }) + text?: string; + + @Embedded(() => CustomParameterEntryEntity, { array: true }) + parameters: CustomParameterEntryEntity[]; + + @Property({ nullable: true }) + availableFrom?: Date; + + @Property({ nullable: true }) + availableUntil?: Date; + + @Property({ nullable: true }) + submissionFrom?: Date; + + @Property({ nullable: true }) + submissionUntil?: Date; + + constructor(props: LtiDeepLinkEmbeddable) { + this.mediaType = props.mediaType; + this.title = props.title; + this.url = props.url; + this.parameters = props.parameters; + this.text = props.text; + this.availableFrom = props.availableFrom; + this.availableUntil = props.availableUntil; + this.submissionFrom = props.submissionFrom; + this.submissionUntil = props.submissionUntil; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts index 0da038d6124..b9d0d5fa18d 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts @@ -1,6 +1,6 @@ -import { ToolStatusResponseMapper } from '../../common/mapper/tool-status-response.mapper'; +import { ToolStatusResponseMapper } from '../../common/mapper'; import { CustomParameterEntryParam, CustomParameterEntryResponse } from '../../school-external-tool/controller/dto'; -import { ContextExternalToolResponse, ToolReferenceResponse } from '../controller/dto'; +import { ContextExternalToolResponse, LtiDeepLinkResponse, ToolReferenceResponse } from '../controller/dto'; import { ContextExternalTool, ToolReference } from '../domain'; export class ContextExternalToolResponseMapper { @@ -43,6 +43,19 @@ export class ContextExternalToolResponseMapper { } static mapToToolReferenceResponse(toolReference: ToolReference): ToolReferenceResponse { + const { ltiDeepLink } = toolReference; + const ltiDeepLinkResponse: LtiDeepLinkResponse | undefined = ltiDeepLink + ? new LtiDeepLinkResponse({ + mediaType: ltiDeepLink.mediaType, + title: ltiDeepLink.title, + text: ltiDeepLink.text, + availableFrom: ltiDeepLink.availableFrom, + availableUntil: ltiDeepLink.availableUntil, + submissionFrom: ltiDeepLink.submissionFrom, + submissionUntil: ltiDeepLink.submissionUntil, + }) + : undefined; + const response = new ToolReferenceResponse({ contextToolId: toolReference.contextToolId, description: toolReference.description, @@ -51,6 +64,8 @@ export class ContextExternalToolResponseMapper { thumbnailUrl: toolReference.thumbnailUrl, openInNewTab: toolReference.openInNewTab, status: ToolStatusResponseMapper.mapToResponse(toolReference.status), + isLtiDeepLinkingTool: toolReference.isLtiDeepLinkingTool, + ltiDeepLink: ltiDeepLinkResponse, }); return response; diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/index.ts b/apps/server/src/modules/tool/context-external-tool/mapper/index.ts index 427f02a713a..31c1924280e 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/index.ts @@ -1,3 +1,4 @@ export * from './context-external-tool-request.mapper'; export * from './context-external-tool-response.mapper'; export * from './tool-reference.mapper'; +export { LtiDeepLinkRequestMapper } from './lti-deep-link-request.mapper'; diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/lti-deep-link-request.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/lti-deep-link-request.mapper.ts new file mode 100644 index 00000000000..92ec4342528 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/mapper/lti-deep-link-request.mapper.ts @@ -0,0 +1,32 @@ +import { CustomParameterEntry } from '../../common/domain'; +import { Lti11DeepLinkContentItemParams, Lti11DeepLinkParams } from '../controller/dto'; +import { LtiDeepLink } from '../domain'; + +export class LtiDeepLinkRequestMapper { + public static mapRequestToDO(params: Lti11DeepLinkParams): LtiDeepLink | undefined { + const contentItem: Lti11DeepLinkContentItemParams | undefined = params.content_items?.['@graph'][0]; + + let parameters: CustomParameterEntry[] = []; + if (contentItem?.custom) { + parameters = Object.entries(contentItem.custom).map( + ([key, value]: [string, string]) => new CustomParameterEntry({ name: key, value }) + ); + } + + const deepLink: LtiDeepLink | undefined = contentItem + ? new LtiDeepLink({ + mediaType: contentItem.mediaType, + title: contentItem.title, + text: contentItem.text, + url: contentItem.url, + parameters, + availableFrom: contentItem.available?.startDatetime, + availableUntil: contentItem.available?.endDatetime, + submissionFrom: contentItem.submission?.startDatetime, + submissionUntil: contentItem.submission?.endDatetime, + }) + : undefined; + + return deepLink; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts index b75c6759dda..3d3aeaf2cf3 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts @@ -16,6 +16,8 @@ export class ToolReferenceMapper { displayName: contextExternalTool.displayName ?? externalTool.name, status, openInNewTab: externalTool.openNewTab, + isLtiDeepLinkingTool: externalTool.isLtiDeepLinkingTool(), + ltiDeepLink: contextExternalTool.ltiDeepLink, }); return toolReference; diff --git a/apps/server/src/modules/tool/context-external-tool/repo/index.ts b/apps/server/src/modules/tool/context-external-tool/repo/index.ts new file mode 100644 index 00000000000..a3f2b765f9f --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/index.ts @@ -0,0 +1,2 @@ +export { LtiDeepLinkTokenRepo, LTI_DEEP_LINK_TOKEN_REPO } from './lti-deep-link-token.repo.interface'; +export { LtiDeepLinkTokenMikroOrmRepo } from './mikro-orm'; diff --git a/apps/server/src/modules/tool/context-external-tool/repo/lti-deep-link-token.repo.interface.ts b/apps/server/src/modules/tool/context-external-tool/repo/lti-deep-link-token.repo.interface.ts new file mode 100644 index 00000000000..6f2e6356fdd --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/lti-deep-link-token.repo.interface.ts @@ -0,0 +1,11 @@ +import { LtiDeepLinkToken } from '../domain'; + +export interface LtiDeepLinkTokenRepo { + save(domainObject: LtiDeepLinkToken): Promise; + + delete(domainObject: LtiDeepLinkToken): Promise; + + findByState(state: string): Promise; +} + +export const LTI_DEEP_LINK_TOKEN_REPO = 'LTI_DEEP_LINK_TOKEN_REPO'; diff --git a/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/index.ts b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/index.ts new file mode 100644 index 00000000000..61fc9fc4ab9 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/index.ts @@ -0,0 +1 @@ +export { LtiDeepLinkTokenMikroOrmRepo } from './lti-deep-link-token.repo'; diff --git a/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.spec.ts b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.spec.ts new file mode 100644 index 00000000000..7bef843ce8b --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.spec.ts @@ -0,0 +1,133 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections } from '@shared/testing'; +import { LtiDeepLinkToken } from '../../domain'; +import { LtiDeepLinkTokenEntity } from '../../entity'; +import { ltiDeepLinkTokenEntityFactory, ltiDeepLinkTokenFactory } from '../../testing'; +import { LTI_DEEP_LINK_TOKEN_REPO } from '../lti-deep-link-token.repo.interface'; +import { LtiDeepLinkTokenMikroOrmRepo } from './lti-deep-link-token.repo'; +import { LtiDeepLinkTokenEntityMapper } from './mapper'; + +describe(LtiDeepLinkTokenMikroOrmRepo.name, () => { + let module: TestingModule; + let repo: LtiDeepLinkTokenMikroOrmRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [{ provide: LTI_DEEP_LINK_TOKEN_REPO, useClass: LtiDeepLinkTokenMikroOrmRepo }], + }).compile(); + + repo = module.get(LTI_DEEP_LINK_TOKEN_REPO); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('save', () => { + describe('when a new object is provided', () => { + const setup = () => { + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build(); + + return { + ltiDeepLinkToken, + }; + }; + + it('should create a new entity', async () => { + const { ltiDeepLinkToken } = setup(); + + await repo.save(ltiDeepLinkToken); + + await expect(em.findOneOrFail(LtiDeepLinkTokenEntity, ltiDeepLinkToken.id)).resolves.toBeDefined(); + }); + + it('should return the object', async () => { + const { ltiDeepLinkToken } = setup(); + + const result = await repo.save(ltiDeepLinkToken); + + expect(result).toEqual(ltiDeepLinkToken); + }); + }); + + describe('when an entity with the id exists', () => { + const setup = async () => { + const ltiDeepLinkTokenEntity = ltiDeepLinkTokenEntityFactory.build({ + state: 'token1', + }); + + await em.persistAndFlush(ltiDeepLinkTokenEntity); + em.clear(); + + const ltiDeepLinkToken = new LtiDeepLinkToken({ + ...LtiDeepLinkTokenEntityMapper.mapEntityToDo(ltiDeepLinkTokenEntity).getProps(), + state: 'token2', + }); + + return { + ltiDeepLinkToken, + }; + }; + + it('should update the entity', async () => { + const { ltiDeepLinkToken } = await setup(); + + await repo.save(ltiDeepLinkToken); + + await expect(em.findOneOrFail(LtiDeepLinkTokenEntity, ltiDeepLinkToken.id)).resolves.toEqual( + expect.objectContaining({ state: 'token2' }) + ); + }); + + it('should return the object', async () => { + const { ltiDeepLinkToken } = await setup(); + + const result = await repo.save(ltiDeepLinkToken); + + expect(result).toEqual(ltiDeepLinkToken); + }); + }); + }); + + describe('findByState', () => { + describe('when a state without a saved token is provided', () => { + it('should return null', async () => { + const result = await repo.findByState('state'); + + expect(result).toBeNull(); + }); + }); + + describe('when a state with a saved token is provided', () => { + const setup = async () => { + const ltiDeepLinkTokenEntity = ltiDeepLinkTokenEntityFactory.buildWithId(); + + await em.persistAndFlush([ltiDeepLinkTokenEntity]); + em.clear(); + + const ltiDeepLinkToken = LtiDeepLinkTokenEntityMapper.mapEntityToDo(ltiDeepLinkTokenEntity); + + return { + ltiDeepLinkToken, + }; + }; + + it('should return the latest session token domain object', async () => { + const { ltiDeepLinkToken } = await setup(); + + const result = await repo.findByState(ltiDeepLinkToken.state); + + expect(result).toEqual(ltiDeepLinkToken); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.ts b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.ts new file mode 100644 index 00000000000..ff74a88b4f4 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/lti-deep-link-token.repo.ts @@ -0,0 +1,33 @@ +import { EntityData, EntityName } from '@mikro-orm/core'; +import { Injectable } from '@nestjs/common'; +import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; +import { LtiDeepLinkToken } from '../../domain'; +import { LtiDeepLinkTokenEntity } from '../../entity'; +import { LtiDeepLinkTokenRepo } from '../lti-deep-link-token.repo.interface'; +import { LtiDeepLinkTokenEntityMapper } from './mapper'; + +@Injectable() +export class LtiDeepLinkTokenMikroOrmRepo + extends BaseDomainObjectRepo + implements LtiDeepLinkTokenRepo +{ + protected get entityName(): EntityName { + return LtiDeepLinkTokenEntity; + } + + protected mapDOToEntityProperties(entityDO: LtiDeepLinkToken): EntityData { + return LtiDeepLinkTokenEntityMapper.mapDOToEntityProperties(entityDO, this.em); + } + + async findByState(state: string): Promise { + const sessionTokenEntity: LtiDeepLinkTokenEntity | null = await this.em.findOne(this.entityName, { state }); + + if (!sessionTokenEntity) { + return null; + } + + const sessionToken: LtiDeepLinkToken = LtiDeepLinkTokenEntityMapper.mapEntityToDo(sessionTokenEntity); + + return sessionToken; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/index.ts b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/index.ts new file mode 100644 index 00000000000..aac11e9acb9 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/index.ts @@ -0,0 +1 @@ +export { LtiDeepLinkTokenEntityMapper } from './lti-deep-link-token-entity.mapper'; diff --git a/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/lti-deep-link-token-entity.mapper.ts b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/lti-deep-link-token-entity.mapper.ts new file mode 100644 index 00000000000..9a9d54cb4ab --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/repo/mikro-orm/mapper/lti-deep-link-token-entity.mapper.ts @@ -0,0 +1,31 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { User } from '@shared/domain/entity'; +import { LtiDeepLinkToken } from '../../../domain'; +import { LtiDeepLinkTokenEntity, LtiDeepLinkTokenEntityProps } from '../../../entity'; + +export class LtiDeepLinkTokenEntityMapper { + public static mapDOToEntityProperties( + domainObject: LtiDeepLinkToken, + em: EntityManager + ): LtiDeepLinkTokenEntityProps { + const entityProps: LtiDeepLinkTokenEntityProps = { + id: domainObject.id, + state: domainObject.state, + user: em.getReference(User, domainObject.userId), + expiresAt: domainObject.expiresAt, + }; + + return entityProps; + } + + public static mapEntityToDo(entity: LtiDeepLinkTokenEntity): LtiDeepLinkToken { + const domainObject = new LtiDeepLinkToken({ + id: entity.id, + userId: entity.user.id, + state: entity.state, + expiresAt: entity.expiresAt, + }); + + return domainObject; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts index 0f5a7f19601..a73bfe44a20 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts @@ -1,23 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ContextExternalToolNameAlreadyExistsLoggableException } from '@modules/tool/common/domain'; -import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; import { CommonToolValidationService } from '../../common/service'; -import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { externalToolFactory } from '../../external-tool/testing'; import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ContextExternalTool } from '../domain'; +import { schoolExternalToolFactory } from '../../school-external-tool/testing'; import { contextExternalToolFactory } from '../testing'; import { ContextExternalToolValidationService } from './context-external-tool-validation.service'; -import { ContextExternalToolService } from './context-external-tool.service'; describe('ContextExternalToolValidationService', () => { let module: TestingModule; let service: ContextExternalToolValidationService; - let contextExternalToolService: DeepMocked; let externalToolService: DeepMocked; let schoolExternalToolService: DeepMocked; let commonToolValidationService: DeepMocked; @@ -26,10 +21,6 @@ describe('ContextExternalToolValidationService', () => { module = await Test.createTestingModule({ providers: [ ContextExternalToolValidationService, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, { provide: ExternalToolService, useValue: createMock(), @@ -46,7 +37,6 @@ describe('ContextExternalToolValidationService', () => { }).compile(); service = module.get(ContextExternalToolValidationService); - contextExternalToolService = module.get(ContextExternalToolService); externalToolService = module.get(ExternalToolService); schoolExternalToolService = module.get(SchoolExternalToolService); commonToolValidationService = module.get(CommonToolValidationService); @@ -61,17 +51,21 @@ describe('ContextExternalToolValidationService', () => { }); describe('validate', () => { - describe('when no tool with the name exists in the context', () => { + describe('when a tool is valid', () => { const setup = () => { - const externalTool: ExternalTool = externalToolFactory.buildWithId(); - externalToolService.findById.mockResolvedValue(externalTool); - - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + const externalTool = externalToolFactory.build(); + const schoolExternalTool = schoolExternalToolFactory.build({ + toolId: externalTool.id, + }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + }, displayName: 'Tool 1', }); - contextExternalToolService.findContextExternalTools.mockResolvedValue([ - contextExternalToolFactory.buildWithId({ displayName: 'Tool 2' }), - ]); + + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); commonToolValidationService.validateParameters.mockReturnValue([]); return { @@ -80,17 +74,6 @@ describe('ContextExternalToolValidationService', () => { }; }; - it('should call contextExternalToolService.findContextExternalTools', async () => { - const { contextExternalTool } = setup(); - - await service.validate(contextExternalTool); - - expect(contextExternalToolService.findContextExternalTools).toBeCalledWith({ - schoolToolRef: contextExternalTool.schoolToolRef, - context: contextExternalTool.contextRef, - }); - }); - it('should call schoolExternalToolService.getSchoolExternalToolById', async () => { const { contextExternalTool } = setup(); @@ -107,83 +90,32 @@ describe('ContextExternalToolValidationService', () => { expect(commonToolValidationService.validateParameters).toBeCalledWith(externalTool, contextExternalTool); }); - it('should not throw UnprocessableEntityException', async () => { + it('should not throw', async () => { const { contextExternalTool } = setup(); const func = () => service.validate(contextExternalTool); - await expect(func()).resolves.not.toThrowError(UnprocessableEntityException); - }); - }); - - describe('when a tool with the same name already exists in that context', () => { - describe('when the displayName is undefined', () => { - const setup = () => { - const contextExternalTool1 = contextExternalToolFactory.buildWithId({ displayName: undefined }); - const contextExternalTool2 = contextExternalToolFactory.buildWithId({ displayName: undefined }); - - contextExternalToolService.findContextExternalTools.mockResolvedValue([contextExternalTool2]); - - return { - contextExternalTool1, - }; - }; - - it('should throw ValidationError', async () => { - const { contextExternalTool1 } = setup(); - - const func = () => service.validate(contextExternalTool1); - - await expect(func()).rejects.toThrowError( - new ContextExternalToolNameAlreadyExistsLoggableException( - contextExternalTool1.id, - contextExternalTool1.displayName - ) - ); - }); - }); - - describe('when the displayName is the same', () => { - const setup = () => { - const contextExternalTool1 = contextExternalToolFactory.buildWithId({ displayName: 'Existing Tool' }); - const contextExternalTool2 = contextExternalToolFactory.buildWithId({ displayName: 'Existing Tool' }); - - contextExternalToolService.findContextExternalTools.mockResolvedValue([contextExternalTool2]); - - return { - contextExternalTool1, - }; - }; - - it('should throw ValidationError', async () => { - const { contextExternalTool1 } = setup(); - - const func = () => service.validate(contextExternalTool1); - - await expect(func()).rejects.toThrowError( - new ContextExternalToolNameAlreadyExistsLoggableException( - contextExternalTool1.id, - contextExternalTool1.displayName - ) - ); - }); + await expect(func()).resolves.not.toThrow(); }); }); describe('when the parameter validation fails', () => { const setup = () => { - const externalTool: ExternalTool = externalToolFactory.buildWithId(); - - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + const externalTool = externalToolFactory.build(); + const schoolExternalTool = schoolExternalToolFactory.build({ + toolId: externalTool.id, + }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + }, displayName: 'Tool 1', }); const error: ValidationError = new ValidationError(''); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); externalToolService.findById.mockResolvedValue(externalTool); - contextExternalToolService.findContextExternalTools.mockResolvedValue([ - contextExternalToolFactory.buildWithId({ displayName: 'Tool 2' }), - ]); commonToolValidationService.validateParameters.mockReturnValue([error]); return { diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts index 97bb49e1882..f83c6d2c597 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts @@ -1,26 +1,21 @@ import { Injectable } from '@nestjs/common'; import { ValidationError } from '@shared/common'; -import { ContextExternalToolNameAlreadyExistsLoggableException } from '@modules/tool/common/domain/error/context-external-tool-name-already-exists.loggable-exception'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ContextExternalTool } from '../domain'; -import { ContextExternalToolService } from './context-external-tool.service'; @Injectable() export class ContextExternalToolValidationService { constructor( - private readonly contextExternalToolService: ContextExternalToolService, private readonly externalToolService: ExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, private readonly commonToolValidationService: CommonToolValidationService ) {} async validate(contextExternalTool: ContextExternalTool): Promise { - await this.checkDuplicateUsesInContext(contextExternalTool); - const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); @@ -36,21 +31,4 @@ export class ContextExternalToolValidationService { throw errors[0]; } } - - private async checkDuplicateUsesInContext(contextExternalTool: ContextExternalTool) { - let duplicate: ContextExternalTool[] = await this.contextExternalToolService.findContextExternalTools({ - schoolToolRef: contextExternalTool.schoolToolRef, - context: contextExternalTool.contextRef, - }); - - // Only leave tools that are not the currently handled tool itself (for updates) or ones with the same name - duplicate = duplicate.filter( - (duplicateTool) => - duplicateTool.id !== contextExternalTool.id && duplicateTool.displayName === contextExternalTool.displayName - ); - - if (duplicate.length > 0) { - throw new ContextExternalToolNameAlreadyExistsLoggableException(duplicate[0].id, duplicate[0].displayName); - } - } } diff --git a/apps/server/src/modules/tool/context-external-tool/service/index.ts b/apps/server/src/modules/tool/context-external-tool/service/index.ts index ca5dd69b3f3..d6c56cef556 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/index.ts @@ -2,3 +2,5 @@ export * from './context-external-tool.service'; export * from './context-external-tool-authorizable.service'; export * from './tool-reference.service'; export { ToolConfigurationStatusService } from './tool-configuration-status.service'; +export { LtiDeepLinkingService } from './lti-deep-linking.service'; +export { LtiDeepLinkTokenService } from './lti-deep-link-token.service'; diff --git a/apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.spec.ts new file mode 100644 index 00000000000..d217bad54cb --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.spec.ts @@ -0,0 +1,108 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ToolConfig } from '../../tool-config'; +import { LtiDeepLinkToken } from '../domain'; +import { LTI_DEEP_LINK_TOKEN_REPO, LtiDeepLinkTokenRepo } from '../repo'; +import { ltiDeepLinkTokenFactory } from '../testing'; +import { LtiDeepLinkTokenService } from './lti-deep-link-token.service'; + +describe(LtiDeepLinkTokenService.name, () => { + let module: TestingModule; + let service: LtiDeepLinkTokenService; + + let ltiDeepLinkTokenRepo: DeepMocked; + let configService: DeepMocked>; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LtiDeepLinkTokenService, + { + provide: LTI_DEEP_LINK_TOKEN_REPO, + useValue: createMock(), + }, + { + provide: ConfigService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(LtiDeepLinkTokenService); + ltiDeepLinkTokenRepo = module.get(LTI_DEEP_LINK_TOKEN_REPO); + configService = module.get(ConfigService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('generateToken', () => { + describe('when generating a token', () => { + const setup = () => { + jest.useFakeTimers().setSystemTime(new Date('2024-01-01')); + const tokenDuration = 2000; + + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build({ + expiresAt: new Date(Date.now() + tokenDuration), + }); + + configService.get.mockReturnValueOnce(tokenDuration); + ltiDeepLinkTokenRepo.save.mockResolvedValueOnce(ltiDeepLinkToken); + + return { + ltiDeepLinkToken, + }; + }; + + it('should save a token', async () => { + const { ltiDeepLinkToken } = setup(); + + await service.generateToken(ltiDeepLinkToken.userId); + + expect(ltiDeepLinkTokenRepo.save).toHaveBeenCalledWith( + new LtiDeepLinkToken({ + ...ltiDeepLinkToken.getProps(), + id: expect.any(String), + state: expect.any(String), + }) + ); + }); + + it('should return a token', async () => { + const { ltiDeepLinkToken } = setup(); + + const result = await service.generateToken(ltiDeepLinkToken.userId); + + expect(result).toEqual(ltiDeepLinkToken); + }); + }); + }); + + describe('findByState', () => { + describe('when searching a token by state', () => { + const setup = () => { + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build(); + + ltiDeepLinkTokenRepo.findByState.mockResolvedValueOnce(ltiDeepLinkToken); + + return { + ltiDeepLinkToken, + }; + }; + + it('should return the token', async () => { + const { ltiDeepLinkToken } = setup(); + + const result = await service.findByState(ltiDeepLinkToken.state); + + expect(result).toEqual(ltiDeepLinkToken); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.ts b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.ts new file mode 100644 index 00000000000..81cc64e5ab7 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-link-token.service.ts @@ -0,0 +1,37 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EntityId } from '@shared/domain/types'; +import { UUID } from 'bson'; +import { ToolConfig } from '../../tool-config'; +import { LtiDeepLinkToken } from '../domain'; +import { LTI_DEEP_LINK_TOKEN_REPO, LtiDeepLinkTokenRepo } from '../repo'; + +@Injectable() +export class LtiDeepLinkTokenService { + constructor( + @Inject(LTI_DEEP_LINK_TOKEN_REPO) private readonly ltiDeepLinkTokenRepo: LtiDeepLinkTokenRepo, + private readonly configService: ConfigService + ) {} + + public async generateToken(userId: EntityId): Promise { + const tokenDurationMs = this.configService.get('CTL_TOOLS_RELOAD_TIME_MS'); + + const ltiDeepLinkToken: LtiDeepLinkToken = await this.ltiDeepLinkTokenRepo.save( + new LtiDeepLinkToken({ + id: new ObjectId().toHexString(), + userId, + state: new UUID().toString(), + expiresAt: new Date(Date.now() + tokenDurationMs), + }) + ); + + return ltiDeepLinkToken; + } + + public async findByState(state: string): Promise { + const ltiDeepLinkToken: LtiDeepLinkToken | null = await this.ltiDeepLinkTokenRepo.findByState(state); + + return ltiDeepLinkToken; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.spec.ts new file mode 100644 index 00000000000..e0f2348de72 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.spec.ts @@ -0,0 +1,62 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ToolConfig } from '../../tool-config'; +import { LtiDeepLinkingService } from './lti-deep-linking.service'; + +describe(LtiDeepLinkingService.name, () => { + let module: TestingModule; + let service: LtiDeepLinkingService; + + let configService: DeepMocked>; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LtiDeepLinkingService, + { + provide: ConfigService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(LtiDeepLinkingService); + configService = module.get(ConfigService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getCallbackUrl', () => { + describe('when requesting the callback url for lti 1.1 deep linking', () => { + const setup = () => { + const contextExternalToolId = new ObjectId().toHexString(); + const publicBackendUrl = 'https://test.com/api'; + + configService.get.mockReturnValueOnce(publicBackendUrl); + + return { + contextExternalToolId, + publicBackendUrl, + }; + }; + + it('should return the callback url', () => { + const { contextExternalToolId, publicBackendUrl } = setup(); + + const result = service.getCallbackUrl(contextExternalToolId); + + expect(result).toEqual( + `${publicBackendUrl}/v3/tools/context-external-tools/${contextExternalToolId}/lti11-deep-link-callback` + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.ts b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.ts new file mode 100644 index 00000000000..4404d5c6204 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/lti-deep-linking.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EntityId } from '@shared/domain/types'; +import { ToolConfig } from '../../tool-config'; + +@Injectable() +export class LtiDeepLinkingService { + constructor(private readonly configService: ConfigService) {} + + public getCallbackUrl(contextExternalToolId: EntityId): string { + const publicBackendUrl: string = this.configService.get('PUBLIC_BACKEND_URL'); + + const callbackUrl = new URL( + `${publicBackendUrl}/v3/tools/context-external-tools/${contextExternalToolId}/lti11-deep-link-callback` + ); + + return callbackUrl.toString(); + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts index e62888c22d8..5b081335e20 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts @@ -1,12 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; +import { LtiMessageType } from '../../common/enum'; import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; -import { externalToolFactory, toolConfigurationStatusFactory } from '../../external-tool/testing'; +import { externalToolFactory, fileRecordRefFactory, toolConfigurationStatusFactory } from '../../external-tool/testing'; import { SchoolExternalToolService } from '../../school-external-tool'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; import { ToolReference } from '../domain'; -import { contextExternalToolFactory } from '../testing'; +import { contextExternalToolFactory, ltiDeepLinkFactory } from '../testing'; import { ContextExternalToolService } from './context-external-tool.service'; import { ToolConfigurationStatusService } from './tool-configuration-status.service'; import { ToolReferenceService } from './tool-reference.service'; @@ -69,13 +70,24 @@ describe('ToolReferenceService', () => { const setup = () => { const userId: string = new ObjectId().toHexString(); const contextExternalToolId = new ObjectId().toHexString(); - const externalTool = externalToolFactory.buildWithId(); + const externalTool = externalToolFactory + .withLti11Config({ + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + }) + .buildWithId({ + thumbnail: fileRecordRefFactory.build(), + }); const schoolExternalTool = schoolExternalToolFactory.buildWithId({ toolId: externalTool.id, }); const contextExternalTool = contextExternalToolFactory .withSchoolExternalToolRef(schoolExternalTool.id) - .buildWithId(undefined, contextExternalToolId); + .buildWithId( + { + ltiDeepLink: ltiDeepLinkFactory.build(), + }, + contextExternalToolId + ); const logoUrl = 'logoUrl'; contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); @@ -135,6 +147,9 @@ describe('ToolReferenceService', () => { }), contextToolId: contextExternalToolId, description: externalTool.description, + thumbnailUrl: externalTool.thumbnail?.getPreviewUrl(), + isLtiDeepLinkingTool: true, + ltiDeepLink: contextExternalTool.ltiDeepLink, }); }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/testing/index.ts b/apps/server/src/modules/tool/context-external-tool/testing/index.ts index fe6d6040ded..a4ee8fd3198 100644 --- a/apps/server/src/modules/tool/context-external-tool/testing/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/testing/index.ts @@ -1,3 +1,8 @@ export { contextExternalToolEntityFactory } from './context-external-tool-entity.factory'; export { contextExternalToolFactory } from './context-external-tool.factory'; export { contextExternalToolConfigurationStatusResponseFactory } from './context-external-tool-configuration-status-response.factory'; +export { ltiDeepLinkFactory } from './lti-deep-link.factory'; +export { ltiDeepLinkTokenFactory } from './lti-deep-link-token.factory'; +export { ltiDeepLinkTokenEntityFactory } from './lti-deep-link-token-entity.factory'; +export { Lti11DeepLinkParamsFactory } from './lti11-deep-link-params.factory'; +export { ltiDeepLinkEmbeddableFactory } from './lti-deep-link-embeddable.factory'; diff --git a/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-embeddable.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-embeddable.factory.ts new file mode 100644 index 00000000000..cdffaa33480 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-embeddable.factory.ts @@ -0,0 +1,20 @@ +import { BaseFactory } from '@shared/testing'; +import { CustomParameterEntry } from '../../common/domain'; +import { LtiDeepLinkEmbeddable } from '../entity'; + +export const ltiDeepLinkEmbeddableFactory = BaseFactory.define( + LtiDeepLinkEmbeddable, + ({ sequence }) => { + return { + mediaType: 'application/vnd.ims.lti.v1.ltiassignment', + title: `Deep Link Content ${sequence}`, + url: 'https://lti.deep.link', + text: 'Deep link description', + parameters: [new CustomParameterEntry({ name: 'dl_param', value: 'dl_value' })], + availableFrom: new Date(), + availableUntil: new Date(), + submissionFrom: new Date(), + submissionUntil: new Date(), + }; + } +); diff --git a/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token-entity.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token-entity.factory.ts new file mode 100644 index 00000000000..2261d29ca68 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token-entity.factory.ts @@ -0,0 +1,18 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory, userFactory } from '@shared/testing'; +import { UUID } from 'bson'; +import { LtiDeepLinkTokenEntity, LtiDeepLinkTokenEntityProps } from '../entity'; + +export const ltiDeepLinkTokenEntityFactory = BaseFactory.define( + LtiDeepLinkTokenEntity, + () => { + const expiryTimestampMs = Date.now() + 1000000; + + return { + id: new ObjectId().toHexString(), + state: new UUID().toString(), + user: userFactory.buildWithId(), + expiresAt: new Date(expiryTimestampMs), + }; + } +); diff --git a/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token.factory.ts new file mode 100644 index 00000000000..8df64cfc115 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link-token.factory.ts @@ -0,0 +1,18 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { UUID } from 'bson'; +import { LtiDeepLinkToken, LtiDeepLinkTokenProps } from '../domain'; + +export const ltiDeepLinkTokenFactory = BaseFactory.define( + LtiDeepLinkToken, + () => { + const expiryTimestampMs = Date.now() + 1000000; + + return { + id: new ObjectId().toHexString(), + state: new UUID().toString(), + userId: new ObjectId().toHexString(), + expiresAt: new Date(expiryTimestampMs), + }; + } +); diff --git a/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link.factory.ts new file mode 100644 index 00000000000..ddaa5216702 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/testing/lti-deep-link.factory.ts @@ -0,0 +1,17 @@ +import { BaseFactory } from '@shared/testing'; +import { CustomParameterEntry } from '../../common/domain'; +import { LtiDeepLink } from '../domain'; + +export const ltiDeepLinkFactory = BaseFactory.define(LtiDeepLink, ({ sequence }) => { + return { + mediaType: 'application/vnd.ims.lti.v1.ltiassignment', + title: `Deep Link Content ${sequence}`, + url: 'https://lti.deep.link', + text: 'Deep link description', + parameters: [new CustomParameterEntry({ name: 'dl_param', value: 'dl_value' })], + availableFrom: new Date(), + availableUntil: new Date(), + submissionFrom: new Date(), + submissionUntil: new Date(), + }; +}); diff --git a/apps/server/src/modules/tool/context-external-tool/testing/lti11-deep-link-params.factory.ts b/apps/server/src/modules/tool/context-external-tool/testing/lti11-deep-link-params.factory.ts new file mode 100644 index 00000000000..21b697a96e0 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/testing/lti11-deep-link-params.factory.ts @@ -0,0 +1,88 @@ +import { UUID } from 'bson'; +import CryptoJS from 'crypto-js'; +import { DeepPartial, Factory } from 'fishery'; +import OAuth, { Authorization, RequestOptions } from 'oauth-1.0a'; +import { Lti11ContentItemType, Lti11DeepLinkParams } from '../controller/dto'; +import { Lti11DeepLinkParamsRaw } from '../controller/dto/lti11-deep-link/lti11-deep-link-raw.params'; + +type Lti11DeepLinkParamsPayload = Omit; + +export const lti11DeepLinkParamsPayloadFactory = Factory.define(() => { + return { + lti_message_type: 'ContentItemSelection', + lti_version: 'LTI-1p0', + data: new UUID().toString(), + content_items: { + '@context': 'context', + '@graph': [ + { + '@type': Lti11ContentItemType.CONTENT_ITEM, + mediaType: 'application/vnd.ims.lti.v1.ltiassignment', + title: 'Deep Link Content', + text: 'descriptive text', + url: 'https://lti.deep.link', + available: { + startDatetime: new Date('2024-01'), + endDatetime: new Date('2024-02'), + }, + submission: { + startDatetime: new Date('2024-01'), + endDatetime: new Date('2024-02'), + }, + custom: { + dl_param: 'dl_value', + }, + }, + ], + }, + oauth_callback: 'about:blank', + }; +}); + +export class Lti11DeepLinkParamsFactory { + private readonly consumer: OAuth; + + constructor( + private readonly url: string = 'https://default.deep-link.url/callback', + private readonly key: string = 'defaultKey', + private readonly secret: string = 'defaultSecret' + ) { + this.consumer = new OAuth({ + consumer: { + key: this.key, + secret: this.secret, + }, + signature_method: 'HMAC-SHA1', + hash_function: (base_string: string, hashKey: string) => + CryptoJS.HmacSHA1(base_string, hashKey).toString(CryptoJS.enc.Base64), + }); + } + + build(params?: DeepPartial): Lti11DeepLinkParams { + const payload: Lti11DeepLinkParamsPayload = lti11DeepLinkParamsPayloadFactory.build(params); + + const requestData: RequestOptions = { + url: this.url, + method: 'POST', + data: payload, + }; + + const authorization: Authorization = this.consumer.authorize(requestData); + + return authorization as Lti11DeepLinkParams; + } + + buildRaw(params?: DeepPartial): Lti11DeepLinkParamsRaw { + const payload: Lti11DeepLinkParamsPayload = lti11DeepLinkParamsPayloadFactory.build(params); + + const requestData: RequestOptions = { + url: this.url, + method: 'POST', + data: { ...payload, content_items: JSON.stringify(payload.content_items) }, + }; + + const authorization: Authorization = this.consumer.authorize(requestData); + + return authorization as Lti11DeepLinkParamsRaw; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts index cda6453c71d..fc4933dafeb 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { Action, @@ -14,31 +15,55 @@ import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { setupEntities, userFactory } from '@shared/testing'; -import { ToolContextType } from '../../common/enum'; +import { UUID } from 'bson'; +import { LtiMessageType, ToolContextType } from '../../common/enum'; +import { Lti11EncryptionService } from '../../common/service'; import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ExternalToolService } from '../../external-tool'; +import { externalToolFactory } from '../../external-tool/testing'; import { SchoolExternalToolService } from '../../school-external-tool'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; -import { ContextExternalTool, ContextExternalToolProps } from '../domain'; -import { ContextExternalToolService } from '../service'; +import { + ContextExternalTool, + ContextExternalToolProps, + InvalidOauthSignatureLoggableException, + InvalidToolTypeLoggableException, + LtiDeepLinkTokenMissingLoggableException, +} from '../domain'; +import { ContextExternalToolService, LtiDeepLinkingService, LtiDeepLinkTokenService } from '../service'; import { ContextExternalToolValidationService } from '../service/context-external-tool-validation.service'; -import { contextExternalToolFactory } from '../testing'; +import { + contextExternalToolFactory, + Lti11DeepLinkParamsFactory, + ltiDeepLinkFactory, + ltiDeepLinkTokenFactory, +} from '../testing'; import { ContextExternalToolUc } from './context-external-tool.uc'; describe(ContextExternalToolUc.name, () => { let module: TestingModule; let uc: ContextExternalToolUc; + let externalToolService: DeepMocked; let schoolExternalToolService: DeepMocked; let contextExternalToolService: DeepMocked; let contextExternalToolValidationService: DeepMocked; let toolPermissionHelper: DeepMocked; let authorizationService: DeepMocked; + let ltiDeepLinkTokenService: DeepMocked; + let ltiDeepLinkingService: DeepMocked; + let lti11EncryptionService: DeepMocked; + let encryptionService: DeepMocked; beforeAll(async () => { await setupEntities(); module = await Test.createTestingModule({ providers: [ ContextExternalToolUc, + { + provide: ExternalToolService, + useValue: createMock(), + }, { provide: SchoolExternalToolService, useValue: createMock(), @@ -59,15 +84,36 @@ describe(ContextExternalToolUc.name, () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: LtiDeepLinkTokenService, + useValue: createMock(), + }, + { + provide: LtiDeepLinkingService, + useValue: createMock(), + }, + { + provide: Lti11EncryptionService, + useValue: createMock(), + }, + { + provide: DefaultEncryptionService, + useValue: createMock(), + }, ], }).compile(); uc = module.get(ContextExternalToolUc); + externalToolService = module.get(ExternalToolService); schoolExternalToolService = module.get(SchoolExternalToolService); contextExternalToolService = module.get(ContextExternalToolService); contextExternalToolValidationService = module.get(ContextExternalToolValidationService); toolPermissionHelper = module.get(ToolPermissionHelper); authorizationService = module.get(AuthorizationService); + ltiDeepLinkTokenService = module.get(LtiDeepLinkTokenService); + ltiDeepLinkingService = module.get(LtiDeepLinkingService); + lti11EncryptionService = module.get(Lti11EncryptionService); + encryptionService = module.get(DefaultEncryptionService); }); afterAll(async () => { @@ -347,6 +393,7 @@ describe(ContextExternalToolUc.name, () => { schoolId, }); + const ltiDeepLink = ltiDeepLinkFactory.build(); const contextExternalTool = contextExternalToolFactory.buildWithId({ displayName: 'Course', schoolToolRef: { @@ -357,6 +404,7 @@ describe(ContextExternalToolUc.name, () => { id: 'contextId', type: ToolContextType.COURSE, }, + ltiDeepLink, }); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); @@ -369,11 +417,12 @@ describe(ContextExternalToolUc.name, () => { contextExternalToolId: contextExternalTool.id, user, schoolId, + ltiDeepLink, }; }; it('should call contextExternalToolService', async () => { - const { contextExternalTool, user, schoolId, contextExternalToolId } = setup(); + const { contextExternalTool, user, schoolId, contextExternalToolId, ltiDeepLink } = setup(); await uc.updateContextExternalTool(user.id, schoolId, contextExternalToolId, contextExternalTool.getProps()); @@ -381,6 +430,7 @@ describe(ContextExternalToolUc.name, () => { expect.objectContaining({ ...contextExternalTool.getProps(), id: expect.any(String), + ltiDeepLink, }) ); }); @@ -823,4 +873,221 @@ describe(ContextExternalToolUc.name, () => { }); }); }); + + describe('updateLtiDeepLink', () => { + describe('when deep linking a content', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const key = 'key'; + const secret = 'secret'; + const state = new UUID().toString(); + const payload = new Lti11DeepLinkParamsFactory().buildRaw({ + data: state, + }); + const ltiDeepLink = ltiDeepLinkFactory.build(); + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build({ userId: user.id, state }); + const externalTool = externalToolFactory + .withLti11Config({ + key, + secret, + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + }) + .build(); + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: { schoolToolId: schoolExternalTool.id, schoolId: user.school.id }, + displayName: 'oldName', + }); + const linkedContextExternalTool = new ContextExternalTool({ + ...contextExternalTool.getProps(), + ltiDeepLink, + displayName: ltiDeepLink.title, + }); + const callbackUrl = 'https://this.cloud/lti-deep-link-callback'; + + ltiDeepLinkTokenService.findByState.mockResolvedValueOnce(ltiDeepLinkToken); + contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + ltiDeepLinkingService.getCallbackUrl.mockReturnValueOnce(callbackUrl); + encryptionService.decrypt.mockReturnValueOnce('decryptedSecret'); + lti11EncryptionService.verify.mockReturnValueOnce(true); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + contextExternalToolService.saveContextExternalTool.mockResolvedValueOnce(linkedContextExternalTool); + + return { + contextExternalTool, + ltiDeepLink, + payload, + user, + key, + secret, + state, + callbackUrl, + linkedContextExternalTool, + }; + }; + + it('should check the oauth signature', async () => { + const { contextExternalTool, payload, ltiDeepLink, key, state, callbackUrl } = setup(); + + await uc.updateLtiDeepLink(contextExternalTool.id, payload, state, ltiDeepLink); + + expect(lti11EncryptionService.verify).toHaveBeenCalledWith(key, 'decryptedSecret', callbackUrl, payload); + }); + + it('should check the user permission', async () => { + const { contextExternalTool, payload, ltiDeepLink, state, user } = setup(); + + await uc.updateLtiDeepLink(contextExternalTool.id, payload, state, ltiDeepLink); + + expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( + user, + contextExternalTool, + AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]) + ); + }); + + it('should should save the linked tool', async () => { + const { contextExternalTool, payload, ltiDeepLink, state, linkedContextExternalTool } = setup(); + + await uc.updateLtiDeepLink(contextExternalTool.id, payload, state, ltiDeepLink); + + expect(contextExternalToolService.saveContextExternalTool).toHaveBeenCalledWith(linkedContextExternalTool); + }); + }); + + describe('when no content was linked', () => { + const setup = () => { + const state = new UUID().toString(); + const payload = new Lti11DeepLinkParamsFactory().buildRaw({ data: state }); + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build({ state }); + const contextExternalTool = contextExternalToolFactory.build(); + + ltiDeepLinkTokenService.findByState.mockResolvedValueOnce(ltiDeepLinkToken); + + return { + contextExternalTool, + payload, + state, + }; + }; + + it('should do nothing', async () => { + const { contextExternalTool, payload, state } = setup(); + + await uc.updateLtiDeepLink(contextExternalTool.id, payload, state); + + expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); + }); + }); + + describe('when deep linking a content', () => { + const setup = () => { + const state = new UUID().toString(); + const payload = new Lti11DeepLinkParamsFactory().buildRaw({ + data: state, + }); + const ltiDeepLink = ltiDeepLinkFactory.build(); + const contextExternalTool = contextExternalToolFactory.build(); + + ltiDeepLinkTokenService.findByState.mockResolvedValueOnce(null); + + return { + contextExternalTool, + payload, + ltiDeepLink, + state, + }; + }; + + it('should throw an error', async () => { + const { contextExternalTool, payload, ltiDeepLink, state } = setup(); + + await expect(uc.updateLtiDeepLink(contextExternalTool.id, payload, state, ltiDeepLink)).rejects.toThrow( + LtiDeepLinkTokenMissingLoggableException + ); + }); + }); + + describe('when the external tool is not an lti 1.1 tool', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const state = new UUID().toString(); + const payload = new Lti11DeepLinkParamsFactory().buildRaw({ + data: state, + }); + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build({ userId: user.id, state }); + const ltiDeepLink = ltiDeepLinkFactory.build(); + const externalTool = externalToolFactory.withBasicConfig().build(); + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: { schoolToolId: schoolExternalTool.id, schoolId: user.school.id }, + }); + + ltiDeepLinkTokenService.findByState.mockResolvedValueOnce(ltiDeepLinkToken); + contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + + return { + contextExternalTool, + ltiDeepLink, + payload, + state, + }; + }; + + it('should throw an error', async () => { + const { contextExternalTool, payload, ltiDeepLink, state } = setup(); + + await expect(uc.updateLtiDeepLink(contextExternalTool.id, payload, state, ltiDeepLink)).rejects.toThrow( + InvalidToolTypeLoggableException + ); + }); + }); + + describe('when the oauth signature is invalid', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const state = new UUID().toString(); + const payload = new Lti11DeepLinkParamsFactory().buildRaw({ + data: state, + }); + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build({ userId: user.id, state }); + const ltiDeepLink = ltiDeepLinkFactory.build(); + const externalTool = externalToolFactory + .withLti11Config({ lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST }) + .build(); + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const contextExternalTool = contextExternalToolFactory.build({ + schoolToolRef: { schoolToolId: schoolExternalTool.id, schoolId: user.school.id }, + }); + const callbackUrl = 'https://this.cloud/lti-deep-link-callback'; + + ltiDeepLinkTokenService.findByState.mockResolvedValueOnce(ltiDeepLinkToken); + contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + ltiDeepLinkingService.getCallbackUrl.mockReturnValueOnce(callbackUrl); + encryptionService.decrypt.mockReturnValueOnce('decryptedSecret'); + lti11EncryptionService.verify.mockReturnValueOnce(false); + + return { + contextExternalTool, + ltiDeepLink, + payload, + state, + }; + }; + + it('should throw an error', async () => { + const { contextExternalTool, payload, ltiDeepLink, state } = setup(); + + await expect(uc.updateLtiDeepLink(contextExternalTool.id, payload, state, ltiDeepLink)).rejects.toThrow( + InvalidOauthSignatureLoggableException + ); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts index dbea96596d3..33c0c7a5cf7 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts @@ -1,3 +1,4 @@ +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { AuthorizationContext, AuthorizationContextBuilder, @@ -5,16 +6,28 @@ import { ForbiddenLoggableException, } from '@modules/authorization'; import { AuthorizableReferenceType } from '@modules/authorization/domain'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { ToolContextType } from '../../common/enum'; +import { Authorization } from 'oauth-1.0a'; +import { ToolConfigType, ToolContextType } from '../../common/enum'; +import { Lti11EncryptionService } from '../../common/service'; import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ExternalToolService } from '../../external-tool'; +import { ExternalTool } from '../../external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ContextExternalTool, ContextRef } from '../domain'; -import { ContextExternalToolService } from '../service'; +import { + ContextExternalTool, + ContextRef, + InvalidOauthSignatureLoggableException, + InvalidToolTypeLoggableException, + LtiDeepLink, + LtiDeepLinkToken, + LtiDeepLinkTokenMissingLoggableException, +} from '../domain'; +import { ContextExternalToolService, LtiDeepLinkingService, LtiDeepLinkTokenService } from '../service'; import { ContextExternalToolValidationService } from '../service/context-external-tool-validation.service'; import { ContextExternalToolDto } from './dto/context-external-tool.types'; @@ -22,10 +35,15 @@ import { ContextExternalToolDto } from './dto/context-external-tool.types'; export class ContextExternalToolUc { constructor( private readonly toolPermissionHelper: ToolPermissionHelper, + private readonly externalToolService: ExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, private readonly contextExternalToolService: ContextExternalToolService, private readonly contextExternalToolValidationService: ContextExternalToolValidationService, - private readonly authorizationService: AuthorizationService + private readonly authorizationService: AuthorizationService, + private readonly ltiDeepLinkTokenService: LtiDeepLinkTokenService, + private readonly ltiDeepLinkingService: LtiDeepLinkingService, + private readonly lti11EncryptionService: Lti11EncryptionService, + @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService ) {} async createContextExternalTool( @@ -81,6 +99,7 @@ export class ContextExternalToolUc { contextExternalTool = new ContextExternalTool({ ...contextExternalToolDto, id: contextExternalTool.id, + ltiDeepLink: contextExternalTool.ltiDeepLink, }); contextExternalTool.schoolToolRef.schoolId = schoolId; @@ -143,4 +162,63 @@ export class ContextExternalToolUc { return toolsWithPermission; } + + public async updateLtiDeepLink( + contextExternalToolId: EntityId, + payload: Authorization, + state: string, + deepLink?: LtiDeepLink + ): Promise { + if (!deepLink) { + return; + } + + const ltiDeepLinkToken: LtiDeepLinkToken | null = await this.ltiDeepLinkTokenService.findByState(state); + + if (!ltiDeepLinkToken) { + throw new LtiDeepLinkTokenMissingLoggableException(state, contextExternalToolId); + } + + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findByIdOrFail( + contextExternalToolId + ); + + await this.checkOauthSignature(contextExternalTool, payload); + + const user: User = await this.authorizationService.getUserWithPermissions(ltiDeepLinkToken.userId); + const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]); + await this.toolPermissionHelper.ensureContextPermissions(user, contextExternalTool, context); + + contextExternalTool.ltiDeepLink = deepLink; + if (deepLink.title) { + contextExternalTool.displayName = deepLink.title; + } + + await this.contextExternalToolService.saveContextExternalTool(contextExternalTool); + } + + private async checkOauthSignature(contextExternalTool: ContextExternalTool, payload: Authorization): Promise { + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( + contextExternalTool.schoolToolRef.schoolToolId + ); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); + + if (!ExternalTool.isLti11Config(externalTool.config)) { + throw new InvalidToolTypeLoggableException(ToolConfigType.LTI11, externalTool.config.type); + } + + const url: string = this.ltiDeepLinkingService.getCallbackUrl(contextExternalTool.id); + const decryptedSecret: string = this.encryptionService.decrypt(externalTool.config.secret); + + const isValidSignature: boolean = this.lti11EncryptionService.verify( + externalTool.config.key, + decryptedSecret, + url, + payload + ); + + if (!isValidSignature) { + throw new InvalidOauthSignatureLoggableException(); + } + } } diff --git a/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts index 4b6519e0bf5..912d27b756d 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts @@ -70,6 +70,7 @@ describe('ToolReferenceUc', () => { isOutdatedOnScopeContext: false, }), openInNewTab: externalTool.openNewTab, + isLtiDeepLinkingTool: false, }); const contextType: ToolContextType = ToolContextType.COURSE; @@ -159,6 +160,7 @@ describe('ToolReferenceUc', () => { isOutdatedOnScopeContext: false, }), openInNewTab: externalTool.openNewTab, + isLtiDeepLinkingTool: false, }); contextExternalToolService.findByIdOrFail.mockResolvedValueOnce(contextExternalTool); diff --git a/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts b/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts index e6d3c1df1ba..ef9c435c155 100644 --- a/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts +++ b/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts @@ -1,7 +1,7 @@ import { InternalServerErrorException } from '@nestjs/common'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { CustomParameter } from '../../common/domain'; -import { ToolConfigType, ToolContextType } from '../../common/enum'; +import { LtiMessageType, ToolConfigType, ToolContextType } from '../../common/enum'; import { BasicToolConfig, ExternalToolConfig, Lti11ToolConfig, Oauth2ToolConfig } from './config'; import { ExternalToolMedium } from './external-tool-medium.do'; import { FileRecordRef } from './file-record-ref'; @@ -212,4 +212,11 @@ export class ExternalTool extends DomainObject { static isLti11Config(config: ExternalToolConfig): config is Lti11ToolConfig { return ToolConfigType.LTI11 === config.type; } + + public isLtiDeepLinkingTool(): boolean { + return ( + ExternalTool.isLti11Config(this.config) && + this.config.lti_message_type === LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST + ); + } } diff --git a/apps/server/src/modules/tool/external-tool/testing/external-tool-datasheet-template-data.factory.ts b/apps/server/src/modules/tool/external-tool/testing/external-tool-datasheet-template-data.factory.ts index 677d33bf4cf..2bf5ad6f2c6 100644 --- a/apps/server/src/modules/tool/external-tool/testing/external-tool-datasheet-template-data.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/external-tool-datasheet-template-data.factory.ts @@ -23,7 +23,7 @@ export class ExternalToolDatasheetTemplateDataFactory extends Factory = { toolType: 'OAuth 2.0', skipConsent: 'Zustimmung überspringen: ja', - toolUrl: 'https://www.oauth2-baseUrl.com/', + toolUrl: 'https://www.oauth2-baseurl.com/', }; return this.params(params); } @@ -33,7 +33,7 @@ export class ExternalToolDatasheetTemplateDataFactory extends Factory( + BasicToolConfigEntity, + () => { + return { + type: ToolConfigType.BASIC, + baseUrl: 'https://mock.de', + }; + } +); + +export const oauth2ToolConfigEntityFactory = BaseFactory.define( + Oauth2ToolConfigEntity, + ({ sequence }) => { + return { + type: ToolConfigType.OAUTH2, + baseUrl: 'https://mock.de', + clientId: `client-${sequence}`, + skipConsent: false, + }; + } +); + +export const lti11ToolConfigEntityFactory = BaseFactory.define( + Lti11ToolConfigEntity, + () => { + return { + type: ToolConfigType.LTI11, + baseUrl: 'https://mock.de', + key: 'key', + secret: 'secret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + launch_presentation_locale: 'de-DE', + }; + } +); + export class ExternalToolEntityFactory extends BaseFactory { withName(name: string): this { const params: DeepPartial = { @@ -28,40 +65,27 @@ export class ExternalToolEntityFactory extends BaseFactory): this { const params: DeepPartial = { - config: new BasicToolConfigEntity({ - type: ToolConfigType.BASIC, - baseUrl: 'mockBaseUrl', - }), + config: basicToolConfigEntityFactory.build(customParam), }; + return this.params(params); } - withOauth2Config(clientId: string): this { + withOauth2Config(customParam?: DeepPartial): this { const params: DeepPartial = { - config: new Oauth2ToolConfigEntity({ - type: ToolConfigType.OAUTH2, - baseUrl: 'mockBaseUrl', - clientId, - skipConsent: false, - }), + config: oauth2ToolConfigEntityFactory.build(customParam), }; + return this.params(params); } - withLti11Config(): this { + withLti11Config(customParam?: DeepPartial): this { const params: DeepPartial = { - config: new Lti11ToolConfigEntity({ - type: ToolConfigType.BASIC, - baseUrl: 'mockBaseUrl', - key: 'key', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - secret: 'secret', - privacy_permission: LtiPrivacyPermission.ANONYMOUS, - launch_presentation_locale: 'de-DE', - }), + config: lti11ToolConfigEntityFactory.build(customParam), }; + return this.params(params); } @@ -114,10 +138,7 @@ export const externalToolEntityFactory = ExternalToolEntityFactory.define( description: 'This is a tool description', url: '', logoUrl: 'https://logourl.com', - config: new BasicToolConfigEntity({ - type: ToolConfigType.BASIC, - baseUrl: 'mockBaseUrl', - }), + config: basicToolConfigEntityFactory.build(), parameters: [customParameterEntityFactory.build()], isHidden: false, isDeactivated: false, diff --git a/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts b/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts index ac3005b33e7..2327eb4eee2 100644 --- a/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts @@ -45,7 +45,7 @@ class Oauth2ToolConfigFactory extends DoBaseFactory { return { type: ToolConfigType.OAUTH2, - baseUrl: 'https://www.oauth2-baseUrl.com/', + baseUrl: 'https://www.oauth2-baseurl.com/', clientId: 'clientId', skipConsent: false, }; @@ -54,7 +54,7 @@ export const oauth2ToolConfigFactory = Oauth2ToolConfigFactory.define(Oauth2Tool export const lti11ToolConfigFactory = DoBaseFactory.define(Lti11ToolConfig, () => { return { type: ToolConfigType.LTI11, - baseUrl: 'https://www.lti11-baseUrl.com/', + baseUrl: 'https://www.lti11-baseurl.com/', key: 'key', secret: 'secret', privacy_permission: LtiPrivacyPermission.PSEUDONYMOUS, diff --git a/apps/server/src/modules/tool/external-tool/testing/file-record-ref.factory.ts b/apps/server/src/modules/tool/external-tool/testing/file-record-ref.factory.ts index 5cde8530834..4439f6ceff5 100644 --- a/apps/server/src/modules/tool/external-tool/testing/file-record-ref.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/file-record-ref.factory.ts @@ -2,16 +2,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Factory } from 'fishery'; import { FileRecordRef } from '../domain'; -export const fileRecordRefFactory = Factory.define(({ sequence }) => { - const fileName = `fileName-${sequence}`; - const fileRecordId = new ObjectId().toHexString(); - - return { - uploadUrl: 'uploadUrl', - fileName, - fileRecordId, - getPreviewUrl(): string { - return `/api/v3/file/preview/${fileRecordId}/${encodeURIComponent(fileName)}`; - }, - }; -}); +export const fileRecordRefFactory = Factory.define( + ({ sequence }) => + new FileRecordRef({ + uploadUrl: 'uploadUrl', + fileName: `fileName-${sequence}`, + fileRecordId: new ObjectId().toHexString(), + }) +); diff --git a/apps/server/src/modules/tool/external-tool/testing/index.ts b/apps/server/src/modules/tool/external-tool/testing/index.ts index 7dd621d10e0..ca4cdceb780 100644 --- a/apps/server/src/modules/tool/external-tool/testing/index.ts +++ b/apps/server/src/modules/tool/external-tool/testing/index.ts @@ -1,4 +1,10 @@ -export { externalToolEntityFactory, customParameterEntityFactory } from './external-tool-entity.factory'; +export { + externalToolEntityFactory, + customParameterEntityFactory, + basicToolConfigEntityFactory, + oauth2ToolConfigEntityFactory, + lti11ToolConfigEntityFactory, +} from './external-tool-entity.factory'; export { externalToolFactory, customParameterFactory, diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 51eec1332ff..1e334abbdd3 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -11,7 +11,7 @@ import { LoggerModule } from '@src/core/logger'; import { LearnroomModule } from '../learnroom'; import { CommonToolModule } from './common'; import { ToolPermissionHelper } from './common/uc/tool-permission-helper'; -import { ToolContextController } from './context-external-tool/controller'; +import { ToolContextController, ToolDeepLinkController } from './context-external-tool/controller'; import { ToolReferenceController } from './context-external-tool/controller/tool-reference.controller'; import { ContextExternalToolUc, ToolReferenceUc } from './context-external-tool/uc'; import { ToolConfigurationController, ToolController } from './external-tool/controller'; @@ -45,6 +45,7 @@ import { ToolModule } from './tool.module'; ToolContextController, ToolReferenceController, ToolController, + ToolDeepLinkController, ], providers: [ LtiToolRepo, diff --git a/apps/server/src/modules/tool/tool-config.ts b/apps/server/src/modules/tool/tool-config.ts index 099a732ab0d..2b65f1b8f3e 100644 --- a/apps/server/src/modules/tool/tool-config.ts +++ b/apps/server/src/modules/tool/tool-config.ts @@ -8,4 +8,5 @@ export interface ToolConfig { FILES_STORAGE__SERVICE_BASE_URL: string; CTL_TOOLS__PREFERRED_TOOLS_LIMIT: number; FEATURE_PREFERRED_CTL_TOOLS_ENABLED: boolean; + PUBLIC_BACKEND_URL: string; } diff --git a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts index 4a695caa828..82f8efba10f 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts @@ -26,7 +26,7 @@ import { } from '../../../external-tool/testing'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; import { schoolExternalToolEntityFactory } from '../../../school-external-tool/testing'; -import { LaunchRequestMethod } from '../../types'; +import { LaunchRequestMethod, LaunchType } from '../../types'; import { ContextExternalToolBodyParams, ContextExternalToolLaunchParams, ToolLaunchRequestResponse } from '../dto'; describe('ToolLaunchController (API)', () => { @@ -123,7 +123,7 @@ describe('ToolLaunchController (API)', () => { method: LaunchRequestMethod.GET, url: 'https://mockurl.de/', openNewTab: true, - isDeepLink: false, + launchType: LaunchType.BASIC, }); }); }); @@ -414,7 +414,7 @@ describe('ToolLaunchController (API)', () => { method: LaunchRequestMethod.GET, url: 'https://mockurl.de/', openNewTab: true, - isDeepLink: false, + launchType: LaunchType.BASIC, }); }); }); diff --git a/apps/server/src/modules/tool/tool-launch/controller/dto/tool-launch-request.response.ts b/apps/server/src/modules/tool/tool-launch/controller/dto/tool-launch-request.response.ts index a488bca921a..fb8f09300ee 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/dto/tool-launch-request.response.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/dto/tool-launch-request.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { LaunchRequestMethod } from '../../types'; +import { LaunchRequestMethod, LaunchType } from '../../types'; export class ToolLaunchRequestResponse { @ApiProperty({ @@ -30,15 +30,17 @@ export class ToolLaunchRequestResponse { openNewTab?: boolean; @ApiProperty({ - description: 'Specifies whether the request is an LTI Deep linking content item selection request', + description: 'Specifies the underlying type of the request', + enum: LaunchType, + enumName: 'LaunchType', }) - isDeepLink: boolean; + launchType: LaunchType; constructor(props: ToolLaunchRequestResponse) { this.url = props.url; this.method = props.method; this.payload = props.payload; this.openNewTab = props.openNewTab; - this.isDeepLink = props.isDeepLink; + this.launchType = props.launchType; } } diff --git a/apps/server/src/modules/tool/tool-launch/mapper/tool-launch.mapper.spec.ts b/apps/server/src/modules/tool/tool-launch/mapper/tool-launch.mapper.spec.ts index 9a095fbf116..5632f65a19d 100644 --- a/apps/server/src/modules/tool/tool-launch/mapper/tool-launch.mapper.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/mapper/tool-launch.mapper.spec.ts @@ -1,6 +1,6 @@ import { CustomParameterLocation, ToolConfigType } from '../../common/enum'; import { ToolLaunchRequestResponse } from '../controller/dto'; -import { LaunchRequestMethod, PropertyLocation, ToolLaunchDataType, ToolLaunchRequest } from '../types'; +import { LaunchRequestMethod, LaunchType, PropertyLocation, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { ToolLaunchMapper } from './tool-launch.mapper'; describe('ToolLaunchMapper', () => { @@ -33,7 +33,7 @@ describe('ToolLaunchMapper', () => { url: 'url', openNewTab: true, payload: 'payload', - isDeepLink: false, + launchType: LaunchType.BASIC, }); const result: ToolLaunchRequestResponse = ToolLaunchMapper.mapToToolLaunchRequestResponse(toolLaunchRequest); @@ -43,7 +43,7 @@ describe('ToolLaunchMapper', () => { url: toolLaunchRequest.url, payload: toolLaunchRequest.payload, openNewTab: toolLaunchRequest.openNewTab, - isDeepLink: toolLaunchRequest.isDeepLink, + launchType: LaunchType.BASIC, }); }); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/index.ts b/apps/server/src/modules/tool/tool-launch/service/index.ts index 9b8c27189b8..7da3dfd168e 100644 --- a/apps/server/src/modules/tool/tool-launch/service/index.ts +++ b/apps/server/src/modules/tool/tool-launch/service/index.ts @@ -1,2 +1 @@ export * from './tool-launch.service'; -export * from './lti11-encryption.service'; diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts index c2a049acef4..146e0eae91b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.spec.ts @@ -17,7 +17,7 @@ import { customParameterFactory, externalToolFactory } from '../../../external-t import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { schoolExternalToolFactory } from '../../../school-external-tool/testing'; import { MissingToolParameterValueLoggableException, ParameterTypeNotImplementedLoggableException } from '../../error'; -import { LaunchRequestMethod, PropertyData, PropertyLocation, ToolLaunchRequest } from '../../types'; +import { LaunchRequestMethod, LaunchType, PropertyData, PropertyLocation, ToolLaunchRequest } from '../../types'; import { AutoContextIdStrategy, AutoContextNameStrategy, @@ -60,6 +60,10 @@ class TestLaunchStrategy extends AbstractLaunchStrategy { public override determineLaunchRequestMethod(properties: PropertyData[]): LaunchRequestMethod { return launchMethod; } + + determineLaunchType(): LaunchType { + return LaunchType.BASIC; + } } describe(AbstractLaunchStrategy.name, () => { @@ -345,8 +349,8 @@ describe(AbstractLaunchStrategy.name, () => { url: expectedUrl.toString(), method: strategy.determineLaunchRequestMethod(expectedProperties), openNewTab: false, - isDeepLink: false, payload: strategy.buildToolLaunchRequestPayload(expectedUrl.toString(), expectedProperties), + launchType: strategy.determineLaunchType(), }); }); }); @@ -385,8 +389,8 @@ describe(AbstractLaunchStrategy.name, () => { url: externalTool.config.baseUrl, method: strategy.determineLaunchRequestMethod([concreteConfigParameter]), openNewTab: false, - isDeepLink: false, payload: strategy.buildToolLaunchRequestPayload(externalTool.config.baseUrl, [concreteConfigParameter]), + launchType: strategy.determineLaunchType(), }); }); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts index 7d5b413a9f7..bf4885ff247 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/abstract-launch.strategy.ts @@ -8,15 +8,22 @@ import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { MissingToolParameterValueLoggableException, ParameterTypeNotImplementedLoggableException } from '../../error'; import { ToolLaunchMapper } from '../../mapper'; -import { LaunchRequestMethod, PropertyData, PropertyLocation, ToolLaunchData, ToolLaunchRequest } from '../../types'; +import { + LaunchRequestMethod, + LaunchType, + PropertyData, + PropertyLocation, + ToolLaunchData, + ToolLaunchRequest, +} from '../../types'; import { AutoContextIdStrategy, AutoContextNameStrategy, + AutoGroupExternalUuidStrategy, AutoMediumIdStrategy, AutoParameterStrategy, AutoSchoolIdStrategy, AutoSchoolNumberStrategy, - AutoGroupExternalUuidStrategy, } from '../auto-parameter-strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; import { ToolLaunchStrategy } from './tool-launch-strategy.interface'; @@ -52,7 +59,9 @@ export abstract class AbstractLaunchStrategy implements ToolLaunchStrategy { public abstract determineLaunchRequestMethod(properties: PropertyData[]): LaunchRequestMethod; - public async createLaunchRequest(userId: EntityId, data: ToolLaunchParams): Promise { + public abstract determineLaunchType(): LaunchType; + + protected async createLaunchData(userId: EntityId, data: ToolLaunchParams): Promise { const launchData: ToolLaunchData = this.buildToolLaunchDataFromExternalTool(data.externalTool); const launchDataProperties: PropertyData[] = await this.buildToolLaunchDataFromTools(data); @@ -64,6 +73,12 @@ export abstract class AbstractLaunchStrategy implements ToolLaunchStrategy { launchData.properties.push(...launchDataProperties); launchData.properties.push(...additionalLaunchDataProperties); + return launchData; + } + + public async createLaunchRequest(userId: EntityId, data: ToolLaunchParams): Promise { + const launchData: ToolLaunchData = await this.createLaunchData(userId, data); + const requestMethod: LaunchRequestMethod = this.determineLaunchRequestMethod(launchData.properties); const url: string = this.buildUrl(launchData); const payload: string | null = this.buildToolLaunchRequestPayload(url, launchData.properties); @@ -73,13 +88,13 @@ export abstract class AbstractLaunchStrategy implements ToolLaunchStrategy { url, payload: payload ?? undefined, openNewTab: launchData.openNewTab, - isDeepLink: false, + launchType: this.determineLaunchType(), }); return toolLaunchRequest; } - private buildUrl(toolLaunchDataDO: ToolLaunchData): string { + protected buildUrl(toolLaunchDataDO: ToolLaunchData): string { const { baseUrl } = toolLaunchDataDO; const pathProperties: PropertyData[] = toolLaunchDataDO.properties.filter( diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts index db7bda486d0..e5c99b8389c 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.spec.ts @@ -6,21 +6,21 @@ import { ExternalTool } from '../../../external-tool/domain'; import { externalToolFactory } from '../../../external-tool/testing'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { schoolExternalToolFactory } from '../../../school-external-tool/testing'; -import { LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; +import { LaunchRequestMethod, LaunchType, PropertyData, PropertyLocation } from '../../types'; import { AutoContextIdStrategy, AutoContextNameStrategy, + AutoGroupExternalUuidStrategy, AutoMediumIdStrategy, AutoSchoolIdStrategy, AutoSchoolNumberStrategy, - AutoGroupExternalUuidStrategy, } from '../auto-parameter-strategy'; import { BasicToolLaunchStrategy } from './basic-tool-launch.strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; describe('BasicToolLaunchStrategy', () => { let module: TestingModule; - let basicToolLaunchStrategy: BasicToolLaunchStrategy; + let strategy: BasicToolLaunchStrategy; beforeAll(async () => { module = await Test.createTestingModule({ @@ -53,7 +53,7 @@ describe('BasicToolLaunchStrategy', () => { ], }).compile(); - basicToolLaunchStrategy = module.get(BasicToolLaunchStrategy); + strategy = module.get(BasicToolLaunchStrategy); }); afterAll(async () => { @@ -83,7 +83,7 @@ describe('BasicToolLaunchStrategy', () => { it('should return null', () => { const { properties } = setup(); - const payload: string | null = basicToolLaunchStrategy.buildToolLaunchRequestPayload('url', properties); + const payload: string | null = strategy.buildToolLaunchRequestPayload('url', properties); expect(payload).toBeNull(); }); @@ -117,7 +117,7 @@ describe('BasicToolLaunchStrategy', () => { it('should build the tool launch request payload correctly', () => { const { properties } = setup(); - const payload: string | null = basicToolLaunchStrategy.buildToolLaunchRequestPayload('url', properties); + const payload: string | null = strategy.buildToolLaunchRequestPayload('url', properties); expect(payload).toEqual('{"param1":"value1","param2":"value2"}'); }); @@ -142,10 +142,7 @@ describe('BasicToolLaunchStrategy', () => { it('should build the tool launch data from the basic tool config correctly', async () => { const { data } = setup(); - const result: PropertyData[] = await basicToolLaunchStrategy.buildToolLaunchDataFromConcreteConfig( - 'userId', - data - ); + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); expect(result).toEqual([]); }); @@ -174,7 +171,7 @@ describe('BasicToolLaunchStrategy', () => { it('should return GET', () => { const { properties } = setup(); - const result: LaunchRequestMethod = basicToolLaunchStrategy.determineLaunchRequestMethod(properties); + const result: LaunchRequestMethod = strategy.determineLaunchRequestMethod(properties); expect(result).toEqual(LaunchRequestMethod.GET); }); @@ -208,10 +205,20 @@ describe('BasicToolLaunchStrategy', () => { it('should return POST', () => { const { properties } = setup(); - const result: LaunchRequestMethod = basicToolLaunchStrategy.determineLaunchRequestMethod(properties); + const result: LaunchRequestMethod = strategy.determineLaunchRequestMethod(properties); expect(result).toEqual(LaunchRequestMethod.POST); }); }); }); + + describe('determineLaunchType', () => { + describe('whenever it is called', () => { + it('should return basic', () => { + const result = strategy.determineLaunchType(); + + expect(result).toEqual(LaunchType.BASIC); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts index 1cf107eedcb..a168e846928 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/basic-tool-launch.strategy.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; +import { LaunchRequestMethod, LaunchType, PropertyData, PropertyLocation } from '../../types'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; @@ -41,4 +41,8 @@ export class BasicToolLaunchStrategy extends AbstractLaunchStrategy { return launchRequestMethod; } + + public override determineLaunchType(): LaunchType { + return LaunchType.BASIC; + } } diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts index 941b2680ec5..8a8c25831df 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts @@ -11,14 +11,24 @@ import { RoleName } from '@shared/domain/interface'; import { userDoFactory } from '@shared/testing'; import { pseudonymFactory } from '@shared/testing/factory/domainobject/pseudonym.factory'; import { Authorization } from 'oauth-1.0a'; +import { CustomParameterEntry } from '../../../common/domain'; import { LtiMessageType, LtiPrivacyPermission, LtiRole, ToolContextType } from '../../../common/enum'; -import { ContextExternalTool } from '../../../context-external-tool/domain'; -import { contextExternalToolFactory } from '../../../context-external-tool/testing'; +import { Lti11EncryptionService } from '../../../common/service'; +import { + ContextExternalTool, + LtiMessageTypeNotImplementedLoggableException, +} from '../../../context-external-tool/domain'; +import { LtiDeepLinkingService, LtiDeepLinkTokenService } from '../../../context-external-tool/service'; +import { + contextExternalToolFactory, + ltiDeepLinkFactory, + ltiDeepLinkTokenFactory, +} from '../../../context-external-tool/testing'; import { ExternalTool } from '../../../external-tool/domain'; import { externalToolFactory } from '../../../external-tool/testing'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { schoolExternalToolFactory } from '../../../school-external-tool/testing'; -import { LaunchRequestMethod, PropertyData, PropertyLocation, ToolLaunchRequest } from '../../types'; +import { LaunchRequestMethod, LaunchType, PropertyData, PropertyLocation, ToolLaunchRequest } from '../../types'; import { AutoContextIdStrategy, AutoContextNameStrategy, @@ -27,7 +37,6 @@ import { AutoSchoolIdStrategy, AutoSchoolNumberStrategy, } from '../auto-parameter-strategy'; -import { Lti11EncryptionService } from '../lti11-encryption.service'; import { Lti11ToolLaunchStrategy } from './lti11-tool-launch.strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; @@ -38,6 +47,8 @@ describe(Lti11ToolLaunchStrategy.name, () => { let userService: DeepMocked; let pseudonymService: DeepMocked; let lti11EncryptionService: DeepMocked; + let ltiDeepLinkTokenService: DeepMocked; + let ltiDeepLinkingService: DeepMocked; let encryptionService: DeepMocked; beforeAll(async () => { @@ -56,6 +67,14 @@ describe(Lti11ToolLaunchStrategy.name, () => { provide: Lti11EncryptionService, useValue: createMock(), }, + { + provide: LtiDeepLinkTokenService, + useValue: createMock(), + }, + { + provide: LtiDeepLinkingService, + useValue: createMock(), + }, { provide: AutoSchoolIdStrategy, useValue: createMock(), @@ -92,6 +111,8 @@ describe(Lti11ToolLaunchStrategy.name, () => { userService = module.get(UserService); pseudonymService = module.get(PseudonymService); lti11EncryptionService = module.get(Lti11EncryptionService); + ltiDeepLinkTokenService = module.get(LtiDeepLinkTokenService); + ltiDeepLinkingService = module.get(LtiDeepLinkingService); encryptionService = module.get(DefaultEncryptionService); }); @@ -104,98 +125,351 @@ describe(Lti11ToolLaunchStrategy.name, () => { }); describe('buildToolLaunchDataFromConcreteConfig', () => { - describe('when building the launch data for the encryption', () => { - const setup = () => { - const mockKey = 'mockKey'; - const mockSecret = 'mockSecret'; - const ltiMessageType = LtiMessageType.BASIC_LTI_LAUNCH_REQUEST; - const launchPresentationLocale = 'de-DE'; + describe('when lti messageType is basic lti launch request', () => { + describe('when building the launch data for the encryption', () => { + const setup = () => { + const mockKey = 'mockKey'; + const mockSecret = 'mockSecret'; + const launchPresentationLocale = 'de-DE'; + + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: mockKey, + secret: mockSecret, + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.PUBLIC, + launch_presentation_locale: launchPresentationLocale, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const user: UserDO = userDoFactory.buildWithId({ + roles: [ + { + id: 'roleId1', + name: RoleName.TEACHER, + }, + { + id: 'roleId2', + name: RoleName.USER, + }, + ], + }); + + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + userService.findById.mockResolvedValue(user); + + return { + data, + decrypted, + user, + mockKey, + mockSecret, + contextExternalTool, + launchPresentationLocale, + }; + }; - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: mockKey, - secret: mockSecret, - lti_message_type: ltiMessageType, - privacy_permission: LtiPrivacyPermission.PUBLIC, - launch_presentation_locale: launchPresentationLocale, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + it('should contain lti key and secret without location', async () => { + const { data, mockKey, decrypted } = setup(); - const data: ToolLaunchParams = { - contextExternalTool, - schoolExternalTool, - externalTool, - }; + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); - const user: UserDO = userDoFactory.buildWithId({ - roles: [ - { - id: 'roleId1', - name: RoleName.TEACHER, - }, + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ name: 'key', value: mockKey }), + new PropertyData({ name: 'secret', value: decrypted }), + ]) + ); + }); + + it('should contain mandatory lti attributes', async () => { + const { data, contextExternalTool, launchPresentationLocale } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ + name: 'lti_message_type', + value: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + location: PropertyLocation.BODY, + }), + new PropertyData({ name: 'lti_version', value: 'LTI-1p0', location: PropertyLocation.BODY }), + new PropertyData({ + name: 'resource_link_id', + value: contextExternalTool.id, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'launch_presentation_document_target', + value: 'window', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'launch_presentation_locale', + value: launchPresentationLocale, + location: PropertyLocation.BODY, + }), + ]) + ); + }); + }); + + describe('when lti privacyPermission is public', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.PUBLIC, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const userEmail = 'user@email.com'; + const user: UserDO = userDoFactory.buildWithId( { - id: 'roleId2', - name: RoleName.USER, + email: userEmail, + roles: [ + { + id: 'roleId1', + name: RoleName.TEACHER, + }, + { + id: 'roleId2', + name: RoleName.USER, + }, + ], }, - ], - }); + userId + ); - const decrypted = 'decryptedSecret'; - encryptionService.decrypt.mockReturnValue(decrypted); - userService.findById.mockResolvedValue(user); + const userDisplayName = 'Hans Peter Test'; - return { - data, - decrypted, - user, - mockKey, - mockSecret, - ltiMessageType, - contextExternalTool, - launchPresentationLocale, + userService.findById.mockResolvedValue(user); + userService.getDisplayName.mockResolvedValue(userDisplayName); + + return { + data, + userId, + userDisplayName, + userEmail, + }; }; - }; - it('should contain lti key and secret without location', async () => { - const { data, mockKey, decrypted } = setup(); + it('should contain all user related attributes', async () => { + const { data, userId, userDisplayName, userEmail } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ + name: 'roles', + value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'lis_person_name_full', + value: userDisplayName, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'lis_person_contact_email_primary', + value: userEmail, + location: PropertyLocation.BODY, + }), + new PropertyData({ name: 'user_id', value: userId, location: PropertyLocation.BODY }), + ]) + ); + }); + }); + + describe('when lti privacyPermission is name', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.NAME, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId( + { + roles: [ + { + id: 'roleId1', + name: RoleName.TEACHER, + }, + { + id: 'roleId2', + name: RoleName.USER, + }, + ], + }, + userId + ); - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); + const userDisplayName = 'Hans Peter Test'; - expect(result).toEqual( - expect.arrayContaining([ - new PropertyData({ name: 'key', value: mockKey }), - new PropertyData({ name: 'secret', value: decrypted }), - ]) - ); + userService.findById.mockResolvedValue(user); + userService.getDisplayName.mockResolvedValue(userDisplayName); + + return { + data, + userId, + userDisplayName, + }; + }; + + it('should contain the user name and id', async () => { + const { data, userId, userDisplayName } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ + name: 'roles', + value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'lis_person_name_full', + value: userDisplayName, + location: PropertyLocation.BODY, + }), + new PropertyData({ name: 'user_id', value: userId, location: PropertyLocation.BODY }), + ]) + ); + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'lis_person_contact_email_primary' })]) + ); + }); }); - }); - describe('when lti privacyPermission is public', () => { - const setup = () => { - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - privacy_permission: LtiPrivacyPermission.PUBLIC, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + describe('when lti privacyPermission is email', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.EMAIL, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const userEmail = 'user@email.com'; + const user: UserDO = userDoFactory.buildWithId( + { + email: userEmail, + roles: [ + { + id: 'roleId1', + name: RoleName.TEACHER, + }, + { + id: 'roleId2', + name: RoleName.USER, + }, + ], + }, + userId + ); - const data: ToolLaunchParams = { - contextExternalTool, - schoolExternalTool, - externalTool, + userService.findById.mockResolvedValue(user); + + return { + data, + userId, + userEmail, + }; }; - const userId: string = new ObjectId().toHexString(); - const userEmail = 'user@email.com'; - const user: UserDO = userDoFactory.buildWithId( - { - email: userEmail, + it('should contain the user email and id', async () => { + const { data, userId, userEmail } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ + name: 'roles', + value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'lis_person_contact_email_primary', + value: userEmail, + location: PropertyLocation.BODY, + }), + new PropertyData({ name: 'user_id', value: userId, location: PropertyLocation.BODY }), + ]) + ); + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'lis_person_name_full' })]) + ); + }); + }); + + describe('when lti privacyPermission is pseudonymous', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.PSEUDONYMOUS, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const user: UserDO = userDoFactory.buildWithId({ roles: [ { id: 'roleId1', @@ -206,69 +480,67 @@ describe(Lti11ToolLaunchStrategy.name, () => { name: RoleName.USER, }, ], - }, - userId - ); + }); - const userDisplayName = 'Hans Peter Test'; + const pseudonym: Pseudonym = pseudonymFactory.build(); - userService.findById.mockResolvedValue(user); - userService.getDisplayName.mockResolvedValue(userDisplayName); + userService.findById.mockResolvedValue(user); + pseudonymService.findOrCreatePseudonym.mockResolvedValue(pseudonym); - return { - data, - userId, - userDisplayName, - userEmail, + return { + data, + pseudonym, + }; }; - }; - - it('should contain all user related attributes', async () => { - const { data, userId, userDisplayName, userEmail } = setup(); - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); - - expect(result).toEqual( - expect.arrayContaining([ - new PropertyData({ - name: 'roles', - value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, - location: PropertyLocation.BODY, - }), - new PropertyData({ name: 'lis_person_name_full', value: userDisplayName, location: PropertyLocation.BODY }), - new PropertyData({ - name: 'lis_person_contact_email_primary', - value: userEmail, - location: PropertyLocation.BODY, - }), - new PropertyData({ name: 'user_id', value: userId, location: PropertyLocation.BODY }), - ]) - ); + it('should contain the pseudonymised user id', async () => { + const { data, pseudonym } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ + name: 'roles', + value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'user_id', + value: pseudonym.pseudonym, + location: PropertyLocation.BODY, + }), + ]) + ); + expect(result).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'lis_person_name_full' }), + expect.objectContaining({ name: 'lis_person_contact_email_primary' }), + ]) + ); + }); }); - }); - - describe('when lti privacyPermission is name', () => { - const setup = () => { - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - privacy_permission: LtiPrivacyPermission.NAME, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - const data: ToolLaunchParams = { - contextExternalTool, - schoolExternalTool, - externalTool, - }; - - const userId: string = new ObjectId().toHexString(); - const user: UserDO = userDoFactory.buildWithId( - { + describe('when lti privacyPermission is anonymous', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const user: UserDO = userDoFactory.buildWithId({ roles: [ { id: 'roleId1', @@ -279,190 +551,388 @@ describe(Lti11ToolLaunchStrategy.name, () => { name: RoleName.USER, }, ], - }, - userId - ); - - const userDisplayName = 'Hans Peter Test'; + }); - userService.findById.mockResolvedValue(user); - userService.getDisplayName.mockResolvedValue(userDisplayName); + userService.findById.mockResolvedValue(user); - return { - data, - userId, - userDisplayName, + return { + data, + }; }; - }; - it('should contain the user name and id', async () => { - const { data, userId, userDisplayName } = setup(); + it('should not contain user related information', async () => { + const { data } = setup(); - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); - expect(result).toEqual( - expect.arrayContaining([ - new PropertyData({ - name: 'roles', - value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, - location: PropertyLocation.BODY, - }), - new PropertyData({ name: 'lis_person_name_full', value: userDisplayName, location: PropertyLocation.BODY }), - new PropertyData({ name: 'user_id', value: userId, location: PropertyLocation.BODY }), - ]) - ); - expect(result).not.toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'lis_person_contact_email_primary' })]) - ); + expect(result).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'lis_person_name_full' }), + expect.objectContaining({ name: 'lis_person_contact_email_primary' }), + expect.objectContaining({ name: 'user_id' }), + expect.objectContaining({ name: 'roles' }), + ]) + ); + }); }); - }); - - describe('when lti privacyPermission is email', () => { - const setup = () => { - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - privacy_permission: LtiPrivacyPermission.EMAIL, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - - const data: ToolLaunchParams = { - contextExternalTool, - schoolExternalTool, - externalTool, - }; - const userId: string = new ObjectId().toHexString(); - const userEmail = 'user@email.com'; - const user: UserDO = userDoFactory.buildWithId( - { - email: userEmail, + describe('when context external tool id is undefined', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const pseudoContextExternalTool = { + ...contextExternalToolFactory.build().getProps(), + id: undefined, + }; + + const data: ToolLaunchParams = { + contextExternalTool: pseudoContextExternalTool, + schoolExternalTool, + externalTool, + }; + + const user: UserDO = userDoFactory.buildWithId({ roles: [ { id: 'roleId1', name: RoleName.TEACHER, }, - { - id: 'roleId2', - name: RoleName.USER, - }, ], - }, - userId - ); + }); - userService.findById.mockResolvedValue(user); + userService.findById.mockResolvedValue(user); - return { - data, - userId, - userEmail, + return { + data, + }; }; - }; - it('should contain the user email and id', async () => { - const { data, userId, userEmail } = setup(); + it('should use a random id', async () => { + const { data } = setup(); - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + const result = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); - expect(result).toEqual( - expect.arrayContaining([ - new PropertyData({ - name: 'roles', - value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, - location: PropertyLocation.BODY, - }), + expect(result).toContainEqual( new PropertyData({ - name: 'lis_person_contact_email_primary', - value: userEmail, + name: 'resource_link_id', + value: expect.any(String), location: PropertyLocation.BODY, - }), - new PropertyData({ name: 'user_id', value: userId, location: PropertyLocation.BODY }), - ]) - ); - expect(result).not.toEqual(expect.arrayContaining([expect.objectContaining({ name: 'lis_person_name_full' })])); + }) + ); + }); }); }); - describe('when lti privacyPermission is pseudonymous', () => { - const setup = () => { - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - privacy_permission: LtiPrivacyPermission.PSEUDONYMOUS, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - - const data: ToolLaunchParams = { - contextExternalTool, - schoolExternalTool, - externalTool, + describe('when lti messageType is content item selection request', () => { + describe('when no content is linked to the tool', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId(undefined, userId); + const ltiDeepLinkToken = ltiDeepLinkTokenFactory.build(); + + const publicBackendUrl = Configuration.get('PUBLIC_BACKEND_URL') as string; + const callbackUrl = `${publicBackendUrl}/v3/tools/context-external-tools/${contextExternalTool.id}/lti11-deep-link-callback`; + + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + ltiDeepLinkingService.getCallbackUrl.mockReturnValueOnce(callbackUrl); + ltiDeepLinkTokenService.generateToken.mockResolvedValueOnce(ltiDeepLinkToken); + + return { + data, + userId, + callbackUrl, + ltiDeepLinkToken, + }; }; - const user: UserDO = userDoFactory.buildWithId({ - roles: [ - { - id: 'roleId1', - name: RoleName.TEACHER, - }, - { - id: 'roleId2', - name: RoleName.USER, - }, - ], + it('should contain the attributes for a content item selection request', async () => { + const { data, userId, callbackUrl, ltiDeepLinkToken } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ name: 'key', value: 'mockKey' }), + new PropertyData({ name: 'secret', value: 'decryptedSecret' }), + new PropertyData({ + name: 'lti_message_type', + value: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'lti_version', + value: 'LTI-1p0', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'resource_link_id', + value: data.contextExternalTool.id as string, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'launch_presentation_document_target', + value: 'window', + location: PropertyLocation.BODY, + }), + new PropertyData({ + location: PropertyLocation.BODY, + name: 'launch_presentation_locale', + value: 'de-DE', + }), + new PropertyData({ + name: 'content_item_return_url', + value: callbackUrl, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'accept_media_types', + value: '*/*', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'accept_presentation_document_targets', + value: 'window', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'accept_unsigned', + value: 'false', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'accept_multiple', + value: 'false', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'accept_copy_advice', + value: 'false', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'auto_create', + value: 'true', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'data', + value: ltiDeepLinkToken.state, + location: PropertyLocation.BODY, + }), + ]) + ); }); + }); - const pseudonym: Pseudonym = pseudonymFactory.build(); + describe('when the linked content is an lti launch', () => { + const setup = () => { + const launchPresentationLocale = 'de-DE'; + + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + launch_presentation_locale: launchPresentationLocale, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const ltiDeepLinkParameter = new CustomParameterEntry({ name: 'dl_param', value: 'dl_value' }); + const ltiDeepLink = ltiDeepLinkFactory.build({ + mediaType: 'application/vnd.ims.lti.v1.ltilink', + parameters: [ltiDeepLinkParameter], + }); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + ltiDeepLink, + }); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId(undefined, userId); + + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + + return { + data, + userId, + contextExternalTool, + launchPresentationLocale, + ltiDeepLinkParameter, + }; + }; - userService.findById.mockResolvedValue(user); - pseudonymService.findOrCreatePseudonym.mockResolvedValue(pseudonym); + it('should contain the attributes for a basic lti launch request with the additional attributes from the deep link', async () => { + const { data, userId, contextExternalTool, launchPresentationLocale, ltiDeepLinkParameter } = setup(); + + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + + expect(result).toEqual( + expect.arrayContaining([ + new PropertyData({ + name: 'lti_message_type', + value: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + location: PropertyLocation.BODY, + }), + new PropertyData({ name: 'lti_version', value: 'LTI-1p0', location: PropertyLocation.BODY }), + new PropertyData({ + name: 'resource_link_id', + value: contextExternalTool.id, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'launch_presentation_document_target', + value: 'window', + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: 'launch_presentation_locale', + value: launchPresentationLocale, + location: PropertyLocation.BODY, + }), + new PropertyData({ + name: `custom_${ltiDeepLinkParameter.name}`, + value: ltiDeepLinkParameter.value as string, + location: PropertyLocation.BODY, + }), + ]) + ); + }); + }); - return { - data, - pseudonym, + describe('when the linked content does not require an lti launch', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const ltiDeepLink = ltiDeepLinkFactory.build({ + mediaType: 'application/pdf', + parameters: undefined, + }); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + ltiDeepLink, + }); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId(undefined, userId); + + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + + return { + data, + userId, + contextExternalTool, + }; }; - }; - it('should contain the pseudonymised user id', async () => { - const { data, pseudonym } = setup(); + it('should not contain parameters', async () => { + const { data, userId } = setup(); - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); + const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); - expect(result).toEqual( - expect.arrayContaining([ - new PropertyData({ - name: 'roles', - value: `${LtiRole.INSTRUCTOR},${LtiRole.LEARNER}`, - location: PropertyLocation.BODY, - }), - new PropertyData({ name: 'user_id', value: pseudonym.pseudonym, location: PropertyLocation.BODY }), - ]) - ); - expect(result).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'lis_person_name_full' }), - expect.objectContaining({ name: 'lis_person_contact_email_primary' }), - ]) - ); + expect(result).toEqual([]); + }); + }); + + describe('when the tool is not permanent', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build(); + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + const pseudoContextExternalTool = { + ...contextExternalToolFactory.build().getProps(), + id: undefined, + }; + + const data: ToolLaunchParams = { + contextExternalTool: pseudoContextExternalTool, + schoolExternalTool, + externalTool, + }; + + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId(undefined, userId); + + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + + return { + data, + userId, + }; + }; + + it('should throw an error', async () => { + const { data, userId } = setup(); + + await expect(() => strategy.buildToolLaunchDataFromConcreteConfig(userId, data)).rejects.toThrow( + new UnprocessableEntityException( + 'Cannot lauch a content selection request with a non-permanent context external tool' + ) + ); + }); }); }); - describe('when lti privacyPermission is anonymous', () => { + describe('when the lti message type is unknown', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - privacy_permission: LtiPrivacyPermission.ANONYMOUS, + lti_message_type: 'unknown' as unknown as LtiMessageType, }) .build(); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); @@ -474,38 +944,20 @@ describe(Lti11ToolLaunchStrategy.name, () => { externalTool, }; - const user: UserDO = userDoFactory.buildWithId({ - roles: [ - { - id: 'roleId1', - name: RoleName.TEACHER, - }, - { - id: 'roleId2', - name: RoleName.USER, - }, - ], - }); - - userService.findById.mockResolvedValue(user); + const userId: string = new ObjectId().toHexString(); return { data, + userId, + contextExternalTool, }; }; - it('should not contain user related information', async () => { - const { data } = setup(); - - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); + it('should throw an error', async () => { + const { data, userId } = setup(); - expect(result).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'lis_person_name_full' }), - expect.objectContaining({ name: 'lis_person_contact_email_primary' }), - expect.objectContaining({ name: 'user_id' }), - expect.objectContaining({ name: 'roles' }), - ]) + await expect(() => strategy.buildToolLaunchDataFromConcreteConfig(userId, data)).rejects.toThrow( + LtiMessageTypeNotImplementedLoggableException ); }); }); @@ -539,249 +991,263 @@ describe(Lti11ToolLaunchStrategy.name, () => { ); }); }); + }); - describe('when context external tool id is undefined', () => { + describe('buildToolLaunchRequestPayload', () => { + describe('when key and secret are provided', () => { const setup = () => { - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, - privacy_permission: LtiPrivacyPermission.ANONYMOUS, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const pseudoContextExternalTool = { - ...contextExternalToolFactory.build().getProps(), - id: undefined, - }; + const property1: PropertyData = new PropertyData({ + name: 'param1', + value: 'value1', + location: PropertyLocation.BODY, + }); - const data: ToolLaunchParams = { - contextExternalTool: pseudoContextExternalTool, - schoolExternalTool, - externalTool, - }; + const property2: PropertyData = new PropertyData({ + name: 'param2', + value: 'value2', + location: PropertyLocation.BODY, + }); - const user: UserDO = userDoFactory.buildWithId({ - roles: [ - { - id: 'roleId1', - name: RoleName.TEACHER, - }, - ], + const property3: PropertyData = new PropertyData({ + name: 'param2', + value: 'value2', + location: PropertyLocation.PATH, }); - userService.findById.mockResolvedValue(user); + const mockKey = 'mockKey'; + const keyProperty: PropertyData = new PropertyData({ + name: 'key', + value: mockKey, + }); + + const secretProperty: PropertyData = new PropertyData({ + name: 'secret', + value: 'mockSecret', + }); + + const url = 'https://example.com/'; + + const signedPayload: Authorization = { + oauth_consumer_key: mockKey, + oauth_nonce: 'nonce', + oauth_signature: 'signature', + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: 1, + oauth_version: '1.0', + [property1.name]: property1.value, + [property2.name]: property2.value, + }; + + lti11EncryptionService.sign.mockReturnValue(signedPayload); return { - data, + properties: [property1, property2, property3, keyProperty, secretProperty], + url, + signedPayload, }; }; - it('should use a random id', async () => { - const { data } = setup(); + it('should return a OAuth1 signed payload', () => { + const { properties, signedPayload } = setup(); + + const payload: string | null = strategy.buildToolLaunchRequestPayload('url', properties); - const result = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); + expect(payload).toEqual(JSON.stringify(signedPayload)); + }); - expect(result).toContainEqual( - new PropertyData({ - name: 'resource_link_id', - value: expect.any(String), - location: PropertyLocation.BODY, - }) + it('should not return a payload with the signing secret', () => { + const { properties } = setup(); + + strategy.buildToolLaunchRequestPayload('url', properties); + + expect(lti11EncryptionService.sign).not.toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ secret: expect.anything() }) ); }); }); - describe('when lti messageType is content item selection request', () => { + describe('when key or secret is missing', () => { const setup = () => { - const externalTool: ExternalTool = externalToolFactory - .withLti11Config({ - key: 'mockKey', - secret: 'mockSecret', - lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, - privacy_permission: LtiPrivacyPermission.ANONYMOUS, - }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); - - const data: ToolLaunchParams = { - contextExternalTool, - schoolExternalTool, - externalTool, - }; + const property1: PropertyData = new PropertyData({ + name: 'param1', + value: 'value1', + location: PropertyLocation.BODY, + }); - const userId: string = new ObjectId().toHexString(); - const user: UserDO = userDoFactory.buildWithId( - { - roles: [ - { - id: 'roleId1', - name: RoleName.TEACHER, - }, - { - id: 'roleId2', - name: RoleName.USER, - }, - ], - }, - userId - ); + const property2: PropertyData = new PropertyData({ + name: 'param2', + value: 'value2', + location: PropertyLocation.BODY, + }); - const publicBackendUrl = Configuration.get('PUBLIC_BACKEND_URL') as string; - const callbackUrl = `${publicBackendUrl}/v3/tools/context-external-tools/${contextExternalTool.id}/lti11-deep-link-callback`; + const property3: PropertyData = new PropertyData({ + name: 'param2', + value: 'value2', + location: PropertyLocation.PATH, + }); - userService.findById.mockResolvedValue(user); - const decrypted = 'decryptedSecret'; - encryptionService.decrypt.mockReturnValue(decrypted); + const url = 'https://example.com/'; return { - data, - userId, - callbackUrl, + properties: [property1, property2, property3], + url, }; }; - it('should contain all user related attributes', async () => { - const { data, userId, callbackUrl } = setup(); + it('should throw an InternalServerErrorException', () => { + const { properties } = setup(); - const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig(userId, data); + const func = () => strategy.buildToolLaunchRequestPayload('url', properties); - expect(result).toEqual( - expect.arrayContaining([ - new PropertyData({ name: 'key', value: 'mockKey' }), - new PropertyData({ name: 'secret', value: 'decryptedSecret' }), - new PropertyData({ - name: 'lti_message_type', - value: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, - location: PropertyLocation.BODY, - }), - new PropertyData({ name: 'lti_version', value: 'LTI-1p0', location: PropertyLocation.BODY }), - new PropertyData({ - name: 'resource_link_id', - value: data.contextExternalTool.id as string, - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'launch_presentation_document_target', - value: 'window', - location: PropertyLocation.BODY, - }), - new PropertyData({ - location: PropertyLocation.BODY, - name: 'launch_presentation_locale', - value: 'de-DE', - }), - new PropertyData({ - name: 'content_item_return_url', - value: callbackUrl, - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'accept_media_types', - value: '*/*', - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'accept_presentation_document_targets', - value: 'window', - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'accept_unsigned', - value: 'false', - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'accept_multiple', - value: 'false', - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'accept_copy_advice', - value: 'false', - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'auto_create', - value: 'true', - location: PropertyLocation.BODY, - }), - new PropertyData({ - name: 'data', - value: expect.any(String), - location: PropertyLocation.BODY, - }), - ]) + expect(func).toThrow( + new InternalServerErrorException( + 'Unable to build LTI 1.1 launch payload. "key" or "secret" is undefined in PropertyData' + ) ); }); }); + }); + + describe('determineLaunchRequestMethod', () => { + it('should return POST', () => { + const result: LaunchRequestMethod = strategy.determineLaunchRequestMethod([]); + + expect(result).toEqual(LaunchRequestMethod.POST); + }); + }); - describe('when a content item selection request is made without a permanent tool', () => { + describe('createLaunchRequest', () => { + describe('when lti message type is content item selection request and no content is selected', () => { const setup = () => { - const externalTool: ExternalTool = externalToolFactory + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId({ id: userId }); + + const externalTool = externalToolFactory .withLti11Config({ key: 'mockKey', secret: 'mockSecret', lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, privacy_permission: LtiPrivacyPermission.ANONYMOUS, }) - .build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); - const pseudoContextExternalTool = { - ...contextExternalToolFactory.build().getProps(), - id: undefined, - }; + .build({ + openNewTab: false, + }); + + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + + const contextExternalToolId = 'contextExternalToolId'; + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + id: contextExternalToolId, + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId: schoolExternalTool.schoolId, + }, + contextRef: { + type: ToolContextType.COURSE, + }, + }); const data: ToolLaunchParams = { - contextExternalTool: pseudoContextExternalTool, + contextExternalTool, schoolExternalTool, externalTool, }; - const userId: string = new ObjectId().toHexString(); - const user: UserDO = userDoFactory.buildWithId( - { - roles: [ - { - id: 'roleId1', - name: RoleName.TEACHER, - }, - { - id: 'roleId2', - name: RoleName.USER, - }, - ], - }, - userId - ); + const property1: PropertyData = new PropertyData({ + name: 'param1', + value: 'value1', + location: PropertyLocation.BODY, + }); + + const property2: PropertyData = new PropertyData({ + name: 'param2', + value: 'value2', + location: PropertyLocation.BODY, + }); + + const signedPayload: Authorization = { + oauth_consumer_key: 'mockKey', + oauth_nonce: 'nonce', + oauth_signature: 'signature', + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: 1, + oauth_version: '1.0', + [property1.name]: property1.value, + [property2.name]: property2.value, + }; userService.findById.mockResolvedValue(user); const decrypted = 'decryptedSecret'; encryptionService.decrypt.mockReturnValue(decrypted); + lti11EncryptionService.sign.mockReturnValueOnce(signedPayload); return { + signedPayload, data, userId, }; }; - it('should throw an error', async () => { - const { data, userId } = setup(); + it('should create a post request with a signed payload and open in a new tab', async () => { + const { signedPayload, data, userId } = setup(); - await expect(() => strategy.buildToolLaunchDataFromConcreteConfig(userId, data)).rejects.toThrow( - new UnprocessableEntityException( - 'Cannot lauch a content selection request with a non-permanent context external tool' - ) - ); + const result: ToolLaunchRequest = await strategy.createLaunchRequest(userId, data); + + expect(result).toEqual({ + method: LaunchRequestMethod.POST, + url: 'https://www.lti11-baseurl.com/', + payload: JSON.stringify(signedPayload), + openNewTab: true, + launchType: LaunchType.LTI11_CONTENT_ITEM_SELECTION, + }); }); }); - }); - describe('buildToolLaunchRequestPayload', () => { - describe('when key and secret are provided', () => { + describe('when there is a deep link with a url', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId({ id: userId }); + + const externalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build({ + openNewTab: false, + }); + + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const ltiDeepLink = ltiDeepLinkFactory.build({ + mediaType: 'application/vnd.ims.lti.v1.ltilink', + url: 'https://lti.deep.link', + }); + + const contextExternalToolId = 'contextExternalToolId'; + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + id: contextExternalToolId, + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId: schoolExternalTool.schoolId, + }, + contextRef: { + type: ToolContextType.COURSE, + }, + ltiDeepLink, + }); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + const property1: PropertyData = new PropertyData({ name: 'param1', value: 'value1', @@ -794,27 +1260,8 @@ describe(Lti11ToolLaunchStrategy.name, () => { location: PropertyLocation.BODY, }); - const property3: PropertyData = new PropertyData({ - name: 'param2', - value: 'value2', - location: PropertyLocation.PATH, - }); - - const mockKey = 'mockKey'; - const keyProperty: PropertyData = new PropertyData({ - name: 'key', - value: mockKey, - }); - - const secretProperty: PropertyData = new PropertyData({ - name: 'secret', - value: 'mockSecret', - }); - - const url = 'https://example.com/'; - const signedPayload: Authorization = { - oauth_consumer_key: mockKey, + oauth_consumer_key: 'mockKey', oauth_nonce: 'nonce', oauth_signature: 'signature', oauth_signature_method: 'HMAC-SHA1', @@ -824,40 +1271,142 @@ describe(Lti11ToolLaunchStrategy.name, () => { [property2.name]: property2.value, }; - lti11EncryptionService.sign.mockReturnValue(signedPayload); + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + lti11EncryptionService.sign.mockReturnValueOnce(signedPayload); return { - properties: [property1, property2, property3, keyProperty, secretProperty], - url, signedPayload, + data, + userId, + ltiDeepLink, }; }; - it('should return a OAuth1 signed payload', () => { - const { properties, signedPayload } = setup(); + it('should use the deep link url', async () => { + const { signedPayload, data, userId, ltiDeepLink } = setup(); - const payload: string | null = strategy.buildToolLaunchRequestPayload('url', properties); + const result: ToolLaunchRequest = await strategy.createLaunchRequest(userId, data); - expect(payload).toEqual(JSON.stringify(signedPayload)); + expect(result).toEqual({ + method: LaunchRequestMethod.POST, + url: ltiDeepLink.url as string, + payload: JSON.stringify(signedPayload), + openNewTab: false, + launchType: LaunchType.LTI11_BASIC_LAUNCH, + }); }); + }); - it('should not return a payload with the signing secret', () => { - const { properties } = setup(); + describe('when there is a deep link resource that does not require an lti launch', () => { + const setup = () => { + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId({ id: userId }); - strategy.buildToolLaunchRequestPayload('url', properties); + const externalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build({ + openNewTab: false, + }); - expect(lti11EncryptionService.sign).not.toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ secret: expect.anything() }) - ); + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const ltiDeepLink = ltiDeepLinkFactory.build({ + mediaType: 'application/pdf', + url: undefined, + }); + + const contextExternalToolId = 'contextExternalToolId'; + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + id: contextExternalToolId, + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId: schoolExternalTool.schoolId, + }, + contextRef: { + type: ToolContextType.COURSE, + }, + ltiDeepLink, + }); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + + return { + data, + userId, + ltiDeepLink, + }; + }; + + it('should use the GET method without a payload', async () => { + const { data, userId } = setup(); + + const result: ToolLaunchRequest = await strategy.createLaunchRequest(userId, data); + + expect(result).toEqual({ + method: LaunchRequestMethod.GET, + url: 'https://www.lti11-baseurl.com/', + payload: undefined, + openNewTab: false, + launchType: LaunchType.BASIC, + }); }); }); - describe('when key or secret is missing', () => { + describe('when there is a deep link resource of type lti assignment', () => { const setup = () => { + const userId: string = new ObjectId().toHexString(); + const user: UserDO = userDoFactory.buildWithId({ id: userId }); + + const externalTool = externalToolFactory + .withLti11Config({ + key: 'mockKey', + secret: 'mockSecret', + lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, + privacy_permission: LtiPrivacyPermission.ANONYMOUS, + }) + .build({ + openNewTab: false, + }); + + const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const ltiDeepLink = ltiDeepLinkFactory.build({ + mediaType: 'application/vnd.ims.lti.v1.ltiassignment', + url: undefined, + }); + + const contextExternalToolId = 'contextExternalToolId'; + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + id: contextExternalToolId, + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId: schoolExternalTool.schoolId, + }, + contextRef: { + type: ToolContextType.COURSE, + }, + ltiDeepLink, + }); + + const data: ToolLaunchParams = { + contextExternalTool, + schoolExternalTool, + externalTool, + }; + const property1: PropertyData = new PropertyData({ name: 'param1', value: 'value1', @@ -870,44 +1419,46 @@ describe(Lti11ToolLaunchStrategy.name, () => { location: PropertyLocation.BODY, }); - const property3: PropertyData = new PropertyData({ - name: 'param2', - value: 'value2', - location: PropertyLocation.PATH, - }); + const signedPayload: Authorization = { + oauth_consumer_key: 'mockKey', + oauth_nonce: 'nonce', + oauth_signature: 'signature', + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: 1, + oauth_version: '1.0', + [property1.name]: property1.value, + [property2.name]: property2.value, + }; - const url = 'https://example.com/'; + userService.findById.mockResolvedValue(user); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); + lti11EncryptionService.sign.mockReturnValueOnce(signedPayload); return { - properties: [property1, property2, property3], - url, + signedPayload, + data, + userId, + ltiDeepLink, }; }; - it('should throw an InternalServerErrorException', () => { - const { properties } = setup(); + it('should create a post request with a signed payload', async () => { + const { signedPayload, data, userId } = setup(); - const func = () => strategy.buildToolLaunchRequestPayload('url', properties); + const result: ToolLaunchRequest = await strategy.createLaunchRequest(userId, data); - expect(func).toThrow( - new InternalServerErrorException( - 'Unable to build LTI 1.1 launch payload. "key" or "secret" is undefined in PropertyData' - ) - ); + expect(result).toEqual({ + method: LaunchRequestMethod.POST, + url: 'https://www.lti11-baseurl.com/', + payload: JSON.stringify(signedPayload), + openNewTab: false, + launchType: LaunchType.LTI11_BASIC_LAUNCH, + }); }); }); - }); - - describe('determineLaunchRequestMethod', () => { - it('should return POST', () => { - const result: LaunchRequestMethod = strategy.determineLaunchRequestMethod([]); - - expect(result).toEqual(LaunchRequestMethod.POST); - }); - }); - describe('createLaunchRequest', () => { - describe('when lti message type is content item selection request', () => { + describe('when there is a deep link resource of type lti link', () => { const setup = () => { const userId: string = new ObjectId().toHexString(); const user: UserDO = userDoFactory.buildWithId({ id: userId }); @@ -919,9 +1470,15 @@ describe(Lti11ToolLaunchStrategy.name, () => { lti_message_type: LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST, privacy_permission: LtiPrivacyPermission.ANONYMOUS, }) - .build(); + .build({ + openNewTab: false, + }); const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); + const ltiDeepLink = ltiDeepLinkFactory.build({ + mediaType: 'application/vnd.ims.lti.v1.ltilink', + url: undefined, + }); const contextExternalToolId = 'contextExternalToolId'; const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ @@ -933,6 +1490,7 @@ describe(Lti11ToolLaunchStrategy.name, () => { contextRef: { type: ToolContextType.COURSE, }, + ltiDeepLink, }); const data: ToolLaunchParams = { @@ -964,35 +1522,35 @@ describe(Lti11ToolLaunchStrategy.name, () => { [property2.name]: property2.value, }; - const toolLaunchRequest: ToolLaunchRequest = new ToolLaunchRequest({ - method: LaunchRequestMethod.POST, - url: 'https://www.lti11-baseurl.com/', - payload: JSON.stringify(signedPayload), - openNewTab: true, - isDeepLink: true, - }); - userService.findById.mockResolvedValue(user); const decrypted = 'decryptedSecret'; encryptionService.decrypt.mockReturnValue(decrypted); + lti11EncryptionService.sign.mockReturnValueOnce(signedPayload); return { - toolLaunchRequest, + signedPayload, data, userId, + ltiDeepLink, }; }; - it('should create a LaunchRequest with the correct method, url and payload', async () => { - const { toolLaunchRequest, data, userId } = setup(); + it('should create a post request with a signed payload', async () => { + const { signedPayload, data, userId } = setup(); const result: ToolLaunchRequest = await strategy.createLaunchRequest(userId, data); - expect(result).toEqual(toolLaunchRequest); + expect(result).toEqual({ + method: LaunchRequestMethod.POST, + url: 'https://www.lti11-baseurl.com/', + payload: JSON.stringify(signedPayload), + openNewTab: false, + launchType: LaunchType.LTI11_BASIC_LAUNCH, + }); }); }); - describe('when lti message type is not content item selection request and no deeplink', () => { + describe('when lti message type is basic lti launch request', () => { const setup = () => { const userId: string = new ObjectId().toHexString(); const user: UserDO = userDoFactory.buildWithId({ id: userId }); @@ -1010,7 +1568,9 @@ describe(Lti11ToolLaunchStrategy.name, () => { privacy_permission: LtiPrivacyPermission.PUBLIC, launch_presentation_locale: launchPresentationLocale, }) - .build(); + .build({ + openNewTab: false, + }); const schoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool.id }); @@ -1055,31 +1615,40 @@ describe(Lti11ToolLaunchStrategy.name, () => { [property2.name]: property2.value, }; - const toolLaunchRequest: ToolLaunchRequest = new ToolLaunchRequest({ - method: LaunchRequestMethod.POST, - url: 'https://www.lti11-baseurl.com/', - payload: JSON.stringify(signedPayload), - openNewTab: false, - isDeepLink: false, - }); - userService.findById.mockResolvedValue(user); const decrypted = 'decryptedSecret'; encryptionService.decrypt.mockReturnValue(decrypted); + lti11EncryptionService.sign.mockReturnValueOnce(signedPayload); return { - toolLaunchRequest, + signedPayload, data, userId, }; }; - it('should create a LaunchRequest with the correct method, url and payload', async () => { - const { toolLaunchRequest, data, userId } = setup(); + it('should create a post request with a signed payload', async () => { + const { signedPayload, data, userId } = setup(); const result: ToolLaunchRequest = await strategy.createLaunchRequest(userId, data); - expect(result).toEqual(toolLaunchRequest); + expect(result).toEqual({ + method: LaunchRequestMethod.POST, + url: 'https://www.lti11-baseurl.com/', + payload: JSON.stringify(signedPayload), + openNewTab: false, + launchType: LaunchType.LTI11_BASIC_LAUNCH, + }); + }); + }); + }); + + describe('determineLaunchType', () => { + describe('whenever it is called', () => { + it('should return lti basic launch', () => { + const result = strategy.determineLaunchType(); + + expect(result).toEqual(LaunchType.LTI11_BASIC_LAUNCH); }); }); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts index 912c25980d4..567c30116d5 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts @@ -1,4 +1,3 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { PseudonymService } from '@modules/pseudonym/service'; @@ -7,16 +6,25 @@ import { Inject, Injectable, InternalServerErrorException, UnprocessableEntityEx import { Pseudonym, RoleReference, UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { UUID } from 'bson'; import { Authorization } from 'oauth-1.0a'; +import { CustomParameterEntry } from '../../../common/domain'; import { LtiMessageType, LtiPrivacyPermission, LtiRole } from '../../../common/enum'; +import { Lti11EncryptionService } from '../../../common/service'; +import { + LtiDeepLink, + LtiDeepLinkToken, + LtiMessageTypeNotImplementedLoggableException, +} from '../../../context-external-tool/domain'; +import { LtiDeepLinkingService, LtiDeepLinkTokenService } from '../../../context-external-tool/service'; import { ExternalTool, Lti11ToolConfig } from '../../../external-tool/domain'; import { LtiRoleMapper } from '../../mapper'; import { AuthenticationValues, LaunchRequestMethod, + LaunchType, PropertyData, PropertyLocation, + ToolLaunchData, ToolLaunchRequest, } from '../../types'; import { @@ -27,7 +35,6 @@ import { AutoSchoolIdStrategy, AutoSchoolNumberStrategy, } from '../auto-parameter-strategy'; -import { Lti11EncryptionService } from '../lti11-encryption.service'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; @@ -37,6 +44,8 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { private readonly userService: UserService, private readonly pseudonymService: PseudonymService, private readonly lti11EncryptionService: Lti11EncryptionService, + private readonly ltiDeepLinkTokenService: LtiDeepLinkTokenService, + private readonly ltiDeepLinkingService: LtiDeepLinkingService, @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, autoSchoolIdStrategy: AutoSchoolIdStrategy, autoSchoolNumberStrategy: AutoSchoolNumberStrategy, @@ -69,15 +78,38 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { } let properties: PropertyData[]; - if (config.lti_message_type === LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST) { - properties = await this.buildToolLaunchDataForContentItemSelectionRequest(userId, data, config); - } else { - properties = await this.buildToolLaunchDataForLtiLaunch( - userId, - data, - config, - LtiMessageType.BASIC_LTI_LAUNCH_REQUEST - ); + switch (config.lti_message_type) { + case LtiMessageType.BASIC_LTI_LAUNCH_REQUEST: { + properties = await this.buildToolLaunchDataForLtiLaunch( + userId, + data, + config, + LtiMessageType.BASIC_LTI_LAUNCH_REQUEST + ); + break; + } + case LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST: { + if (!data.contextExternalTool.ltiDeepLink) { + properties = await this.buildToolLaunchDataForContentItemSelectionRequest(userId, data, config); + } else if ( + data.contextExternalTool.ltiDeepLink.mediaType === 'application/vnd.ims.lti.v1.ltilink' || + data.contextExternalTool.ltiDeepLink.mediaType === 'application/vnd.ims.lti.v1.ltiassignment' + ) { + properties = await this.buildToolLaunchDataForLtiLaunch( + userId, + data, + config, + LtiMessageType.BASIC_LTI_LAUNCH_REQUEST + ); + + properties.push(...this.buildToolLaunchDataFromDeepLink(data.contextExternalTool.ltiDeepLink)); + } else { + properties = []; + } + break; + } + default: + throw new LtiMessageTypeNotImplementedLoggableException(config.lti_message_type); } return properties; @@ -101,17 +133,14 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST ); - const publicBackendUrl = Configuration.get('PUBLIC_BACKEND_URL') as string; - const callbackUrl = new URL( - `${publicBackendUrl}/v3/tools/context-external-tools/${data.contextExternalTool.id}/lti11-deep-link-callback` - ); + const callbackUrl: string = this.ltiDeepLinkingService.getCallbackUrl(data.contextExternalTool.id); - const state = new UUID().toString(); + const ltiDeepLinkToken: LtiDeepLinkToken = await this.ltiDeepLinkTokenService.generateToken(userId); additionalProperties.push( new PropertyData({ name: 'content_item_return_url', - value: callbackUrl.toString(), + value: callbackUrl, location: PropertyLocation.BODY, }), new PropertyData({ @@ -148,7 +177,7 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { }), new PropertyData({ name: 'data', - value: state, + value: ltiDeepLinkToken.state, location: PropertyLocation.BODY, }) ); @@ -256,6 +285,24 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { return additionalProperties; } + private buildToolLaunchDataFromDeepLink(deepLink: LtiDeepLink): PropertyData[] { + const deepLinkProperties: PropertyData[] = []; + + deepLink.parameters.forEach((parameter: CustomParameterEntry): void => { + if (parameter.value) { + deepLinkProperties.push( + new PropertyData({ + name: `custom_${parameter.name}`, + value: parameter.value, + location: PropertyLocation.BODY, + }) + ); + } + }); + + return deepLinkProperties; + } + // eslint-disable-next-line @typescript-eslint/require-await public override buildToolLaunchRequestPayload(url: string, properties: PropertyData[]): string | null { const bodyProperties: PropertyData[] = properties.filter( @@ -302,17 +349,54 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { return LaunchRequestMethod.POST; } + public override determineLaunchType(): LaunchType { + return LaunchType.LTI11_BASIC_LAUNCH; + } + public override async createLaunchRequest(userId: EntityId, data: ToolLaunchParams): Promise { - const request: ToolLaunchRequest = await super.createLaunchRequest(userId, data); + const launchData: ToolLaunchData = await this.createLaunchData(userId, data); + const { ltiDeepLink } = data.contextExternalTool; - if ( - ExternalTool.isLti11Config(data.externalTool.config) && - data.externalTool.config.lti_message_type === LtiMessageType.CONTENT_ITEM_SELECTION_REQUEST - ) { - request.openNewTab = true; - request.isDeepLink = true; + let method: LaunchRequestMethod; + let url: string; + let payload: string | null; + let launchType: LaunchType; + let { openNewTab } = launchData; + + if (ltiDeepLink?.url) { + url = ltiDeepLink?.url; + } else { + url = this.buildUrl(launchData); + } + + const isLtiLaunch: boolean = + !ltiDeepLink || + ltiDeepLink.mediaType === 'application/vnd.ims.lti.v1.ltilink' || + ltiDeepLink.mediaType === 'application/vnd.ims.lti.v1.ltiassignment'; + if (isLtiLaunch) { + method = this.determineLaunchRequestMethod(launchData.properties); + payload = this.buildToolLaunchRequestPayload(url, launchData.properties); + launchType = this.determineLaunchType(); + } else { + method = LaunchRequestMethod.GET; + payload = null; + launchType = LaunchType.BASIC; } - return request; + const isContentItemSelectionRequest: boolean = data.externalTool.isLtiDeepLinkingTool() && !ltiDeepLink; + if (isContentItemSelectionRequest) { + openNewTab = true; + launchType = LaunchType.LTI11_CONTENT_ITEM_SELECTION; + } + + const toolLaunchRequest = new ToolLaunchRequest({ + method, + url, + payload: payload ?? undefined, + openNewTab, + launchType, + }); + + return toolLaunchRequest; } } diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts index dcda4d88a86..ef5d05f39fc 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.spec.ts @@ -6,14 +6,14 @@ import { ExternalTool } from '../../../external-tool/domain'; import { externalToolFactory } from '../../../external-tool/testing'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; import { schoolExternalToolFactory } from '../../../school-external-tool/testing'; -import { LaunchRequestMethod, PropertyData } from '../../types'; +import { LaunchRequestMethod, LaunchType, PropertyData } from '../../types'; import { AutoContextIdStrategy, AutoContextNameStrategy, + AutoGroupExternalUuidStrategy, AutoMediumIdStrategy, AutoSchoolIdStrategy, AutoSchoolNumberStrategy, - AutoGroupExternalUuidStrategy, } from '../auto-parameter-strategy'; import { OAuth2ToolLaunchStrategy } from './oauth2-tool-launch.strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; @@ -101,4 +101,14 @@ describe('OAuth2ToolLaunchStrategy', () => { }); }); }); + + describe('determineLaunchType', () => { + describe('whenever it is called', () => { + it('should return oauth2', () => { + const result = strategy.determineLaunchType(); + + expect(result).toEqual(LaunchType.OAUTH2); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts index bf061ad75d5..580a40b0dbc 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/oauth2-tool-launch.strategy.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { LaunchRequestMethod, PropertyData } from '../../types'; +import { LaunchRequestMethod, LaunchType, PropertyData } from '../../types'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { ToolLaunchParams } from './tool-launch-params.interface'; @@ -24,4 +24,8 @@ export class OAuth2ToolLaunchStrategy extends AbstractLaunchStrategy { public override determineLaunchRequestMethod(properties: PropertyData[]): LaunchRequestMethod { return LaunchRequestMethod.GET; } + + public override determineLaunchType(): LaunchType { + return LaunchType.OAUTH2; + } } diff --git a/apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.spec.ts deleted file mode 100644 index 5f35cafe3be..00000000000 --- a/apps/server/src/modules/tool/tool-launch/service/lti11-encryption.service.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Authorization } from 'oauth-1.0a'; -import { Lti11EncryptionService } from './lti11-encryption.service'; - -describe('Lti11EncryptionService', () => { - let module: TestingModule; - let service: Lti11EncryptionService; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [Lti11EncryptionService], - }).compile(); - - service = module.get(Lti11EncryptionService); - }); - - afterAll(async () => { - await module.close(); - }); - - describe('sign', () => { - describe('when signing with OAuth1', () => { - const setup = () => { - const mockKey = 'mockKey'; - const mockSecret = 'mockSecret'; - const mockUrl = 'https://mockurl.com/'; - const testPayload: Record = { - param1: 'test1', - }; - - return { - mockKey, - mockSecret, - mockUrl, - testPayload, - }; - }; - - it('should sign the payload with OAuth1', () => { - const { mockKey, mockSecret, mockUrl, testPayload } = setup(); - - const result: Authorization = service.sign(mockKey, mockSecret, mockUrl, testPayload); - - expect(result).toEqual({ - oauth_consumer_key: mockKey, - oauth_nonce: expect.any(String), - oauth_signature: expect.any(String), - oauth_signature_method: 'HMAC-SHA1', - oauth_timestamp: expect.any(Number), - oauth_version: '1.0', - ...testPayload, - }); - }); - }); - }); -}); diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts index aa333f1c186..7e66754a99f 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts @@ -12,7 +12,8 @@ import { externalToolFactory } from '../../external-tool/testing'; import { SchoolExternalToolService } from '../../school-external-tool'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; import { ToolStatusNotLaunchableLoggableException } from '../error'; -import { LaunchRequestMethod, ToolLaunchRequest } from '../types'; +import { toolLaunchRequestFactory } from '../testing'; +import { ToolLaunchRequest } from '../types'; import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, @@ -95,13 +96,7 @@ describe(ToolLaunchService.name, () => { }, }); - const expectedLaunchRequest: ToolLaunchRequest = new ToolLaunchRequest({ - url: 'https://example.com/tool-launch', - method: LaunchRequestMethod.GET, - payload: '{ "key": "value" }', - openNewTab: false, - isDeepLink: true, - }); + const expectedLaunchRequest: ToolLaunchRequest = toolLaunchRequestFactory.build(); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); externalToolService.findById.mockResolvedValueOnce(externalTool); @@ -157,13 +152,7 @@ describe(ToolLaunchService.name, () => { }, }); - const expectedLaunchRequest: ToolLaunchRequest = new ToolLaunchRequest({ - url: 'https://example.com/tool-launch', - method: LaunchRequestMethod.GET, - payload: '{ "key": "value" }', - openNewTab: false, - isDeepLink: true, - }); + const expectedLaunchRequest: ToolLaunchRequest = toolLaunchRequestFactory.build(); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); externalToolService.findById.mockResolvedValueOnce(externalTool); @@ -219,13 +208,7 @@ describe(ToolLaunchService.name, () => { }, }); - const expectedLaunchRequest: ToolLaunchRequest = new ToolLaunchRequest({ - url: 'https://example.com/tool-launch', - method: LaunchRequestMethod.GET, - payload: '{ "key": "value" }', - openNewTab: false, - isDeepLink: true, - }); + const expectedLaunchRequest: ToolLaunchRequest = toolLaunchRequestFactory.build(); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); externalToolService.findById.mockResolvedValueOnce(externalTool); diff --git a/apps/server/src/modules/tool/tool-launch/testing/index.ts b/apps/server/src/modules/tool/tool-launch/testing/index.ts new file mode 100644 index 00000000000..5063cff353c --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/testing/index.ts @@ -0,0 +1 @@ +export { toolLaunchRequestFactory } from './tool-launch-request.factory'; diff --git a/apps/server/src/modules/tool/tool-launch/testing/tool-launch-request.factory.ts b/apps/server/src/modules/tool/tool-launch/testing/tool-launch-request.factory.ts new file mode 100644 index 00000000000..4faed72d7b1 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/testing/tool-launch-request.factory.ts @@ -0,0 +1,13 @@ +import { Factory } from 'fishery'; +import { LaunchRequestMethod, LaunchType, ToolLaunchRequest } from '../types'; + +export const toolLaunchRequestFactory = Factory.define( + () => + new ToolLaunchRequest({ + url: 'https://example.com/tool-launch', + method: LaunchRequestMethod.GET, + payload: '{ "key": "value" }', + openNewTab: false, + launchType: LaunchType.BASIC, + }) +); diff --git a/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts b/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts index d58f0ad2cca..d3d294baa55 100644 --- a/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts +++ b/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts @@ -10,7 +10,7 @@ import { CommonToolModule } from '../common'; import { ContextExternalToolModule } from '../context-external-tool'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; -import { Lti11EncryptionService, ToolLaunchService } from './service'; +import { ToolLaunchService } from './service'; import { AutoContextIdStrategy, AutoContextNameStrategy, @@ -37,7 +37,6 @@ import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrat ], providers: [ ToolLaunchService, - Lti11EncryptionService, BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, diff --git a/apps/server/src/modules/tool/tool-launch/types/index.ts b/apps/server/src/modules/tool/tool-launch/types/index.ts index 47a1fe842cf..34e79244b31 100644 --- a/apps/server/src/modules/tool/tool-launch/types/index.ts +++ b/apps/server/src/modules/tool/tool-launch/types/index.ts @@ -5,3 +5,4 @@ export * from './tool-launch-request'; export * from './tool-launch-data-type'; export * from './launch-request-method'; export * from './authentication-values'; +export * from './launch-type.enum'; diff --git a/apps/server/src/modules/tool/tool-launch/types/launch-type.enum.ts b/apps/server/src/modules/tool/tool-launch/types/launch-type.enum.ts new file mode 100644 index 00000000000..663fceedd92 --- /dev/null +++ b/apps/server/src/modules/tool/tool-launch/types/launch-type.enum.ts @@ -0,0 +1,6 @@ +export enum LaunchType { + BASIC = 'basic', + OAUTH2 = 'oauth2', + LTI11_BASIC_LAUNCH = 'lti11BasicLaunch', + LTI11_CONTENT_ITEM_SELECTION = 'lti11ContentItemSelection', +} diff --git a/apps/server/src/modules/tool/tool-launch/types/tool-launch-request.ts b/apps/server/src/modules/tool/tool-launch/types/tool-launch-request.ts index 9b7d34a130d..ee51eca6f8c 100644 --- a/apps/server/src/modules/tool/tool-launch/types/tool-launch-request.ts +++ b/apps/server/src/modules/tool/tool-launch/types/tool-launch-request.ts @@ -1,4 +1,5 @@ import { LaunchRequestMethod } from './launch-request-method'; +import { LaunchType } from './launch-type.enum'; export class ToolLaunchRequest { method: LaunchRequestMethod; @@ -9,13 +10,13 @@ export class ToolLaunchRequest { openNewTab: boolean; - isDeepLink: boolean; + launchType: LaunchType; constructor(props: ToolLaunchRequest) { this.url = props.url; this.method = props.method; this.payload = props.payload; this.openNewTab = props.openNewTab; - this.isDeepLink = props.isDeepLink; + this.launchType = props.launchType; } } diff --git a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts index a8b2e81e942..e83fb3afc64 100644 --- a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts @@ -15,7 +15,8 @@ import { contextExternalToolFactory } from '../../context-external-tool/testing' import { SchoolExternalToolService } from '../../school-external-tool'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; import { ToolLaunchService } from '../service'; -import { LaunchRequestMethod, ToolLaunchRequest } from '../types'; +import { toolLaunchRequestFactory } from '../testing'; +import { ToolLaunchRequest } from '../types'; import { ToolLaunchUc } from './tool-launch.uc'; describe('ToolLaunchUc', () => { @@ -84,13 +85,7 @@ describe('ToolLaunchUc', () => { id: contextExternalToolId, }); - const toolLaunchRequest: ToolLaunchRequest = new ToolLaunchRequest({ - url: 'baseUrl', - method: LaunchRequestMethod.GET, - payload: '', - openNewTab: true, - isDeepLink: true, - }); + const toolLaunchRequest: ToolLaunchRequest = toolLaunchRequestFactory.build(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); @@ -165,13 +160,7 @@ describe('ToolLaunchUc', () => { parameters: [], }; - const toolLaunchRequest = new ToolLaunchRequest({ - openNewTab: true, - method: LaunchRequestMethod.GET, - payload: '', - url: 'https://mock.com/', - isDeepLink: false, - }); + const toolLaunchRequest = toolLaunchRequestFactory.build(); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); diff --git a/apps/server/src/shared/controller/transformer/index.ts b/apps/server/src/shared/controller/transformer/index.ts index 26b5d83fa86..889f33130c7 100644 --- a/apps/server/src/shared/controller/transformer/index.ts +++ b/apps/server/src/shared/controller/transformer/index.ts @@ -3,3 +3,4 @@ export * from './decode-html-entities.transformer'; export * from './single-value-to-array.transformer'; export * from './sanitize-html.transformer'; export { PolymorphicArrayTransform } from './polymorphic-array.transformer'; +export { StringToObject } from './string-to-object.transformer'; diff --git a/apps/server/src/shared/controller/transformer/string-to-object.transformer.spec.ts b/apps/server/src/shared/controller/transformer/string-to-object.transformer.spec.ts new file mode 100644 index 00000000000..5480c49955f --- /dev/null +++ b/apps/server/src/shared/controller/transformer/string-to-object.transformer.spec.ts @@ -0,0 +1,75 @@ +import { plainToClass } from 'class-transformer'; +import { StringToObject } from './index'; + +class TestObject { + string!: string; + + number!: number; + + boolean!: boolean; + + array!: Array; +} + +class Dto { + @StringToObject(TestObject) + obj!: TestObject; +} + +describe('StringToObject Decorator', () => { + describe('when transform a string to an object', () => { + const setup = () => { + const obj: TestObject = { + string: 'test', + number: 1, + boolean: true, + array: [], + }; + + const plain = { + obj: JSON.stringify(obj), + }; + + return { + obj, + plain, + }; + }; + + it('should transform a string to an object', () => { + const { obj, plain } = setup(); + + const result = plainToClass(Dto, plain); + + expect(result.obj).toEqual(obj); + }); + }); + + describe('when the object is already an object', () => { + const setup = () => { + const obj: TestObject = { + string: 'test', + number: 1, + boolean: true, + array: [], + }; + + const plain = { + obj, + }; + + return { + obj, + plain, + }; + }; + + it('should stay an object', () => { + const { obj, plain } = setup(); + + const result = plainToClass(Dto, plain); + + expect(result.obj).toEqual(obj); + }); + }); +}); diff --git a/apps/server/src/shared/controller/transformer/string-to-object.transformer.ts b/apps/server/src/shared/controller/transformer/string-to-object.transformer.ts new file mode 100644 index 00000000000..c40c5404fdf --- /dev/null +++ b/apps/server/src/shared/controller/transformer/string-to-object.transformer.ts @@ -0,0 +1,15 @@ +import { ClassConstructor, plainToClass, Transform, TransformFnParams } from 'class-transformer'; + +export function StringToObject(classType: ClassConstructor): PropertyDecorator { + return Transform((params: TransformFnParams): unknown => { + if (typeof params.value === 'string') { + const res: unknown = JSON.parse(params.value); + + const obj: unknown = plainToClass(classType, res, params.options); + + return obj; + } + + return params.value; + }); +} diff --git a/apps/server/src/shared/controller/validator/index.ts b/apps/server/src/shared/controller/validator/index.ts index 711a5e04be2..e62c3e876bb 100644 --- a/apps/server/src/shared/controller/validator/index.ts +++ b/apps/server/src/shared/controller/validator/index.ts @@ -1 +1,2 @@ export * from './privacy-protect.validator'; +export { ValidateRecord } from './validate-record.validator'; diff --git a/apps/server/src/shared/controller/validator/validate-record.validator.spec.ts b/apps/server/src/shared/controller/validator/validate-record.validator.spec.ts new file mode 100644 index 00000000000..d7ad1d283f3 --- /dev/null +++ b/apps/server/src/shared/controller/validator/validate-record.validator.spec.ts @@ -0,0 +1,96 @@ +import { plainToClass } from 'class-transformer'; +import { isString, validate } from 'class-validator'; +import { ValidateRecord } from './validate-record.validator'; + +class Dto { + @ValidateRecord(isString) + obj!: Record; +} + +describe('ValidateRecord Validator', () => { + describe('when the record has only valid values', () => { + const setup = () => { + const dto: Dto = { + obj: { + string1: 'string1', + string2: 'string2', + }, + }; + + return { + dto, + }; + }; + + it('should return no errors', async () => { + const { dto } = setup(); + + const result = await validate(plainToClass(Dto, dto)); + + expect(result).toHaveLength(0); + }); + }); + + describe('when the record has an invalid value', () => { + const setup = () => { + const dto = { + obj: { + string1: 'string1', + number1: 1, + }, + }; + + return { + dto, + }; + }; + + it('should return an error', async () => { + const { dto } = setup(); + + const result = await validate(plainToClass(Dto, dto)); + + expect(result).toHaveLength(1); + }); + }); + + describe('when the target is not an object', () => { + const setup = () => { + const dto = { + obj: 1, + }; + + return { + dto, + }; + }; + + it('should return an error', async () => { + const { dto } = setup(); + + const result = await validate(plainToClass(Dto, dto)); + + expect(result).toHaveLength(1); + }); + }); + + describe('when the target is null', () => { + const setup = () => { + const dto = { + obj: null, + }; + + return { + dto, + }; + }; + + it('should return an error', async () => { + const { dto } = setup(); + + const result = await validate(plainToClass(Dto, dto)); + + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/apps/server/src/shared/controller/validator/validate-record.validator.ts b/apps/server/src/shared/controller/validator/validate-record.validator.ts new file mode 100644 index 00000000000..33a02dfe101 --- /dev/null +++ b/apps/server/src/shared/controller/validator/validate-record.validator.ts @@ -0,0 +1,23 @@ +import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'; + +export function ValidateRecord(validationFn: (value: unknown) => boolean, validationOptions?: ValidationOptions) { + return (object: object, propertyName: string): void => { + registerDecorator({ + name: 'ValidateRecord', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: unknown): boolean { + if (typeof value !== 'object' || value === null) { + return false; + } + return Object.values(value).every((val: unknown): boolean => validationFn(val)); + }, + defaultMessage(args: ValidationArguments): string { + return `${args.property} must be a record with valid values`; + }, + }, + }); + }; +} diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 537d17b30d5..45d69efea23 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -11,15 +11,15 @@ import { ExternalToolPseudonymEntity, PseudonymEntity } from '@modules/pseudonym import { RegistrationPinEntity } from '@modules/registration-pin/entity'; import { RocketChatUserEntity } from '@modules/rocketchat-user/entity'; import { RoomEntity } from '@modules/room/repo/entity'; -import { RoomMemberEntity } from '@src/modules/room-member/repo/entity/room-member.entity'; import { ShareToken } from '@modules/sharing/entity/share-token.entity'; import { SystemEntity } from '@modules/system/entity/system.entity'; import { TldrawDrawing } from '@modules/tldraw/entities'; -import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; +import { ContextExternalToolEntity, LtiDeepLinkTokenEntity } from '@modules/tool/context-external-tool/entity'; import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { ImportUser } from '@modules/user-import/entity'; import { MediaSourceEntity, MediaUserLicenseEntity, UserLicenseEntity } from '@modules/user-license/entity'; +import { RoomMemberEntity } from '@src/modules/room-member/repo/entity/room-member.entity'; import { ColumnBoardNode } from './column-board-node.entity'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -105,4 +105,5 @@ export const ALL_ENTITIES = [ InstanceEntity, MediaSourceEntity, OauthSessionTokenEntity, + LtiDeepLinkTokenEntity, ]; diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.spec.ts index a17efc3df59..b96b2150517 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.spec.ts @@ -8,6 +8,7 @@ import { ContextExternalToolEntity, ContextExternalToolType } from '@modules/too import { contextExternalToolEntityFactory, contextExternalToolFactory, + ltiDeepLinkFactory, } from '@modules/tool/context-external-tool/testing'; import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; @@ -151,10 +152,12 @@ describe(ContextExternalToolRepo.name, () => { const result: ContextExternalTool = await repo.save(domainObject); - expect(result).toMatchObject({ - ...domainObject.getProps(), - id: expect.any(String), - }); + expect(result).toEqual( + new ContextExternalTool({ + ...domainObject.getProps(), + id: expect.any(String), + }) + ); }); }); @@ -171,6 +174,7 @@ describe(ContextExternalToolRepo.name, () => { schoolToolId: new ObjectId().toHexString(), schoolId: undefined, }, + ltiDeepLink: ltiDeepLinkFactory.build(), }); return { @@ -183,10 +187,12 @@ describe(ContextExternalToolRepo.name, () => { const result: ContextExternalTool = await repo.save(domainObject); - expect(result).toMatchObject({ - ...domainObject.getProps(), - id: expect.any(String), - }); + expect(result).toEqual( + new ContextExternalTool({ + ...domainObject.getProps(), + id: expect.any(String), + }) + ); }); }); diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts index a5bd792ca85..e6165215b28 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts @@ -1,11 +1,12 @@ import { EntityName, Primary, Utils } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { ToolContextType } from '@modules/tool/common/enum/tool-context-type.enum'; -import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalTool, ContextRef, LtiDeepLink } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolEntity, ContextExternalToolEntityProps, ContextExternalToolType, + LtiDeepLinkEmbeddable, } from '@modules/tool/context-external-tool/entity'; import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; import { SchoolExternalToolRef } from '@modules/tool/school-external-tool/domain'; @@ -140,22 +141,55 @@ export class ContextExternalToolRepo { type: this.mapContextTypeToDomainObjectType(entity.contextType), }); + const ltiDeepLinkEntity: LtiDeepLinkEmbeddable | undefined = entity.ltiDeepLink; + const ltiDeepLink: LtiDeepLink | undefined = ltiDeepLinkEntity + ? new LtiDeepLink({ + mediaType: ltiDeepLinkEntity.mediaType, + url: ltiDeepLinkEntity.url, + title: ltiDeepLinkEntity.title, + text: ltiDeepLinkEntity.text, + parameters: ExternalToolRepoMapper.mapCustomParameterEntryEntitiesToDOs(ltiDeepLinkEntity.parameters), + availableFrom: ltiDeepLinkEntity.availableFrom, + availableUntil: ltiDeepLinkEntity.availableUntil, + submissionFrom: ltiDeepLinkEntity.submissionFrom, + submissionUntil: ltiDeepLinkEntity.submissionUntil, + }) + : undefined; + return new ContextExternalTool({ id: entity.id, schoolToolRef, contextRef, displayName: entity.displayName, parameters: ExternalToolRepoMapper.mapCustomParameterEntryEntitiesToDOs(entity.parameters), + ltiDeepLink, }); } private mapDomainObjectToEntityProps(entityDO: ContextExternalTool): ContextExternalToolEntityProps { + const { ltiDeepLink } = entityDO; + + const ltiDeepLinkEntity: LtiDeepLinkEmbeddable | undefined = ltiDeepLink + ? new LtiDeepLinkEmbeddable({ + mediaType: ltiDeepLink.mediaType, + url: ltiDeepLink.url, + title: ltiDeepLink.title, + text: ltiDeepLink.text, + parameters: ExternalToolRepoMapper.mapCustomParameterEntryDOsToEntities(ltiDeepLink.parameters), + availableFrom: ltiDeepLink.availableFrom, + availableUntil: ltiDeepLink.availableUntil, + submissionFrom: ltiDeepLink.submissionFrom, + submissionUntil: ltiDeepLink.submissionUntil, + }) + : undefined; + return { contextId: entityDO.contextRef.id, contextType: this.mapContextTypeToEntityType(entityDO.contextRef.type), displayName: entityDO.displayName, schoolTool: this.em.getReference(SchoolExternalToolEntity, entityDO.schoolToolRef.schoolToolId), parameters: ExternalToolRepoMapper.mapCustomParameterEntryDOsToEntities(entityDO.parameters), + ltiDeepLink: ltiDeepLinkEntity, }; } diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.spec.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.spec.ts index 88f57335a34..d538e13efed 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.spec.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.spec.ts @@ -56,8 +56,12 @@ describe(ExternalToolRepo.name, () => { const client2Id = 'client-2'; const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.withBasicConfig().buildWithId(); - const externalOauthTool: ExternalToolEntity = externalToolEntityFactory.withOauth2Config('client-1').buildWithId(); - const externalOauthTool2: ExternalToolEntity = externalToolEntityFactory.withOauth2Config('client-2').buildWithId(); + const externalOauthTool: ExternalToolEntity = externalToolEntityFactory + .withOauth2Config({ clientId: 'client-1' }) + .buildWithId(); + const externalOauthTool2: ExternalToolEntity = externalToolEntityFactory + .withOauth2Config({ clientId: 'client-2' }) + .buildWithId(); const externalLti11Tool: ExternalToolEntity = externalToolEntityFactory.withLti11Config().buildWithId(); await em.persistAndFlush([externalToolEntity, externalOauthTool, externalOauthTool2, externalLti11Tool]); diff --git a/apps/server/src/shared/testing/date-to-string.ts b/apps/server/src/shared/testing/date-to-string.ts new file mode 100644 index 00000000000..ecdfaea67bb --- /dev/null +++ b/apps/server/src/shared/testing/date-to-string.ts @@ -0,0 +1 @@ +export type DateToString = T extends Date ? string : T extends object ? { [K in keyof T]: DateToString } : T; diff --git a/apps/server/src/shared/testing/dates-to-strings.ts b/apps/server/src/shared/testing/dates-to-strings.ts deleted file mode 100644 index 96cc18d2c34..00000000000 --- a/apps/server/src/shared/testing/dates-to-strings.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type DatesToStrings = { - [k in keyof T]: T[k] extends Date ? string : DatesToStrings; -}; diff --git a/apps/server/src/shared/testing/index.ts b/apps/server/src/shared/testing/index.ts index 1ab2378f8be..97f44e7dd32 100644 --- a/apps/server/src/shared/testing/index.ts +++ b/apps/server/src/shared/testing/index.ts @@ -71,4 +71,4 @@ export { cleanupCollections } from './cleanup-collections'; export { mapUserToCurrentUser } from './map-user-to-current-user'; export { TestApiClient } from './test-api-client'; export { WebSocketReadyStateEnum } from './web-socket-ready-state-enum'; -export { DatesToStrings } from './dates-to-strings'; +export { DateToString } from './date-to-string'; diff --git a/backup/setup/external-tools.json b/backup/setup/external-tools.json index 9dfb704a35b..68da38a66ab 100644 --- a/backup/setup/external-tools.json +++ b/backup/setup/external-tools.json @@ -1126,8 +1126,8 @@ "parameters": [], "isHidden": false, "isDeactivated": false, - "openNewTab": false, - "restrictToContexts": [], + "openNewTab": true, + "restrictToContexts": ["board-element", "course"], "isPreferred": true, "iconName": "mdiMovieRoll" }, From 8ed89d1dfbbeeec58eaa24b121ac04a243a4c154 Mon Sep 17 00:00:00 2001 From: Max <53796487+dyedwiper@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:41:10 +0100 Subject: [PATCH 14/55] BC-7994 Remove unused dependency nodemon (#5366) --- .nodemon.json | 7 ---- package-lock.json | 104 ---------------------------------------------- package.json | 1 - 3 files changed, 112 deletions(-) delete mode 100644 .nodemon.json diff --git a/.nodemon.json b/.nodemon.json deleted file mode 100644 index 0011ae62de5..00000000000 --- a/.nodemon.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ignore": ["**/*.test.js", "**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"], - "watch": ["src", "config", "public"], - "ext": "js,json,ts,yml,yaml", - "verbose": true, - "delay": "2500" -} diff --git a/package-lock.json b/package-lock.json index c0ccab39c35..f7644fe0da6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -205,7 +205,6 @@ "mocha": "^9.1.3", "mockery": "^2.0.0", "nock": "^13.0.0", - "nodemon": "^3.1.4", "nyc": "^15.0.1", "prettier": "^2.8.1", "prettier-eslint": "^12.0.0", @@ -9680,11 +9679,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/abbrev": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, "node_modules/accepts": { "version": "1.3.8", "license": "MIT", @@ -15566,11 +15560,6 @@ "node": ">= 4" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, "node_modules/image-size": { "version": "1.0.2", "license": "MIT", @@ -20508,54 +20497,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nodemon": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", - "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/noms": { "version": "0.0.0", "dev": true, @@ -20581,17 +20522,6 @@ "string_decoder": "~0.10.x" } }, - "node_modules/nopt": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "dev": true, @@ -22203,11 +22133,6 @@ "version": "1.8.0", "license": "MIT" }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.0", "license": "MIT", @@ -23719,19 +23644,6 @@ "version": "0.3.2", "license": "MIT" }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sinon": { "version": "11.1.2", "dev": true, @@ -24968,17 +24880,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/touch": { - "version": "3.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { "version": "4.1.3", "license": "BSD-3-Clause", @@ -25633,11 +25534,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, "node_modules/underscore": { "version": "1.12.1", "license": "MIT" diff --git a/package.json b/package.json index 1159b4954ad..8b1e6e52817 100644 --- a/package.json +++ b/package.json @@ -323,7 +323,6 @@ "mocha": "^9.1.3", "mockery": "^2.0.0", "nock": "^13.0.0", - "nodemon": "^3.1.4", "nyc": "^15.0.1", "prettier": "^2.8.1", "prettier-eslint": "^12.0.0", From f8ac73193e5d608e32a061e4e0f2d8fdd53998c2 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Tue, 3 Dec 2024 16:12:38 +0100 Subject: [PATCH 15/55] EW-1057: reverted non-needed changes to the index.ts files in folders outside of infra, provisioning and tsp --- apps/server/src/core/error/loggable/index.ts | 4 +- apps/server/src/core/logger/index.ts | 21 ++--- .../src/core/logger/interfaces/index.ts | 4 +- apps/server/src/core/logger/types/index.ts | 9 +- apps/server/src/core/validation/index.ts | 2 +- .../learnroom/service/board-copy.service.ts | 8 +- .../school/repo/mikro-orm/mapper/index.ts | 6 +- .../src/modules/synchronization/repo/index.ts | 4 +- .../synchronization/repo/mapper/index.ts | 2 +- .../tool/external-tool/entity/index.ts | 17 +--- apps/server/src/shared/common/error/index.ts | 16 ++-- .../src/shared/domain/domainobject/index.ts | 18 ++-- apps/server/src/shared/domain/entity/index.ts | 90 +++++-------------- .../domain/entity/legacy-board/index.ts | 15 ++-- .../src/shared/domain/interface/index.ts | 18 ++-- apps/server/src/shared/testing/index.ts | 80 ++--------------- 16 files changed, 88 insertions(+), 226 deletions(-) diff --git a/apps/server/src/core/error/loggable/index.ts b/apps/server/src/core/error/loggable/index.ts index 108f319b561..0470cbee690 100644 --- a/apps/server/src/core/error/loggable/index.ts +++ b/apps/server/src/core/error/loggable/index.ts @@ -1,2 +1,2 @@ -export { ErrorLoggable } from './error.loggable'; -export { AxiosErrorLoggable } from './axios-error.loggable'; +export * from './error.loggable'; +export * from './axios-error.loggable'; diff --git a/apps/server/src/core/logger/index.ts b/apps/server/src/core/logger/index.ts index 005753c7b4d..1037edaaa34 100644 --- a/apps/server/src/core/logger/index.ts +++ b/apps/server/src/core/logger/index.ts @@ -1,15 +1,8 @@ -export { Loggable, ILegacyLogger } from './interfaces'; -export { LoggerModule } from './logger.module'; -export { LegacyLogger } from './legacy-logger.service'; -export { Logger } from './logger'; -export { ErrorLogger } from './error-logger'; -export { - LogMessage, - LogMessageData, - LogMessageDataObject, - LogMessageWithContext, - ErrorLogMessage, - ValidationErrorLogMessage, -} from './types'; -export { LoggingUtils } from './logging.utils'; +export * from './interfaces'; +export * from './logger.module'; +export * from './legacy-logger.service'; +export * from './logger'; +export * from './error-logger'; +export * from './types'; +export * from './logging.utils'; export { LoggerConfig } from './logger.config'; diff --git a/apps/server/src/core/logger/interfaces/index.ts b/apps/server/src/core/logger/interfaces/index.ts index 871f806743d..77c1c08176d 100644 --- a/apps/server/src/core/logger/interfaces/index.ts +++ b/apps/server/src/core/logger/interfaces/index.ts @@ -1,2 +1,2 @@ -export { ILegacyLogger, RequestLoggingBody } from './legacy-logger.interface'; -export { Loggable } from './loggable'; +export * from './legacy-logger.interface'; +export * from './loggable'; diff --git a/apps/server/src/core/logger/types/index.ts b/apps/server/src/core/logger/types/index.ts index 0b9dc3b409f..71e27b34575 100644 --- a/apps/server/src/core/logger/types/index.ts +++ b/apps/server/src/core/logger/types/index.ts @@ -1,8 +1 @@ -export { - LogMessage, - ErrorLogMessage, - ValidationErrorLogMessage, - LogMessageWithContext, - LogMessageData, - LogMessageDataObject, -} from './logging.types'; +export * from './logging.types'; diff --git a/apps/server/src/core/validation/index.ts b/apps/server/src/core/validation/index.ts index 177ecd810c2..28dc72dd458 100644 --- a/apps/server/src/core/validation/index.ts +++ b/apps/server/src/core/validation/index.ts @@ -1 +1 @@ -export { ValidationModule } from './validation.module'; +export * from './validation.module'; diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.ts b/apps/server/src/modules/learnroom/service/board-copy.service.ts index 67ace1924ee..b77dd910e6f 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.ts @@ -4,10 +4,6 @@ import { LessonCopyService } from '@modules/lesson'; import { TaskCopyService } from '@modules/task'; import { Injectable } from '@nestjs/common'; import { getResolvedValues } from '@shared/common/utils/promise'; -import { EntityId } from '@shared/domain/types'; -import { LegacyBoardRepo } from '@shared/repo'; -import { LegacyLogger } from '@src/core/logger'; -import { sortBy } from 'lodash'; import { ColumnboardBoardElement, ColumnBoardNode, @@ -23,6 +19,10 @@ import { TaskBoardElement, User, } from '@shared/domain/entity'; +import { EntityId } from '@shared/domain/types'; +import { LegacyBoardRepo } from '@shared/repo'; +import { LegacyLogger } from '@src/core/logger'; +import { sortBy } from 'lodash'; import { ColumnBoardNodeRepo } from '../repo'; type BoardCopyParams = { diff --git a/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts b/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts index 0b07f6c24f9..545b7213ea7 100644 --- a/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts +++ b/apps/server/src/modules/school/repo/mikro-orm/mapper/index.ts @@ -1,3 +1,3 @@ -export { FederalStateEntityMapper } from './federal-state.entity.mapper'; -export { SchoolYearEntityMapper } from './school-year.entity.mapper'; -export { SchoolEntityMapper } from './school.entity.mapper'; +export * from './federal-state.entity.mapper'; +export * from './school-year.entity.mapper'; +export * from './school.entity.mapper'; diff --git a/apps/server/src/modules/synchronization/repo/index.ts b/apps/server/src/modules/synchronization/repo/index.ts index 026819e3d40..d9e439d5871 100644 --- a/apps/server/src/modules/synchronization/repo/index.ts +++ b/apps/server/src/modules/synchronization/repo/index.ts @@ -1,2 +1,2 @@ -export { SynchronizationEntity, SynchronizationEntityProps } from './entity'; -export { SynchronizationRepo } from './synchronization.repo'; +export * from './entity'; +export * from './synchronization.repo'; diff --git a/apps/server/src/modules/synchronization/repo/mapper/index.ts b/apps/server/src/modules/synchronization/repo/mapper/index.ts index a27eb43fb06..5b1ccd6a474 100644 --- a/apps/server/src/modules/synchronization/repo/mapper/index.ts +++ b/apps/server/src/modules/synchronization/repo/mapper/index.ts @@ -1 +1 @@ -export { SynchronizationMapper } from './synchronization.mapper'; +export * from './synchronization.mapper'; diff --git a/apps/server/src/modules/tool/external-tool/entity/index.ts b/apps/server/src/modules/tool/external-tool/entity/index.ts index 772848e2f9d..07ade6ea541 100644 --- a/apps/server/src/modules/tool/external-tool/entity/index.ts +++ b/apps/server/src/modules/tool/external-tool/entity/index.ts @@ -1,16 +1,5 @@ -export { ExternalToolEntity, ExternalToolEntityProps } from './external-tool.entity'; -export { - BasicToolConfigEntity, - ExternalToolConfigEntity, - Lti11ToolConfigEntity, - Oauth2ToolConfigEntity, -} from './config'; -export { - CustomParameterEntity, - CustomParameterLocation, - CustomParameterScope, - CustomParameterType, - autoParameters, -} from './custom-parameter'; +export * from './external-tool.entity'; +export * from './config'; +export * from './custom-parameter'; export { ExternalToolMediumEntity } from './external-tool-medium.entity'; export { FileRecordRefEmbeddable } from './file-record-ref.embeddable'; diff --git a/apps/server/src/shared/common/error/index.ts b/apps/server/src/shared/common/error/index.ts index a157fd4a7c0..58446694939 100644 --- a/apps/server/src/shared/common/error/index.ts +++ b/apps/server/src/shared/common/error/index.ts @@ -1,10 +1,10 @@ -export { ApiValidationError } from './api-validation.error'; -export { AuthorizationError } from './authorization.error'; -export { BusinessError } from './business.error'; -export { EntityNotFoundError } from './entity-not-found.error'; -export { ForbiddenOperationError } from './forbidden-operation.error'; -export { ValidationError } from './validation.error'; -export { ValidationErrorLogMessage, ErrorLogMessage } from './interfaces'; +export * from './api-validation.error'; +export * from './authorization.error'; +export * from './business.error'; +export * from './entity-not-found.error'; +export * from './forbidden-operation.error'; +export * from './validation.error'; +export * from './interfaces'; // business errors -export { UserAlreadyAssignedToImportUserError } from './user-already-assigned-to-import-user.business-error'; +export * from './user-already-assigned-to-import-user.business-error'; diff --git a/apps/server/src/shared/domain/domainobject/index.ts b/apps/server/src/shared/domain/domainobject/index.ts index f1e15d4290a..00e53066f61 100644 --- a/apps/server/src/shared/domain/domainobject/index.ts +++ b/apps/server/src/shared/domain/domainobject/index.ts @@ -1,11 +1,11 @@ export * from './base.do'; -export { Pseudonym, PseudonymProps } from './pseudonym.do'; -export { VideoConferenceDO, VideoConferenceOptionsDO } from './video-conference.do'; -export { UserLoginMigrationDO } from './user-login-migration.do'; +export * from './pseudonym.do'; +export * from './video-conference.do'; +export * from './user-login-migration.do'; export * from './legacy-school.do'; -export { UserDO } from './user.do'; -export { Page } from './page'; -export { RoleReference } from './role-reference'; -export { CustomLtiPropertyDO, LtiToolDO } from './ltitool.do'; -export { ExternalSource } from './external-source'; -export { UserSourceOptions, UserSourceOptionsProps } from './user-source-options.do'; +export * from './user.do'; +export * from './page'; +export * from './role-reference'; +export * from './ltitool.do'; +export * from './external-source'; +export * from './user-source-options.do'; diff --git a/apps/server/src/shared/domain/entity/index.ts b/apps/server/src/shared/domain/entity/index.ts index 8e3be771d10..0925cb5d2d9 100644 --- a/apps/server/src/shared/domain/entity/index.ts +++ b/apps/server/src/shared/domain/entity/index.ts @@ -1,66 +1,24 @@ -export { ALL_ENTITIES } from './all-entities'; -export { BaseEntity, BaseEntityReference, BaseEntityWithTimestamps, baseEntityProperties } from './base.entity'; -export { ColumnBoardNodeProps, ColumnBoardNode } from './column-board-node.entity'; -export { Course, CourseFeatures, CourseProperties, UsersList, SyncAttribute } from './course.entity'; -export { CourseGroup, CourseGroupProperties } from './coursegroup.entity'; -export { - DashboardEntity, - DashboardProps, - GridElement, - GridElementContent, - GridElementWithPosition, - GridPosition, - GridPositionWithGroupIndex, - IGridElement, -} from './dashboard.entity'; -export { - DashboardGridElementModel, - DashboardGridElementModelProperties, - DashboardModelEntity, - DashboardModelProperties, -} from './dashboard.model.entity'; -export { CountyEmbeddable, FederalStateEntity, FederalStateProperties } from './federal-state.entity'; -export { - BoardProps, - ColumnboardBoardElement, - LegacyBoard, - LegacyBoardElement, - LegacyBoardElementProps, - LegacyBoardElementReference, - LegacyBoardElementType, - LessonBoardElement, - TaskBoardElement, -} from './legacy-board'; -export { - ComponentEtherpadProperties, - ComponentGeogebraProperties, - ComponentInternalProperties, - ComponentLernstoreProperties, - ComponentNexboardProperties, - ComponentProperties, - ComponentTextProperties, - ComponentType, - LessonEntity, - LessonParent, - LessonProperties, - isLesson, -} from './lesson.entity'; -export { CustomLtiProperty, ILtiToolProperties, LtiPrivacyPermission, LtiRoleType, LtiTool } from './ltitool.entity'; -export { Material, MaterialProperties, RelatedResourceProperties, TargetGroupProperties } from './materials.entity'; -export { CourseNews, News, NewsProperties, SchoolNews, TeamNews } from './news.entity'; -export { Role, RoleProperties } from './role.entity'; -export { SchoolEntity, SchoolProperties, SchoolRolePermission, SchoolRoles } from './school.entity'; -export { SchoolYearEntity, SchoolYearProperties } from './schoolyear.entity'; -export { StorageProviderEntity, StorageProviderProperties } from './storageprovider.entity'; -export { Submission, SubmissionProperties } from './submission.entity'; -export { Task, TaskParent, TaskParentDescriptions, TaskWithStatusVo, isTask } from './task.entity'; -export { TeamEntity, TeamProperties, TeamUserEntity, TeamUserProperties } from './team.entity'; -export { IUserLoginMigration, UserLoginMigrationEntity } from './user-login-migration.entity'; -export { User, UserProperties, UserSchoolEmbeddable } from './user.entity'; -export { - IVideoConferenceProperties, - TargetModels, - VideoConference, - VideoConferenceOptions, -} from './video-conference.entity'; -export { ConsentEntity, ParentConsentEntity, UserConsentEntity } from './consent'; +export * from './all-entities'; +export * from './base.entity'; +export * from './column-board-node.entity'; +export * from './course.entity'; +export * from './coursegroup.entity'; +export * from './dashboard.entity'; +export * from './dashboard.model.entity'; +export * from './federal-state.entity'; +export * from './legacy-board'; +export * from './lesson.entity'; +export * from './ltitool.entity'; +export * from './materials.entity'; +export * from './news.entity'; +export * from './role.entity'; +export * from './school.entity'; +export * from './schoolyear.entity'; +export * from './storageprovider.entity'; +export * from './submission.entity'; +export * from './task.entity'; +export * from './team.entity'; +export * from './user-login-migration.entity'; +export * from './user.entity'; +export * from './video-conference.entity'; +export * from './consent'; diff --git a/apps/server/src/shared/domain/entity/legacy-board/index.ts b/apps/server/src/shared/domain/entity/legacy-board/index.ts index ba9072ecc1b..bf6b64bda35 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/index.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/index.ts @@ -1,10 +1,5 @@ -export { BoardProps, LegacyBoard } from './legacy-board.entity'; -export { - LegacyBoardElement, - LegacyBoardElementProps, - LegacyBoardElementReference, - LegacyBoardElementType, -} from './legacy-boardelement.entity'; -export { ColumnboardBoardElement } from './column-board-boardelement'; -export { LessonBoardElement } from './lesson-boardelement.entity'; -export { TaskBoardElement } from './task-boardelement.entity'; +export * from './legacy-board.entity'; +export * from './legacy-boardelement.entity'; +export * from './column-board-boardelement'; +export * from './lesson-boardelement.entity'; +export * from './task-boardelement.entity'; diff --git a/apps/server/src/shared/domain/interface/index.ts b/apps/server/src/shared/domain/interface/index.ts index 5fb0a74d7f4..b810ad12971 100644 --- a/apps/server/src/shared/domain/interface/index.ts +++ b/apps/server/src/shared/domain/interface/index.ts @@ -1,9 +1,9 @@ -export { IdmAccount, IdmAccountUpdate } from './account'; -export { EntityWithSchool, IEntity, IEntityWithTimestamps } from './entity'; -export { IFindOptions, Pagination, SortOrder, SortOrderMap, SortOrderNumberType } from './find-options'; -export { LanguageType } from './language-type.enum'; -export { Learnroom, LearnroomElement } from './learnroom'; -export { Permission } from './permission.enum'; -export { GuestRole, GuestRoleArray, IUserRoleName, RoleName, RoomRole, RoomRoleArray } from './rolename.enum'; -export { VideoConferenceScope } from './video-conference-scope.enum'; -export { SystemProvisioningStrategy } from './system-provisioning.strategy'; +export * from './account'; +export * from './entity'; +export * from './find-options'; +export * from './language-type.enum'; +export * from './learnroom'; +export * from './permission.enum'; +export * from './rolename.enum'; +export * from './video-conference-scope.enum'; +export * from './system-provisioning.strategy'; diff --git a/apps/server/src/shared/testing/index.ts b/apps/server/src/shared/testing/index.ts index 97f44e7dd32..c891be689a6 100644 --- a/apps/server/src/shared/testing/index.ts +++ b/apps/server/src/shared/testing/index.ts @@ -1,74 +1,8 @@ -export { - AxiosHeadersKeyValue, - BaseFactory, - CurrentUserFactory, - DoBaseFactory, - DomainObjectFactory, - EntityFactory, - JwtPayloadFactory, - JwtTestFactory, - SystemEntityFactory, - UserAndAccountParams, - UserAndAccountTestFactory, - axiosErrorFactory, - axiosResponseFactory, - boardFactory, - columnBoardNodeFactory, - columnboardBoardElementFactory, - countyEmbeddableFactory, - courseFactory, - courseGroupFactory, - courseNewsFactory, - courseUnpublishedNewsFactory, - currentUserFactory, - externalGroupDtoFactory, - externalSchoolDtoFactory, - externalToolPseudonymEntityFactory, - federalStateFactory, - fileRecordFactory, - groupEntityFactory, - groupFactory, - h5pContentFactory, - importUserFactory, - jwtPayloadFactory, - legacyFileEntityMockFactory, - legacySchoolDoFactory, - lessonBoardElementFactory, - lessonFactory, - ltiToolDOFactory, - materialFactory, - pseudonymFactory, - roleDtoFactory, - roleFactory, - schoolEntityFactory, - schoolNewsFactory, - schoolSystemOptionsEntityFactory, - schoolSystemOptionsFactory, - schoolUnpublishedNewsFactory, - schoolYearFactory, - shareTokenFactory, - storageProviderFactory, - submissionFactory, - systemEntityFactory, - systemLdapConfigEntityFactory, - systemOauthConfigEntityFactory, - systemOidcConfigEntityFactory, - taskBoardElementFactory, - taskFactory, - teamFactory, - teamNewsFactory, - teamUnpublishedNewsFactory, - teamUserFactory, - tldrawFileDtoFactory, - userDoFactory, - userFactory, - userLoginMigrationDOFactory, - userLoginMigrationFactory, -} from './factory'; -export { setupEntities } from './setup-entities'; -export { createCollections } from './create-collections'; -export { cleanupCollections } from './cleanup-collections'; -export { mapUserToCurrentUser } from './map-user-to-current-user'; -export { TestApiClient } from './test-api-client'; -export { WebSocketReadyStateEnum } from './web-socket-ready-state-enum'; +export * from './factory'; +export * from './setup-entities'; +export * from './create-collections'; +export * from './cleanup-collections'; +export * from './map-user-to-current-user'; +export * from './test-api-client'; +export * from './web-socket-ready-state-enum'; export { DateToString } from './date-to-string'; From 3e6a35c589260e16a02b12dc89e21d7970e1b0af Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Tue, 3 Dec 2024 17:15:12 +0100 Subject: [PATCH 16/55] EW-1057: using src in core/logger --- apps/server/src/infra/sync/sync.module.ts | 2 +- .../src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts | 2 +- .../sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts | 2 +- .../loggable/tsp-legacy-migration-system-missing.loggable.ts | 2 +- .../tsp/loggable/tsp-legacy-school-migration-count.loggable.ts | 2 +- .../loggable/tsp-legacy-school-migration-success.loggable.ts | 2 +- .../infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts | 2 +- .../src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts | 2 +- .../src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts | 2 +- .../infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts | 2 +- .../infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts | 2 +- .../infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts | 2 +- .../src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts | 2 +- .../src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts | 2 +- .../tsp/loggable/tsp-system-not-found.loggable-exception.ts | 2 +- .../infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts | 2 +- .../infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts | 2 +- .../src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-fetch.service.ts | 2 +- .../sync/tsp/tsp-legacy-migration.service.integration.spec.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts | 2 +- 25 files changed, 25 insertions(+), 25 deletions(-) diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index ea4bddffbb8..164508ceb4b 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -9,7 +9,7 @@ import { ProvisioningModule } from '@modules/provisioning'; import { TspClientModule } from '@infra/tsp-client'; import { ConsoleWriterModule } from '@infra/console'; import { RabbitMQWrapperModule } from '@infra/rabbitmq'; -import { LoggerModule } from '../../core/logger'; +import { LoggerModule } from '@src/core/logger'; import { SyncConsole } from './console/sync.console'; import { SyncService } from './service/sync.service'; import { TspLegacyMigrationService } from './tsp/tsp-legacy-migration.service'; diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts index 3606755d851..e096993c328 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspDataFetchedLoggable implements Loggable { constructor( diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts index 3277bace76d..c3af20dc0c7 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspLegacyMigrationStartLoggable implements Loggable { getLogMessage(): LogMessage { diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts index 07a555c93f0..fcdf3b26d0a 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspLegacyMigrationSystemMissingLoggable implements Loggable { getLogMessage(): LogMessage { diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts index bb9679433fc..c04fc6b5a53 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspLegacySchoolMigrationCountLoggable implements Loggable { constructor(private readonly total: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts index a81a62f549a..b6ac7b247e2 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspLegacySchoolMigrationSuccessLoggable implements Loggable { constructor(private readonly total: number, private readonly migrated: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts index 262c6d97703..1ef7d8114b2 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspMissingExternalIdLoggable implements Loggable { constructor(private readonly objectType: string) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts index 5e7f777a917..f2f5bf512a7 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspSchoolsFetchedLoggable implements Loggable { constructor(private readonly tspSchoolCount: number, private readonly daysFetched: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts index 126942ef5e5..1068270a151 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspSchoolsSyncedLoggable implements Loggable { constructor( diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts index 2b7245862d1..3be67a149ee 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspSchulnummerMissingLoggable implements Loggable { constructor(private readonly schulName?: string) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts index db4d2f67e3c..5a8af5758b3 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspStudentsFetchedLoggable implements Loggable { constructor(private readonly tspStudentCount: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts index 0465556da9c..5937433ea6e 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspStudentsMigratedLoggable implements Loggable { constructor(private readonly migratedStudents: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts index 29b59ecbeb3..58a15f94cfa 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspSyncedUsersLoggable implements Loggable { constructor(private readonly syncedUsers: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts index 73c3cacc775..58efd8e41c8 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspSyncingUsersLoggable implements Loggable { constructor(private readonly syncingUsers: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts index 6394a258cb6..c65fd7ad992 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts @@ -1,6 +1,6 @@ import { HttpStatus } from '@nestjs/common'; import { BusinessError, ErrorLogMessage } from '@shared/common'; -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspSystemNotFoundLoggableException extends BusinessError implements Loggable { constructor() { diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts index 9896045abaa..476327462bc 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspTeachersFetchedLoggable implements Loggable { constructor(private readonly tspTeacherCount: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts index c3fe90566b4..ebbe515d06c 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspTeachersMigratedLoggable implements Loggable { constructor(private readonly migratedTeachers: number) {} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts index b93e3e2549b..9000de6b9bc 100644 --- a/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts @@ -1,4 +1,4 @@ -import { Loggable, LogMessage } from '../../../../core/logger'; +import { Loggable, LogMessage } from '@src/core/logger'; export class TspUsersMigratedLoggable implements Loggable { constructor(private readonly migratedUsers: number) {} diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts index f49e2c37d8c..3acdb507e4f 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { AxiosError, AxiosResponse } from 'axios'; +import { Logger } from '@src/core/logger'; import { ExportApiInterface, RobjExportKlasse, @@ -14,7 +15,6 @@ import { } from '../../tsp-client'; import { systemFactory } from '../../../modules/system/testing'; import { OauthConfigMissingLoggableException } from '../../../modules/oauth/loggable'; -import { Logger } from '../../../core/logger'; import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; import { TspFetchService } from './tsp-fetch.service'; diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts index 93f45386b96..10f27ca31d2 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts @@ -3,8 +3,8 @@ import { AxiosError, AxiosResponse } from 'axios'; import moment from 'moment'; import { System } from '@modules/system'; import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; +import { Logger } from '@src/core/logger'; import { ExportApiInterface, TspClientFactory } from '../../tsp-client'; -import { Logger } from '../../../core/logger'; import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; @Injectable() diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts index 01d09432ec1..3de6608b8f3 100644 --- a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts @@ -6,7 +6,7 @@ import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '@s import { SchoolFeature } from '@shared/domain/types'; import { SystemProvisioningStrategy } from '@shared/domain/interface'; import { SchoolEntity } from '@shared/domain/entity'; -import { Logger } from '../../../core/logger'; +import { Logger } from '@src/core/logger'; import { MongoMemoryDatabaseModule } from '../../database'; import { TspLegacyMigrationSystemMissingLoggable } from './loggable/tsp-legacy-migration-system-missing.loggable'; import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts index f01ed36cb84..1bff95ef2b5 100644 --- a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts @@ -1,7 +1,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { EntityId, SchoolFeature } from '@shared/domain/types'; -import { Logger } from '../../../core/logger'; +import { Logger } from '@src/core/logger'; import { TspLegacyMigrationStartLoggable } from './loggable/tsp-legacy-migration-start.loggable'; import { TspLegacyMigrationSystemMissingLoggable } from './loggable/tsp-legacy-migration-system-missing.loggable'; import { TspLegacySchoolMigrationCountLoggable } from './loggable/tsp-legacy-school-migration-count.loggable'; diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index 6e43463af91..662fa1ac244 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -13,7 +13,7 @@ import { ProvisioningSystemDto, } from '@modules/provisioning'; import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; -import { Logger } from '../../../core/logger'; +import { Logger } from '@src/core/logger'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index 91984b046d4..a6baeca2ed9 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -11,7 +11,7 @@ import { OauthDataDto, ProvisioningSystemDto, } from '@modules/provisioning'; -import { Logger } from '../../../core/logger'; +import { Logger } from '@src/core/logger'; import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index de780853688..dbb5ddb897a 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -12,7 +12,7 @@ import { systemFactory } from '@modules/system/testing'; import { userDoFactory } from '@shared/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface'; import { UserDO } from '@shared/domain/domainobject'; -import { Logger } from '../../../core/logger'; +import { Logger } from '@src/core/logger'; import { RobjExportKlasse, RobjExportLehrer, From 14ac944b4d47fb13d3f24bcfea98073f1ae271bd Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Tue, 3 Dec 2024 17:51:21 +0100 Subject: [PATCH 17/55] EW-1057: fixing deep imports --- apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts index 3acdb507e4f..77355db9389 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts @@ -3,6 +3,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { AxiosError, AxiosResponse } from 'axios'; import { Logger } from '@src/core/logger'; +import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; +import { systemFactory } from '@modules/system/testing'; +import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; import { ExportApiInterface, RobjExportKlasse, @@ -13,9 +16,6 @@ import { RobjExportSchule, TspClientFactory, } from '../../tsp-client'; -import { systemFactory } from '../../../modules/system/testing'; -import { OauthConfigMissingLoggableException } from '../../../modules/oauth/loggable'; -import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; import { TspFetchService } from './tsp-fetch.service'; describe(TspFetchService.name, () => { From 959791be6c87a0512a93fc9205e05905c429aa9a Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Tue, 3 Dec 2024 18:02:54 +0100 Subject: [PATCH 18/55] EW-1057: fixed src/core/logger imports --- apps/server/src/infra/sync/tsp/tsp-fetch.service.ts | 2 +- apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts | 2 +- apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts | 4 ++-- apps/server/src/modules/provisioning/provisioning.module.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts index 10f27ca31d2..d5bc02c2cf9 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts @@ -4,8 +4,8 @@ import moment from 'moment'; import { System } from '@modules/system'; import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; import { Logger } from '@src/core/logger'; +import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; import { ExportApiInterface, TspClientFactory } from '../../tsp-client'; -import { AxiosErrorLoggable, ErrorLoggable } from '../../../core/error/loggable'; @Injectable() export class TspFetchService { diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index 662fa1ac244..61529d17b4b 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -12,8 +12,8 @@ import { OauthDataDto, ProvisioningSystemDto, } from '@modules/provisioning'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; import { Logger } from '@src/core/logger'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index bd7af8d05e8..6f9c771eaa6 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -5,8 +5,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import axios, { AxiosError } from 'axios'; import { ServerConfig } from '@modules/server'; import { OauthAdapterService } from '@modules/oauth'; -import { AxiosErrorLoggable, ErrorLoggable } from '../../core/error/loggable'; -import { Logger } from '../../core/logger'; +import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; +import { Logger } from '@src/core/logger'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 3dce3da8a1b..50fd4bfbfa3 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -9,7 +9,7 @@ import { ExternalToolModule } from '@modules/tool'; import { SchoolExternalToolModule } from '@modules/tool/school-external-tool'; import { UserModule } from '@modules/user'; import { AccountModule } from '@modules/account'; -import { LoggerModule } from '../../core/logger'; +import { LoggerModule } from '@src/core/logger'; import { SchulconnexClientModule } from '../../infra/schulconnex-client'; import { ClassModule } from '../class'; import { UserLicenseModule } from '../user-license'; From 6e3a643fa9bb29007ff40e2d6f234560ca83d33c Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Wed, 4 Dec 2024 11:17:37 +0100 Subject: [PATCH 19/55] add and use factories for robj export models --- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 64 +++++++++---------- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 51 +++++++-------- .../factory/robj-export-klasse.factory.ts | 14 ++++ .../robj-export-lehrer-migration.factory.ts | 12 ++++ .../factory/robj-export-lehrer.factory.ts | 13 ++++ .../factory/robj-export-schueler.factory.ts | 13 ++++ .../factory/robj-export-schule.factory.ts | 9 +++ 7 files changed, 115 insertions(+), 61 deletions(-) create mode 100644 apps/server/src/shared/testing/factory/robj-export-klasse.factory.ts create mode 100644 apps/server/src/shared/testing/factory/robj-export-lehrer-migration.factory.ts create mode 100644 apps/server/src/shared/testing/factory/robj-export-lehrer.factory.ts create mode 100644 apps/server/src/shared/testing/factory/robj-export-schueler.factory.ts create mode 100644 apps/server/src/shared/testing/factory/robj-export-schule.factory.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index 8dca6da47d1..7b25d5c2ba7 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ExternalClassDto, ProvisioningSystemDto } from '@modules/provisioning'; +import { ProvisioningSystemDto } from '@modules/provisioning'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; @@ -8,8 +8,10 @@ import { externalClassDtoFactory } from '@shared/testing/factory/external-class- import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { robjExportKlasseFactory } from '@shared/testing/factory/robj-export-klasse.factory'; +import { robjExportLehrerFactory } from '@shared/testing/factory/robj-export-lehrer.factory'; +import { robjExportSchuelerFactory } from '@shared/testing/factory/robj-export-schueler.factory'; import { Logger } from '@src/core/logger'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; import { schoolFactory } from '@src/modules/school/testing'; import { systemFactory } from '@src/modules/system/testing'; @@ -62,41 +64,35 @@ describe(TspOauthDataMapper.name, () => { const lehrerUid = faker.string.alpha(); - const tspTeachers: RobjExportLehrer[] = [ - { - lehrerUid, - lehrerNachname: faker.string.alpha(), - lehrerVorname: faker.string.alpha(), - schuleNummer: school.externalId, - }, - ]; + const tspTeacher = robjExportLehrerFactory.build({ + lehrerUid, + schuleNummer: school.externalId, + }); + const tspTeachers = [tspTeacher]; const klasseId = faker.string.alpha(); - const tspClasses: RobjExportKlasse[] = [ - { - klasseId, - klasseName: faker.string.alpha(), - lehrerUid, - }, - ]; - - const tspStudents: RobjExportSchueler[] = [ - { - schuelerUid: faker.string.alpha(), - schuelerNachname: faker.string.alpha(), - schuelerVorname: faker.string.alpha(), - schuleNummer: school.externalId, - klasseId, - }, - ]; + const tspClass = robjExportKlasseFactory.build({ + klasseId, + lehrerUid, + }); + const tspClasses = [tspClass]; + + const tspStudent = robjExportSchuelerFactory.build({ + schuelerUid: faker.string.alpha(), + schuelerNachname: faker.string.alpha(), + schuelerVorname: faker.string.alpha(), + schuleNummer: school.externalId, + klasseId, + }); + const tspStudents = [tspStudent]; const provisioningSystemDto: ProvisioningSystemDto = provisioningSystemDtoFactory.build({ systemId: system.id, provisioningStrategy: SystemProvisioningStrategy.TSP, }); - const externalClassDto: ExternalClassDto = externalClassDtoFactory.build({ + const externalClassDto = externalClassDtoFactory.build({ externalId: tspClasses[0].klasseId ?? '', name: tspClasses[0].klasseName, }); @@ -163,9 +159,9 @@ describe(TspOauthDataMapper.name, () => { const setup = () => { const system = systemFactory.build(); - const tspClass: RobjExportKlasse = { + const tspClass = robjExportKlasseFactory.build({ klasseId: undefined, - }; + }); return { system, tspClass }; }; @@ -183,9 +179,9 @@ describe(TspOauthDataMapper.name, () => { const setup = () => { const system = systemFactory.build(); - const tspTeacher: RobjExportLehrer = { + const tspTeacher = robjExportLehrerFactory.build({ lehrerUid: undefined, - }; + }); return { system, tspTeacher }; }; @@ -203,9 +199,9 @@ describe(TspOauthDataMapper.name, () => { const setup = () => { const system = systemFactory.build(); - const tspStudent: RobjExportSchueler = { + const tspStudent = robjExportSchuelerFactory.build({ schuelerUid: undefined, - }; + }); return { system, tspStudent }; }; diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index b4d8a3b8a52..835aa126b6e 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -13,17 +13,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { userDoFactory } from '@shared/testing'; +import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { robjExportSchuleFactory } from '@shared/testing/factory/robj-export-schule.factory'; +import { robjExportLehrerMigrationFactory } from '@shared/testing/factory/robj-export-lehrer-migration.factory'; import { Logger } from '@src/core/logger'; import { Account } from '@src/modules/account'; import { accountDoFactory } from '@src/modules/account/testing'; -import { ExternalUserDto, OauthDataDto, ProvisioningService, ProvisioningSystemDto } from '@src/modules/provisioning'; +import { OauthDataDto, ProvisioningService } from '@src/modules/provisioning'; import { School } from '@src/modules/school'; import { schoolFactory } from '@src/modules/school/testing'; import { System } from '@src/modules/system'; import { systemFactory } from '@src/modules/system/testing'; import { SyncStrategyTarget } from '../sync-strategy.types'; -import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspFetchService } from './tsp-fetch.service'; +import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; @@ -164,12 +169,12 @@ describe(TspSyncStrategy.name, () => { describe('sync', () => { describe('when sync is called', () => { const setup = () => { - const oauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ + const oauthDataDto = oauthDataDtoFactory.build({ + system: provisioningSystemDtoFactory.build({ systemId: faker.string.alpha(), provisioningStrategy: SystemProvisioningStrategy.TSP, }), - externalUser: new ExternalUserDto({ + externalUser: externalUserDtoFactory.build({ externalId: faker.string.alpha(), }), }); @@ -301,10 +306,7 @@ describe(TspSyncStrategy.name, () => { describe('when school does not exist', () => { const setup = () => { - const tspSchool: RobjExportSchule = { - schuleNummer: faker.string.alpha(), - schuleName: faker.string.alpha(), - }; + const tspSchool = robjExportSchuleFactory.build(); const tspSchools = [tspSchool]; setupMockServices({ @@ -323,10 +325,7 @@ describe(TspSyncStrategy.name, () => { describe('when school does exist', () => { const setup = () => { - const tspSchool: RobjExportSchule = { - schuleNummer: faker.string.alpha(), - schuleName: faker.string.alpha(), - }; + const tspSchool = robjExportSchuleFactory.build(); const tspSchools = [tspSchool]; const school = schoolFactory.build(); @@ -347,10 +346,8 @@ describe(TspSyncStrategy.name, () => { describe('when tsp school does not have a schulnummer', () => { const setup = () => { - const tspSchool: RobjExportSchule = { - schuleNummer: undefined, - schuleName: faker.string.alpha(), - }; + const tspSchool = robjExportSchuleFactory.build(); + tspSchool.schuleNummer = undefined; const tspSchools = [tspSchool]; setupMockServices({ @@ -371,14 +368,14 @@ describe(TspSyncStrategy.name, () => { describe('when UidAlt or UidNeu is missing during migration', () => { const setup = () => { - const tspTeacher: RobjExportLehrerMigration = { + const tspTeacher = robjExportLehrerMigrationFactory.build({ lehrerUidAlt: undefined, lehrerUidNeu: faker.string.alpha(), - }; - const tspStudent: RobjExportSchuelerMigration = { - schuelerUidAlt: faker.string.alpha(), - schuelerUidNeu: undefined, - }; + }); + const tspStudent = robjExportLehrerMigrationFactory.build({ + lehrerUidAlt: faker.string.alpha(), + lehrerUidNeu: undefined, + }) as RobjExportSchuelerMigration; setupMockServices({ fetchedStudentMigrations: [tspStudent], @@ -397,10 +394,10 @@ describe(TspSyncStrategy.name, () => { describe('when no user is found during migration', () => { const setup = () => { - const tspTeacher: RobjExportLehrerMigration = { + const tspTeacher = robjExportLehrerMigrationFactory.build({ lehrerUidAlt: faker.string.alpha(), lehrerUidNeu: faker.string.alpha(), - }; + }); setupMockServices({ fetchedTeacherMigrations: [tspTeacher], @@ -421,10 +418,10 @@ describe(TspSyncStrategy.name, () => { describe('when no account is found during migration', () => { const setup = () => { - const tspTeacher: RobjExportLehrerMigration = { + const tspTeacher = robjExportLehrerMigrationFactory.build({ lehrerUidAlt: faker.string.alpha(), lehrerUidNeu: faker.string.alpha(), - }; + }); setupMockServices({ fetchedTeacherMigrations: [tspTeacher], diff --git a/apps/server/src/shared/testing/factory/robj-export-klasse.factory.ts b/apps/server/src/shared/testing/factory/robj-export-klasse.factory.ts new file mode 100644 index 00000000000..cbdff111a16 --- /dev/null +++ b/apps/server/src/shared/testing/factory/robj-export-klasse.factory.ts @@ -0,0 +1,14 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RobjExportKlasse } from '@src/infra/tsp-client'; +import { Factory } from 'fishery'; + +export const robjExportKlasseFactory = Factory.define(({ sequence }) => { + return { + id: new ObjectId().toHexString(), + version: `version ${sequence}`, + klasseName: `klasseName ${sequence}`, + schuleNummer: `schuleNummer ${sequence}`, + klasseId: `klasseId ${sequence}`, + lehrerUid: `lehrerUid ${sequence}`, + }; +}); diff --git a/apps/server/src/shared/testing/factory/robj-export-lehrer-migration.factory.ts b/apps/server/src/shared/testing/factory/robj-export-lehrer-migration.factory.ts new file mode 100644 index 00000000000..60c92b94d50 --- /dev/null +++ b/apps/server/src/shared/testing/factory/robj-export-lehrer-migration.factory.ts @@ -0,0 +1,12 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RobjExportLehrerMigration } from '@src/infra/tsp-client'; +import { Factory } from 'fishery'; + +export const robjExportLehrerMigrationFactory = Factory.define( + () => { + return { + lehrerUidAlt: new ObjectId().toHexString(), + lehrerUidNeu: new ObjectId().toHexString(), + }; + } +); diff --git a/apps/server/src/shared/testing/factory/robj-export-lehrer.factory.ts b/apps/server/src/shared/testing/factory/robj-export-lehrer.factory.ts new file mode 100644 index 00000000000..4dffa58a85c --- /dev/null +++ b/apps/server/src/shared/testing/factory/robj-export-lehrer.factory.ts @@ -0,0 +1,13 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RobjExportLehrer } from '@src/infra/tsp-client'; +import { Factory } from 'fishery'; + +export const robjExportLehrerFactory = Factory.define(({ sequence }) => { + return { + lehrerUid: new ObjectId().toHexString(), + lehrerTitel: `lehrerTitel ${sequence}`, + lehrerVorname: `lehrerVorname ${sequence}`, + lehrerNachname: `lehrerNachname ${sequence}`, + schuleNummer: `schuleNummer ${sequence}`, + }; +}); diff --git a/apps/server/src/shared/testing/factory/robj-export-schueler.factory.ts b/apps/server/src/shared/testing/factory/robj-export-schueler.factory.ts new file mode 100644 index 00000000000..4dd5a4e7ac6 --- /dev/null +++ b/apps/server/src/shared/testing/factory/robj-export-schueler.factory.ts @@ -0,0 +1,13 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RobjExportSchueler } from '@src/infra/tsp-client'; +import { Factory } from 'fishery'; + +export const robjExportSchuelerFactory = Factory.define(({ sequence }) => { + return { + schuelerUid: new ObjectId().toHexString(), + schuelerVorname: `schuelerVorname ${sequence}`, + schuelerNachname: `schuelerNachname ${sequence}`, + schuleNummer: `schuleNummer ${sequence}`, + klasseId: `klasseId ${sequence}`, + }; +}); diff --git a/apps/server/src/shared/testing/factory/robj-export-schule.factory.ts b/apps/server/src/shared/testing/factory/robj-export-schule.factory.ts new file mode 100644 index 00000000000..460fdf6ea5e --- /dev/null +++ b/apps/server/src/shared/testing/factory/robj-export-schule.factory.ts @@ -0,0 +1,9 @@ +import { RobjExportSchule } from '@src/infra/tsp-client'; +import { Factory } from 'fishery'; + +export const robjExportSchuleFactory = Factory.define(({ sequence }) => { + return { + schuleName: `schuleName ${sequence}`, + schuleNummer: `schuleNummer ${sequence}`, + }; +}); From e4aa8a3ad6b2242d6862a99f81843da0c8daadb3 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Wed, 4 Dec 2024 13:18:54 +0100 Subject: [PATCH 20/55] add robj-export-schueler-migration factory --- .../src/infra/sync/tsp/tsp-sync.strategy.spec.ts | 11 ++++++----- .../robj-export-schueler-migration.factory.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/shared/testing/factory/robj-export-schueler-migration.factory.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index 835aa126b6e..bab633afa90 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -16,8 +16,9 @@ import { userDoFactory } from '@shared/testing'; import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; -import { robjExportSchuleFactory } from '@shared/testing/factory/robj-export-schule.factory'; import { robjExportLehrerMigrationFactory } from '@shared/testing/factory/robj-export-lehrer-migration.factory'; +import { robjExportSchuelerMigrationFactory } from '@shared/testing/factory/robj-export-schueler-migration.factory'; +import { robjExportSchuleFactory } from '@shared/testing/factory/robj-export-schule.factory'; import { Logger } from '@src/core/logger'; import { Account } from '@src/modules/account'; import { accountDoFactory } from '@src/modules/account/testing'; @@ -372,10 +373,10 @@ describe(TspSyncStrategy.name, () => { lehrerUidAlt: undefined, lehrerUidNeu: faker.string.alpha(), }); - const tspStudent = robjExportLehrerMigrationFactory.build({ - lehrerUidAlt: faker.string.alpha(), - lehrerUidNeu: undefined, - }) as RobjExportSchuelerMigration; + const tspStudent = robjExportSchuelerMigrationFactory.build({ + schuelerUidAlt: faker.string.alpha(), + schuelerUidNeu: undefined, + }); setupMockServices({ fetchedStudentMigrations: [tspStudent], diff --git a/apps/server/src/shared/testing/factory/robj-export-schueler-migration.factory.ts b/apps/server/src/shared/testing/factory/robj-export-schueler-migration.factory.ts new file mode 100644 index 00000000000..52c509f40df --- /dev/null +++ b/apps/server/src/shared/testing/factory/robj-export-schueler-migration.factory.ts @@ -0,0 +1,13 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { RobjExportSchuelerMigration } from '@src/infra/tsp-client'; +import { Factory } from 'fishery'; + +export const robjExportSchuelerMigrationFactory = Factory.define< + RobjExportSchuelerMigration, + RobjExportSchuelerMigration +>(() => { + return { + schuelerUidAlt: new ObjectId().toHexString(), + schuelerUidNeu: new ObjectId().toHexString(), + }; +}); From 2fcee4cf16eada0ad6f43c5b53bc2cf5443d1337 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Wed, 4 Dec 2024 15:27:17 +0100 Subject: [PATCH 21/55] add new factories to index --- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 30 +++++++++++++------ .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 16 +++++----- .../service/provisioning.service.spec.ts | 4 +-- .../service/tsp-provisioning.service.spec.ts | 15 ++++++---- .../factory/external-school-dto.factory.ts | 8 +++-- .../src/shared/testing/factory/index.ts | 11 +++++++ 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index 7b25d5c2ba7..caddda43118 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -4,13 +4,16 @@ import { ProvisioningSystemDto } from '@modules/provisioning'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; -import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; -import { robjExportKlasseFactory } from '@shared/testing/factory/robj-export-klasse.factory'; -import { robjExportLehrerFactory } from '@shared/testing/factory/robj-export-lehrer.factory'; -import { robjExportSchuelerFactory } from '@shared/testing/factory/robj-export-schueler.factory'; +import { + externalClassDtoFactory, + externalSchoolDtoFactory, + externalUserDtoFactory, + oauthDataDtoFactory, + provisioningSystemDtoFactory, + robjExportKlasseFactory, + robjExportLehrerFactory, + robjExportSchuelerFactory, +} from '@shared/testing/factory'; import { Logger } from '@src/core/logger'; import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; import { schoolFactory } from '@src/modules/school/testing'; @@ -113,16 +116,25 @@ describe(TspOauthDataMapper.name, () => { email: undefined, }); + const externalSchoolDto = externalSchoolDtoFactory.build({ + externalId: school.externalId, + name: school.name, + officialSchoolNumber: undefined, + location: undefined, + }); + const expected = [ oauthDataDtoFactory.build({ system: provisioningSystemDto, externalUser: externalTeacherUserDto, externalClasses: [externalClassDto], + externalSchool: externalSchoolDto, }), oauthDataDtoFactory.build({ system: provisioningSystemDto, externalUser: externalStudentUserDto, externalClasses: [externalClassDto], + externalSchool: externalSchoolDto, }), ]; @@ -130,9 +142,9 @@ describe(TspOauthDataMapper.name, () => { }; it('should return an array of oauth data dtos', () => { - const { system, tspTeachers, tspStudents, tspClasses, expected } = setup(); + const { system, school, tspTeachers, tspStudents, tspClasses, expected } = setup(); - const result = sut.mapTspDataToOauthData(system, [], tspTeachers, tspStudents, tspClasses); + const result = sut.mapTspDataToOauthData(system, [school], tspTeachers, tspStudents, tspClasses); expect(result).toStrictEqual(expected); }); diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index bab633afa90..345a4cac0ec 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -12,13 +12,15 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { userDoFactory } from '@shared/testing'; -import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; -import { robjExportLehrerMigrationFactory } from '@shared/testing/factory/robj-export-lehrer-migration.factory'; -import { robjExportSchuelerMigrationFactory } from '@shared/testing/factory/robj-export-schueler-migration.factory'; -import { robjExportSchuleFactory } from '@shared/testing/factory/robj-export-schule.factory'; +import { + externalUserDtoFactory, + oauthDataDtoFactory, + provisioningSystemDtoFactory, + robjExportLehrerMigrationFactory, + robjExportSchuelerMigrationFactory, + robjExportSchuleFactory, + userDoFactory, +} from '@shared/testing/factory'; import { Logger } from '@src/core/logger'; import { Account } from '@src/modules/account'; import { accountDoFactory } from '@src/modules/account/testing'; diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 79894dd1514..d601a016563 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -3,9 +3,7 @@ import { System, SystemService } from '@modules/system'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningDtoFactory } from '@shared/testing/factory/provisioning-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { oauthDataDtoFactory, provisioningDtoFactory, provisioningSystemDtoFactory } from '@shared/testing/factory'; import { systemFactory } from '@src/modules/system/testing'; import { ExternalUserDto, diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index c2ed303cd63..1f2ee5bc09d 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -3,11 +3,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; -import { externalSchoolDtoFactory, roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; -import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; -import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { + externalClassDtoFactory, + externalSchoolDtoFactory, + externalUserDtoFactory, + oauthDataDtoFactory, + provisioningSystemDtoFactory, + roleDtoFactory, + roleFactory, + userDoFactory, +} from '@shared/testing/factory'; import { AccountService } from '@src/modules/account'; import { ClassService } from '@src/modules/class'; import { classFactory } from '@src/modules/class/domain/testing'; diff --git a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts b/apps/server/src/shared/testing/factory/external-school-dto.factory.ts index e56e64d9243..21f35cf0f03 100644 --- a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/external-school-dto.factory.ts @@ -1,8 +1,10 @@ -import { ExternalSchoolDto } from '@modules/provisioning/dto'; import { ObjectId } from '@mikro-orm/mongodb'; -import { Factory } from 'fishery'; +import { ExternalSchoolDto } from '@modules/provisioning/dto'; +import { BaseFactory } from './base.factory'; + +class ExternalSchoolDtoFactory extends BaseFactory> {} -export const externalSchoolDtoFactory = Factory.define(({ sequence }) => { +export const externalSchoolDtoFactory = ExternalSchoolDtoFactory.define(ExternalSchoolDto, ({ sequence }) => { return { externalId: new ObjectId().toHexString(), name: `External School ${sequence}`, diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index d0d2e9568d9..8820070172d 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -41,3 +41,14 @@ export * from './user-and-account.test.factory'; export * from './user-login-migration.factory'; export * from './user.do.factory'; export * from './user.factory'; +export * from './external-class-dto.factory'; +export * from './oauth-data-dto.factory'; +export * from './provisioning-dto.factory'; +export * from './provisioning-system-dto.factory'; +export * from './external-user-dto.factory'; +export * from './robj-export-schule.factory'; +export * from './robj-export-klasse.factory'; +export * from './robj-export-lehrer.factory'; +export * from './robj-export-schueler.factory'; +export * from './robj-export-lehrer-migration.factory'; +export * from './robj-export-schueler-migration.factory'; From 66a753e3adf32883de69b01a251fa6873e980033 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Wed, 4 Dec 2024 23:45:36 +0100 Subject: [PATCH 22/55] EW-1057: reverted all non needed import changes --- apps/server/src/infra/database/index.ts | 4 +-- .../src/infra/database/management/index.ts | 4 +-- .../database/mongo-memory-database/index.ts | 4 +-- .../src/infra/schulconnex-client/index.ts | 30 +------------------ .../provisioning/provisioning.module.ts | 2 +- .../strategy/tsp/tsp.strategy.spec.ts | 3 +- .../request-logging.interceptor.ts | 3 +- .../src/shared/domain/domainobject/index.ts | 1 - .../src/shared/domain/interface/index.ts | 1 - 9 files changed, 11 insertions(+), 41 deletions(-) diff --git a/apps/server/src/infra/database/index.ts b/apps/server/src/infra/database/index.ts index 29ed7f7282c..618339f9c52 100644 --- a/apps/server/src/infra/database/index.ts +++ b/apps/server/src/infra/database/index.ts @@ -1,2 +1,2 @@ -export { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from './mongo-memory-database'; -export { DatabaseManagementModule, DatabaseManagementService } from './management'; +export * from './mongo-memory-database'; +export * from './management'; diff --git a/apps/server/src/infra/database/management/index.ts b/apps/server/src/infra/database/management/index.ts index 5bc56f7264a..8f89890cd94 100644 --- a/apps/server/src/infra/database/management/index.ts +++ b/apps/server/src/infra/database/management/index.ts @@ -1,2 +1,2 @@ -export { DatabaseManagementService } from './database-management.service'; -export { DatabaseManagementModule } from './database-management.module'; +export * from './database-management.service'; +export * from './database-management.module'; diff --git a/apps/server/src/infra/database/mongo-memory-database/index.ts b/apps/server/src/infra/database/mongo-memory-database/index.ts index 9f9e928232e..cce731df885 100644 --- a/apps/server/src/infra/database/mongo-memory-database/index.ts +++ b/apps/server/src/infra/database/mongo-memory-database/index.ts @@ -1,2 +1,2 @@ -export { MongoMemoryDatabaseModule } from './mongo-memory-database.module'; -export { MongoDatabaseModuleOptions } from './types'; +export * from './mongo-memory-database.module'; +export * from './types'; diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts index daf5cf5eba0..ef1cc753c41 100644 --- a/apps/server/src/infra/schulconnex-client/index.ts +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -1,32 +1,4 @@ export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; export { SchulconnexRestClient } from './schulconnex-rest-client'; -export { - SchulconnexAnschriftResponse, - SchulconnexCommunicationType, - SchulconnexErreichbarkeitenResponse, - SchulconnexGroupRole, - SchulconnexGroupType, - SchulconnexGruppeResponse, - SchulconnexGruppenResponse, - SchulconnexGruppenzugehoerigkeitResponse, - SchulconnexLaufzeitResponse, - SchulconnexNameResponse, - SchulconnexOrganisationResponse, - SchulconnexPersonResponse, - SchulconnexPersonenkontextResponse, - SchulconnexPoliciesInfoAccessControlResponse, - SchulconnexPoliciesInfoActionType, - SchulconnexPoliciesInfoErrorDescriptionResponse, - SchulconnexPoliciesInfoErrorResponse, - SchulconnexPoliciesInfoLicenseResponse, - SchulconnexPoliciesInfoPermissionResponse, - SchulconnexPoliciesInfoResponse, - SchulconnexPoliciesInfoTargetResponse, - SchulconnexResponse, - SchulconnexResponseValidationGroups, - SchulconnexRole, - SchulconnexSonstigeGruppenzugehoerigeResponse, - lernperiodeFormat, -} from './response'; +export * from './response'; export { SchulconnexClientConfig } from './schulconnex-client-config'; -export { SchulconnexClientModule } from './schulconnex-client.module'; diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 50fd4bfbfa3..3c88b1b243d 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -10,7 +10,7 @@ import { SchoolExternalToolModule } from '@modules/tool/school-external-tool'; import { UserModule } from '@modules/user'; import { AccountModule } from '@modules/account'; import { LoggerModule } from '@src/core/logger'; -import { SchulconnexClientModule } from '../../infra/schulconnex-client'; +import { SchulconnexClientModule } from '@src/infra/schulconnex-client/schulconnex-client.module'; import { ClassModule } from '../class'; import { UserLicenseModule } from '../user-license'; import { ProvisioningService } from './service/provisioning.service'; diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts index 82b2c409aef..4094b127546 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts @@ -3,8 +3,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import jwt from 'jsonwebtoken'; import { IdTokenExtractionFailureLoggableException } from '@modules/oauth/loggable'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { userDoFactory } from '@shared/testing'; -import { RoleName, SystemProvisioningStrategy } from '@shared/domain/interface'; +import { RoleName } from '@shared/domain/interface'; import { schoolFactory } from '@modules/school/testing'; import { ExternalClassDto, diff --git a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts index abde0bc30c3..b1fab899762 100644 --- a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts +++ b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts @@ -1,7 +1,6 @@ import { ICurrentUser } from '@infra/auth-guard'; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { LegacyLogger } from '@src/core/logger'; -import { RequestLoggingBody } from '@src/core/logger/interfaces'; +import { LegacyLogger, RequestLoggingBody } from '@src/core/logger'; import { Request } from 'express'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; diff --git a/apps/server/src/shared/domain/domainobject/index.ts b/apps/server/src/shared/domain/domainobject/index.ts index 00e53066f61..51fbc5000a2 100644 --- a/apps/server/src/shared/domain/domainobject/index.ts +++ b/apps/server/src/shared/domain/domainobject/index.ts @@ -8,4 +8,3 @@ export * from './page'; export * from './role-reference'; export * from './ltitool.do'; export * from './external-source'; -export * from './user-source-options.do'; diff --git a/apps/server/src/shared/domain/interface/index.ts b/apps/server/src/shared/domain/interface/index.ts index b810ad12971..e5eb9ca8da5 100644 --- a/apps/server/src/shared/domain/interface/index.ts +++ b/apps/server/src/shared/domain/interface/index.ts @@ -6,4 +6,3 @@ export * from './learnroom'; export * from './permission.enum'; export * from './rolename.enum'; export * from './video-conference-scope.enum'; -export * from './system-provisioning.strategy'; From 73d7c0973bf86253ee73afdf2ee1f9080a2a2677 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Wed, 4 Dec 2024 23:48:21 +0100 Subject: [PATCH 23/55] EW-1057: imports reverted --- apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts | 4 ++-- apps/server/src/infra/sync/tsp/tsp-sync.service.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index a6baeca2ed9..eb09370a2ad 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { BadDataLoggableException } from '@modules/provisioning/loggable'; import { System } from '@modules/system'; -import { RoleName, SystemProvisioningStrategy } from '@shared/domain/interface'; - +import { RoleName } from '@shared/domain/interface'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { School } from '@modules/school'; import { ExternalClassDto, diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts index 9cd679c57a8..2d296ff230a 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts @@ -3,7 +3,8 @@ import { ObjectId } from 'bson'; import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; import { School, SchoolService } from '@modules/school'; import { System, SystemService, SystemType } from '@modules/system'; -import { UserSourceOptions, UserDO } from '@shared/domain/domainobject'; +import { UserDO } from '@shared/domain/domainobject'; +import { UserSourceOptions } from '@shared/domain/domainobject/user-source-options.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { EntityId, SchoolFeature } from '@shared/domain/types'; import { Account, AccountService } from '@modules/account'; From 6ac88ccf7098ef2a67318f6ab01a181d7b1a4c6c Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Thu, 28 Nov 2024 12:37:23 +0100 Subject: [PATCH 24/55] EW-1055: moved tsp to strategy --- apps/server/src/infra/sync/strategy/tsp.config.ts | 0 apps/server/src/infra/sync/{ => strategy}/tsp/index.ts | 0 .../src/infra/sync/{ => strategy}/tsp/loggable/index.ts | 0 .../tsp/loggable/tsp-data-fetched.loggable.spec.ts | 0 .../tsp/loggable/tsp-data-fetched.loggable.ts | 0 .../loggable/tsp-legacy-migration-start.loggable.spec.ts | 0 .../tsp/loggable/tsp-legacy-migration-start.loggable.ts | 0 .../tsp-legacy-migration-system-missing.loggable.spec.ts | 0 .../tsp-legacy-migration-system-missing.loggable.ts | 0 .../tsp-legacy-school-migration-count.loggable.spec.ts | 0 .../loggable/tsp-legacy-school-migration-count.loggable.ts | 0 .../tsp-legacy-school-migration-success.loggable.spec.ts | 0 .../tsp-legacy-school-migration-success.loggable.ts | 0 .../tsp/loggable/tsp-missing-external-id.loggable.spec.ts | 0 .../tsp/loggable/tsp-missing-external-id.loggable.ts | 0 .../tsp/loggable/tsp-schools-fetched.loggable.spec.ts | 0 .../tsp/loggable/tsp-schools-fetched.loggable.ts | 0 .../tsp/loggable/tsp-schools-synced.loggable.spec.ts | 0 .../tsp/loggable/tsp-schools-synced.loggable.ts | 0 .../tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts | 0 .../tsp/loggable/tsp-schulnummer-missing.loggable.ts | 0 .../tsp/loggable/tsp-students-fetched.loggable.spec.ts | 0 .../tsp/loggable/tsp-students-fetched.loggable.ts | 0 .../tsp/loggable/tsp-students-migrated.loggable.spec.ts | 0 .../tsp/loggable/tsp-students-migrated.loggable.ts | 0 .../tsp/loggable/tsp-synced-users.loggable.spec.ts | 0 .../tsp/loggable/tsp-synced-users.loggable.ts | 0 .../tsp/loggable/tsp-syncing-users.loggable.spec.ts | 0 .../tsp/loggable/tsp-syncing-users.loggable.ts | 0 .../tsp-system-not-found.loggable-exception.spec.ts | 0 .../tsp/loggable/tsp-system-not-found.loggable-exception.ts | 0 .../tsp/loggable/tsp-teachers-fetched.loggable.spec.ts | 0 .../tsp/loggable/tsp-teachers-fetched.loggable.ts | 0 .../tsp/loggable/tsp-teachers-migrated.loggable.spec.ts | 0 .../tsp/loggable/tsp-teachers-migrated.loggable.ts | 0 .../tsp/loggable/tsp-users-migrated.loggable.spec.ts | 0 .../tsp/loggable/tsp-users-migrated.loggable.ts | 0 .../infra/sync/{ => strategy}/tsp/tsp-fetch.service.spec.ts | 0 .../src/infra/sync/{ => strategy}/tsp/tsp-fetch.service.ts | 0 .../tsp/tsp-legacy-migration.service.integration.spec.ts | 0 .../sync/{ => strategy}/tsp/tsp-legacy-migration.service.ts | 0 .../sync/{ => strategy}/tsp/tsp-oauth-data.mapper.spec.ts | 0 .../infra/sync/{ => strategy}/tsp/tsp-oauth-data.mapper.ts | 0 .../src/infra/sync/{ => strategy}/tsp/tsp-sync.config.ts | 0 .../infra/sync/{ => strategy}/tsp/tsp-sync.service.spec.ts | 0 .../src/infra/sync/{ => strategy}/tsp/tsp-sync.service.ts | 0 .../infra/sync/{ => strategy}/tsp/tsp-sync.strategy.spec.ts | 6 +++--- .../src/infra/sync/{ => strategy}/tsp/tsp-sync.strategy.ts | 0 48 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 apps/server/src/infra/sync/strategy/tsp.config.ts rename apps/server/src/infra/sync/{ => strategy}/tsp/index.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/index.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-data-fetched.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-data-fetched.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-migration-start.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-migration-start.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-migration-system-missing.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-school-migration-count.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-school-migration-success.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-missing-external-id.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-missing-external-id.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-schools-fetched.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-schools-fetched.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-schools-synced.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-schools-synced.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-schulnummer-missing.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-students-fetched.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-students-fetched.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-students-migrated.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-students-migrated.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-synced-users.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-synced-users.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-syncing-users.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-syncing-users.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-system-not-found.loggable-exception.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-teachers-fetched.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-teachers-fetched.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-teachers-migrated.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-teachers-migrated.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-users-migrated.loggable.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/loggable/tsp-users-migrated.loggable.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-fetch.service.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-fetch.service.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-legacy-migration.service.integration.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-legacy-migration.service.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-oauth-data.mapper.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-oauth-data.mapper.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-sync.config.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-sync.service.spec.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-sync.service.ts (100%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-sync.strategy.spec.ts (98%) rename apps/server/src/infra/sync/{ => strategy}/tsp/tsp-sync.strategy.ts (100%) diff --git a/apps/server/src/infra/sync/strategy/tsp.config.ts b/apps/server/src/infra/sync/strategy/tsp.config.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/server/src/infra/sync/tsp/index.ts b/apps/server/src/infra/sync/strategy/tsp/index.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/index.ts rename to apps/server/src/infra/sync/strategy/tsp/index.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/index.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/index.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/index.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/index.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-data-fetched.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-data-fetched.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-data-fetched.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-data-fetched.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-data-fetched.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-start.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-start.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-start.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-start.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-start.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-system-missing.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-system-missing.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-migration-system-missing.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-count.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-count.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-count.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-success.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-success.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-legacy-school-migration-success.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-missing-external-id.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-missing-external-id.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-missing-external-id.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-missing-external-id.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-missing-external-id.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-fetched.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-fetched.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-fetched.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-fetched.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-synced.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-synced.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-synced.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schools-synced.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schulnummer-missing.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-schulnummer-missing.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-fetched.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-fetched.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-fetched.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-students-fetched.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-fetched.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-migrated.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-migrated.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-migrated.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-students-migrated.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-students-migrated.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-synced-users.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-synced-users.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-synced-users.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-synced-users.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-synced-users.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-syncing-users.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-syncing-users.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-syncing-users.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-syncing-users.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-syncing-users.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-system-not-found.loggable-exception.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-system-not-found.loggable-exception.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-fetched.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-fetched.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-fetched.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-teachers-fetched.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-fetched.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-migrated.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-migrated.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-migrated.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-teachers-migrated.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-teachers-migrated.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-users-migrated.loggable.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-users-migrated.loggable.spec.ts diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-users-migrated.loggable.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/loggable/tsp-users-migrated.loggable.ts rename to apps/server/src/infra/sync/strategy/tsp/loggable/tsp-users-migrated.loggable.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-fetch.service.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.integration.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.integration.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.integration.spec.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-legacy-migration.service.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.spec.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.config.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-sync.config.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-sync.service.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts similarity index 98% rename from apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts index dbb5ddb897a..1e4a56e02df 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts @@ -10,7 +10,7 @@ import { schoolFactory } from '@modules/school/testing'; import { System } from '@modules/system'; import { systemFactory } from '@modules/system/testing'; import { userDoFactory } from '@shared/testing'; -import { SystemProvisioningStrategy } from '@shared/domain/interface'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { UserDO } from '@shared/domain/domainobject'; import { Logger } from '@src/core/logger'; import { @@ -20,8 +20,8 @@ import { RobjExportSchueler, RobjExportSchuelerMigration, RobjExportSchule, -} from '../../tsp-client'; -import { SyncStrategyTarget } from '../sync-strategy.types'; +} from '../../../tsp-client'; +import { SyncStrategyTarget } from '../../sync-strategy.types'; import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspFetchService } from './tsp-fetch.service'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts similarity index 100% rename from apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts From bb958fbdfa620fe5d0c883c5e1aaeff0a5e2f7f9 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Thu, 28 Nov 2024 13:40:01 +0100 Subject: [PATCH 25/55] EW-1055: All imports modified to suit the folder structural change and the parent config added and modified --- apps/server/src/infra/sync/index.ts | 2 +- apps/server/src/infra/sync/service/sync.service.ts | 2 +- apps/server/src/infra/sync/strategy/tsp.config.ts | 2 ++ .../src/infra/sync/strategy/tsp/tsp-sync.config.ts | 4 +++- .../src/infra/sync/strategy/tsp/tsp-sync.strategy.ts | 4 ++-- apps/server/src/infra/sync/sync.module.ts | 10 +++++----- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/server/src/infra/sync/index.ts b/apps/server/src/infra/sync/index.ts index ad799139a2e..89b15867369 100644 --- a/apps/server/src/infra/sync/index.ts +++ b/apps/server/src/infra/sync/index.ts @@ -1 +1 @@ -export { TspSyncConfig, TspSyncStrategy } from './tsp'; +export { TspSyncConfig, TspSyncStrategy } from './strategy/tsp'; diff --git a/apps/server/src/infra/sync/service/sync.service.ts b/apps/server/src/infra/sync/service/sync.service.ts index 528de238c50..af6fc95c2a3 100644 --- a/apps/server/src/infra/sync/service/sync.service.ts +++ b/apps/server/src/infra/sync/service/sync.service.ts @@ -1,6 +1,6 @@ import { Injectable, Optional } from '@nestjs/common'; import { Logger } from '@src/core/logger'; -import { TspSyncStrategy } from '../tsp/tsp-sync.strategy'; +import { TspSyncStrategy } from '../strategy/tsp/tsp-sync.strategy'; import { SyncStrategy } from '../strategy/sync-strategy'; import { SyncStrategyTarget } from '../sync-strategy.types'; import { InvalidTargetLoggable } from '../errors/invalid-target.loggable'; diff --git a/apps/server/src/infra/sync/strategy/tsp.config.ts b/apps/server/src/infra/sync/strategy/tsp.config.ts index e69de29bb2d..a77ea4beacd 100644 --- a/apps/server/src/infra/sync/strategy/tsp.config.ts +++ b/apps/server/src/infra/sync/strategy/tsp.config.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TspConfig {} diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts index e26d0cb1369..e9f2263c696 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts @@ -1,4 +1,6 @@ -export interface TspSyncConfig { +import { TspConfig } from '../tsp.config'; + +export interface TspSyncConfig extends TspConfig { TSP_SYNC_SCHOOL_LIMIT: number; TSP_SYNC_SCHOOL_DAYS_TO_FETCH: number; TSP_SYNC_DATA_LIMIT: number; diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts index 07469f5abd6..87acf8b5df7 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts @@ -8,8 +8,8 @@ import { Account } from '@src/modules/account'; import { ProvisioningService } from '@src/modules/provisioning'; import { System } from '@src/modules/system'; import pLimit from 'p-limit'; -import { SyncStrategy } from '../strategy/sync-strategy'; -import { SyncStrategyTarget } from '../sync-strategy.types'; +import { SyncStrategy } from '../sync-strategy'; +import { SyncStrategyTarget } from '../../sync-strategy.types'; import { TspDataFetchedLoggable } from './loggable/tsp-data-fetched.loggable'; import { TspSchoolsFetchedLoggable } from './loggable/tsp-schools-fetched.loggable'; import { TspSchoolsSyncedLoggable } from './loggable/tsp-schools-synced.loggable'; diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 164508ceb4b..d7d0477253b 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -12,12 +12,12 @@ import { RabbitMQWrapperModule } from '@infra/rabbitmq'; import { LoggerModule } from '@src/core/logger'; import { SyncConsole } from './console/sync.console'; import { SyncService } from './service/sync.service'; -import { TspLegacyMigrationService } from './tsp/tsp-legacy-migration.service'; -import { TspOauthDataMapper } from './tsp/tsp-oauth-data.mapper'; -import { TspSyncService } from './tsp/tsp-sync.service'; -import { TspSyncStrategy } from './tsp/tsp-sync.strategy'; +import { TspLegacyMigrationService } from './strategy/tsp/tsp-legacy-migration.service'; +import { TspOauthDataMapper } from './strategy/tsp/tsp-oauth-data.mapper'; +import { TspSyncService } from './strategy/tsp/tsp-sync.service'; +import { TspSyncStrategy } from './strategy/tsp/tsp-sync.strategy'; import { SyncUc } from './uc/sync.uc'; -import { TspFetchService } from './tsp/tsp-fetch.service'; +import { TspFetchService } from './strategy/tsp/tsp-fetch.service'; @Module({ imports: [ From c2406d351a8ae494fa62661a5582350807cfce3d Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Thu, 28 Nov 2024 14:00:48 +0100 Subject: [PATCH 26/55] EW-1055: another import modification --- apps/server/src/infra/sync/service/sync.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/infra/sync/service/sync.service.spec.ts b/apps/server/src/infra/sync/service/sync.service.spec.ts index 3c069beb4a8..4f3e620b9b6 100644 --- a/apps/server/src/infra/sync/service/sync.service.spec.ts +++ b/apps/server/src/infra/sync/service/sync.service.spec.ts @@ -3,7 +3,7 @@ import { createMock } from '@golevelup/ts-jest'; import { faker } from '@faker-js/faker'; import { Logger } from '@src/core/logger'; import { SyncService } from './sync.service'; -import { TspSyncStrategy } from '../tsp/tsp-sync.strategy'; +import { TspSyncStrategy } from '../strategy/tsp/tsp-sync.strategy'; import { SyncStrategyTarget } from '../sync-strategy.types'; import { InvalidTargetLoggable } from '../errors/invalid-target.loggable'; From 1adc16ab7210327f9bb35457e8d0a2a25e2df71b Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 13:32:28 +0100 Subject: [PATCH 27/55] EW-1055: renamed tsp.config to be sync.config --- .../src/infra/sync/strategy/{tsp.config.ts => sync.config.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/server/src/infra/sync/strategy/{tsp.config.ts => sync.config.ts} (100%) diff --git a/apps/server/src/infra/sync/strategy/tsp.config.ts b/apps/server/src/infra/sync/strategy/sync.config.ts similarity index 100% rename from apps/server/src/infra/sync/strategy/tsp.config.ts rename to apps/server/src/infra/sync/strategy/sync.config.ts From f7bbac147be06ef0d700c86efac05819525148df Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Fri, 29 Nov 2024 14:26:06 +0100 Subject: [PATCH 28/55] EW-1055: modifed config name and import --- apps/server/src/infra/sync/strategy/sync.config.ts | 2 +- apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/sync.config.ts b/apps/server/src/infra/sync/strategy/sync.config.ts index a77ea4beacd..b1ed13e9d86 100644 --- a/apps/server/src/infra/sync/strategy/sync.config.ts +++ b/apps/server/src/infra/sync/strategy/sync.config.ts @@ -1,2 +1,2 @@ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TspConfig {} +export interface SyncConfig {} diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts index e9f2263c696..c8c667a0c7b 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts @@ -1,6 +1,6 @@ -import { TspConfig } from '../tsp.config'; +import { SyncConfig } from '../sync.config'; -export interface TspSyncConfig extends TspConfig { +export interface TspSyncConfig extends SyncConfig { TSP_SYNC_SCHOOL_LIMIT: number; TSP_SYNC_SCHOOL_DAYS_TO_FETCH: number; TSP_SYNC_DATA_LIMIT: number; From 6c80f6a340c60ce4d65c87b3e70d6206c246b8e9 Mon Sep 17 00:00:00 2001 From: Hussam Kayed Date: Thu, 5 Dec 2024 00:07:38 +0100 Subject: [PATCH 29/55] EW-1057: fixed imports --- apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts | 2 +- .../server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts index d5bc02c2cf9..e1510c4cede 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts @@ -5,7 +5,7 @@ import { System } from '@modules/system'; import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; import { Logger } from '@src/core/logger'; import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; -import { ExportApiInterface, TspClientFactory } from '../../tsp-client'; +import { ExportApiInterface, TspClientFactory } from '../../../tsp-client'; @Injectable() export class TspFetchService { diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts index eb09370a2ad..74c4dde6596 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts @@ -12,7 +12,7 @@ import { ProvisioningSystemDto, } from '@modules/provisioning'; import { Logger } from '@src/core/logger'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../tsp-client'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../../tsp-client'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; @Injectable() From c71904b52abefe4e7b5afcbfbc8a3668bf73ebc0 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Thu, 5 Dec 2024 15:27:07 +0100 Subject: [PATCH 30/55] revert adding new factories to index file --- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 18 ++++++++---------- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 16 +++++++--------- .../service/provisioning.service.spec.ts | 4 +++- .../service/tsp-provisioning.service.spec.ts | 15 +++++---------- .../server/src/shared/testing/factory/index.ts | 11 ----------- 5 files changed, 23 insertions(+), 41 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index caddda43118..a8d4e6772c4 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -4,16 +4,14 @@ import { ProvisioningSystemDto } from '@modules/provisioning'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { - externalClassDtoFactory, - externalSchoolDtoFactory, - externalUserDtoFactory, - oauthDataDtoFactory, - provisioningSystemDtoFactory, - robjExportKlasseFactory, - robjExportLehrerFactory, - robjExportSchuelerFactory, -} from '@shared/testing/factory'; +import { externalSchoolDtoFactory } from '@shared/testing'; +import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; +import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { robjExportKlasseFactory } from '@shared/testing/factory/robj-export-klasse.factory'; +import { robjExportLehrerFactory } from '@shared/testing/factory/robj-export-lehrer.factory'; +import { robjExportSchuelerFactory } from '@shared/testing/factory/robj-export-schueler.factory'; import { Logger } from '@src/core/logger'; import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; import { schoolFactory } from '@src/modules/school/testing'; diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index 345a4cac0ec..bab633afa90 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -12,15 +12,13 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { - externalUserDtoFactory, - oauthDataDtoFactory, - provisioningSystemDtoFactory, - robjExportLehrerMigrationFactory, - robjExportSchuelerMigrationFactory, - robjExportSchuleFactory, - userDoFactory, -} from '@shared/testing/factory'; +import { userDoFactory } from '@shared/testing'; +import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { robjExportLehrerMigrationFactory } from '@shared/testing/factory/robj-export-lehrer-migration.factory'; +import { robjExportSchuelerMigrationFactory } from '@shared/testing/factory/robj-export-schueler-migration.factory'; +import { robjExportSchuleFactory } from '@shared/testing/factory/robj-export-schule.factory'; import { Logger } from '@src/core/logger'; import { Account } from '@src/modules/account'; import { accountDoFactory } from '@src/modules/account/testing'; diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index d601a016563..79894dd1514 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -3,7 +3,9 @@ import { System, SystemService } from '@modules/system'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { oauthDataDtoFactory, provisioningDtoFactory, provisioningSystemDtoFactory } from '@shared/testing/factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningDtoFactory } from '@shared/testing/factory/provisioning-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; import { systemFactory } from '@src/modules/system/testing'; import { ExternalUserDto, diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index 1f2ee5bc09d..c2ed303cd63 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -3,16 +3,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; -import { - externalClassDtoFactory, - externalSchoolDtoFactory, - externalUserDtoFactory, - oauthDataDtoFactory, - provisioningSystemDtoFactory, - roleDtoFactory, - roleFactory, - userDoFactory, -} from '@shared/testing/factory'; +import { externalSchoolDtoFactory, roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; +import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; +import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; +import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; +import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; import { AccountService } from '@src/modules/account'; import { ClassService } from '@src/modules/class'; import { classFactory } from '@src/modules/class/domain/testing'; diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index 8820070172d..d0d2e9568d9 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -41,14 +41,3 @@ export * from './user-and-account.test.factory'; export * from './user-login-migration.factory'; export * from './user.do.factory'; export * from './user.factory'; -export * from './external-class-dto.factory'; -export * from './oauth-data-dto.factory'; -export * from './provisioning-dto.factory'; -export * from './provisioning-system-dto.factory'; -export * from './external-user-dto.factory'; -export * from './robj-export-schule.factory'; -export * from './robj-export-klasse.factory'; -export * from './robj-export-lehrer.factory'; -export * from './robj-export-schueler.factory'; -export * from './robj-export-lehrer-migration.factory'; -export * from './robj-export-schueler-migration.factory'; From 2d49b6b57debbb3e44831e0caf52ad14334562a1 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Wed, 18 Dec 2024 15:18:12 +0100 Subject: [PATCH 31/55] modify externalUserDtoFactory --- .../src/shared/testing/factory/external-user-dto.factory.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/shared/testing/factory/external-user-dto.factory.ts b/apps/server/src/shared/testing/factory/external-user-dto.factory.ts index 19ea7fb2119..f6e887439d1 100644 --- a/apps/server/src/shared/testing/factory/external-user-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/external-user-dto.factory.ts @@ -1,5 +1,6 @@ -import { ExternalUserDto } from '@src/modules/provisioning'; import { ObjectId } from '@mikro-orm/mongodb'; +import { RoleName } from '@shared/domain/interface'; +import { ExternalUserDto } from '@src/modules/provisioning'; import { Factory } from 'fishery'; export const externalUserDtoFactory = Factory.define( @@ -9,5 +10,6 @@ export const externalUserDtoFactory = Factory.define Date: Thu, 19 Dec 2024 14:59:49 +0100 Subject: [PATCH 32/55] reorder test factories to main modules --- .../sync/tsp/tsp-oauth-data.mapper.spec.ts | 22 ++++++++++------ .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 17 ++++++++----- .../src/infra/tsp-client/testing/index.ts | 6 +++++ .../testing}/robj-export-klasse.factory.ts | 0 .../robj-export-lehrer-migration.factory.ts | 0 .../testing}/robj-export-lehrer.factory.ts | 0 .../robj-export-schueler-migration.factory.ts | 0 .../testing}/robj-export-schueler.factory.ts | 0 .../testing}/robj-export-schule.factory.ts | 0 .../provisioning/dto/provisioning.dto.ts | 14 +++++------ ...chool-for-group-not-found.loggable.spec.ts | 3 +-- .../service/provisioning.service.spec.ts | 5 ++-- .../service/tsp-provisioning.service.spec.ts | 13 ++++++---- .../schulconnex/schulconnex.strategy.spec.ts | 10 ++------ ...lconnex-group-provisioning.service.spec.ts | 11 ++------ .../testing}/external-class-dto.factory.ts | 0 .../testing}/external-school-dto.factory.ts | 2 +- .../src/modules/provisioning/testing/index.ts | 5 ++++ .../testing}/oauth-data-dto.factory.ts | 0 .../testing}/provisioning-dto.factory.ts | 0 .../provisioning-system-dto.factory.ts | 0 .../factory/external-group-dto.factory.ts | 25 ------------------- .../factory/external-user-dto.factory.ts | 15 ----------- .../src/shared/testing/factory/index.ts | 2 -- 24 files changed, 59 insertions(+), 91 deletions(-) create mode 100644 apps/server/src/infra/tsp-client/testing/index.ts rename apps/server/src/{shared/testing/factory => infra/tsp-client/testing}/robj-export-klasse.factory.ts (100%) rename apps/server/src/{shared/testing/factory => infra/tsp-client/testing}/robj-export-lehrer-migration.factory.ts (100%) rename apps/server/src/{shared/testing/factory => infra/tsp-client/testing}/robj-export-lehrer.factory.ts (100%) rename apps/server/src/{shared/testing/factory => infra/tsp-client/testing}/robj-export-schueler-migration.factory.ts (100%) rename apps/server/src/{shared/testing/factory => infra/tsp-client/testing}/robj-export-schueler.factory.ts (100%) rename apps/server/src/{shared/testing/factory => infra/tsp-client/testing}/robj-export-schule.factory.ts (100%) rename apps/server/src/{shared/testing/factory => modules/provisioning/testing}/external-class-dto.factory.ts (100%) rename apps/server/src/{shared/testing/factory => modules/provisioning/testing}/external-school-dto.factory.ts (84%) rename apps/server/src/{shared/testing/factory => modules/provisioning/testing}/oauth-data-dto.factory.ts (100%) rename apps/server/src/{shared/testing/factory => modules/provisioning/testing}/provisioning-dto.factory.ts (100%) rename apps/server/src/{shared/testing/factory => modules/provisioning/testing}/provisioning-system-dto.factory.ts (100%) delete mode 100644 apps/server/src/shared/testing/factory/external-group-dto.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/external-user-dto.factory.ts diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index a8d4e6772c4..4b7dd9d571d 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -1,17 +1,21 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ProvisioningSystemDto } from '@modules/provisioning'; +import { + externalUserDtoFactory, + oauthDataDtoFactory, + externalClassDtoFactory, + provisioningSystemDtoFactory, + externalSchoolDtoFactory, +} from '@modules/provisioning/testing'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { externalSchoolDtoFactory } from '@shared/testing'; -import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; -import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; -import { robjExportKlasseFactory } from '@shared/testing/factory/robj-export-klasse.factory'; -import { robjExportLehrerFactory } from '@shared/testing/factory/robj-export-lehrer.factory'; -import { robjExportSchuelerFactory } from '@shared/testing/factory/robj-export-schueler.factory'; +import { + robjExportSchuelerFactory, + robjExportLehrerFactory, + robjExportKlasseFactory, +} from '@src/infra/tsp-client/testing'; import { Logger } from '@src/core/logger'; import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; import { schoolFactory } from '@src/modules/school/testing'; @@ -104,6 +108,7 @@ describe(TspOauthDataMapper.name, () => { lastName: tspTeachers[0].lehrerNachname, roles: [RoleName.TEACHER], email: undefined, + birthday: undefined, }); const externalStudentUserDto = externalUserDtoFactory.build({ @@ -112,6 +117,7 @@ describe(TspOauthDataMapper.name, () => { lastName: tspStudents[0].schuelerNachname, roles: [RoleName.STUDENT], email: undefined, + birthday: undefined, }); const externalSchoolDto = externalSchoolDtoFactory.build({ diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index 1585a7fea7d..38f97f56db9 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -13,12 +13,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { userDoFactory } from '@shared/testing'; -import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; -import { robjExportLehrerMigrationFactory } from '@shared/testing/factory/robj-export-lehrer-migration.factory'; -import { robjExportSchuelerMigrationFactory } from '@shared/testing/factory/robj-export-schueler-migration.factory'; -import { robjExportSchuleFactory } from '@shared/testing/factory/robj-export-schule.factory'; + +import { + externalUserDtoFactory, + oauthDataDtoFactory, + provisioningSystemDtoFactory, +} from '@src/modules/provisioning/testing'; +import { + robjExportSchuleFactory, + robjExportLehrerMigrationFactory, + robjExportSchuelerMigrationFactory, +} from '@src/infra/tsp-client/testing'; import { Logger } from '@src/core/logger'; import { Account } from '@src/modules/account'; import { accountDoFactory } from '@src/modules/account/testing'; diff --git a/apps/server/src/infra/tsp-client/testing/index.ts b/apps/server/src/infra/tsp-client/testing/index.ts new file mode 100644 index 00000000000..84d95f3ceaa --- /dev/null +++ b/apps/server/src/infra/tsp-client/testing/index.ts @@ -0,0 +1,6 @@ +export { robjExportSchuleFactory } from './robj-export-schule.factory'; +export { robjExportKlasseFactory } from './robj-export-klasse.factory'; +export { robjExportLehrerFactory } from './robj-export-lehrer.factory'; +export { robjExportSchuelerFactory } from './robj-export-schueler.factory'; +export { robjExportLehrerMigrationFactory } from './robj-export-lehrer-migration.factory'; +export { robjExportSchuelerMigrationFactory } from './robj-export-schueler-migration.factory'; diff --git a/apps/server/src/shared/testing/factory/robj-export-klasse.factory.ts b/apps/server/src/infra/tsp-client/testing/robj-export-klasse.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/robj-export-klasse.factory.ts rename to apps/server/src/infra/tsp-client/testing/robj-export-klasse.factory.ts diff --git a/apps/server/src/shared/testing/factory/robj-export-lehrer-migration.factory.ts b/apps/server/src/infra/tsp-client/testing/robj-export-lehrer-migration.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/robj-export-lehrer-migration.factory.ts rename to apps/server/src/infra/tsp-client/testing/robj-export-lehrer-migration.factory.ts diff --git a/apps/server/src/shared/testing/factory/robj-export-lehrer.factory.ts b/apps/server/src/infra/tsp-client/testing/robj-export-lehrer.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/robj-export-lehrer.factory.ts rename to apps/server/src/infra/tsp-client/testing/robj-export-lehrer.factory.ts diff --git a/apps/server/src/shared/testing/factory/robj-export-schueler-migration.factory.ts b/apps/server/src/infra/tsp-client/testing/robj-export-schueler-migration.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/robj-export-schueler-migration.factory.ts rename to apps/server/src/infra/tsp-client/testing/robj-export-schueler-migration.factory.ts diff --git a/apps/server/src/shared/testing/factory/robj-export-schueler.factory.ts b/apps/server/src/infra/tsp-client/testing/robj-export-schueler.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/robj-export-schueler.factory.ts rename to apps/server/src/infra/tsp-client/testing/robj-export-schueler.factory.ts diff --git a/apps/server/src/shared/testing/factory/robj-export-schule.factory.ts b/apps/server/src/infra/tsp-client/testing/robj-export-schule.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/robj-export-schule.factory.ts rename to apps/server/src/infra/tsp-client/testing/robj-export-schule.factory.ts diff --git a/apps/server/src/modules/provisioning/dto/provisioning.dto.ts b/apps/server/src/modules/provisioning/dto/provisioning.dto.ts index 0c9bf53867b..f35fbc311d4 100644 --- a/apps/server/src/modules/provisioning/dto/provisioning.dto.ts +++ b/apps/server/src/modules/provisioning/dto/provisioning.dto.ts @@ -1,7 +1,7 @@ -export class ProvisioningDto { - externalUserId: string; - - constructor(provisioningDto: ProvisioningDto) { - this.externalUserId = provisioningDto.externalUserId; - } -} +export class ProvisioningDto { + externalUserId: string; + + constructor(provisioningDto: ProvisioningDto) { + this.externalUserId = provisioningDto.externalUserId; + } +} diff --git a/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts index 888a6a58514..1526b21dfba 100644 --- a/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts +++ b/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts @@ -1,5 +1,4 @@ -import { externalSchoolDtoFactory } from '@shared/testing'; -import { externalGroupDtoFactory } from '@shared/testing/factory/external-group-dto.factory'; +import { externalGroupDtoFactory, externalSchoolDtoFactory } from '@modules/provisioning/testing'; import { ExternalGroupDto, ExternalSchoolDto } from '../dto'; import { SchoolForGroupNotFoundLoggable } from './school-for-group-not-found.loggable'; diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index b86324b50ec..3a62d5cea50 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -3,9 +3,8 @@ import { System, SystemService } from '@modules/system'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningDtoFactory } from '@shared/testing/factory/provisioning-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { provisioningDtoFactory, oauthDataDtoFactory } from '@modules/provisioning/testing'; +import { provisioningSystemDtoFactory } from '@src/modules/provisioning/testing/provisioning-system-dto.factory'; import { systemFactory } from '@src/modules/system/testing'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index c2ed303cd63..d8e9963db7d 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -3,11 +3,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; -import { externalSchoolDtoFactory, roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; -import { externalClassDtoFactory } from '@shared/testing/factory/external-class-dto.factory'; -import { externalUserDtoFactory } from '@shared/testing/factory/external-user-dto.factory'; -import { oauthDataDtoFactory } from '@shared/testing/factory/oauth-data-dto.factory'; -import { provisioningSystemDtoFactory } from '@shared/testing/factory/provisioning-system-dto.factory'; +import { roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; import { AccountService } from '@src/modules/account'; import { ClassService } from '@src/modules/class'; import { classFactory } from '@src/modules/class/domain/testing'; @@ -17,6 +13,13 @@ import { schoolFactory } from '@src/modules/school/testing'; import { UserService } from '@src/modules/user'; import { BadDataLoggableException } from '../loggable'; import { TspProvisioningService } from './tsp-provisioning.service'; +import { + provisioningSystemDtoFactory, + externalClassDtoFactory, + oauthDataDtoFactory, + externalUserDtoFactory, + externalSchoolDtoFactory, +} from '../testing'; describe('TspProvisioningService', () => { let module: TestingModule; diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts index f86346d37eb..8bbde69d3fe 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts @@ -7,13 +7,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { - externalGroupDtoFactory, - externalSchoolDtoFactory, - groupFactory, - legacySchoolDoFactory, - userDoFactory, -} from '@shared/testing'; +import { groupFactory, legacySchoolDoFactory, userDoFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { ExternalGroupDto, @@ -24,7 +18,7 @@ import { ProvisioningSystemDto, } from '../../dto'; import { ProvisioningConfig } from '../../provisioning.config'; -import { externalUserDtoFactory } from '../../testing'; +import { externalGroupDtoFactory, externalSchoolDtoFactory, externalUserDtoFactory } from '../../testing'; import { SchulconnexProvisioningStrategy } from './schulconnex.strategy'; import { SchulconnexCourseSyncService, diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts index f5d48e48d4f..e005b01873b 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts @@ -16,16 +16,9 @@ import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { ExternalSource, LegacySchoolDo, Page, RoleReference, UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { - externalGroupDtoFactory, - externalSchoolDtoFactory, - groupFactory, - legacySchoolDoFactory, - roleDtoFactory, - roleFactory, - userDoFactory, -} from '@shared/testing'; +import { groupFactory, legacySchoolDoFactory, roleDtoFactory, roleFactory, userDoFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { externalGroupDtoFactory, externalSchoolDtoFactory } from '@modules/provisioning/testing'; import { ExternalGroupDto, ExternalSchoolDto } from '../../../dto'; import { SchoolForGroupNotFoundLoggable, UserForGroupNotFoundLoggable } from '../../../loggable'; import { SchulconnexGroupProvisioningService } from './schulconnex-group-provisioning.service'; diff --git a/apps/server/src/shared/testing/factory/external-class-dto.factory.ts b/apps/server/src/modules/provisioning/testing/external-class-dto.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/external-class-dto.factory.ts rename to apps/server/src/modules/provisioning/testing/external-class-dto.factory.ts diff --git a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts b/apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts similarity index 84% rename from apps/server/src/shared/testing/factory/external-school-dto.factory.ts rename to apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts index 21f35cf0f03..31d93c4924e 100644 --- a/apps/server/src/shared/testing/factory/external-school-dto.factory.ts +++ b/apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ExternalSchoolDto } from '@modules/provisioning/dto'; -import { BaseFactory } from './base.factory'; +import { BaseFactory } from '../../../shared/testing/factory/base.factory'; class ExternalSchoolDtoFactory extends BaseFactory> {} diff --git a/apps/server/src/modules/provisioning/testing/index.ts b/apps/server/src/modules/provisioning/testing/index.ts index 32854894142..4d180c58f64 100644 --- a/apps/server/src/modules/provisioning/testing/index.ts +++ b/apps/server/src/modules/provisioning/testing/index.ts @@ -1,3 +1,8 @@ export { externalUserDtoFactory } from './external-user-dto.factory'; export { externalGroupDtoFactory } from './external-group-dto.factory'; export { externalGroupUserDtoFactory } from './external-group-user-dto.factory'; +export { oauthDataDtoFactory } from './oauth-data-dto.factory'; +export { externalClassDtoFactory } from './external-class-dto.factory'; +export { externalSchoolDtoFactory } from './external-school-dto.factory'; +export { provisioningSystemDtoFactory } from './provisioning-system-dto.factory'; +export { provisioningDtoFactory } from './provisioning-dto.factory'; diff --git a/apps/server/src/shared/testing/factory/oauth-data-dto.factory.ts b/apps/server/src/modules/provisioning/testing/oauth-data-dto.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/oauth-data-dto.factory.ts rename to apps/server/src/modules/provisioning/testing/oauth-data-dto.factory.ts diff --git a/apps/server/src/shared/testing/factory/provisioning-dto.factory.ts b/apps/server/src/modules/provisioning/testing/provisioning-dto.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/provisioning-dto.factory.ts rename to apps/server/src/modules/provisioning/testing/provisioning-dto.factory.ts diff --git a/apps/server/src/shared/testing/factory/provisioning-system-dto.factory.ts b/apps/server/src/modules/provisioning/testing/provisioning-system-dto.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/provisioning-system-dto.factory.ts rename to apps/server/src/modules/provisioning/testing/provisioning-system-dto.factory.ts diff --git a/apps/server/src/shared/testing/factory/external-group-dto.factory.ts b/apps/server/src/shared/testing/factory/external-group-dto.factory.ts deleted file mode 100644 index a6172ff6acf..00000000000 --- a/apps/server/src/shared/testing/factory/external-group-dto.factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GroupTypes } from '@modules/group'; -import { ExternalGroupDto } from '@modules/provisioning/dto'; -import { RoleName } from '@shared/domain/interface'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { Factory } from 'fishery'; - -export const externalGroupDtoFactory = Factory.define(({ sequence }) => { - return { - externalId: new ObjectId().toHexString(), - name: `Group ${sequence}`, - type: GroupTypes.CLASS, - user: { - externalUserId: new ObjectId().toHexString(), - roleName: RoleName.TEACHER, - }, - otherUsers: [ - { - externalUserId: new ObjectId().toHexString(), - roleName: RoleName.STUDENT, - }, - ], - from: new Date(2023, 1), - until: new Date(2023, 6), - }; -}); diff --git a/apps/server/src/shared/testing/factory/external-user-dto.factory.ts b/apps/server/src/shared/testing/factory/external-user-dto.factory.ts deleted file mode 100644 index f6e887439d1..00000000000 --- a/apps/server/src/shared/testing/factory/external-user-dto.factory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { RoleName } from '@shared/domain/interface'; -import { ExternalUserDto } from '@src/modules/provisioning'; -import { Factory } from 'fishery'; - -export const externalUserDtoFactory = Factory.define( - ({ sequence }) => - new ExternalUserDto({ - externalId: new ObjectId().toHexString(), - firstName: `Firstname ${sequence}`, - lastName: `Lastname ${sequence}`, - email: `Email ${sequence}`, - roles: [RoleName.STUDENT], - }) -); diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index d0d2e9568d9..9a6500b12c5 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -10,8 +10,6 @@ export * from './coursegroup.factory'; export * from './currentuser.factory'; export * from './domainobject'; export * from './entity.factory'; -export * from './external-group-dto.factory'; -export { externalSchoolDtoFactory } from './external-school-dto.factory'; export * from './external-tool-pseudonym.factory'; export * from './federal-state.factory'; export * from './filerecord.factory'; From 102ff376f2ef1f2a384f028b5e95b512f8aeb86b Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 7 Jan 2025 11:56:25 +0100 Subject: [PATCH 33/55] fix nest lint. --- apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index 194089c70ef..05fb651e2f4 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -14,9 +14,7 @@ import { UserDO } from '@shared/domain/domainobject'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { Logger } from '@src/core/logger'; -import { - robjExportSchuleFactory -} from '@src/infra/tsp-client/testing'; +import { robjExportSchuleFactory } from '@src/infra/tsp-client/testing'; import { Account } from '@src/modules/account'; import { OauthDataDto, ProvisioningService } from '@src/modules/provisioning'; import { From 45d12063933fc6f439c25f2f6d7130d1c0a6611c Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Mon, 13 Jan 2025 15:55:25 +0100 Subject: [PATCH 34/55] orgabize imports --- .../src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts | 6 +----- apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts | 9 ++++----- .../src/modules/provisioning/dto/provisioning.dto.ts | 2 +- .../provisioning/service/provisioning.service.spec.ts | 4 ++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts index 4b7dd9d571d..93e924dce7a 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.spec.ts @@ -11,11 +11,7 @@ import { import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { - robjExportSchuelerFactory, - robjExportLehrerFactory, - robjExportKlasseFactory, -} from '@src/infra/tsp-client/testing'; +import { robjExportSchuelerFactory, robjExportLehrerFactory, robjExportKlasseFactory } from '@infra/tsp-client/testing'; import { Logger } from '@src/core/logger'; import { BadDataLoggableException } from '@src/modules/provisioning/loggable'; import { schoolFactory } from '@src/modules/school/testing'; diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index 05fb651e2f4..5c007c9f7f9 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -12,16 +12,15 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; - import { Logger } from '@src/core/logger'; -import { robjExportSchuleFactory } from '@src/infra/tsp-client/testing'; -import { Account } from '@src/modules/account'; -import { OauthDataDto, ProvisioningService } from '@src/modules/provisioning'; +import { robjExportSchuleFactory } from '@infra/tsp-client/testing'; +import { Account } from '@modules/account'; +import { OauthDataDto, ProvisioningService } from '@modules/provisioning'; import { externalUserDtoFactory, oauthDataDtoFactory, provisioningSystemDtoFactory, -} from '@src/modules/provisioning/testing'; +} from '@modules/provisioning/testing'; import { School } from '@src/modules/school'; import { schoolFactory } from '@src/modules/school/testing'; import { System } from '@src/modules/system'; diff --git a/apps/server/src/modules/provisioning/dto/provisioning.dto.ts b/apps/server/src/modules/provisioning/dto/provisioning.dto.ts index f35fbc311d4..e38f52f9270 100644 --- a/apps/server/src/modules/provisioning/dto/provisioning.dto.ts +++ b/apps/server/src/modules/provisioning/dto/provisioning.dto.ts @@ -1,5 +1,5 @@ export class ProvisioningDto { - externalUserId: string; + public externalUserId: string; constructor(provisioningDto: ProvisioningDto) { this.externalUserId = provisioningDto.externalUserId; diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 3a62d5cea50..406362c36c6 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -4,8 +4,8 @@ import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { provisioningDtoFactory, oauthDataDtoFactory } from '@modules/provisioning/testing'; -import { provisioningSystemDtoFactory } from '@src/modules/provisioning/testing/provisioning-system-dto.factory'; -import { systemFactory } from '@src/modules/system/testing'; +import { provisioningSystemDtoFactory } from '@modules/provisioning/testing/provisioning-system-dto.factory'; +import { systemFactory } from '@modules/system/testing'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; import { TspProvisioningStrategy } from '../strategy/tsp/tsp.strategy'; From 69b41d5e044b59daa2e8575ee19935394f8764d2 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 13 Jan 2025 16:15:04 +0100 Subject: [PATCH 35/55] EW-1056: Remove for await and improve legibility. --- .../infra/sync/tsp/tsp-oauth-data.mapper.ts | 157 +++++++++++------- .../sync/tsp/tsp-sync-migration.service.ts | 1 + .../service/tsp-provisioning.service.spec.ts | 22 ++- .../service/tsp-provisioning.service.ts | 75 +++++---- .../provisioning/strategy/tsp/tsp.strategy.ts | 83 +++++---- 5 files changed, 219 insertions(+), 119 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index dcadb75014f..a644ad37542 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -33,11 +33,21 @@ export class TspOauthDataMapper { provisioningStrategy: SystemProvisioningStrategy.TSP, }); - const externalSchools = new Map(); - const externalClasses = new Map(); - const teacherForClasses = new Map>(); + const externalSchools = this.createMapOfExternalSchoolDtos(schools); + const { externalClasses, teacherForClasses } = this.createMapsOfClasses(tspClasses); const oauthDataDtos: OauthDataDto[] = []; + oauthDataDtos.push( + ...this.mapTspTeacherToOauthDataDtos(tspTeachers, systemDto, externalSchools, externalClasses, teacherForClasses) + ); + oauthDataDtos.push(...this.mapTspStudentsToOauthDataDtos(tspStudents, systemDto, externalSchools, externalClasses)); + + return oauthDataDtos; + } + + private createMapOfExternalSchoolDtos(schools: School[]): Map { + const externalSchools = new Map(); + schools.forEach((school) => { if (!school.externalId) { throw new BadDataLoggableException(`School ${school.id} has no externalId`); @@ -51,6 +61,16 @@ export class TspOauthDataMapper { ); }); + return externalSchools; + } + + private createMapsOfClasses(tspClasses: RobjExportKlasse[]): { + externalClasses: Map; + teacherForClasses: Map>; + } { + const externalClasses = new Map(); + const teacherForClasses = new Map>(); + tspClasses.forEach((tspClass) => { if (!tspClass.klasseId) { this.logger.info(new TspMissingExternalIdLoggable('class')); @@ -71,62 +91,87 @@ export class TspOauthDataMapper { } }); - tspTeachers.forEach((tspTeacher) => { - if (!tspTeacher.lehrerUid) { - this.logger.info(new TspMissingExternalIdLoggable('teacher')); - return; - } - - const externalUser = new ExternalUserDto({ - externalId: tspTeacher.lehrerUid, - firstName: tspTeacher.lehrerVorname, - lastName: tspTeacher.lehrerNachname, - roles: [RoleName.TEACHER], - }); - - const classIds = teacherForClasses.get(tspTeacher.lehrerUid) ?? []; - const classes: ExternalClassDto[] = classIds - .map((classId) => externalClasses.get(classId)) - .filter((externalClass: ExternalClassDto | undefined): externalClass is ExternalClassDto => !!externalClass); - - const externalSchool = tspTeacher.schuleNummer == null ? undefined : externalSchools.get(tspTeacher.schuleNummer); - - const oauthDataDto = new OauthDataDto({ - system: systemDto, - externalUser, - externalSchool, - externalClasses: classes, - }); - - oauthDataDtos.push(oauthDataDto); - }); - - tspStudents.forEach((tspStudent) => { - if (!tspStudent.schuelerUid) { - this.logger.info(new TspMissingExternalIdLoggable('student')); - return; - } - - const externalUser = new ExternalUserDto({ - externalId: tspStudent.schuelerUid, - firstName: tspStudent.schuelerVorname, - lastName: tspStudent.schuelerNachname, - roles: [RoleName.STUDENT], - }); - - const classStudent = tspStudent.klasseId == null ? undefined : externalClasses.get(tspStudent.klasseId); + return { externalClasses, teacherForClasses }; + } - const externalSchool = tspStudent.schuleNummer == null ? undefined : externalSchools.get(tspStudent.schuleNummer); + private mapTspTeacherToOauthDataDtos( + tspTeachers: RobjExportLehrer[], + systemDto: ProvisioningSystemDto, + externalSchools: Map, + externalClasses: Map, + teacherForClasses: Map> + ): OauthDataDto[] { + const oauthDataDtos = tspTeachers + .map((tspTeacher) => { + if (!tspTeacher.lehrerUid) { + this.logger.info(new TspMissingExternalIdLoggable('teacher')); + return null; + } + + const externalUser = new ExternalUserDto({ + externalId: tspTeacher.lehrerUid, + firstName: tspTeacher.lehrerVorname, + lastName: tspTeacher.lehrerNachname, + roles: [RoleName.TEACHER], + }); + + const classIds = teacherForClasses.get(tspTeacher.lehrerUid) ?? []; + const classes: ExternalClassDto[] = classIds + .map((classId) => externalClasses.get(classId)) + .filter((externalClass: ExternalClassDto | undefined): externalClass is ExternalClassDto => !!externalClass); + + const externalSchool = + tspTeacher.schuleNummer == null ? undefined : externalSchools.get(tspTeacher.schuleNummer); + + const oauthDataDto = new OauthDataDto({ + system: systemDto, + externalUser, + externalSchool, + externalClasses: classes, + }); + + return oauthDataDto; + }) + .filter((oauthDataDto) => oauthDataDto !== null); - const oauthDataDto = new OauthDataDto({ - system: systemDto, - externalUser, - externalSchool, - externalClasses: classStudent ? [classStudent] : [], - }); + return oauthDataDtos; + } - oauthDataDtos.push(oauthDataDto); - }); + private mapTspStudentsToOauthDataDtos( + tspStudents: RobjExportSchueler[], + systemDto: ProvisioningSystemDto, + externalSchools: Map, + externalClasses: Map + ): OauthDataDto[] { + const oauthDataDtos = tspStudents + .map((tspStudent) => { + if (!tspStudent.schuelerUid) { + this.logger.info(new TspMissingExternalIdLoggable('student')); + return null; + } + + const externalUser = new ExternalUserDto({ + externalId: tspStudent.schuelerUid, + firstName: tspStudent.schuelerVorname, + lastName: tspStudent.schuelerNachname, + roles: [RoleName.STUDENT], + }); + + const classStudent = tspStudent.klasseId == null ? undefined : externalClasses.get(tspStudent.klasseId); + + const externalSchool = + tspStudent.schuleNummer == null ? undefined : externalSchools.get(tspStudent.schuleNummer); + + const oauthDataDto = new OauthDataDto({ + system: systemDto, + externalUser, + externalSchool, + externalClasses: classStudent ? [classStudent] : [], + }); + + return oauthDataDto; + }) + .filter((oauthDataDto) => oauthDataDto !== null); return oauthDataDtos; } diff --git a/apps/server/src/infra/sync/tsp/tsp-sync-migration.service.ts b/apps/server/src/infra/sync/tsp/tsp-sync-migration.service.ts index a5d54f56b57..652c4566a5b 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync-migration.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync-migration.service.ts @@ -145,6 +145,7 @@ export class TspSyncMigrationService { } private async saveUsersAndAccounts(users: UserDO[], accounts: Account[]): Promise { + // These statement should probably not be combined with Promise.all() because last time I tried the performance massively decreased. await this.userService.saveAll(users); await this.accountService.saveAll(accounts); } diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index b42ff993b92..0fd91fdbd43 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -87,6 +87,8 @@ describe('TspProvisioningService', () => { beforeEach(() => { jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); }); it('should be defined', () => { @@ -133,7 +135,7 @@ describe('TspProvisioningService', () => { }); describe('provisionClasses', () => { - describe('when user ID is missing', () => { + describe('when user ID is missing and class exists', () => { const setup = () => { const school = schoolFactory.build(); const classes = [setupExternalClass()]; @@ -149,6 +151,24 @@ describe('TspProvisioningService', () => { }); }); + describe('when user ID is missing and class does not exist', () => { + const setup = () => { + const school = schoolFactory.build(); + const classes = [setupExternalClass()]; + const user = userDoFactory.build(); + + classServiceMock.findClassWithSchoolIdAndExternalId.mockResolvedValueOnce(null); + + return { school, classes, user }; + }; + + it('should throw', async () => { + const { school, classes, user } = setup(); + + await expect(sut.provisionClasses(school, classes, user)).rejects.toThrow(BadDataLoggableException); + }); + }); + describe('when class exists', () => { const setup = () => { const school = schoolFactory.build(); diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts index d4948292985..8b18d4eddff 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts @@ -1,5 +1,5 @@ import { AccountSave, AccountService } from '@modules/account'; -import { ClassFactory, ClassService, ClassSourceOptions } from '@modules/class'; +import { Class, ClassFactory, ClassService, ClassSourceOptions } from '@modules/class'; import { RoleService } from '@modules/role'; import { Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -45,41 +45,60 @@ export class TspProvisioningService { } public async provisionClasses(school: School, classes: ExternalClassDto[], user: UserDO): Promise { - if (!user.id) + const promises = classes.map(async (clazz) => { + const currentClass = await this.classService.findClassWithSchoolIdAndExternalId(school.id, clazz.externalId); + + if (currentClass) { + await this.updateClass(currentClass, clazz, school, user); + } else { + await this.createClass(clazz, school, user); + } + }); + + await Promise.all(promises); + } + + private async updateClass(currentClass: Class, clazz: ExternalClassDto, school: School, user: UserDO): Promise { + if (!user.id) { throw new BadDataLoggableException('User ID is missing', { externalId: user.externalId, }); + } - for await (const clazz of classes) { - const currentClass = await this.classService.findClassWithSchoolIdAndExternalId(school.id, clazz.externalId); + currentClass.schoolId = school.id; + currentClass.name = clazz.name ?? currentClass.name; + currentClass.year = school.currentYear?.id; + currentClass.source = this.ENTITY_SOURCE; + currentClass.sourceOptions = new ClassSourceOptions({ tspUid: clazz.externalId }); - if (currentClass) { - // Case: Class exists -> update class - currentClass.schoolId = school.id; - currentClass.name = clazz.name ?? currentClass.name; - currentClass.year = school.currentYear?.id; - currentClass.source = this.ENTITY_SOURCE; - currentClass.sourceOptions = new ClassSourceOptions({ tspUid: clazz.externalId }); + if (user.roles.some((role) => role.name === RoleName.TEACHER)) { + currentClass.addTeacher(user.id); + } + if (user.roles.some((role) => role.name === RoleName.STUDENT)) { + currentClass.addUser(user.id); + } - if (user.roles.some((role) => role.name === RoleName.TEACHER)) currentClass.addTeacher(user.id); - if (user.roles.some((role) => role.name === RoleName.STUDENT)) currentClass.addUser(user.id); + await this.classService.save(currentClass); + } - await this.classService.save(currentClass); - } else { - // Case: Class does not exist yet -> create new class - const newClass = ClassFactory.create({ - name: clazz.name, - schoolId: school.id, - year: school.currentYear?.id, - teacherIds: user.roles.some((role) => role.name === RoleName.TEACHER) ? [user.id] : [], - userIds: user.roles.some((role) => role.name === RoleName.STUDENT) ? [user.id] : [], - source: this.ENTITY_SOURCE, - sourceOptions: new ClassSourceOptions({ tspUid: clazz.externalId }), - }); - - await this.classService.save(newClass); - } + private async createClass(clazz: ExternalClassDto, school: School, user: UserDO): Promise { + if (!user.id) { + throw new BadDataLoggableException('User ID is missing', { + externalId: user.externalId, + }); } + + const newClass = ClassFactory.create({ + name: clazz.name, + schoolId: school.id, + year: school.currentYear?.id, + teacherIds: user.roles.some((role) => role.name === RoleName.TEACHER) ? [user.id] : [], + userIds: user.roles.some((role) => role.name === RoleName.STUDENT) ? [user.id] : [], + source: this.ENTITY_SOURCE, + sourceOptions: new ClassSourceOptions({ tspUid: clazz.externalId }), + }); + + await this.classService.save(newClass); } public async provisionUser(data: OauthDataDto, school: School): Promise { diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts index e9dc4d7b8b7..ccba8d22e0e 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -28,40 +28,8 @@ export class TspProvisioningStrategy extends ProvisioningStrategy { } public override async getData(input: OauthDataStrategyInputDto): Promise { - const decodedAccessToken: JwtPayload | null = jwt.decode(input.accessToken, { json: true }); - - if (!decodedAccessToken) { - throw new IdTokenExtractionFailureLoggableException('sub'); - } - - const payload = new TspJwtPayload(decodedAccessToken); - const errors = await validate(payload); - - if (errors.length > 0) { - throw new IdTokenExtractionFailureLoggableException(errors.map((error) => error.property).join(', ')); - } - - const externalUserDto = new ExternalUserDto({ - externalId: payload.sub, - firstName: payload.personVorname, - lastName: payload.personNachname, - roles: payload.ptscListRolle - .split(',') - .map((role) => this.mapRoles(role)) - .filter(Boolean) as RoleName[], - }); - - if (externalUserDto.roles && externalUserDto.roles.length < 1) { - throw new IdTokenExtractionFailureLoggableException('ptscListRolle'); - } - - const externalSchoolDto = new ExternalSchoolDto({ - externalId: payload.ptscSchuleNummer, - }); - - const externalClassDtoList = payload.ptscListKlasseId - ? payload.ptscListKlasseId.split(',').map((externalId) => new ExternalClassDto({ externalId })) - : []; + const payload = await this.parseAndValidateToken(input); + const { externalUserDto, externalSchoolDto, externalClassDtoList } = this.extractDateFromPayload(payload); const oauthDataDto = new OauthDataDto({ system: input.system, @@ -107,4 +75,51 @@ export class TspProvisioningStrategy extends ProvisioningStrategy { return null; } } + + private async parseAndValidateToken(input: OauthDataStrategyInputDto): Promise { + const decodedAccessToken: JwtPayload | null = jwt.decode(input.accessToken, { json: true }); + + if (!decodedAccessToken) { + throw new IdTokenExtractionFailureLoggableException('sub'); + } + + const payload = new TspJwtPayload(decodedAccessToken); + const errors = await validate(payload); + + if (errors.length > 0) { + throw new IdTokenExtractionFailureLoggableException(errors.map((error) => error.property).join(', ')); + } + + return payload; + } + + private extractDateFromPayload(payload: TspJwtPayload): { + externalUserDto: ExternalUserDto; + externalSchoolDto: ExternalSchoolDto; + externalClassDtoList: ExternalClassDto[]; + } { + const externalUserDto = new ExternalUserDto({ + externalId: payload.sub, + firstName: payload.personVorname, + lastName: payload.personNachname, + roles: payload.ptscListRolle + .split(',') + .map((role) => this.mapRoles(role)) + .filter(Boolean) as RoleName[], + }); + + if (externalUserDto.roles && externalUserDto.roles.length < 1) { + throw new IdTokenExtractionFailureLoggableException('ptscListRolle'); + } + + const externalSchoolDto = new ExternalSchoolDto({ + externalId: payload.ptscSchuleNummer, + }); + + const externalClassDtoList = payload.ptscListKlasseId + ? payload.ptscListKlasseId.split(',').map((externalId) => new ExternalClassDto({ externalId })) + : []; + + return { externalUserDto, externalSchoolDto, externalClassDtoList }; + } } From 410750bf179377c3a814134462383d20309fd0c0 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 15 Jan 2025 09:30:53 +0100 Subject: [PATCH 36/55] Fix typos. --- apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts | 4 ++-- .../src/modules/provisioning/strategy/tsp/tsp.strategy.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index a644ad37542..f748477315b 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -38,7 +38,7 @@ export class TspOauthDataMapper { const oauthDataDtos: OauthDataDto[] = []; oauthDataDtos.push( - ...this.mapTspTeacherToOauthDataDtos(tspTeachers, systemDto, externalSchools, externalClasses, teacherForClasses) + ...this.mapTspTeachersToOauthDataDtos(tspTeachers, systemDto, externalSchools, externalClasses, teacherForClasses) ); oauthDataDtos.push(...this.mapTspStudentsToOauthDataDtos(tspStudents, systemDto, externalSchools, externalClasses)); @@ -94,7 +94,7 @@ export class TspOauthDataMapper { return { externalClasses, teacherForClasses }; } - private mapTspTeacherToOauthDataDtos( + private mapTspTeachersToOauthDataDtos( tspTeachers: RobjExportLehrer[], systemDto: ProvisioningSystemDto, externalSchools: Map, diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts index ccba8d22e0e..36ebfbd2507 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -29,7 +29,7 @@ export class TspProvisioningStrategy extends ProvisioningStrategy { public override async getData(input: OauthDataStrategyInputDto): Promise { const payload = await this.parseAndValidateToken(input); - const { externalUserDto, externalSchoolDto, externalClassDtoList } = this.extractDateFromPayload(payload); + const { externalUserDto, externalSchoolDto, externalClassDtoList } = this.extractDataFromPayload(payload); const oauthDataDto = new OauthDataDto({ system: input.system, @@ -93,7 +93,7 @@ export class TspProvisioningStrategy extends ProvisioningStrategy { return payload; } - private extractDateFromPayload(payload: TspJwtPayload): { + private extractDataFromPayload(payload: TspJwtPayload): { externalUserDto: ExternalUserDto; externalSchoolDto: ExternalSchoolDto; externalClassDtoList: ExternalClassDto[]; From e1374a47ef9db8a1efa1f5d8ebbad9a87e9afb12 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 20 Jan 2025 12:21:56 +0100 Subject: [PATCH 37/55] Fix test error. --- .../service/schulconnex-group-provisioning.service.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts index 43be0c95343..7a2c4145181 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts @@ -8,7 +8,7 @@ import { SchoolSystemOptionsService, SchulConneXProvisioningOptions, } from '@modules/legacy-school'; -import { externalGroupDtoFactory } from '@modules/provisioning/testing'; +import { externalGroupDtoFactory, externalSchoolDtoFactory } from '@modules/provisioning/testing'; import { RoleDto, RoleService } from '@modules/role'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; @@ -19,7 +19,6 @@ import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { courseFactory } from '@src/modules/learnroom/testing'; import { groupFactory, legacySchoolDoFactory } from '@testing/factory/domainobject'; -import { externalSchoolDtoFactory } from '@testing/factory/external-school-dto.factory'; import { roleDtoFactory } from '@testing/factory/role-dto.factory'; import { roleFactory } from '@testing/factory/role.factory'; import { userDoFactory } from '@testing/factory/user.do.factory'; From 3e96b726cc70a8d48e2cb33f8b85c0a117a1faa8 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 20 Jan 2025 12:48:05 +0100 Subject: [PATCH 38/55] Fix test after merge. --- .../provisioning/service/tsp-provisioning.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index 70402a71153..715b87eedb3 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -140,7 +140,7 @@ describe('TspProvisioningService', () => { describe('when user ID is missing and class does not exist', () => { const setup = () => { const school = schoolFactory.build(); - const classes = [setupExternalClass()]; + const classes = [externalClassDtoFactory.build()]; const user = userDoFactory.build(); classServiceMock.findClassWithSchoolIdAndExternalId.mockResolvedValueOnce(null); From 6face401d6d863c0152303a35377f0573fb3118f Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 20 Jan 2025 15:12:21 +0100 Subject: [PATCH 39/55] Remaining comments from EW-1057. --- .../src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts | 2 +- .../server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts | 2 +- apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts | 2 +- apps/server/src/infra/sync/{strategy => }/sync.config.ts | 0 .../modules/provisioning/service/provisioning.service.spec.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename apps/server/src/infra/sync/{strategy => }/sync.config.ts (100%) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts index 7d0591f986d..243a2d54ee1 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts @@ -15,7 +15,7 @@ import { RobjExportSchuelerMigration, RobjExportSchule, TspClientFactory, -} from '../../../tsp-client'; +} from '@infra/tsp-client'; import { TspFetchService } from './tsp-fetch.service'; describe(TspFetchService.name, () => { diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts index 7f0b76f3054..42e127b4552 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts @@ -12,7 +12,7 @@ import { ProvisioningSystemDto, } from '@modules/provisioning'; import { Logger } from '@src/core/logger'; -import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '../../../tsp-client'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@infra/tsp-client'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; @Injectable() diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts index c8c667a0c7b..5cf20ec8376 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.config.ts @@ -1,4 +1,4 @@ -import { SyncConfig } from '../sync.config'; +import { SyncConfig } from '../../sync.config'; export interface TspSyncConfig extends SyncConfig { TSP_SYNC_SCHOOL_LIMIT: number; diff --git a/apps/server/src/infra/sync/strategy/sync.config.ts b/apps/server/src/infra/sync/sync.config.ts similarity index 100% rename from apps/server/src/infra/sync/strategy/sync.config.ts rename to apps/server/src/infra/sync/sync.config.ts diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 1c75c15c0fe..bde25a78230 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -6,7 +6,7 @@ import { systemFactory } from '@modules/system/testing'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '..'; +import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; import { TspProvisioningStrategy } from '../strategy/tsp/tsp.strategy'; import { ProvisioningService } from './provisioning.service'; From 768d1bf53d49deab8772c1efe26720f10b9179a4 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 21 Jan 2025 11:43:41 +0100 Subject: [PATCH 40/55] Use infer for types of config values. --- .../sync/strategy/tsp/tsp-sync-migration.service.ts | 2 +- .../src/infra/sync/strategy/tsp/tsp-sync.strategy.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync-migration.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync-migration.service.ts index 652c4566a5b..0df0fdf47f1 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync-migration.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync-migration.service.ts @@ -117,7 +117,7 @@ export class TspSyncMigrationService { private getOldIdBatches(oldToNewMappings: Map): string[][] { const oldIds = Array.from(oldToNewMappings.keys()); - const batchSize = this.configService.getOrThrow('TSP_SYNC_MIGRATION_LIMIT'); + const batchSize = this.configService.getOrThrow('TSP_SYNC_MIGRATION_LIMIT', { infer: true }); const batchCount = Math.ceil(oldIds.length / batchSize); const batches: string[][] = []; diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts index 1586ba2d8e0..ce4d8e56920 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts @@ -50,7 +50,7 @@ export class TspSyncStrategy extends SyncStrategy { const schools = await this.tspSyncService.findSchoolsForSystem(system); - if (this.configService.getOrThrow('FEATURE_TSP_MIGRATION_ENABLED')) { + if (this.configService.getOrThrow('FEATURE_TSP_MIGRATION_ENABLED', { infer: true })) { await this.runMigration(system); } @@ -58,11 +58,11 @@ export class TspSyncStrategy extends SyncStrategy { } private async syncSchools(system: System): Promise { - const schoolDaysToFetch = this.configService.getOrThrow('TSP_SYNC_SCHOOL_DAYS_TO_FETCH'); + const schoolDaysToFetch = this.configService.getOrThrow('TSP_SYNC_SCHOOL_DAYS_TO_FETCH', { infer: true }); const tspSchools = await this.tspFetchService.fetchTspSchools(system, schoolDaysToFetch); this.logger.info(new TspSchoolsFetchedLoggable(tspSchools.length, schoolDaysToFetch)); - const schoolLimit = this.configService.getOrThrow('TSP_SYNC_SCHOOL_LIMIT'); + const schoolLimit = this.configService.getOrThrow('TSP_SYNC_SCHOOL_LIMIT', { infer: true }); const schoolLimitFn = pLimit(schoolLimit); const schoolPromises = tspSchools.map((tspSchool) => @@ -100,7 +100,7 @@ export class TspSyncStrategy extends SyncStrategy { } private async syncData(system: System, schools: School[]): Promise { - const schoolDataDaysToFetch = this.configService.getOrThrow('TSP_SYNC_DATA_DAYS_TO_FETCH'); + const schoolDataDaysToFetch = this.configService.getOrThrow('TSP_SYNC_DATA_DAYS_TO_FETCH', { infer: true }); const tspTeachers = await this.tspFetchService.fetchTspTeachers(system, schoolDataDaysToFetch); const tspStudents = await this.tspFetchService.fetchTspStudents(system, schoolDataDaysToFetch); const tspClasses = await this.tspFetchService.fetchTspClasses(system, schoolDataDaysToFetch); @@ -118,7 +118,7 @@ export class TspSyncStrategy extends SyncStrategy { this.logger.info(new TspSyncingUsersLoggable(oauthDataDtos.length)); - const dataLimit = this.configService.getOrThrow('TSP_SYNC_DATA_LIMIT'); + const dataLimit = this.configService.getOrThrow('TSP_SYNC_DATA_LIMIT', { infer: true }); const dataLimitFn = pLimit(dataLimit); const dataPromises = oauthDataDtos.map((oauthDataDto) => From 22fc71c0bd0d7a1eea7f103ae9f19a1c788e9e43 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 21 Jan 2025 14:49:59 +0100 Subject: [PATCH 41/55] Make order of calls clear and use Promise.all --- ...gacy-migration.service.integration.spec.ts | 6 +- .../tsp/tsp-legacy-migration.service.ts | 2 +- .../strategy/tsp/tsp-sync.service.spec.ts | 4 +- .../sync/strategy/tsp/tsp-sync.service.ts | 2 +- .../strategy/tsp/tsp-sync.strategy.spec.ts | 6 +- .../sync/strategy/tsp/tsp-sync.strategy.ts | 58 ++++++++++++------- 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.integration.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.integration.spec.ts index 6d4ef1f1ba8..5b23c17f095 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.integration.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.integration.spec.ts @@ -46,10 +46,10 @@ describe('account repo', () => { await cleanupCollections(em); }); - describe('migrateLegacyData', () => { + describe('prepareLegacySyncDataForNewSync', () => { describe('when legacy system is not found', () => { it('should log TspLegacyMigrationSystemMissingLoggable', async () => { - await sut.migrateLegacyData(''); + await sut.prepareLegacySyncDataForNewSync(''); expect(logger.info).toHaveBeenCalledWith(new TspLegacyMigrationSystemMissingLoggable()); }); @@ -94,7 +94,7 @@ describe('account repo', () => { it('should update the school to the new format', async () => { const { newSystem, legacySchool, schoolId: schoolIdentifier } = await setup(); - await sut.migrateLegacyData(newSystem.id); + await sut.prepareLegacySyncDataForNewSync(newSystem.id); const migratedSchool = await em.findOne(SchoolEntity.name, { id: legacySchool.id, diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts index 1bff95ef2b5..b7c0153843c 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts @@ -24,7 +24,7 @@ export class TspLegacyMigrationService { logger.setContext(TspLegacyMigrationService.name); } - public async migrateLegacyData(newSystemId: EntityId): Promise { + public async prepareLegacySyncDataForNewSync(newSystemId: EntityId): Promise { this.logger.info(new TspLegacyMigrationStartLoggable()); const legacySystemId = await this.findLegacySystemId(); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts index a225cef6236..495e2447caa 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts @@ -140,7 +140,7 @@ describe(TspSyncService.name, () => { }); }); - describe('findSchoolsForSystem', () => { + describe('findAllSchoolsForSystem', () => { describe('when findSchoolsForSystem is called', () => { const setup = () => { const system = systemFactory.build(); @@ -154,7 +154,7 @@ describe(TspSyncService.name, () => { it('should return an array of schools', async () => { const { system, school } = setup(); - const schools = await sut.findSchoolsForSystem(system); + const schools = await sut.findAllSchoolsForSystem(system); expect(schools).toEqual([school]); }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts index 4ec2fa7cacd..cf874af5dc8 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts @@ -49,7 +49,7 @@ export class TspSyncService { return schools[0]; } - public async findSchoolsForSystem(system: System): Promise { + public async findAllSchoolsForSystem(system: System): Promise { const schools = await this.schoolService.getSchools({ systemId: system.id, }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts index 2baa741dff0..1da6c8e0fc0 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts @@ -148,7 +148,7 @@ describe(TspSyncStrategy.name, () => { tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce(params.fetchedStudentMigrations ?? []); tspSyncService.findSchool.mockResolvedValue(params.foundSchool ?? undefined); - tspSyncService.findSchoolsForSystem.mockResolvedValueOnce(params.foundSystemSchools ?? []); + tspSyncService.findAllSchoolsForSystem.mockResolvedValueOnce(params.foundSystemSchools ?? []); tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(params.foundSystem ?? systemFactory.build()); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce(params.mappedOauthDto ?? []); @@ -210,7 +210,7 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspLegacyMigrationService.migrateLegacyData).toHaveBeenCalled(); + expect(tspLegacyMigrationService.prepareLegacySyncDataForNewSync).toHaveBeenCalled(); }); it('should fetch the schools', async () => { @@ -236,7 +236,7 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.findSchoolsForSystem).toHaveBeenCalled(); + expect(tspSyncService.findAllSchoolsForSystem).toHaveBeenCalled(); }); it('should map to OauthDataDto', async () => { diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts index ce4d8e56920..b1ba48d6234 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts @@ -2,11 +2,12 @@ import { School } from '@modules/school'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Logger } from '@src/core/logger'; +import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@src/infra/tsp-client'; import { ProvisioningService } from '@src/modules/provisioning'; import { System } from '@src/modules/system'; import pLimit from 'p-limit'; -import { SyncStrategy } from '../sync-strategy'; import { SyncStrategyTarget } from '../../sync-strategy.types'; +import { SyncStrategy } from '../sync-strategy'; import { TspDataFetchedLoggable } from './loggable/tsp-data-fetched.loggable'; import { TspSchoolsFetchedLoggable } from './loggable/tsp-schools-fetched.loggable'; import { TspSchoolsSyncedLoggable } from './loggable/tsp-schools-synced.loggable'; @@ -21,6 +22,12 @@ import { TspSyncMigrationService } from './tsp-sync-migration.service'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; +type TspSchoolData = { + tspTeachers: RobjExportLehrer[]; + tspStudents: RobjExportSchueler[]; + tspClasses: RobjExportKlasse[]; +}; + @Injectable() export class TspSyncStrategy extends SyncStrategy { constructor( @@ -42,22 +49,23 @@ export class TspSyncStrategy extends SyncStrategy { } public async sync(): Promise { + // Please keep the order of this steps/methods as each relies on the data processed in the ones before. const system = await this.tspSyncService.findTspSystemOrFail(); - await this.tspLegacyMigrationService.migrateLegacyData(system.id); + await this.tspLegacyMigrationService.prepareLegacySyncDataForNewSync(system.id); - await this.syncSchools(system); + await this.syncTspSchools(system); - const schools = await this.tspSyncService.findSchoolsForSystem(system); + const schools = await this.tspSyncService.findAllSchoolsForSystem(system); if (this.configService.getOrThrow('FEATURE_TSP_MIGRATION_ENABLED', { infer: true })) { - await this.runMigration(system); + await this.runMigrationOfExistingUsers(system); } - await this.syncData(system, schools); + await this.syncDataOfSyncedTspSchools(system, schools); } - private async syncSchools(system: System): Promise { + private async syncTspSchools(system: System): Promise { const schoolDaysToFetch = this.configService.getOrThrow('TSP_SYNC_SCHOOL_DAYS_TO_FETCH', { infer: true }); const tspSchools = await this.tspFetchService.fetchTspSchools(system, schoolDaysToFetch); this.logger.info(new TspSchoolsFetchedLoggable(tspSchools.length, schoolDaysToFetch)); @@ -99,14 +107,8 @@ export class TspSyncStrategy extends SyncStrategy { return scSchools.filter((scSchool) => scSchool != null).map((scSchool) => scSchool.school); } - private async syncData(system: System, schools: School[]): Promise { - const schoolDataDaysToFetch = this.configService.getOrThrow('TSP_SYNC_DATA_DAYS_TO_FETCH', { infer: true }); - const tspTeachers = await this.tspFetchService.fetchTspTeachers(system, schoolDataDaysToFetch); - const tspStudents = await this.tspFetchService.fetchTspStudents(system, schoolDataDaysToFetch); - const tspClasses = await this.tspFetchService.fetchTspClasses(system, schoolDataDaysToFetch); - this.logger.info( - new TspDataFetchedLoggable(tspTeachers.length, tspStudents.length, tspClasses.length, schoolDataDaysToFetch) - ); + private async syncDataOfSyncedTspSchools(system: System, schools: School[]): Promise { + const { tspTeachers, tspStudents, tspClasses } = await this.fetchSchoolData(system); const oauthDataDtos = this.tspOauthDataMapper.mapTspDataToOauthData( system, @@ -120,26 +122,40 @@ export class TspSyncStrategy extends SyncStrategy { const dataLimit = this.configService.getOrThrow('TSP_SYNC_DATA_LIMIT', { infer: true }); const dataLimitFn = pLimit(dataLimit); - const dataPromises = oauthDataDtos.map((oauthDataDto) => dataLimitFn(() => this.provisioningService.provisionData(oauthDataDto)) ); - const results = await Promise.allSettled(dataPromises); this.logger.info(new TspSyncedUsersLoggable(results.length)); } - private async runMigration(system: System): Promise { + private async fetchSchoolData(system: System): Promise { + const schoolDataDaysToFetch = this.configService.getOrThrow('TSP_SYNC_DATA_DAYS_TO_FETCH', { infer: true }); + const [tspTeachers, tspStudents, tspClasses] = await Promise.all([ + this.tspFetchService.fetchTspTeachers(system, schoolDataDaysToFetch), + this.tspFetchService.fetchTspStudents(system, schoolDataDaysToFetch), + this.tspFetchService.fetchTspClasses(system, schoolDataDaysToFetch), + ]); + this.logger.info( + new TspDataFetchedLoggable(tspTeachers.length, tspStudents.length, tspClasses.length, schoolDataDaysToFetch) + ); + + return { tspTeachers, tspStudents, tspClasses }; + } + + private async runMigrationOfExistingUsers(system: System): Promise { const oldToNewMappings = new Map(); - const teacherMigrations = await this.tspFetchService.fetchTspTeacherMigrations(system); + const [teacherMigrations, studentsMigrations] = await Promise.all([ + this.tspFetchService.fetchTspTeacherMigrations(system), + this.tspFetchService.fetchTspStudentMigrations(system), + ]); + teacherMigrations.forEach(({ lehrerUidAlt, lehrerUidNeu }) => { if (lehrerUidAlt && lehrerUidNeu) { oldToNewMappings.set(lehrerUidAlt, lehrerUidNeu); } }); - - const studentsMigrations = await this.tspFetchService.fetchTspStudentMigrations(system); studentsMigrations.forEach(({ schuelerUidAlt, schuelerUidNeu }) => { if (schuelerUidAlt && schuelerUidNeu) { oldToNewMappings.set(schuelerUidAlt, schuelerUidNeu); From f24381ed575c018083a9c9751ff198b72d9f81ca Mon Sep 17 00:00:00 2001 From: Simone Radtke Date: Tue, 21 Jan 2025 15:24:59 +0100 Subject: [PATCH 42/55] EW-1083 Refactoring tests --- .../strategy/tsp/tsp-fetch.service.spec.ts | 71 +++++++------------ .../tsp/tsp-oauth-data.mapper.spec.ts | 13 ++-- .../strategy/tsp/tsp-sync.service.spec.ts | 19 +++-- .../strategy/tsp/tsp-sync.strategy.spec.ts | 26 +++---- .../tsp-client/tsp-client-factory.spec.ts | 60 ++++++++-------- .../service/tsp-provisioning.service.spec.ts | 56 +++++++-------- .../strategy/tsp/tsp.strategy.spec.ts | 14 ++-- 7 files changed, 115 insertions(+), 144 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts index 243a2d54ee1..6042c8b1b19 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts @@ -1,11 +1,5 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AxiosError, AxiosResponse } from 'axios'; -import { Logger } from '@src/core/logger'; -import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; -import { systemFactory } from '@modules/system/testing'; -import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; import { ExportApiInterface, RobjExportKlasse, @@ -16,6 +10,20 @@ import { RobjExportSchule, TspClientFactory, } from '@infra/tsp-client'; +import { + robjExportKlasseFactory, + robjExportLehrerFactory, + robjExportLehrerMigrationFactory, + robjExportSchuelerFactory, + robjExportSchuelerMigrationFactory, + robjExportSchuleFactory, +} from '@infra/tsp-client/testing'; +import { OauthConfigMissingLoggableException } from '@modules/oauth/loggable'; +import { systemFactory } from '@modules/system/testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; +import { Logger } from '@src/core/logger'; +import { AxiosError, AxiosResponse } from 'axios'; import { TspFetchService } from './tsp-fetch.service'; describe(TspFetchService.name, () => { @@ -45,7 +53,6 @@ describe(TspFetchService.name, () => { }); afterEach(() => { - jest.clearAllMocks(); jest.resetAllMocks(); }); @@ -60,8 +67,8 @@ describe(TspFetchService.name, () => { }); const setupTspClient = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); + const clientId = faker.string.uuid(); + const clientSecret = faker.string.alphanumeric(40); const tokenEndpoint = faker.internet.url(); const system = systemFactory.build({ oauthConfig: { @@ -71,61 +78,37 @@ describe(TspFetchService.name, () => { }, }); - const tspSchool: RobjExportSchule = { - schuleName: faker.string.alpha(), - schuleNummer: faker.string.alpha(), - }; + const tspSchool = robjExportSchuleFactory.build(); const schools = [tspSchool]; const responseSchools = createMock>>({ data: schools, }); - const tspTeacher: RobjExportLehrer = { - schuleNummer: faker.string.alpha(), - lehrerVorname: faker.string.alpha(), - lehrerNachname: faker.string.alpha(), - lehrerUid: faker.string.alpha(), - }; + const tspTeacher = robjExportLehrerFactory.build(); const teachers = [tspTeacher]; const responseTeachers = createMock>>({ data: teachers, }); - const tspStudent: RobjExportSchueler = { - schuleNummer: faker.string.alpha(), - schuelerVorname: faker.string.alpha(), - schuelerNachname: faker.string.alpha(), - schuelerUid: faker.string.alpha(), - }; + const tspStudent = robjExportSchuelerFactory.build(); const students = [tspStudent]; const responseStudents = createMock>>({ data: students, }); - const tspClass: RobjExportKlasse = { - schuleNummer: faker.string.alpha(), - klasseId: faker.string.alpha(), - klasseName: faker.string.alpha(), - lehrerUid: faker.string.alpha(), - }; + const tspClass = robjExportKlasseFactory.build(); const classes = [tspClass]; const responseClasses = createMock>>({ data: classes, }); - const tspTeacherMigration: RobjExportLehrerMigration = { - lehrerUidAlt: faker.string.alpha(), - lehrerUidNeu: faker.string.alpha(), - }; + const tspTeacherMigration = robjExportLehrerMigrationFactory.build(); const teacherMigrations = [tspTeacherMigration]; const responseTeacherMigrations = createMock>>({ data: teacherMigrations, }); - const tspStudentMigration: RobjExportSchuelerMigration = { - schuelerUidAlt: faker.string.alpha(), - schuelerUidNeu: faker.string.alpha(), - }; + const tspStudentMigration = robjExportSchuelerMigrationFactory.build(); const studentMigrations = [tspStudentMigration]; const responseStudentMigrations = createMock>>({ data: studentMigrations, @@ -359,8 +342,8 @@ describe(TspFetchService.name, () => { const setup = () => { const system = systemFactory.build({ oauthConfig: { - clientId: faker.string.alpha(), - clientSecret: faker.string.alpha(), + clientId: faker.string.uuid(), + clientSecret: faker.string.alphanumeric(40), tokenEndpoint: faker.internet.url(), }, }); @@ -391,8 +374,8 @@ describe(TspFetchService.name, () => { const setup = () => { const system = systemFactory.build({ oauthConfig: { - clientId: faker.string.alpha(), - clientSecret: faker.string.alpha(), + clientId: faker.string.uuid(), + clientSecret: faker.string.alphanumeric(40), tokenEndpoint: faker.internet.url(), }, }); @@ -431,7 +414,7 @@ describe(TspFetchService.name, () => { it('should throw an OauthConfigMissingLoggableException', async () => { const { system } = setup(); - await expect(async () => sut.fetchTspSchools(system, 1)).rejects.toThrow(OauthConfigMissingLoggableException); + await expect(() => sut.fetchTspSchools(system, 1)).rejects.toThrow(OauthConfigMissingLoggableException); }); }); }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.spec.ts index 1619a212b8d..5d5ae688e52 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.spec.ts @@ -40,7 +40,6 @@ describe(TspOauthDataMapper.name, () => { }); afterEach(() => { - jest.clearAllMocks(); jest.resetAllMocks(); }); @@ -60,10 +59,10 @@ describe(TspOauthDataMapper.name, () => { const system = systemFactory.build(); const school = schoolFactory.build({ - externalId: faker.string.alpha(), + externalId: faker.string.uuid(), }); - const lehrerUid = faker.string.alpha(); + const lehrerUid = faker.string.uuid(); const tspTeacher = robjExportLehrerFactory.build({ lehrerUid, @@ -71,7 +70,7 @@ describe(TspOauthDataMapper.name, () => { }); const tspTeachers = [tspTeacher]; - const klasseId = faker.string.alpha(); + const klasseId = faker.string.uuid(); const tspClass = robjExportKlasseFactory.build({ klasseId, @@ -80,9 +79,9 @@ describe(TspOauthDataMapper.name, () => { const tspClasses = [tspClass]; const tspStudent = robjExportSchuelerFactory.build({ - schuelerUid: faker.string.alpha(), - schuelerNachname: faker.string.alpha(), - schuelerVorname: faker.string.alpha(), + schuelerUid: faker.string.uuid(), + schuelerNachname: faker.person.lastName(), + schuelerVorname: faker.person.firstName(), schuleNummer: school.externalId, klasseId, }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts index 495e2447caa..355a8e3a778 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts @@ -52,7 +52,6 @@ describe(TspSyncService.name, () => { }); afterEach(() => { - jest.clearAllMocks(); jest.resetAllMocks(); }); @@ -102,7 +101,7 @@ describe(TspSyncService.name, () => { describe('findSchool', () => { describe('when school is found', () => { const setup = () => { - const externalId = faker.string.alpha(); + const externalId = faker.string.uuid(); const system = systemFactory.build(); const school = schoolFactory.build(); @@ -122,7 +121,7 @@ describe(TspSyncService.name, () => { describe('when school is not found', () => { const setup = () => { - const externalId = faker.string.alpha(); + const externalId = faker.string.uuid(); const system = systemFactory.build(); schoolService.getSchools.mockResolvedValueOnce([]); @@ -164,8 +163,8 @@ describe(TspSyncService.name, () => { describe('updateSchool', () => { describe('when school is updated', () => { const setup = () => { - const newName = faker.string.alpha(); - const oldName = faker.string.alpha(); + const newName = faker.person.fullName(); + const oldName = faker.person.fullName(); const school = schoolFactory.build({ name: oldName, }); @@ -189,7 +188,7 @@ describe(TspSyncService.name, () => { describe('when school name is undefined', () => { const setup = () => { const newName = undefined; - const oldName = faker.string.alpha(); + const oldName = faker.person.fullName(); const school = schoolFactory.build({ name: oldName, }); @@ -211,8 +210,8 @@ describe(TspSyncService.name, () => { describe('when school is created', () => { const setup = () => { const system = systemFactory.build(); - const name = faker.string.alpha(); - const externalId = faker.string.alpha(); + const name = faker.word.noun(); + const externalId = faker.string.uuid(); const schoolYearEntity = schoolYearFactory.build(); const schoolYear = SchoolYearEntityMapper.mapToDo(schoolYearEntity); @@ -249,8 +248,8 @@ describe(TspSyncService.name, () => { describe('when federalState is already cached', () => { const setup = () => { const system = systemFactory.build(); - const name = faker.string.alpha(); - const externalId = faker.string.alpha(); + const name = faker.word.noun(); + const externalId = faker.string.uuid(); const schoolYearEntity = schoolYearFactory.build(); const schoolYear = SchoolYearEntityMapper.mapToDo(schoolYearEntity); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts index 1da6c8e0fc0..319a2445e2e 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts @@ -8,7 +8,11 @@ import { RobjExportSchuelerMigration, RobjExportSchule, } from '@infra/tsp-client'; -import { robjExportSchuleFactory } from '@infra/tsp-client/testing'; +import { + robjExportLehrerMigrationFactory, + robjExportSchuelerMigrationFactory, + robjExportSchuleFactory, +} from '@infra/tsp-client/testing'; import { Account } from '@modules/account'; import { OauthDataDto, ProvisioningService } from '@modules/provisioning'; import { @@ -95,9 +99,7 @@ describe(TspSyncStrategy.name, () => { }); afterEach(() => { - jest.clearAllMocks(); jest.resetAllMocks(); - jest.restoreAllMocks(); }); afterAll(async () => { @@ -147,7 +149,7 @@ describe(TspSyncStrategy.name, () => { tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce(params.fetchedTeacherMigrations ?? []); tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce(params.fetchedStudentMigrations ?? []); - tspSyncService.findSchool.mockResolvedValue(params.foundSchool ?? undefined); + tspSyncService.findSchool.mockResolvedValueOnce(params.foundSchool ?? undefined); tspSyncService.findAllSchoolsForSystem.mockResolvedValueOnce(params.foundSystemSchools ?? []); tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(params.foundSystem ?? systemFactory.build()); @@ -169,23 +171,17 @@ describe(TspSyncStrategy.name, () => { const setup = () => { const oauthDataDto = oauthDataDtoFactory.build({ system: provisioningSystemDtoFactory.build({ - systemId: faker.string.alpha(), + systemId: faker.string.uuid(), provisioningStrategy: SystemProvisioningStrategy.TSP, }), externalUser: externalUserDtoFactory.build({ - externalId: faker.string.alpha(), + externalId: faker.string.uuid(), roles: [], }), }); - const tspTeacher: RobjExportLehrerMigration = { - lehrerUidAlt: faker.string.alpha(), - lehrerUidNeu: faker.string.alpha(), - }; - - const tspStudent: RobjExportSchuelerMigration = { - schuelerUidAlt: faker.string.alpha(), - schuelerUidNeu: faker.string.alpha(), - }; + const tspTeacher = robjExportLehrerMigrationFactory.build(); + + const tspStudent = robjExportSchuelerMigrationFactory.build(); setupMockServices({ fetchedStudentMigrations: [tspStudent], diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index 6f9c771eaa6..9b737ff0674 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -1,12 +1,12 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { OauthAdapterService } from '@modules/oauth'; +import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import axios, { AxiosError } from 'axios'; -import { ServerConfig } from '@modules/server'; -import { OauthAdapterService } from '@modules/oauth'; import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; import { Logger } from '@src/core/logger'; +import axios, { AxiosError } from 'axios'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; @@ -59,14 +59,12 @@ describe('TspClientFactory', () => { logger = module.get(Logger); }); - afterAll(async () => { - await module.close(); + afterEach(() => { + jest.resetAllMocks(); }); - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); + afterAll(async () => { + await module.close(); }); it('should be defined', () => { @@ -77,8 +75,8 @@ describe('TspClientFactory', () => { describe('when createExportClient is called', () => { it('should return ExportApiInterface', () => { const result = sut.createExportClient({ - clientId: faker.string.alpha(), - clientSecret: faker.string.alpha(), + clientId: faker.string.uuid(), + clientSecret: faker.string.alphanumeric(40), tokenEndpoint: faker.internet.url(), }); @@ -91,18 +89,16 @@ describe('TspClientFactory', () => { describe('getAccessToken', () => { describe('when called successfully', () => { const setup = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); + const clientId = faker.string.uuid(); + const clientSecret = faker.string.alphanumeric(40); const tokenEndpoint = faker.internet.url(); - oauthAdapterServiceMock.sendTokenRequest.mockResolvedValue({ - accessToken: faker.string.alpha(), - idToken: faker.string.alpha(), - refreshToken: faker.string.alpha(), + oauthAdapterServiceMock.sendTokenRequest.mockResolvedValueOnce({ + accessToken: faker.string.alphanumeric(40), + idToken: faker.string.alphanumeric(40), + refreshToken: faker.string.alphanumeric(40), }); - Reflect.set(sut, 'cachedToken', undefined); - return { clientId, clientSecret, @@ -123,8 +119,8 @@ describe('TspClientFactory', () => { describe('when token is cached', () => { const setup = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); + const clientId = faker.string.uuid(); + const clientSecret = faker.string.alphanumeric(40); const tokenEndpoint = faker.internet.url(); const client = sut.createExportClient({ clientId, @@ -132,7 +128,7 @@ describe('TspClientFactory', () => { tokenEndpoint, }); - const cached = faker.string.alpha(); + const cached = faker.string.alphanumeric(40); Reflect.set(sut, 'cachedToken', cached); Reflect.set(sut, 'tokenExpiresAt', Date.now() + 60000); @@ -151,8 +147,8 @@ describe('TspClientFactory', () => { describe('when an AxiosError occurs', () => { const setup = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); + const clientId = faker.string.uuid(); + const clientSecret = faker.string.alphanumeric(40); const tokenEndpoint = faker.internet.url(); oauthAdapterServiceMock.sendTokenRequest.mockImplementation(() => { @@ -179,8 +175,8 @@ describe('TspClientFactory', () => { describe('when a generic error occurs', () => { const setup = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); + const clientId = faker.string.uuid(); + const clientSecret = faker.string.alphanumeric(40); const tokenEndpoint = faker.internet.url(); oauthAdapterServiceMock.sendTokenRequest.mockImplementation(() => { @@ -216,16 +212,16 @@ describe('TspClientFactory', () => { jest.mock('axios'); - oauthAdapterServiceMock.sendTokenRequest.mockResolvedValue({ - accessToken: faker.string.alpha(), - idToken: faker.string.alpha(), - refreshToken: faker.string.alpha(), + oauthAdapterServiceMock.sendTokenRequest.mockResolvedValueOnce({ + accessToken: faker.string.alphanumeric(40), + idToken: faker.string.alphanumeric(40), + refreshToken: faker.string.alphanumeric(40), }); const axiosMock = axios as jest.Mocked; - axiosMock.request = jest.fn(); - axiosMock.request.mockResolvedValue({ + jest.spyOn(axiosMock, 'request').mockImplementation(); + axiosMock.request.mockResolvedValueOnce({ data: { version: '1.1', }, diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index 715b87eedb3..c8c8c9e0ada 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -1,8 +1,5 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { RoleName } from '@shared/domain/interface'; import { AccountService } from '@modules/account'; import { ClassService } from '@modules/class'; import { classFactory } from '@modules/class/domain/testing'; @@ -10,6 +7,9 @@ import { RoleService } from '@modules/role'; import { SchoolService } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { RoleName } from '@shared/domain/interface'; import { roleDtoFactory } from '@testing/factory/role-dto.factory'; import { roleFactory } from '@testing/factory/role.factory'; import { userDoFactory } from '@testing/factory/user.do.factory'; @@ -67,14 +67,12 @@ describe('TspProvisioningService', () => { accountServiceMock = module.get(AccountService); }); - afterAll(async () => { - await module.close(); + afterEach(() => { + jest.resetAllMocks(); }); - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); + afterAll(async () => { + await module.close(); }); it('should be defined', () => { @@ -164,7 +162,7 @@ describe('TspProvisioningService', () => { roles: [roleFactory.build({ name: RoleName.TEACHER }), roleFactory.build({ name: RoleName.STUDENT })], }); - classServiceMock.findClassWithSchoolIdAndExternalId.mockResolvedValue(clazz); + classServiceMock.findClassWithSchoolIdAndExternalId.mockResolvedValueOnce(clazz); return { school, classes, user }; }; @@ -186,7 +184,7 @@ describe('TspProvisioningService', () => { roles: [roleFactory.build({ name: RoleName.TEACHER }), roleFactory.build({ name: RoleName.STUDENT })], }); - classServiceMock.findClassWithSchoolIdAndExternalId.mockResolvedValue(null); + classServiceMock.findClassWithSchoolIdAndExternalId.mockResolvedValueOnce(null); return { school, classes, user }; }; @@ -232,9 +230,9 @@ describe('TspProvisioningService', () => { }); const user = userDoFactory.build({ id: faker.string.uuid() }); - userServiceMock.findByExternalId.mockResolvedValue(user); - userServiceMock.save.mockResolvedValue(user); - schoolServiceMock.getSchools.mockResolvedValue([school]); + userServiceMock.findByExternalId.mockResolvedValueOnce(user); + userServiceMock.save.mockResolvedValueOnce(user); + schoolServiceMock.getSchools.mockResolvedValueOnce([school]); return { data, school }; }; @@ -263,10 +261,10 @@ describe('TspProvisioningService', () => { roleDtoFactory.build({ name: RoleName.STUDENT }), ]; - userServiceMock.findByExternalId.mockResolvedValue(user); - userServiceMock.save.mockResolvedValue(user); - schoolServiceMock.getSchools.mockResolvedValue([school]); - roleServiceMock.findByNames.mockResolvedValue(roles); + userServiceMock.findByExternalId.mockResolvedValueOnce(user); + userServiceMock.save.mockResolvedValueOnce(user); + schoolServiceMock.getSchools.mockResolvedValueOnce([school]); + roleServiceMock.findByNames.mockResolvedValueOnce(roles); return { data, school }; }; @@ -294,9 +292,9 @@ describe('TspProvisioningService', () => { externalSchool: externalSchoolDtoFactory.build(), }); - userServiceMock.findByExternalId.mockResolvedValue(null); - schoolServiceMock.getSchools.mockResolvedValue([school]); - roleServiceMock.findByNames.mockResolvedValue([]); + userServiceMock.findByExternalId.mockResolvedValueOnce(null); + schoolServiceMock.getSchools.mockResolvedValueOnce([school]); + roleServiceMock.findByNames.mockResolvedValueOnce([]); return { data, school }; }; @@ -328,10 +326,10 @@ describe('TspProvisioningService', () => { }); const user = userDoFactory.build({ id: faker.string.uuid(), roles: [] }); - userServiceMock.findByExternalId.mockResolvedValue(null); - userServiceMock.save.mockResolvedValue(user); - schoolServiceMock.getSchools.mockResolvedValue([school]); - roleServiceMock.findByNames.mockResolvedValue([]); + userServiceMock.findByExternalId.mockResolvedValueOnce(null); + userServiceMock.save.mockResolvedValueOnce(user); + schoolServiceMock.getSchools.mockResolvedValueOnce([school]); + roleServiceMock.findByNames.mockResolvedValueOnce([]); return { data, school }; }; @@ -360,10 +358,10 @@ describe('TspProvisioningService', () => { }); const user = userDoFactory.build({ id: undefined, roles: [] }); - userServiceMock.findByExternalId.mockResolvedValue(null); - userServiceMock.save.mockResolvedValue(user); - schoolServiceMock.getSchools.mockResolvedValue([school]); - roleServiceMock.findByNames.mockResolvedValue([]); + userServiceMock.findByExternalId.mockResolvedValueOnce(null); + userServiceMock.save.mockResolvedValueOnce(user); + schoolServiceMock.getSchools.mockResolvedValueOnce([school]); + roleServiceMock.findByNames.mockResolvedValueOnce([]); return { data, school }; }; diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts index 8a81fb77112..9eb5a52a66e 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts @@ -38,12 +38,12 @@ describe('TspProvisioningStrategy', () => { provisioningServiceMock = module.get(TspProvisioningService); }); - afterAll(async () => { - await module.close(); + afterEach(() => { + jest.resetAllMocks(); }); - beforeEach(() => { - jest.resetAllMocks(); + afterAll(async () => { + await module.close(); }); describe('getType', () => { @@ -230,12 +230,12 @@ describe('TspProvisioningStrategy', () => { firstName: faker.person.firstName(), lastName: faker.person.lastName(), }); - const externalSchool = new ExternalSchoolDto({ externalId: faker.string.uuid(), name: faker.string.alpha() }); + const externalSchool = new ExternalSchoolDto({ externalId: faker.string.uuid(), name: faker.word.noun() }); const externalClasses = []; const data = new OauthDataDto({ system, externalUser, externalSchool, externalClasses }); - provisioningServiceMock.findSchoolOrFail.mockResolvedValue(school); - provisioningServiceMock.provisionUser.mockResolvedValue(user); + provisioningServiceMock.findSchoolOrFail.mockResolvedValueOnce(school); + provisioningServiceMock.provisionUser.mockResolvedValueOnce(user); return { school, user, system, externalUser, externalSchool, externalClasses, data }; }; From 3c71e56049b3779ce82ded30b13632e1d1c024b0 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 21 Jan 2025 16:22:53 +0100 Subject: [PATCH 43/55] Change error handling and rename fetch function. --- .../strategy/tsp/tsp-fetch.service.spec.ts | 16 +++++++++------- .../sync/strategy/tsp/tsp-fetch.service.ts | 17 +++++++++-------- .../tsp-client/tsp-client-factory.spec.ts | 18 ++++++++++-------- .../src/infra/tsp-client/tsp-client-factory.ts | 10 +++++----- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts index fb5e9e87a42..f4f2012bff9 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts @@ -1,5 +1,5 @@ +import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; -import { Logger } from '@core/logger'; import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { @@ -30,7 +30,7 @@ describe(TspFetchService.name, () => { let module: TestingModule; let sut: TspFetchService; let tspClientFactory: DeepMocked; - let logger: DeepMocked; + let domainErrorHandler: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -41,15 +41,15 @@ describe(TspFetchService.name, () => { useValue: createMock(), }, { - provide: Logger, - useValue: createMock(), + provide: DomainErrorHandler, + useValue: createMock(), }, ], }).compile(); sut = module.get(TspFetchService); tspClientFactory = module.get(TspClientFactory); - logger = module.get(Logger); + domainErrorHandler = module.get(DomainErrorHandler); }); afterEach(() => { @@ -366,7 +366,9 @@ describe(TspFetchService.name, () => { await sut.fetchTspSchools(system, 1); - expect(logger.warning).toHaveBeenCalledWith(new AxiosErrorLoggable(new AxiosError(), 'TSP_FETCH_ERROR')); + expect(domainErrorHandler.exec).toHaveBeenCalledWith( + new AxiosErrorLoggable(new AxiosError(), 'TSP_FETCH_ERROR') + ); }); }); @@ -398,7 +400,7 @@ describe(TspFetchService.name, () => { await sut.fetchTspSchools(system, 1); - expect(logger.warning).toHaveBeenCalledWith(new ErrorLoggable(new Error())); + expect(domainErrorHandler.exec).toHaveBeenCalledWith(new ErrorLoggable(new Error())); }); }); }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts index 75db14b0241..0ada277dde5 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts @@ -1,5 +1,5 @@ +import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; -import { Logger } from '@core/logger'; import { ExportApiInterface, RobjExportKlasse, @@ -18,9 +18,10 @@ import moment from 'moment'; @Injectable() export class TspFetchService { - constructor(private readonly tspClientFactory: TspClientFactory, private readonly logger: Logger) { - this.logger.setContext(TspFetchService.name); - } + constructor( + private readonly tspClientFactory: TspClientFactory, + private readonly domainErrorHandler: DomainErrorHandler + ) {} public fetchTspSchools(system: System, daysToFetch: number): Promise { const lastChangeDate = this.formatChangeDate(daysToFetch); @@ -64,20 +65,20 @@ export class TspFetchService { private async fetchTsp( system: System, - fetch: (client: ExportApiInterface) => Promise>, + fetchFunction: (client: ExportApiInterface) => Promise>, defaultValue: T ): Promise { const client = this.createClient(system); try { - const response = await fetch(client); + const response = await fetchFunction(client); const { data } = response; return data; } catch (e) { if (e instanceof AxiosError) { - this.logger.warning(new AxiosErrorLoggable(e, 'TSP_FETCH_ERROR')); + this.domainErrorHandler.exec(new AxiosErrorLoggable(e, 'TSP_FETCH_ERROR')); } else { - this.logger.warning(new ErrorLoggable(e)); + this.domainErrorHandler.exec(new ErrorLoggable(e)); } } return defaultValue; diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index e9811765a7e..4a7dd0de650 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -1,11 +1,11 @@ +import { DomainErrorHandler } from '@core/error'; +import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { OauthAdapterService } from '@modules/oauth'; import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; -import { Logger } from '@core/logger'; import axios, { AxiosError } from 'axios'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; @@ -16,7 +16,7 @@ describe('TspClientFactory', () => { let configServiceMock: DeepMocked>; let oauthAdapterServiceMock: DeepMocked; let encryptionService: DeepMocked; - let logger: DeepMocked; + let domainErrorHandler: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -46,8 +46,8 @@ describe('TspClientFactory', () => { useValue: createMock(), }, { - provide: Logger, - useValue: createMock(), + provide: DomainErrorHandler, + useValue: createMock(), }, ], }).compile(); @@ -56,7 +56,7 @@ describe('TspClientFactory', () => { configServiceMock = module.get(ConfigService); oauthAdapterServiceMock = module.get(OauthAdapterService); encryptionService = module.get(DefaultEncryptionService); - logger = module.get(Logger); + domainErrorHandler = module.get(DomainErrorHandler); }); afterEach(() => { @@ -169,7 +169,9 @@ describe('TspClientFactory', () => { await expect(() => sut.getAccessToken(params)).rejects.toBeUndefined(); - expect(logger.warning).toHaveBeenCalledWith(new AxiosErrorLoggable(new AxiosError(), 'TSP_OAUTH_ERROR')); + expect(domainErrorHandler.exec).toHaveBeenCalledWith( + new AxiosErrorLoggable(new AxiosError(), 'TSP_OAUTH_ERROR') + ); }); }); @@ -197,7 +199,7 @@ describe('TspClientFactory', () => { await expect(() => sut.getAccessToken(params)).rejects.toBeUndefined(); - expect(logger.warning).toHaveBeenCalledWith(new ErrorLoggable(new Error())); + expect(domainErrorHandler.exec).toHaveBeenCalledWith(new ErrorLoggable(new Error())); }); }); }); diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.ts index f7f55d6bef9..2bb124da71f 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.ts @@ -1,5 +1,5 @@ +import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; -import { Logger } from '@core/logger'; import { OauthAdapterService } from '@modules/oauth'; import { OAuthGrantType } from '@modules/oauth/interface/oauth-grant-type.enum'; import { ClientCredentialsGrantTokenRequest } from '@modules/oauth/service/dto'; @@ -31,7 +31,7 @@ export class TspClientFactory { private readonly oauthAdapterService: OauthAdapterService, configService: ConfigService, @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, - private readonly logger: Logger + private readonly domainErrorHandler: DomainErrorHandler ) { this.baseUrl = configService.getOrThrow('TSP_API_CLIENT_BASE_URL'); this.tokenLifetime = configService.getOrThrow('TSP_API_CLIENT_TOKEN_LIFETIME_MS'); @@ -42,7 +42,7 @@ export class TspClientFactory { new Configuration({ // accessToken has to be a function otherwise it will be called once // and will not be refresh the access token when it expires - apiKey: async () => this.getAccessToken(params), + apiKey: () => this.getAccessToken(params), basePath: this.baseUrl, }) ); @@ -74,9 +74,9 @@ export class TspClientFactory { return `Bearer ${this.cachedToken}`; } catch (e) { if (e instanceof AxiosError) { - this.logger.warning(new AxiosErrorLoggable(e, 'TSP_OAUTH_ERROR')); + this.domainErrorHandler.exec(new AxiosErrorLoggable(e, 'TSP_OAUTH_ERROR')); } else { - this.logger.warning(new ErrorLoggable(e)); + this.domainErrorHandler.exec(new ErrorLoggable(e)); } return Promise.reject(); } From 746d67620340d21e67dfbf307f47a47ff46d94bb Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 22 Jan 2025 11:46:02 +0100 Subject: [PATCH 44/55] Make client throw access token exception. --- .../tsp-access-token.loggable-error.spec.ts | 23 ++++++++++++++++ .../tsp-access-token.loggable-error.ts | 26 +++++++++++++++++++ .../strategy/tsp/tsp-fetch.service.spec.ts | 9 +++++-- .../sync/strategy/tsp/tsp-fetch.service.ts | 3 ++- apps/server/src/infra/sync/sync.module.ts | 2 ++ .../tsp-client/tsp-client-factory.spec.ts | 5 ++-- .../infra/tsp-client/tsp-client-factory.ts | 3 ++- .../src/infra/tsp-client/tsp-client.module.ts | 5 ++-- 8 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.spec.ts create mode 100644 apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts diff --git a/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.spec.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.spec.ts new file mode 100644 index 00000000000..f9626ee1a8a --- /dev/null +++ b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.spec.ts @@ -0,0 +1,23 @@ +import { TspAccessTokenLoggableError } from './tsp-access-token.loggable-error'; + +describe(TspAccessTokenLoggableError.name, () => { + describe('getLogMessage is called', () => { + const setup = () => { + const expected = { + type: 'TSP_ACCESS_TOKEN_ERROR', + message: 'The TSP access token could not be generated during the sync', + stack: expect.any(String), + }; + + return { expected }; + }; + + it('should return a log message', () => { + const { expected } = setup(); + + const loggable = new TspAccessTokenLoggableError(); + + expect(loggable.getLogMessage()).toEqual(expected); + }); + }); +}); diff --git a/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts new file mode 100644 index 00000000000..a34b7e2d87e --- /dev/null +++ b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts @@ -0,0 +1,26 @@ +import { Loggable, LogMessage } from '@core/logger'; +import { HttpStatus } from '@nestjs/common'; +import { BusinessError, ErrorLogMessage } from '@shared/common'; + +export class TspAccessTokenLoggableError extends BusinessError implements Loggable { + constructor() { + super( + { + type: 'TSP_ACCESS_TOKEN_ERROR', + title: 'The TSP access token could not be generated', + defaultMessage: 'The TSP access token could not be generated during the sync', + }, + HttpStatus.BAD_REQUEST + ); + } + + public getLogMessage(): LogMessage | ErrorLogMessage { + const message: LogMessage | ErrorLogMessage = { + message: this.message, + type: this.type, + stack: this.stack, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts index f4f2012bff9..486c3b09ad9 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.spec.ts @@ -413,10 +413,15 @@ describe(TspFetchService.name, () => { return { system }; }; - it('should throw an OauthConfigMissingLoggableException', async () => { + it('should throw an OauthConfigMissingLoggableException into domainErrorHandler', async () => { const { system } = setup(); - await expect(() => sut.fetchTspSchools(system, 1)).rejects.toThrow(OauthConfigMissingLoggableException); + const result = await sut.fetchTspSchools(system, 1); + expect(result).toStrictEqual([]); + + expect(domainErrorHandler.exec).toHaveBeenCalledWith( + new ErrorLoggable(new OauthConfigMissingLoggableException(system.id)) + ); }); }); }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts index 0ada277dde5..492a39b4dd5 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-fetch.service.ts @@ -68,8 +68,9 @@ export class TspFetchService { fetchFunction: (client: ExportApiInterface) => Promise>, defaultValue: T ): Promise { - const client = this.createClient(system); try { + const client = this.createClient(system); + const response = await fetchFunction(client); const { data } = response; diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 53757cfedb6..10c9038a810 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -1,3 +1,4 @@ +import { ErrorModule } from '@core/error'; import { LoggerModule } from '@core/logger'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ConsoleWriterModule } from '@infra/console'; @@ -28,6 +29,7 @@ import { SyncUc } from './uc/sync.uc'; @Module({ imports: [ LoggerModule, + ErrorModule, ConsoleWriterModule, SystemModule, SchoolModule, diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index 4a7dd0de650..94200356013 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -2,6 +2,7 @@ import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { TspAccessTokenLoggableError } from '@infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error'; import { OauthAdapterService } from '@modules/oauth'; import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; @@ -167,7 +168,7 @@ describe('TspClientFactory', () => { it('should log an AxiosErrorLoggable as warning and reject', async () => { const params = setup(); - await expect(() => sut.getAccessToken(params)).rejects.toBeUndefined(); + await expect(() => sut.getAccessToken(params)).rejects.toThrow(TspAccessTokenLoggableError); expect(domainErrorHandler.exec).toHaveBeenCalledWith( new AxiosErrorLoggable(new AxiosError(), 'TSP_OAUTH_ERROR') @@ -197,7 +198,7 @@ describe('TspClientFactory', () => { it('should log an ErrorLoggable as warning and reject', async () => { const params = setup(); - await expect(() => sut.getAccessToken(params)).rejects.toBeUndefined(); + await expect(() => sut.getAccessToken(params)).rejects.toThrow(TspAccessTokenLoggableError); expect(domainErrorHandler.exec).toHaveBeenCalledWith(new ErrorLoggable(new Error())); }); diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.ts index 2bb124da71f..5fd6781b82e 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.ts @@ -1,5 +1,6 @@ import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; +import { TspAccessTokenLoggableError } from '@infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error'; import { OauthAdapterService } from '@modules/oauth'; import { OAuthGrantType } from '@modules/oauth/interface/oauth-grant-type.enum'; import { ClientCredentialsGrantTokenRequest } from '@modules/oauth/service/dto'; @@ -78,7 +79,7 @@ export class TspClientFactory { } else { this.domainErrorHandler.exec(new ErrorLoggable(e)); } - return Promise.reject(); + return Promise.reject(new TspAccessTokenLoggableError()); } } diff --git a/apps/server/src/infra/tsp-client/tsp-client.module.ts b/apps/server/src/infra/tsp-client/tsp-client.module.ts index be59be65e7c..764bc0c7d4b 100644 --- a/apps/server/src/infra/tsp-client/tsp-client.module.ts +++ b/apps/server/src/infra/tsp-client/tsp-client.module.ts @@ -1,11 +1,12 @@ +import { ErrorModule } from '@core/error'; +import { LoggerModule } from '@core/logger'; import { OauthModule } from '@modules/oauth'; import { Module } from '@nestjs/common'; -import { LoggerModule } from '@core/logger'; import { EncryptionModule } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; @Module({ - imports: [LoggerModule, OauthModule, EncryptionModule], + imports: [LoggerModule, OauthModule, EncryptionModule, ErrorModule], providers: [TspClientFactory], exports: [TspClientFactory], }) From 7cf9c1b73faf1b88cb320fd5e359d1deb080091b Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Thu, 23 Jan 2025 10:36:10 +0100 Subject: [PATCH 45/55] Fix after refactoring. --- .../strategy/tsp/loggable/tsp-access-token.loggable-error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts index a34b7e2d87e..03a6edd4172 100644 --- a/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts +++ b/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts @@ -1,6 +1,6 @@ import { Loggable, LogMessage } from '@core/logger'; import { HttpStatus } from '@nestjs/common'; -import { BusinessError, ErrorLogMessage } from '@shared/common'; +import { BusinessError, ErrorLogMessage } from '@shared/common/error'; export class TspAccessTokenLoggableError extends BusinessError implements Loggable { constructor() { From 44d03ea8a699a0106e4a5ffa0bc769dcb0ac4801 Mon Sep 17 00:00:00 2001 From: Simone Radtke Date: Thu, 23 Jan 2025 15:51:49 +0100 Subject: [PATCH 46/55] EW-1083 Refactoring test for tsp sync strategy --- .../strategy/tsp/tsp-sync.strategy.spec.ts | 139 +++++++++++++----- 1 file changed, 102 insertions(+), 37 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts index 25e9b86a7e4..ebd260c4bca 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts @@ -10,7 +10,10 @@ import { RobjExportSchule, } from '@infra/tsp-client'; import { + robjExportKlasseFactory, + robjExportLehrerFactory, robjExportLehrerMigrationFactory, + robjExportSchuelerFactory, robjExportSchuelerMigrationFactory, robjExportSchuleFactory, } from '@infra/tsp-client/testing'; @@ -169,28 +172,54 @@ describe(TspSyncStrategy.name, () => { describe('sync', () => { describe('when sync is called', () => { const setup = () => { - const oauthDataDto = oauthDataDtoFactory.build({ - system: provisioningSystemDtoFactory.build({ - systemId: faker.string.uuid(), - provisioningStrategy: SystemProvisioningStrategy.TSP, - }), - externalUser: externalUserDtoFactory.build({ - externalId: faker.string.uuid(), - roles: [], - }), + const system = systemFactory.build(); + + const systemDto = provisioningSystemDtoFactory.build({ + systemId: system.id, + provisioningStrategy: SystemProvisioningStrategy.TSP, + }); + + const externalUser = externalUserDtoFactory.build({ + externalId: faker.string.uuid(), + roles: [], }); - const tspTeacher = robjExportLehrerMigrationFactory.build(); - const tspStudent = robjExportSchuelerMigrationFactory.build(); + const oauthDataDto = oauthDataDtoFactory.build({ system: systemDto, externalUser }); + + const tspTeacherMigration = robjExportLehrerMigrationFactory.build(); + + const tspStudentMigration = robjExportSchuelerMigrationFactory.build(); + + const school = schoolFactory.build(); + + const tspTeachers = robjExportLehrerFactory.build(); + + const tspStudents = robjExportSchuelerFactory.build(); + + const tspClasses = robjExportKlasseFactory.build(); setupMockServices({ - fetchedStudentMigrations: [tspStudent], - fetchedTeacherMigrations: [tspTeacher], + fetchedClasses: [tspClasses], + fetchedTeachers: [tspTeachers], + fetchedStudents: [tspStudents], + fetchedStudentMigrations: [tspStudentMigration], + fetchedTeacherMigrations: [tspTeacherMigration], mappedOauthDto: [oauthDataDto], + foundSystemSchools: [school], + foundSystem: system, configValues: [1, 10, true, 10, 1, 50], }); - return { oauthDataDto }; + return { + oauthDataDto, + system, + school, + tspTeachers, + tspStudents, + tspClasses, + tspStudentMigration, + tspTeacherMigration, + }; }; it('should find the tsp system', async () => { @@ -198,49 +227,66 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.findTspSystemOrFail).toHaveBeenCalled(); + expect(tspSyncService.findTspSystemOrFail).toHaveBeenCalledTimes(1); }); it('should migrate the legacy data', async () => { - setup(); + const { oauthDataDto } = setup(); await sut.sync(); - expect(tspLegacyMigrationService.prepareLegacySyncDataForNewSync).toHaveBeenCalled(); + expect(tspLegacyMigrationService.prepareLegacySyncDataForNewSync).toHaveBeenCalledTimes(1); + expect(tspLegacyMigrationService.prepareLegacySyncDataForNewSync).toHaveBeenCalledWith( + oauthDataDto.system.systemId + ); }); it('should fetch the schools', async () => { - setup(); + const { system } = setup(); await sut.sync(); - expect(tspFetchService.fetchTspSchools).toHaveBeenCalled(); + expect(tspFetchService.fetchTspSchools).toHaveBeenCalledTimes(1); + expect(tspFetchService.fetchTspSchools).toHaveBeenCalledWith(system, 1); }); it('should fetch the data', async () => { - setup(); + const { system } = setup(); await sut.sync(); - expect(tspFetchService.fetchTspTeachers).toHaveBeenCalled(); - expect(tspFetchService.fetchTspStudents).toHaveBeenCalled(); - expect(tspFetchService.fetchTspClasses).toHaveBeenCalled(); + expect(tspFetchService.fetchTspTeachers).toHaveBeenCalledTimes(1); + expect(tspFetchService.fetchTspTeachers).toHaveBeenCalledWith(system, 10); + + expect(tspFetchService.fetchTspStudents).toHaveBeenCalledTimes(1); + expect(tspFetchService.fetchTspStudents).toHaveBeenCalledWith(system, 10); + + expect(tspFetchService.fetchTspClasses).toHaveBeenCalledTimes(1); + expect(tspFetchService.fetchTspClasses).toHaveBeenCalledWith(system, 10); }); it('should load all schools', async () => { - setup(); + const { system } = setup(); await sut.sync(); - expect(tspSyncService.findAllSchoolsForSystem).toHaveBeenCalled(); + expect(tspSyncService.findAllSchoolsForSystem).toHaveBeenCalledTimes(1); + expect(tspSyncService.findAllSchoolsForSystem).toHaveBeenCalledWith(system); }); it('should map to OauthDataDto', async () => { - setup(); + const { system, school, tspTeachers, tspStudents, tspClasses } = setup(); await sut.sync(); - expect(tspOauthDataMapper.mapTspDataToOauthData).toHaveBeenCalled(); + expect(tspOauthDataMapper.mapTspDataToOauthData).toHaveBeenCalledTimes(1); + expect(tspOauthDataMapper.mapTspDataToOauthData).toHaveBeenCalledWith( + system, + [school], + [tspTeachers], + [tspStudents], + [tspClasses] + ); }); it('should call provisioning service with mapped OauthDataDtos', async () => { @@ -248,53 +294,69 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); + expect(provisioningService.provisionData).toHaveBeenCalledTimes(1); expect(provisioningService.provisionData).toHaveBeenCalledWith(oauthDataDto); }); describe('when feature tsp migration is enabled', () => { it('should fetch teacher migrations', async () => { - setup(); + const { system } = setup(); await sut.sync(); - expect(tspFetchService.fetchTspTeacherMigrations).toHaveBeenCalled(); + expect(tspFetchService.fetchTspTeacherMigrations).toHaveBeenCalledTimes(1); + expect(tspFetchService.fetchTspTeacherMigrations).toHaveBeenCalledWith(system); }); it('should fetch student migrations', async () => { - setup(); + const { system } = setup(); await sut.sync(); - expect(tspFetchService.fetchTspStudentMigrations).toHaveBeenCalled(); + expect(tspFetchService.fetchTspStudentMigrations).toHaveBeenCalledTimes(1); + expect(tspFetchService.fetchTspStudentMigrations).toHaveBeenCalledWith(system); }); it('should call tspSyncMigrationService', async () => { - setup(); + const { system, tspStudentMigration, tspTeacherMigration } = setup(); await sut.sync(); - expect(tspSyncMigrationService.migrateTspUsers).toHaveBeenCalled(); + expect(tspSyncMigrationService.migrateTspUsers).toHaveBeenCalledTimes(1); + expect(tspSyncMigrationService.migrateTspUsers).toHaveBeenCalledWith( + system, + new Map([ + [tspStudentMigration.schuelerUidAlt, tspStudentMigration.schuelerUidNeu], + [tspTeacherMigration.lehrerUidAlt, tspTeacherMigration.lehrerUidNeu], + ]) + ); }); }); }); describe('when school does not exist', () => { const setup = () => { + const system = systemFactory.build(); + const tspSchool = robjExportSchuleFactory.build(); const tspSchools = [tspSchool]; setupMockServices({ fetchedSchools: tspSchools, + foundSystem: system, configValues: [1, 10, true, 10, 1, 50], }); + + return { system, tspSchool }; }; it('should create the school', async () => { - setup(); + const { system, tspSchool } = setup(); await sut.sync(); - expect(tspSyncService.createSchool).toHaveBeenCalled(); + expect(tspSyncService.createSchool).toHaveBeenCalledTimes(1); + expect(tspSyncService.createSchool).toHaveBeenCalledWith(system, tspSchool.schuleNummer, tspSchool.schuleName); }); }); @@ -309,14 +371,17 @@ describe(TspSyncStrategy.name, () => { foundSchool: school, configValues: [1, 10, true, 10, 1, 50], }); + + return { school, tspSchool }; }; it('should update the school', async () => { - setup(); + const { school, tspSchool } = setup(); await sut.sync(); - expect(tspSyncService.updateSchool).toHaveBeenCalled(); + expect(tspSyncService.updateSchool).toHaveBeenCalledTimes(1); + expect(tspSyncService.updateSchool).toHaveBeenCalledWith(school, tspSchool.schuleName); }); }); From 7caf6194965348b25e2e407de48feeb4df795154 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Fri, 24 Jan 2025 09:41:01 +0100 Subject: [PATCH 47/55] Use typeguard in oauth data mapper. --- .../tsp/tsp-legacy-migration.service.ts | 4 +- .../strategy/tsp/tsp-oauth-data.mapper.ts | 66 ++++++++++--------- .../testing/external-school-dto.factory.ts | 13 ++-- .../src/shared/common/guards/type.guard.ts | 16 ++++- 4 files changed, 58 insertions(+), 41 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts index f5d95b81bed..a238d514f80 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-legacy-migration.service.ts @@ -69,7 +69,7 @@ export class TspLegacyMigrationService { this.logger.info(new TspLegacySchoolMigrationSuccessLoggable(schoolIds.length, successfulMigrations)); } - private async findLegacySystemId() { + private async findLegacySystemId(): Promise { const tspLegacySystem = await this.em.getCollection(SYSTEMS_COLLECTION).findOne({ type: TSP_LEGACY_SYSTEM_TYPE, }); @@ -77,7 +77,7 @@ export class TspLegacyMigrationService { return tspLegacySystem?._id; } - private async findIdsOfLegacyTspSchools(legacySystemId: ObjectId) { + private async findIdsOfLegacyTspSchools(legacySystemId: ObjectId): Promise { const schools = await this.em .getCollection(SCHOOLS_COLLECTION) .find({ diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts index 8f4a3e52441..8f4fdb12a3a 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-oauth-data.mapper.ts @@ -11,6 +11,7 @@ import { BadDataLoggableException } from '@modules/provisioning/loggable'; import { School } from '@modules/school'; import { System } from '@modules/system'; import { Injectable } from '@nestjs/common'; +import { TypeGuard } from '@shared/common/guards'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { TspMissingExternalIdLoggable } from './loggable/tsp-missing-external-id.loggable'; @@ -49,9 +50,11 @@ export class TspOauthDataMapper { const externalSchools = new Map(); schools.forEach((school) => { - if (!school.externalId) { - throw new BadDataLoggableException(`School ${school.id} has no externalId`); - } + TypeGuard.requireKeys( + school, + ['externalId'], + new BadDataLoggableException(`School ${school.id} has no externalId`) + ); externalSchools.set( school.externalId, @@ -71,25 +74,24 @@ export class TspOauthDataMapper { const externalClasses = new Map(); const teacherForClasses = new Map>(); - tspClasses.forEach((tspClass) => { - if (!tspClass.klasseId) { - this.logger.info(new TspMissingExternalIdLoggable('class')); - return; - } + tspClasses + .filter((tspClass) => this.ensureExternalId(tspClass.klasseId, 'class')) + .forEach((tspClass) => { + TypeGuard.requireKeys(tspClass, ['klasseId']); - const externalClass = new ExternalClassDto({ - externalId: tspClass.klasseId, - name: tspClass.klasseName, - }); + const externalClass = new ExternalClassDto({ + externalId: tspClass.klasseId, + name: tspClass.klasseName, + }); - externalClasses.set(tspClass.klasseId, externalClass); + externalClasses.set(tspClass.klasseId, externalClass); - if (tspClass.lehrerUid) { - const classSet = teacherForClasses.get(tspClass.lehrerUid) ?? []; - classSet.push(tspClass.klasseId); - teacherForClasses.set(tspClass.lehrerUid, classSet); - } - }); + if (tspClass.lehrerUid) { + const classSet = teacherForClasses.get(tspClass.lehrerUid) ?? []; + classSet.push(tspClass.klasseId); + teacherForClasses.set(tspClass.lehrerUid, classSet); + } + }); return { externalClasses, teacherForClasses }; } @@ -102,11 +104,9 @@ export class TspOauthDataMapper { teacherForClasses: Map> ): OauthDataDto[] { const oauthDataDtos = tspTeachers + .filter((tspTeacher) => this.ensureExternalId(tspTeacher.lehrerUid, 'teacher')) .map((tspTeacher) => { - if (!tspTeacher.lehrerUid) { - this.logger.info(new TspMissingExternalIdLoggable('teacher')); - return null; - } + TypeGuard.requireKeys(tspTeacher, ['lehrerUid']); const externalUser = new ExternalUserDto({ externalId: tspTeacher.lehrerUid, @@ -131,8 +131,7 @@ export class TspOauthDataMapper { }); return oauthDataDto; - }) - .filter((oauthDataDto) => oauthDataDto !== null); + }); return oauthDataDtos; } @@ -144,11 +143,9 @@ export class TspOauthDataMapper { externalClasses: Map ): OauthDataDto[] { const oauthDataDtos = tspStudents + .filter((tspStudent) => this.ensureExternalId(tspStudent.schuelerUid, 'student')) .map((tspStudent) => { - if (!tspStudent.schuelerUid) { - this.logger.info(new TspMissingExternalIdLoggable('student')); - return null; - } + TypeGuard.requireKeys(tspStudent, ['schuelerUid']); const externalUser = new ExternalUserDto({ externalId: tspStudent.schuelerUid, @@ -170,9 +167,16 @@ export class TspOauthDataMapper { }); return oauthDataDto; - }) - .filter((oauthDataDto) => oauthDataDto !== null); + }); return oauthDataDtos; } + + private ensureExternalId(externalId: string | undefined, type: string): boolean { + if (!externalId) { + this.logger.info(new TspMissingExternalIdLoggable(type)); + return false; + } + return true; + } } diff --git a/apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts b/apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts index f7165741a28..87c3a8cae22 100644 --- a/apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts +++ b/apps/server/src/modules/provisioning/testing/external-school-dto.factory.ts @@ -2,9 +2,10 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Factory } from 'fishery'; import { ExternalSchoolDto } from '../dto'; -export const externalSchoolDtoFactory = Factory.define(({ sequence }) => { - return { - externalId: new ObjectId().toHexString(), - name: `External School ${sequence}`, - }; -}); +export const externalSchoolDtoFactory = Factory.define( + ({ sequence }) => + new ExternalSchoolDto({ + externalId: new ObjectId().toHexString(), + name: `External School ${sequence}`, + }) +); diff --git a/apps/server/src/shared/common/guards/type.guard.ts b/apps/server/src/shared/common/guards/type.guard.ts index 342fc38c534..3a88f13b063 100644 --- a/apps/server/src/shared/common/guards/type.guard.ts +++ b/apps/server/src/shared/common/guards/type.guard.ts @@ -1,4 +1,4 @@ -type EnsureKeysAreSet = T & { [P in K]-?: T[P] }; +type EnsureKeysAreSet = T & { [P in K]-?: NonNullable }; export class TypeGuard { public static isError(value: unknown): value is Error { @@ -180,13 +180,25 @@ export class TypeGuard { contextInfo = '' ): EnsureKeysAreSet { for (const key of keys) { - if (!(key in obj) || obj[key] === undefined) { + if (!(key in obj) || obj[key] === undefined || obj[key] === null) { throw new Error(`Object lacks this property: ${String(key)}. ${contextInfo}`); } } return obj as EnsureKeysAreSet; } + public static requireKeys( + obj: T, + keys: K[], + toThrow?: Error + ): asserts obj is EnsureKeysAreSet { + for (const key of keys) { + if (!(key in obj) || obj[key] === undefined || obj[key] === null) { + throw toThrow || new Error(`Object lacks this property: ${String(key)}.`); + } + } + } + public static checkNotNullOrUndefined(value: T | null | undefined, toThrow?: Error): T { if (TypeGuard.isNull(value)) { throw toThrow || new Error('Type is null.'); From f2296c9f8509be770fab2c24ecb309947d73dc69 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Fri, 24 Jan 2025 11:27:14 +0100 Subject: [PATCH 48/55] Split TspSyncService and rename to TspSchoolService. --- ...ice.spec.ts => tsp-school.service.spec.ts} | 51 ++-------------- ...-sync.service.ts => tsp-school.service.ts} | 21 +------ .../strategy/tsp/tsp-sync.strategy.spec.ts | 58 +++++++++++++++---- .../sync/strategy/tsp/tsp-sync.strategy.ts | 27 +++++++-- apps/server/src/infra/sync/sync.module.ts | 4 +- 5 files changed, 78 insertions(+), 83 deletions(-) rename apps/server/src/infra/sync/strategy/tsp/{tsp-sync.service.spec.ts => tsp-school.service.spec.ts} (84%) rename apps/server/src/infra/sync/strategy/tsp/{tsp-sync.service.ts => tsp-school.service.ts} (80%) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-school.service.spec.ts similarity index 84% rename from apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-school.service.spec.ts index 355a8e3a778..58821b915fd 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-school.service.spec.ts @@ -5,18 +5,15 @@ import { School, SchoolService } from '@modules/school'; import { FileStorageType, SchoolProps } from '@modules/school/domain'; import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@modules/school/repo/mikro-orm/mapper'; import { schoolFactory } from '@modules/school/testing'; -import { SystemService, SystemType } from '@modules/system'; import { systemFactory } from '@modules/system/testing'; import { Test, TestingModule } from '@nestjs/testing'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { federalStateFactory } from '@testing/factory/federal-state.factory'; import { schoolYearFactory } from '@testing/factory/schoolyear.factory'; -import { TspSyncService } from './tsp-sync.service'; +import { TspSchoolService } from './tsp-school.service'; -describe(TspSyncService.name, () => { +describe(TspSchoolService.name, () => { let module: TestingModule; - let sut: TspSyncService; - let systemService: DeepMocked; + let sut: TspSchoolService; let schoolService: DeepMocked; let federalStateService: DeepMocked; let schoolYearService: DeepMocked; @@ -24,11 +21,7 @@ describe(TspSyncService.name, () => { beforeAll(async () => { module = await Test.createTestingModule({ providers: [ - TspSyncService, - { - provide: SystemService, - useValue: createMock(), - }, + TspSchoolService, { provide: SchoolService, useValue: createMock(), @@ -44,8 +37,7 @@ describe(TspSyncService.name, () => { ], }).compile(); - sut = module.get(TspSyncService); - systemService = module.get(SystemService); + sut = module.get(TspSchoolService); schoolService = module.get(SchoolService); federalStateService = module.get(FederalStateService); schoolYearService = module.get(SchoolYearService); @@ -65,39 +57,6 @@ describe(TspSyncService.name, () => { }); }); - describe('findTspSystemOrFail', () => { - describe('when tsp system is found', () => { - const setup = () => { - const system = systemFactory.build({ - type: SystemType.OAUTH, - provisioningStrategy: SystemProvisioningStrategy.TSP, - }); - - systemService.find.mockResolvedValueOnce([system]); - }; - - it('should be returned', async () => { - setup(); - - const system = await sut.findTspSystemOrFail(); - - expect(system).toBeDefined(); - }); - }); - - describe('when tsp system is not found', () => { - const setup = () => { - systemService.find.mockResolvedValueOnce([]); - }; - - it('should throw a TspSystemNotFound exception', async () => { - setup(); - - await expect(sut.findTspSystemOrFail()).rejects.toThrow(); - }); - }); - }); - describe('findSchool', () => { describe('when school is found', () => { const setup = () => { diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-school.service.ts similarity index 80% rename from apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts rename to apps/server/src/infra/sync/strategy/tsp/tsp-school.service.ts index cf874af5dc8..ae3067c9593 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-school.service.ts @@ -5,38 +5,21 @@ import { FederalState, FileStorageType } from '@modules/school/domain'; import { SchoolFactory } from '@modules/school/domain/factory'; import { SchoolPermissions } from '@modules/school/domain/type'; import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@modules/school/repo/mikro-orm/mapper'; -import { System, SystemService, SystemType } from '@modules/system'; +import { System } from '@modules/system'; import { Injectable } from '@nestjs/common'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { SchoolFeature } from '@shared/domain/types'; import { ObjectId } from 'bson'; -import { TspSystemNotFoundLoggableException } from './loggable'; @Injectable() -export class TspSyncService { +export class TspSchoolService { private federalState: FederalState | undefined; constructor( - private readonly systemService: SystemService, private readonly schoolService: SchoolService, private readonly federalStateService: FederalStateService, private readonly schoolYearService: SchoolYearService ) {} - public async findTspSystemOrFail(): Promise { - const systems = ( - await this.systemService.find({ - types: [SystemType.OAUTH, SystemType.OIDC], - }) - ).filter((system) => system.provisioningStrategy === SystemProvisioningStrategy.TSP); - - if (systems.length === 0) { - throw new TspSystemNotFoundLoggableException(); - } - - return systems[0]; - } - public async findSchool(system: System, identifier: string): Promise { const schools = await this.schoolService.getSchools({ externalId: identifier, diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts index ebd260c4bca..d7d1a6a6dd2 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.spec.ts @@ -26,8 +26,8 @@ import { } from '@modules/provisioning/testing'; import { School } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; -import { System } from '@modules/system'; -import { systemFactory } from '@modules/system/testing'; +import { System, SystemService, SystemType } from '@modules/system'; +import { systemFactory, systemOauthConfigFactory } from '@modules/system/testing'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; @@ -38,27 +38,28 @@ import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncMigrationService } from './tsp-sync-migration.service'; import { TspSyncConfig } from './tsp-sync.config'; -import { TspSyncService } from './tsp-sync.service'; +import { TspSchoolService } from './tsp-school.service'; import { TspSyncStrategy } from './tsp-sync.strategy'; describe(TspSyncStrategy.name, () => { let module: TestingModule; let sut: TspSyncStrategy; - let tspSyncService: DeepMocked; + let tspSyncService: DeepMocked; let tspFetchService: DeepMocked; let provisioningService: DeepMocked; let tspOauthDataMapper: DeepMocked; let tspLegacyMigrationService: DeepMocked; let tspSyncMigrationService: DeepMocked; let configService: DeepMocked>; + let systemService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ TspSyncStrategy, { - provide: TspSyncService, - useValue: createMock(), + provide: TspSchoolService, + useValue: createMock(), }, { provide: TspFetchService, @@ -88,17 +89,22 @@ describe(TspSyncStrategy.name, () => { provide: TspSyncMigrationService, useValue: createMock(), }, + { + provide: SystemService, + useValue: createMock(), + }, ], }).compile(); sut = module.get(TspSyncStrategy); - tspSyncService = module.get(TspSyncService); + tspSyncService = module.get(TspSchoolService); tspFetchService = module.get(TspFetchService); provisioningService = module.get(ProvisioningService); tspOauthDataMapper = module.get(TspOauthDataMapper); tspLegacyMigrationService = module.get(TspLegacyMigrationService); tspSyncMigrationService = module.get(TspSyncMigrationService); configService = module.get(ConfigService); + systemService = module.get(SystemService); }); afterEach(() => { @@ -154,7 +160,7 @@ describe(TspSyncStrategy.name, () => { tspSyncService.findSchool.mockResolvedValueOnce(params.foundSchool ?? undefined); tspSyncService.findAllSchoolsForSystem.mockResolvedValueOnce(params.foundSystemSchools ?? []); - tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(params.foundSystem ?? systemFactory.build()); + systemService.find.mockResolvedValueOnce(params.foundSystem ? [params.foundSystem] : []); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce(params.mappedOauthDto ?? []); @@ -172,7 +178,11 @@ describe(TspSyncStrategy.name, () => { describe('sync', () => { describe('when sync is called', () => { const setup = () => { - const system = systemFactory.build(); + const system = systemFactory.build({ + type: SystemType.OIDC, + provisioningStrategy: SystemProvisioningStrategy.TSP, + oauthConfig: systemOauthConfigFactory.build(), + }); const systemDto = provisioningSystemDtoFactory.build({ systemId: system.id, @@ -227,7 +237,7 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.findTspSystemOrFail).toHaveBeenCalledTimes(1); + expect(systemService.find).toHaveBeenCalledTimes(1); }); it('should migrate the legacy data', async () => { @@ -334,9 +344,25 @@ describe(TspSyncStrategy.name, () => { }); }); + describe('when tsp system is not found', () => { + const setup = () => { + systemService.find.mockResolvedValueOnce([]); + }; + + it('should throw a TspSystemNotFound exception', async () => { + setup(); + + await expect(sut.sync()).rejects.toThrow(); + }); + }); + describe('when school does not exist', () => { const setup = () => { - const system = systemFactory.build(); + const system = systemFactory.build({ + type: SystemType.OIDC, + provisioningStrategy: SystemProvisioningStrategy.TSP, + oauthConfig: systemOauthConfigFactory.build(), + }); const tspSchool = robjExportSchuleFactory.build(); const tspSchools = [tspSchool]; @@ -367,6 +393,11 @@ describe(TspSyncStrategy.name, () => { const school = schoolFactory.build(); setupMockServices({ + foundSystem: systemFactory.build({ + type: SystemType.OIDC, + provisioningStrategy: SystemProvisioningStrategy.TSP, + oauthConfig: systemOauthConfigFactory.build(), + }), fetchedSchools: tspSchools, foundSchool: school, configValues: [1, 10, true, 10, 1, 50], @@ -392,6 +423,11 @@ describe(TspSyncStrategy.name, () => { const tspSchools = [tspSchool]; setupMockServices({ + foundSystem: systemFactory.build({ + type: SystemType.OIDC, + provisioningStrategy: SystemProvisioningStrategy.TSP, + oauthConfig: systemOauthConfigFactory.build(), + }), fetchedSchools: tspSchools, configValues: [1, 10, true, 10, 1, 50], }); diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts index ecf61f219e6..3afb4f81a9c 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts @@ -2,12 +2,14 @@ import { Logger } from '@core/logger'; import { RobjExportKlasse, RobjExportLehrer, RobjExportSchueler } from '@infra/tsp-client'; import { ProvisioningService } from '@modules/provisioning'; import { School } from '@modules/school'; -import { System } from '@modules/system'; +import { System, SystemService, SystemType } from '@modules/system'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import pLimit from 'p-limit'; import { SyncStrategyTarget } from '../../sync-strategy.types'; import { SyncStrategy } from '../sync-strategy'; +import { TspSystemNotFoundLoggableException } from './loggable'; import { TspDataFetchedLoggable } from './loggable/tsp-data-fetched.loggable'; import { TspSchoolsFetchedLoggable } from './loggable/tsp-schools-fetched.loggable'; import { TspSchoolsSyncedLoggable } from './loggable/tsp-schools-synced.loggable'; @@ -20,7 +22,7 @@ import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncMigrationService } from './tsp-sync-migration.service'; import { TspSyncConfig } from './tsp-sync.config'; -import { TspSyncService } from './tsp-sync.service'; +import { TspSchoolService } from './tsp-school.service'; type TspSchoolData = { tspTeachers: RobjExportLehrer[]; @@ -32,13 +34,14 @@ type TspSchoolData = { export class TspSyncStrategy extends SyncStrategy { constructor( private readonly logger: Logger, - private readonly tspSyncService: TspSyncService, + private readonly tspSyncService: TspSchoolService, private readonly tspFetchService: TspFetchService, private readonly tspOauthDataMapper: TspOauthDataMapper, private readonly tspLegacyMigrationService: TspLegacyMigrationService, private readonly configService: ConfigService, private readonly provisioningService: ProvisioningService, - private readonly tspSyncMigrationService: TspSyncMigrationService + private readonly tspSyncMigrationService: TspSyncMigrationService, + private readonly systemService: SystemService ) { super(); this.logger.setContext(TspSyncStrategy.name); @@ -50,7 +53,7 @@ export class TspSyncStrategy extends SyncStrategy { public async sync(): Promise { // Please keep the order of this steps/methods as each relies on the data processed in the ones before. - const system = await this.tspSyncService.findTspSystemOrFail(); + const system = await this.findTspSystemOrFail(); await this.tspLegacyMigrationService.prepareLegacySyncDataForNewSync(system.id); @@ -171,4 +174,18 @@ export class TspSyncStrategy extends SyncStrategy { ) ); } + + private async findTspSystemOrFail(): Promise { + const systems = ( + await this.systemService.find({ + types: [SystemType.OAUTH, SystemType.OIDC], + }) + ).filter((system) => system.provisioningStrategy === SystemProvisioningStrategy.TSP); + + if (systems.length === 0) { + throw new TspSystemNotFoundLoggableException(); + } + + return systems[0]; + } } diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 5938a2ffcb9..4350d121119 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -23,7 +23,7 @@ import { TspFetchService } from './strategy/tsp/tsp-fetch.service'; import { TspLegacyMigrationService } from './strategy/tsp/tsp-legacy-migration.service'; import { TspOauthDataMapper } from './strategy/tsp/tsp-oauth-data.mapper'; import { TspSyncMigrationService } from './strategy/tsp/tsp-sync-migration.service'; -import { TspSyncService } from './strategy/tsp/tsp-sync.service'; +import { TspSchoolService } from './strategy/tsp/tsp-school.service'; import { TspSyncStrategy } from './strategy/tsp/tsp-sync.strategy'; import { SyncUc } from './uc/sync.uc'; @@ -58,7 +58,7 @@ import { SyncUc } from './uc/sync.uc'; ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) ? [ TspSyncStrategy, - TspSyncService, + TspSchoolService, TspOauthDataMapper, TspFetchService, TspLegacyMigrationService, From 5e821d0a5656bf46ff41a21e8d9ed8b2061a6418 Mon Sep 17 00:00:00 2001 From: Simone Radtke Date: Fri, 24 Jan 2025 14:51:48 +0100 Subject: [PATCH 49/55] EW-1083 Add typeguard to tsp provisioning service --- .../service/tsp-provisioning.service.ts | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts index 0dadf3acbdd..813b4ec1059 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts @@ -4,6 +4,7 @@ import { RoleService } from '@modules/role'; import { School, SchoolService } from '@modules/school'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; +import { TypeGuard } from '@shared/common/guards'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { RoleReference, UserDO } from '@shared/domain/domainobject'; import { Consent } from '@shared/domain/domainobject/consent'; @@ -59,11 +60,11 @@ export class TspProvisioningService { } private async updateClass(currentClass: Class, clazz: ExternalClassDto, school: School, user: UserDO): Promise { - if (!user.id) { - throw new BadDataLoggableException('User ID is missing', { - externalId: user.externalId, - }); - } + TypeGuard.requireKeys( + user, + ['id'], + new BadDataLoggableException('User ID is missing', { externalId: user.externalId }) + ); currentClass.schoolId = school.id; currentClass.name = clazz.name ?? currentClass.name; @@ -82,11 +83,11 @@ export class TspProvisioningService { } private async createClass(clazz: ExternalClassDto, school: School, user: UserDO): Promise { - if (!user.id) { - throw new BadDataLoggableException('User ID is missing', { - externalId: user.externalId, - }); - } + TypeGuard.requireKeys( + user, + ['id'], + new BadDataLoggableException('User ID is missing', { externalId: user.externalId }) + ); const newClass = ClassFactory.create({ name: clazz.name, @@ -102,11 +103,11 @@ export class TspProvisioningService { } public async provisionUser(data: OauthDataDto, school: School): Promise { - if (!data.externalSchool) { - throw new BadDataLoggableException('External school is missing for user', { - externalId: data.externalUser.externalId, - }); - } + TypeGuard.requireKeys( + data, + ['externalSchool'], + new BadDataLoggableException('External school is missing for user', { externalId: data.externalUser.externalId }) + ); const existingUser = await this.userService.findByExternalId(data.externalUser.externalId, data.system.systemId); const roleRefs = await this.getRoleReferencesForUser(data.externalUser); @@ -145,11 +146,11 @@ export class TspProvisioningService { roleRefs: RoleReference[], schoolId: string ): Promise { - if (!externalUser.firstName || !externalUser.lastName) { - throw new BadDataLoggableException('User firstname or lastname is missing. TspUid:', { - externalId: externalUser.externalId, - }); - } + TypeGuard.requireKeys( + externalUser, + ['firstName', 'lastName'], + new BadDataLoggableException('User firstname or lastname is missing', { externalId: externalUser.externalId }) + ); const newUser = new UserDO({ roles: roleRefs, @@ -170,10 +171,11 @@ export class TspProvisioningService { } private async createOrUpdateAccount(systemId: string, user: UserDO): Promise { - if (!user.id) - throw new BadDataLoggableException('user ID is missing', { - externalId: user.externalId, - }); + TypeGuard.requireKeys( + user, + ['id'], + new BadDataLoggableException('User ID is missing', { externalId: user.externalId }) + ); const account = await this.accountService.findByUserId(user.id); From 407e2256f639655753d3fe725d64d393ce355089 Mon Sep 17 00:00:00 2001 From: Simone Radtke Date: Wed, 29 Jan 2025 10:36:55 +0100 Subject: [PATCH 50/55] EW-1083 Adjust readme --- apps/server/src/infra/sync/console/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/server/src/infra/sync/console/README.md b/apps/server/src/infra/sync/console/README.md index 8141b6b0610..f297a6862cc 100644 --- a/apps/server/src/infra/sync/console/README.md +++ b/apps/server/src/infra/sync/console/README.md @@ -1,19 +1,23 @@ # Sync console + This is a console application that allows you to start the synchronization process for different sources. ## Usage + To start the synchronization process, run the following command: + ```bash npm run nest:start:console sync run ``` Where `` is the name of the system you want to start the synchronization for. The currently available systems are: + - `tsp` - Synchronize Thüringer schulportal. - `vidis` - Synchronize Vidis Activation Data. - `media_metadata` - Synchronize media metadata in ctl tools in SVS with data from its media source. - If the target is not provided, the synchronization will not start and the available targets will be displayed in an error message. + ```bash { message: 'Either synchronization is not activated or the target entered is invalid', @@ -22,4 +26,5 @@ If the target is not provided, the synchronization will not start and the availa ``` ## TSP synchronization -The TSP synchronization is controlled with a feature flag `FEATURE_TSP_SYNC_ENABLED`. This is now set to `false`. \ No newline at end of file + +The TSP synchronization is controlled with a feature flag `FEATURE_TSP_SYNC_ENABLED`. This is now set to `false`. From 042911eb7c1b2cfe25a4acfa9464cb4db20c0135 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 29 Jan 2025 10:38:54 +0100 Subject: [PATCH 51/55] Fix off by one error in batch logging. --- apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts index 5ae3701b9dd..dcf46638616 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts @@ -139,7 +139,7 @@ export class TspSyncStrategy extends SyncStrategy { const batchPromises = batches.map((batch, index) => batchLimit(async () => { const processed = await this.provisioningService.provisionBatch(batch); - this.logger.info(new TspDataSyncBatchFinishedLoggable(processed, batchSize, batchCount, index)); + this.logger.info(new TspDataSyncBatchFinishedLoggable(processed, batchSize, batchCount, index + 1)); return processed; }) ); From 8e04b67fe1323ee2f7d2dd909b5f96403ef4bcb0 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 29 Jan 2025 10:49:13 +0100 Subject: [PATCH 52/55] use typeguard and collapse object literals. --- .../service/tsp-provisioning.service.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts index 12f4bb864c2..73eda368bb5 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts @@ -94,16 +94,10 @@ export class TspProvisioningService { } public async findSchoolOrFail(system: ProvisioningSystemDto, school: ExternalSchoolDto): Promise { - const schools = await this.schoolService.getSchools({ - systemId: system.systemId, - externalId: school.externalId, - }); + const schools = await this.schoolService.getSchools({ systemId: system.systemId, externalId: school.externalId }); if (schools.length !== 1) { - throw new NotFoundLoggableException(School.name, { - systemId: system.systemId, - externalId: school.externalId, - }); + throw new NotFoundLoggableException(School.name, { systemId: system.systemId, externalId: school.externalId }); } return schools[0]; @@ -178,9 +172,7 @@ export class TspProvisioningService { const user = this.createOrUpdateUser(data.externalUser, roleRefs, school.id, existingUser); if (!user) { - throw new BadDataLoggableException(`Couldn't process user`, { - externalId: data.externalUser.externalId, - }); + throw new BadDataLoggableException(`Couldn't process user`, { externalId: data.externalUser.externalId }); } const savedUser = await this.userService.save(user); @@ -229,11 +221,11 @@ export class TspProvisioningService { } private createOrUpdateAccount(systemId: string, user: UserDO, account: Account | null): AccountSave { - if (!user.id) { - throw new BadDataLoggableException('user ID is missing', { - externalId: user.externalId, - }); - } + TypeGuard.requireKeys( + user, + ['id'], + new BadDataLoggableException('user ID is missing', { externalId: user.externalId }) + ); if (account) { const updated = new AccountSave({ From 58eada068de56a2e936db7fe316162a5e8c89a5b Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 29 Jan 2025 13:42:12 +0100 Subject: [PATCH 53/55] Sync strategy refactoring. --- .../infra/sync/strategy/tsp/tsp-sync.strategy.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts index dcf46638616..d6f2ac1f62a 100644 --- a/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/strategy/tsp/tsp-sync.strategy.ts @@ -126,7 +126,13 @@ export class TspSyncStrategy extends SyncStrategy { this.logger.info(new TspSyncingUsersLoggable(oauthDataDtos.length)); const batchSize = this.configService.getOrThrow('TSP_SYNC_DATA_LIMIT'); + const batches = this.createOauthDataBatches(batchSize, oauthDataDtos); + const total = await this.runSyncOfOauthDataBatches(batchSize, batches); + this.logger.info(new TspSyncedUsersLoggable(total)); + } + + private createOauthDataBatches(batchSize: number, oauthDataDtos: OauthDataDto[]): OauthDataDto[][] { const batchCount = Math.ceil(oauthDataDtos.length / batchSize); const batches: OauthDataDto[][] = []; for (let i = 0; i < batchCount; i += 1) { @@ -134,12 +140,15 @@ export class TspSyncStrategy extends SyncStrategy { const end = Math.min((i + 1) * batchSize, oauthDataDtos.length); batches.push(oauthDataDtos.slice(start, end)); } + return batches; + } + private async runSyncOfOauthDataBatches(batchSize: number, batches: OauthDataDto[][]): Promise { const batchLimit = pLimit(1); const batchPromises = batches.map((batch, index) => batchLimit(async () => { const processed = await this.provisioningService.provisionBatch(batch); - this.logger.info(new TspDataSyncBatchFinishedLoggable(processed, batchSize, batchCount, index + 1)); + this.logger.info(new TspDataSyncBatchFinishedLoggable(processed, batchSize, batches.length, index + 1)); return processed; }) ); @@ -147,7 +156,7 @@ export class TspSyncStrategy extends SyncStrategy { const results = await Promise.all(batchPromises); const total = results.reduce((previousValue, currentValue) => previousValue + currentValue, 0); - this.logger.info(new TspSyncedUsersLoggable(total)); + return total; } private async fetchSchoolData(system: System): Promise { From c81bd471ed824c2c95f1dd261d7c35a41ae41f29 Mon Sep 17 00:00:00 2001 From: Simone Radtke Date: Mon, 3 Feb 2025 13:29:54 +0100 Subject: [PATCH 54/55] EW-1083 Working on DP review comments --- .../modules/provisioning/service/provisioning.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index bde25a78230..9b9dd6735f1 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -1,14 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { oauthDataDtoFactory, provisioningDtoFactory } from '@modules/provisioning/testing'; -import { provisioningSystemDtoFactory } from '@modules/provisioning/testing/provisioning-system-dto.factory'; import { System, SystemService } from '@modules/system'; import { systemFactory } from '@modules/system/testing'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; +import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; import { TspProvisioningStrategy } from '../strategy/tsp/tsp.strategy'; +import { provisioningSystemDtoFactory } from '../testing/provisioning-system-dto.factory'; import { ProvisioningService } from './provisioning.service'; describe('ProvisioningService', () => { From 0c7153eace2fa03cd9091df296336ee95d40fb36 Mon Sep 17 00:00:00 2001 From: Simone Radtke Date: Mon, 3 Feb 2025 13:49:17 +0100 Subject: [PATCH 55/55] EW-1083 Working on DP review comments --- .../loggable/tsp-access-token.loggable-error.spec.ts | 0 .../loggable/tsp-access-token.loggable-error.ts | 0 apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts | 2 +- apps/server/src/infra/tsp-client/tsp-client-factory.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename apps/server/src/infra/{sync/strategy/tsp => tsp-client}/loggable/tsp-access-token.loggable-error.spec.ts (100%) rename apps/server/src/infra/{sync/strategy/tsp => tsp-client}/loggable/tsp-access-token.loggable-error.ts (100%) diff --git a/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.spec.ts b/apps/server/src/infra/tsp-client/loggable/tsp-access-token.loggable-error.spec.ts similarity index 100% rename from apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.spec.ts rename to apps/server/src/infra/tsp-client/loggable/tsp-access-token.loggable-error.spec.ts diff --git a/apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts b/apps/server/src/infra/tsp-client/loggable/tsp-access-token.loggable-error.ts similarity index 100% rename from apps/server/src/infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error.ts rename to apps/server/src/infra/tsp-client/loggable/tsp-access-token.loggable-error.ts diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index 94200356013..e19f10fbfda 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -2,7 +2,7 @@ import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { TspAccessTokenLoggableError } from '@infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error'; +import { TspAccessTokenLoggableError } from '@infra/tsp-client/loggable/tsp-access-token.loggable-error'; import { OauthAdapterService } from '@modules/oauth'; import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.ts index 5fd6781b82e..9e1a5c6b9be 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.ts @@ -1,6 +1,5 @@ import { DomainErrorHandler } from '@core/error'; import { AxiosErrorLoggable, ErrorLoggable } from '@core/error/loggable'; -import { TspAccessTokenLoggableError } from '@infra/sync/strategy/tsp/loggable/tsp-access-token.loggable-error'; import { OauthAdapterService } from '@modules/oauth'; import { OAuthGrantType } from '@modules/oauth/interface/oauth-grant-type.enum'; import { ClientCredentialsGrantTokenRequest } from '@modules/oauth/service/dto'; @@ -10,6 +9,7 @@ import { AxiosError } from 'axios'; import * as jwt from 'jsonwebtoken'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { Configuration, ExportApiFactory, ExportApiInterface } from './generated'; +import { TspAccessTokenLoggableError } from './loggable/tsp-access-token.loggable-error'; import { TspClientConfig } from './tsp-client-config'; type FactoryParams = {