From d984ac34d51f34d462b9fd3ccb7fa8a755f9ff25 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Fri, 15 Nov 2024 14:41:04 +0100 Subject: [PATCH 1/4] Move fetch into separate service, add error handling, don't log everything for some objects. --- apps/server/src/infra/sync/sync.module.ts | 3 +- .../infra/sync/tsp/tsp-fetch.service.spec.ts | 438 ++++++++++++++++++ .../src/infra/sync/tsp/tsp-fetch.service.ts | 93 ++++ .../infra/sync/tsp/tsp-sync.service.spec.ts | 335 +------------- .../src/infra/sync/tsp/tsp-sync.service.ts | 80 ---- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 103 ++-- .../src/infra/sync/tsp/tsp-sync.strategy.ts | 14 +- .../service/tsp-provisioning.service.ts | 20 +- .../provisioning/strategy/tsp/tsp.strategy.ts | 12 +- 9 files changed, 625 insertions(+), 473 deletions(-) create mode 100644 apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/tsp-fetch.service.ts diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 04e3fbca432..75a9a0d45d4 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -16,6 +16,7 @@ import { TspOauthDataMapper } from './tsp/tsp-oauth-data.mapper'; import { TspSyncService } from './tsp/tsp-sync.service'; import { TspSyncStrategy } from './tsp/tsp-sync.strategy'; import { SyncUc } from './uc/sync.uc'; +import { TspFetchService } from './tsp/tsp-fetch.service'; @Module({ imports: [ @@ -39,7 +40,7 @@ import { SyncUc } from './uc/sync.uc'; SyncUc, SyncService, ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) - ? [TspSyncStrategy, TspSyncService, TspOauthDataMapper] + ? [TspSyncStrategy, TspSyncService, TspOauthDataMapper, TspFetchService] : []), ], exports: [SyncConsole], 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 new file mode 100644 index 00000000000..4c7c76c8ea5 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.spec.ts @@ -0,0 +1,438 @@ +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 { + ExportApiInterface, + RobjExportKlasse, + RobjExportLehrer, + RobjExportLehrerMigration, + RobjExportSchueler, + 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'; +import { TspFetchService } from './tsp-fetch.service'; + +describe(TspFetchService.name, () => { + let module: TestingModule; + let sut: TspFetchService; + let tspClientFactory: DeepMocked; + let logger: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TspFetchService, + { + provide: TspClientFactory, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, + ], + }).compile(); + + sut = module.get(TspFetchService); + tspClientFactory = module.get(TspClientFactory); + logger = module.get(Logger); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('when sync service is initialized', () => { + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + }); + + const setupTspClient = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); + const system = systemFactory.build({ + oauthConfig: { + clientId, + clientSecret, + tokenEndpoint, + }, + }); + + const tspSchool: RobjExportSchule = { + schuleName: faker.string.alpha(), + schuleNummer: faker.string.alpha(), + }; + 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 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 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 classes = [tspClass]; + const responseClasses = createMock>>({ + data: classes, + }); + + const tspTeacherMigration: RobjExportLehrerMigration = { + lehrerUidAlt: faker.string.alpha(), + lehrerUidNeu: faker.string.alpha(), + }; + const teacherMigrations = [tspTeacherMigration]; + const responseTeacherMigrations = createMock>>({ + data: teacherMigrations, + }); + + const tspStudentMigration: RobjExportSchuelerMigration = { + schuelerUidAlt: faker.string.alpha(), + schuelerUidNeu: faker.string.alpha(), + }; + const studentMigrations = [tspStudentMigration]; + const responseStudentMigrations = createMock>>({ + data: studentMigrations, + }); + + const exportApiMock = createMock(); + exportApiMock.exportSchuleList.mockResolvedValueOnce(responseSchools); + exportApiMock.exportLehrerList.mockResolvedValueOnce(responseTeachers); + exportApiMock.exportSchuelerList.mockResolvedValueOnce(responseStudents); + exportApiMock.exportKlasseList.mockResolvedValueOnce(responseClasses); + exportApiMock.exportLehrerListMigration.mockResolvedValueOnce(responseTeacherMigrations); + exportApiMock.exportSchuelerListMigration.mockResolvedValueOnce(responseStudentMigrations); + + tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); + + return { + clientId, + clientSecret, + tokenEndpoint, + system, + exportApiMock, + schools, + teachers, + students, + classes, + teacherMigrations, + studentMigrations, + }; + }; + + describe('fetchTspSchools', () => { + describe('when tsp schools are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspSchools(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportSchuleList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspSchools(system, 1); + + expect(exportApiMock.exportSchuleList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of schools', async () => { + const { system } = setupTspClient(); + + const schools = await sut.fetchTspSchools(system, 1); + + expect(schools).toBeDefined(); + expect(schools).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspTeachers', () => { + describe('when tsp teachers are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspTeachers(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportLehrerList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspTeachers(system, 1); + + expect(exportApiMock.exportLehrerList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of teachers', async () => { + const { system } = setupTspClient(); + + const teachers = await sut.fetchTspTeachers(system, 1); + + expect(teachers).toBeDefined(); + expect(teachers).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspStudents', () => { + describe('when tsp students are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspStudents(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportSchuelerList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspStudents(system, 1); + + expect(exportApiMock.exportSchuelerList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of students', async () => { + const { system } = setupTspClient(); + + const students = await sut.fetchTspStudents(system, 1); + + expect(students).toBeDefined(); + expect(students).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspClasses', () => { + describe('when tsp classes are fetched', () => { + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspClasses(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportKlasseList', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspClasses(system, 1); + + expect(exportApiMock.exportKlasseList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of classes', async () => { + const { system } = setupTspClient(); + + const classes = await sut.fetchTspClasses(system, 1); + + expect(classes).toBeDefined(); + expect(classes).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspTeacherMigrations', () => { + describe('when tsp teacher migrations are fetched', () => { + it('should create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspTeacherMigrations(system); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportLehrerListMigration', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspTeacherMigrations(system); + + expect(exportApiMock.exportLehrerListMigration).toHaveBeenCalledTimes(1); + }); + + it('should return an array of teacher migrations', async () => { + const { system } = setupTspClient(); + + const result = await sut.fetchTspTeacherMigrations(system); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTspStudentMigrations', () => { + describe('when tsp student migrations are fetched', () => { + it('should create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); + + await sut.fetchTspStudentMigrations(system); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportSchuelerListMigration', async () => { + const { system, exportApiMock } = setupTspClient(); + + await sut.fetchTspStudentMigrations(system); + + expect(exportApiMock.exportSchuelerListMigration).toHaveBeenCalledTimes(1); + }); + + it('should return an array of student migrations', async () => { + const { system } = setupTspClient(); + + const result = await sut.fetchTspStudentMigrations(system); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(Array); + }); + }); + }); + + describe('fetchTsp', () => { + describe('when AxiosError is thrown', () => { + const setup = () => { + const system = systemFactory.build({ + oauthConfig: { + clientId: faker.string.alpha(), + clientSecret: faker.string.alpha(), + tokenEndpoint: faker.internet.url(), + }, + }); + + const exportApiMock = createMock(); + exportApiMock.exportSchuleList.mockImplementation(() => { + throw new AxiosError(); + }); + + tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); + + return { + system, + exportApiMock, + }; + }; + + it('should log a AxiosErrorLoggable as warning', async () => { + const { system } = setup(); + + await sut.fetchTspSchools(system, 1); + + expect(logger.warning).toHaveBeenCalledWith(new AxiosErrorLoggable(new AxiosError(), 'TSP_FETCH_ERROR')); + }); + }); + + describe('when generic Error is thrown', () => { + const setup = () => { + const system = systemFactory.build({ + oauthConfig: { + clientId: faker.string.alpha(), + clientSecret: faker.string.alpha(), + tokenEndpoint: faker.internet.url(), + }, + }); + + const exportApiMock = createMock(); + exportApiMock.exportSchuleList.mockImplementation(() => { + throw new Error(); + }); + + tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); + + return { + system, + exportApiMock, + }; + }; + + it('should log a ErrorLoggable as warning', async () => { + const { system } = setup(); + + await sut.fetchTspSchools(system, 1); + + expect(logger.warning).toHaveBeenCalledWith(new ErrorLoggable(new Error())); + }); + }); + }); + + describe('createClient', () => { + describe('when oauthConfig is missing', () => { + const setup = () => { + const system = systemFactory.build({ oauthConfig: undefined }); + + return { system }; + }; + + it('should throw an OauthConfigMissingLoggableException', async () => { + const { system } = setup(); + + await expect(async () => sut.fetchTspSchools(system, 1)).rejects.toThrow(OauthConfigMissingLoggableException); + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts new file mode 100644 index 00000000000..4f00e7d81d3 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts @@ -0,0 +1,93 @@ +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'; + +@Injectable() +export class TspFetchService { + constructor(private readonly tspClientFactory: TspClientFactory, private readonly logger: Logger) { + this.logger.setContext(TspFetchService.name); + } + + public async fetchTspSchools(system: System, daysToFetch: number) { + const lastChangeDate = this.formatChangeDate(daysToFetch); + const test = await this.fetchTsp(system, (client) => client.exportSchuleList(lastChangeDate), []); + return test; + } + + public async fetchTspTeachers(system: System, daysToFetch: number) { + const lastChangeDate = this.formatChangeDate(daysToFetch); + const teachers = await this.fetchTsp(system, (client) => client.exportLehrerList(lastChangeDate), []); + + return teachers; + } + + public async fetchTspStudents(system: System, daysToFetch: number) { + const lastChangeDate = this.formatChangeDate(daysToFetch); + const students = await this.fetchTsp(system, (client) => client.exportSchuelerList(lastChangeDate), []); + + return students; + } + + public async fetchTspClasses(system: System, daysToFetch: number) { + const lastChangeDate = this.formatChangeDate(daysToFetch); + const classes = await this.fetchTsp(system, (client) => client.exportKlasseList(lastChangeDate), []); + + return classes; + } + + public async fetchTspTeacherMigrations(system: System) { + const migrations = this.fetchTsp(system, (client) => client.exportLehrerListMigration(), []); + + return migrations; + } + + public async fetchTspStudentMigrations(system: System) { + const migrations = this.fetchTsp(system, (client) => client.exportSchuelerListMigration(), []); + + return migrations; + } + + private async fetchTsp( + system: System, + fetch: (client: ExportApiInterface) => Promise>, + defaultValue: T + ): Promise { + const client = this.createClient(system); + try { + const response = await fetch(client); + const { data } = response; + + return data; + } catch (e) { + if (e instanceof AxiosError) { + this.logger.warning(new AxiosErrorLoggable(e, 'TSP_FETCH_ERROR')); + } else { + this.logger.warning(new ErrorLoggable(e)); + } + } + return defaultValue; + } + + private formatChangeDate(daysToFetch: number): string { + return moment(new Date()).subtract(daysToFetch, 'days').subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss.SSS'); + } + + private createClient(system: System) { + if (!system.oauthConfig) { + throw new OauthConfigMissingLoggableException(system.id); + } + + const client = this.tspClientFactory.createExportClient({ + clientId: system.oauthConfig.clientId, + clientSecret: system.oauthConfig.clientSecret, + tokenEndpoint: system.oauthConfig.tokenEndpoint, + }); + + return client; + } +} 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 4647822ab95..77556ed6897 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,36 +1,23 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { - ExportApiInterface, - RobjExportKlasse, - RobjExportLehrer, - RobjExportLehrerMigration, - RobjExportSchueler, - RobjExportSchuelerMigration, - RobjExportSchule, - TspClientFactory, -} from '@infra/tsp-client'; import { School, SchoolService } from '@modules/school'; import { SystemService, SystemType } from '@modules/system'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { federalStateFactory, schoolYearFactory, userDoFactory } from '@shared/testing'; -import { AccountService } from '@src/modules/account'; +import { AccountService } from '@modules/account'; import { accountDoFactory } from '@src/modules/account/testing'; -import { FederalStateService, SchoolYearService } from '@src/modules/legacy-school'; -import { OauthConfigMissingLoggableException } from '@src/modules/oauth/loggable'; +import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; import { 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 { UserService } from '@src/modules/user'; -import { AxiosResponse } from 'axios'; +import { UserService } from '@modules/user'; import { TspSyncService } from './tsp-sync.service'; describe(TspSyncService.name, () => { let module: TestingModule; let sut: TspSyncService; - let tspClientFactory: DeepMocked; let systemService: DeepMocked; let schoolService: DeepMocked; let federalStateService: DeepMocked; @@ -42,10 +29,6 @@ describe(TspSyncService.name, () => { module = await Test.createTestingModule({ providers: [ TspSyncService, - { - provide: TspClientFactory, - useValue: createMock(), - }, { provide: SystemService, useValue: createMock(), @@ -74,7 +57,6 @@ describe(TspSyncService.name, () => { }).compile(); sut = module.get(TspSyncService); - tspClientFactory = module.get(TspClientFactory); systemService = module.get(SystemService); schoolService = module.get(SchoolService); federalStateService = module.get(FederalStateService); @@ -131,235 +113,6 @@ describe(TspSyncService.name, () => { }); }); - const setupTspClient = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); - const tokenEndpoint = faker.internet.url(); - const system = systemFactory.build({ - oauthConfig: { - clientId, - clientSecret, - tokenEndpoint, - }, - }); - - const tspSchool: RobjExportSchule = { - schuleName: faker.string.alpha(), - schuleNummer: faker.string.alpha(), - }; - 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 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 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 classes = [tspClass]; - const responseClasses = createMock>>({ - data: classes, - }); - - const tspTeacherMigration: RobjExportLehrerMigration = { - lehrerUidAlt: faker.string.alpha(), - lehrerUidNeu: faker.string.alpha(), - }; - const teacherMigrations = [tspTeacherMigration]; - const responseTeacherMigrations = createMock>>({ - data: teacherMigrations, - }); - - const tspStudentMigration: RobjExportSchuelerMigration = { - schuelerUidAlt: faker.string.alpha(), - schuelerUidNeu: faker.string.alpha(), - }; - const studentMigrations = [tspStudentMigration]; - const responseStudentMigrations = createMock>>({ - data: studentMigrations, - }); - - const exportApiMock = createMock(); - exportApiMock.exportSchuleList.mockResolvedValueOnce(responseSchools); - exportApiMock.exportLehrerList.mockResolvedValueOnce(responseTeachers); - exportApiMock.exportSchuelerList.mockResolvedValueOnce(responseStudents); - exportApiMock.exportKlasseList.mockResolvedValueOnce(responseClasses); - exportApiMock.exportLehrerListMigration.mockResolvedValueOnce(responseTeacherMigrations); - exportApiMock.exportSchuelerListMigration.mockResolvedValueOnce(responseStudentMigrations); - - tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); - - return { - clientId, - clientSecret, - tokenEndpoint, - system, - exportApiMock, - schools, - teachers, - students, - classes, - teacherMigrations, - studentMigrations, - }; - }; - - describe('fetchTspSchools', () => { - describe('when tsp schools are fetched', () => { - it('should use the oauthConfig to create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); - - await sut.fetchTspSchools(system, 1); - - expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ - clientId, - clientSecret, - tokenEndpoint, - }); - }); - - it('should call exportSchuleList', async () => { - const { system, exportApiMock } = setupTspClient(); - - await sut.fetchTspSchools(system, 1); - - expect(exportApiMock.exportSchuleList).toHaveBeenCalledTimes(1); - }); - - it('should return an array of schools', async () => { - const { system } = setupTspClient(); - - const schools = await sut.fetchTspSchools(system, 1); - - expect(schools).toBeDefined(); - expect(schools).toBeInstanceOf(Array); - }); - }); - }); - - describe('fetchTspTeachers', () => { - describe('when tsp teachers are fetched', () => { - it('should use the oauthConfig to create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); - - await sut.fetchTspTeachers(system, 1); - - expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ - clientId, - clientSecret, - tokenEndpoint, - }); - }); - - it('should call exportLehrerList', async () => { - const { system, exportApiMock } = setupTspClient(); - - await sut.fetchTspTeachers(system, 1); - - expect(exportApiMock.exportLehrerList).toHaveBeenCalledTimes(1); - }); - - it('should return an array of teachers', async () => { - const { system } = setupTspClient(); - - const teachers = await sut.fetchTspTeachers(system, 1); - - expect(teachers).toBeDefined(); - expect(teachers).toBeInstanceOf(Array); - }); - }); - }); - - describe('fetchTspStudents', () => { - describe('when tsp students are fetched', () => { - it('should use the oauthConfig to create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); - - await sut.fetchTspStudents(system, 1); - - expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ - clientId, - clientSecret, - tokenEndpoint, - }); - }); - - it('should call exportSchuelerList', async () => { - const { system, exportApiMock } = setupTspClient(); - - await sut.fetchTspStudents(system, 1); - - expect(exportApiMock.exportSchuelerList).toHaveBeenCalledTimes(1); - }); - - it('should return an array of students', async () => { - const { system } = setupTspClient(); - - const students = await sut.fetchTspStudents(system, 1); - - expect(students).toBeDefined(); - expect(students).toBeInstanceOf(Array); - }); - }); - }); - - describe('fetchTspClasses', () => { - describe('when tsp classes are fetched', () => { - it('should use the oauthConfig to create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); - - await sut.fetchTspClasses(system, 1); - - expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ - clientId, - clientSecret, - tokenEndpoint, - }); - }); - - it('should call exportKlasseList', async () => { - const { system, exportApiMock } = setupTspClient(); - - await sut.fetchTspClasses(system, 1); - - expect(exportApiMock.exportKlasseList).toHaveBeenCalledTimes(1); - }); - - it('should return an array of classes', async () => { - const { system } = setupTspClient(); - - const classes = await sut.fetchTspClasses(system, 1); - - expect(classes).toBeDefined(); - expect(classes).toBeInstanceOf(Array); - }); - }); - }); - describe('findSchool', () => { describe('when school is found', () => { const setup = () => { @@ -545,72 +298,6 @@ describe(TspSyncService.name, () => { }); }); - describe('fetchTspTeacherMigrations', () => { - describe('when tsp teacher migrations are fetched', () => { - it('should create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); - - await sut.fetchTspTeacherMigrations(system); - - expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ - clientId, - clientSecret, - tokenEndpoint, - }); - }); - - it('should call exportLehrerListMigration', async () => { - const { system, exportApiMock } = setupTspClient(); - - await sut.fetchTspTeacherMigrations(system); - - expect(exportApiMock.exportLehrerListMigration).toHaveBeenCalledTimes(1); - }); - - it('should return an array of teacher migrations', async () => { - const { system } = setupTspClient(); - - const result = await sut.fetchTspTeacherMigrations(system); - - expect(result).toBeDefined(); - expect(result).toBeInstanceOf(Array); - }); - }); - }); - - describe('fetchTspStudentMigrations', () => { - describe('when tsp student migrations are fetched', () => { - it('should create the client', async () => { - const { clientId, clientSecret, tokenEndpoint, system } = setupTspClient(); - - await sut.fetchTspStudentMigrations(system); - - expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ - clientId, - clientSecret, - tokenEndpoint, - }); - }); - - it('should call exportSchuelerListMigration', async () => { - const { system, exportApiMock } = setupTspClient(); - - await sut.fetchTspStudentMigrations(system); - - expect(exportApiMock.exportSchuelerListMigration).toHaveBeenCalledTimes(1); - }); - - it('should return an array of student migrations', async () => { - const { system } = setupTspClient(); - - const result = await sut.fetchTspStudentMigrations(system); - - expect(result).toBeDefined(); - expect(result).toBeInstanceOf(Array); - }); - }); - }); - describe('findUserByTspUid', () => { describe('when user is found', () => { const setup = () => { @@ -740,20 +427,4 @@ describe(TspSyncService.name, () => { }); }); }); - - describe('createClient', () => { - describe('when oauthConfig is missing', () => { - const setup = () => { - const system = systemFactory.build({ oauthConfig: undefined }); - - return { system }; - }; - - it('should throw an OauthConfigMissingLoggableException', async () => { - const { system } = setup(); - - await expect(async () => sut.fetchTspSchools(system, 1)).rejects.toThrow(OauthConfigMissingLoggableException); - }); - }); - }); }); 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 2d48792c356..7e9de63e6c2 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts @@ -1,4 +1,3 @@ -import { TspClientFactory } from '@infra/tsp-client'; import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; import { School, SchoolService } from '@modules/school'; import { System, SystemService, SystemType } from '@modules/system'; @@ -8,13 +7,11 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { SchoolFeature } from '@shared/domain/types'; import { Account, AccountService } from '@src/modules/account'; import { FederalStateNames } from '@src/modules/legacy-school/types'; -import { OauthConfigMissingLoggableException } from '@src/modules/oauth/loggable'; import { FederalState } from '@src/modules/school/domain'; import { SchoolFactory } from '@src/modules/school/domain/factory'; import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@src/modules/school/repo/mikro-orm/mapper'; import { UserService } from '@src/modules/user'; import { ObjectId } from 'bson'; -import moment from 'moment/moment'; import { TspSystemNotFoundLoggableException } from './loggable/tsp-system-not-found.loggable-exception'; @Injectable() @@ -22,7 +19,6 @@ export class TspSyncService { private federalState: FederalState | undefined; constructor( - private readonly tspClientFactory: TspClientFactory, private readonly systemService: SystemService, private readonly schoolService: SchoolService, private readonly federalStateService: FederalStateService, @@ -45,46 +41,6 @@ export class TspSyncService { return systems[0]; } - public async fetchTspSchools(system: System, daysToFetch: number) { - const client = this.createClient(system); - - const lastChangeDate = this.formatChangeDate(daysToFetch); - const schoolsResponse = await client.exportSchuleList(lastChangeDate); - const schools = schoolsResponse.data; - - return schools; - } - - public async fetchTspTeachers(system: System, daysToFetch: number) { - const client = this.createClient(system); - - const lastChangeDate = this.formatChangeDate(daysToFetch); - const teachersResponse = await client.exportLehrerList(lastChangeDate); - const teachers = teachersResponse.data; - - return teachers; - } - - public async fetchTspStudents(system: System, daysToFetch: number) { - const client = this.createClient(system); - - const lastChangeDate = this.formatChangeDate(daysToFetch); - const studentsResponse = await client.exportSchuelerList(lastChangeDate); - const students = studentsResponse.data; - - return students; - } - - public async fetchTspClasses(system: System, daysToFetch: number) { - const client = this.createClient(system); - - const lastChangeDate = this.formatChangeDate(daysToFetch); - const classesResponse = await client.exportKlasseList(lastChangeDate); - const classes = classesResponse.data; - - return classes; - } - public async findSchool(system: System, identifier: string): Promise { const schools = await this.schoolService.getSchools({ externalId: identifier, @@ -149,24 +105,6 @@ export class TspSyncService { return this.federalState; } - public async fetchTspTeacherMigrations(system: System) { - const client = this.createClient(system); - - const teacherMigrationsResponse = await client.exportLehrerListMigration(); - const teacherMigrations = teacherMigrationsResponse.data; - - return teacherMigrations; - } - - public async fetchTspStudentMigrations(system: System) { - const client = this.createClient(system); - - const studentMigrationsResponse = await client.exportSchuelerListMigration(); - const studentMigrations = studentMigrationsResponse.data; - - return studentMigrations; - } - public async findUserByTspUid(tspUid: string): Promise { const tspUser = await this.userService.findUsers({ tspUid }); @@ -208,22 +146,4 @@ export class TspSyncService { return this.accountService.save(account); } - - private formatChangeDate(daysToFetch: number): string { - return moment(new Date()).subtract(daysToFetch, 'days').subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss.SSS'); - } - - private createClient(system: System) { - if (!system.oauthConfig) { - throw new OauthConfigMissingLoggableException(system.id); - } - - const client = this.tspClientFactory.createExportClient({ - clientId: system.oauthConfig.clientId, - clientSecret: system.oauthConfig.clientSecret, - tokenEndpoint: system.oauthConfig.tokenEndpoint, - }); - - return client; - } } 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 0b483133e90..b71ae0cbd17 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 @@ -15,11 +15,13 @@ import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; import { TspSyncStrategy } from './tsp-sync.strategy'; +import { TspFetchService } from './tsp-fetch.service'; describe(TspSyncStrategy.name, () => { let module: TestingModule; let sut: TspSyncStrategy; let tspSyncService: DeepMocked; + let tspFetchService: DeepMocked; let provisioningService: DeepMocked; let tspOauthDataMapper: DeepMocked; @@ -31,6 +33,10 @@ describe(TspSyncStrategy.name, () => { provide: TspSyncService, useValue: createMock(), }, + { + provide: TspFetchService, + useValue: createMock(), + }, { provide: Logger, useValue: createMock(), @@ -71,6 +77,7 @@ describe(TspSyncStrategy.name, () => { sut = module.get(TspSyncStrategy); tspSyncService = module.get(TspSyncService); + tspFetchService = module.get(TspFetchService); provisioningService = module.get(ProvisioningService); tspOauthDataMapper = module.get(TspOauthDataMapper); }); @@ -101,10 +108,10 @@ describe(TspSyncStrategy.name, () => { describe('sync', () => { describe('when sync is called', () => { const setup = () => { - tspSyncService.fetchTspSchools.mockResolvedValueOnce([]); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); const oauthDataDto = new OauthDataDto({ @@ -129,8 +136,8 @@ describe(TspSyncStrategy.name, () => { schuelerUidNeu: faker.string.alpha(), }; - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([tspStudent]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([tspStudent]); tspSyncService.findUserByTspUid.mockResolvedValueOnce(userDoFactory.build()); tspSyncService.updateUser.mockResolvedValueOnce(userDoFactory.build()); tspSyncService.findAccountByTspUid.mockResolvedValueOnce(accountDoFactory.build()); @@ -152,7 +159,7 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.fetchTspSchools).toHaveBeenCalled(); + expect(tspFetchService.fetchTspSchools).toHaveBeenCalled(); }); it('should fetch the data', async () => { @@ -160,9 +167,9 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.fetchTspTeachers).toHaveBeenCalled(); - expect(tspSyncService.fetchTspStudents).toHaveBeenCalled(); - expect(tspSyncService.fetchTspClasses).toHaveBeenCalled(); + expect(tspFetchService.fetchTspTeachers).toHaveBeenCalled(); + expect(tspFetchService.fetchTspStudents).toHaveBeenCalled(); + expect(tspFetchService.fetchTspClasses).toHaveBeenCalled(); }); it('should load all schools', async () => { @@ -195,7 +202,7 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.fetchTspTeacherMigrations).toHaveBeenCalled(); + expect(tspFetchService.fetchTspTeacherMigrations).toHaveBeenCalled(); }); it('should fetch student migrations', async () => { @@ -203,7 +210,7 @@ describe(TspSyncStrategy.name, () => { await sut.sync(); - expect(tspSyncService.fetchTspStudentMigrations).toHaveBeenCalled(); + expect(tspFetchService.fetchTspStudentMigrations).toHaveBeenCalled(); }); it('find user by tsp Uid', async () => { @@ -247,16 +254,16 @@ describe(TspSyncStrategy.name, () => { schuleName: faker.string.alpha(), }; const tspSchools = [tspSchool]; - tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + tspFetchService.fetchTspSchools.mockResolvedValueOnce(tspSchools); tspSyncService.findSchool.mockResolvedValueOnce(undefined); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; @@ -276,17 +283,17 @@ describe(TspSyncStrategy.name, () => { schuleName: faker.string.alpha(), }; const tspSchools = [tspSchool]; - tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + tspFetchService.fetchTspSchools.mockResolvedValueOnce(tspSchools); const school = schoolFactory.build(); tspSyncService.findSchool.mockResolvedValueOnce(school); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; @@ -306,14 +313,14 @@ describe(TspSyncStrategy.name, () => { schuleName: faker.string.alpha(), }; const tspSchools = [tspSchool]; - tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + tspFetchService.fetchTspSchools.mockResolvedValueOnce(tspSchools); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; @@ -335,19 +342,19 @@ describe(TspSyncStrategy.name, () => { lehrerUidNeu: faker.string.alpha(), }; - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); const tspStudent: RobjExportSchuelerMigration = { schuelerUidAlt: faker.string.alpha(), schuelerUidNeu: undefined, }; - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([tspStudent]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([tspStudent]); - tspSyncService.fetchTspSchools.mockResolvedValueOnce([]); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); }; @@ -368,14 +375,14 @@ describe(TspSyncStrategy.name, () => { lehrerUidNeu: faker.string.alpha(), }; - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); tspSyncService.findUserByTspUid.mockResolvedValueOnce(null); - tspSyncService.fetchTspSchools.mockResolvedValueOnce([]); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); @@ -398,15 +405,15 @@ describe(TspSyncStrategy.name, () => { lehrerUidNeu: faker.string.alpha(), }; - tspSyncService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); tspSyncService.findAccountByTspUid.mockResolvedValueOnce(null); - tspSyncService.fetchTspSchools.mockResolvedValueOnce([]); - tspSyncService.fetchTspClasses.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudents.mockResolvedValueOnce([]); - tspSyncService.fetchTspTeachers.mockResolvedValueOnce([]); + tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); + tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.fetchTspStudentMigrations.mockResolvedValueOnce([]); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); tspSyncService.findUserByTspUid.mockResolvedValueOnce(userDoFactory.build()); tspSyncService.updateUser.mockResolvedValueOnce(userDoFactory.build()); tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts index e8b99104cc0..c5b1fd6a93c 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts @@ -24,6 +24,7 @@ import { TspUsersMigratedLoggable } from './loggable/tsp-users-migrated.loggable import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; +import { TspFetchService } from './tsp-fetch.service'; @Injectable() export class TspSyncStrategy extends SyncStrategy { @@ -42,6 +43,7 @@ export class TspSyncStrategy extends SyncStrategy { constructor( private readonly logger: Logger, private readonly tspSyncService: TspSyncService, + private readonly tspFetchService: TspFetchService, private readonly tspOauthDataMapper: TspOauthDataMapper, configService: ConfigService, private readonly provisioningService: ProvisioningService @@ -81,7 +83,7 @@ export class TspSyncStrategy extends SyncStrategy { } private async syncSchools(system: System): Promise { - const tspSchools = await this.tspSyncService.fetchTspSchools(system, this.schoolDaysToFetch); + const tspSchools = await this.tspFetchService.fetchTspSchools(system, this.schoolDaysToFetch); this.logger.info(new TspSchoolsFetchedLoggable(tspSchools.length, this.schoolDaysToFetch)); const schoolPromises = tspSchools.map((tspSchool) => @@ -119,9 +121,9 @@ export class TspSyncStrategy extends SyncStrategy { } private async syncData(system: System, schools: School[]): Promise { - const tspTeachers = await this.tspSyncService.fetchTspTeachers(system, this.schoolDataDaysToFetch); - const tspStudents = await this.tspSyncService.fetchTspStudents(system, this.schoolDataDaysToFetch); - const tspClasses = await this.tspSyncService.fetchTspClasses(system, this.schoolDataDaysToFetch); + const tspTeachers = await this.tspFetchService.fetchTspTeachers(system, this.schoolDataDaysToFetch); + const tspStudents = await this.tspFetchService.fetchTspStudents(system, this.schoolDataDaysToFetch); + const tspClasses = await this.tspFetchService.fetchTspClasses(system, this.schoolDataDaysToFetch); this.logger.info( new TspDataFetchedLoggable(tspTeachers.length, tspStudents.length, tspClasses.length, this.schoolDataDaysToFetch) ); @@ -146,7 +148,7 @@ export class TspSyncStrategy extends SyncStrategy { } private async migrateTspTeachers(system: System): Promise<{ total: number }> { - const tspTeacherIds = await this.tspSyncService.fetchTspTeacherMigrations(system); + const tspTeacherIds = await this.tspFetchService.fetchTspTeacherMigrations(system); this.logger.info(new TspTeachersFetchedLoggable(tspTeacherIds.length)); const teacherMigrationPromises = tspTeacherIds.map(({ lehrerUidAlt, lehrerUidNeu }) => @@ -168,7 +170,7 @@ export class TspSyncStrategy extends SyncStrategy { } private async migrateTspStudents(system: System): Promise<{ total: number }> { - const tspStudentIds = await this.tspSyncService.fetchTspStudentMigrations(system); + const tspStudentIds = await this.tspFetchService.fetchTspStudentMigrations(system); this.logger.info(new TspStudentsFetchedLoggable(tspStudentIds.length)); const studentMigrationPromises = tspStudentIds.map(({ schuelerUidAlt, schuelerUidNeu }) => 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 1efb774d9de..b5d4b8ba1bc 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts @@ -41,7 +41,10 @@ export class TspProvisioningService { } public async provisionClasses(school: School, classes: ExternalClassDto[], user: UserDO): Promise { - if (!user.id) throw new BadDataLoggableException('User ID is missing', { user }); + 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); @@ -76,7 +79,11 @@ export class TspProvisioningService { } public async provisionUser(data: OauthDataDto, school: School): Promise { - if (!data.externalSchool) throw new BadDataLoggableException('External school is missing', { data }); + if (!data.externalSchool) { + throw 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); @@ -116,7 +123,9 @@ export class TspProvisioningService { schoolId: string ): Promise { if (!externalUser.firstName || !externalUser.lastName) { - throw new BadDataLoggableException('User firstname or lastname is missing', { externalUser }); + throw new BadDataLoggableException('User firstname or lastname is missing. TspUid:', { + externalId: externalUser.externalId, + }); } const newUser = new UserDO({ @@ -134,7 +143,10 @@ export class TspProvisioningService { } private async createOrUpdateAccount(systemId: string, user: UserDO): Promise { - if (!user.id) throw new BadDataLoggableException('user ID is missing', { user }); + if (!user.id) + throw new BadDataLoggableException('user ID is missing', { + externalId: user.externalId, + }); const account = await this.accountService.findByUserId(user.id); 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 ebfdfce242e..f9f8ca71c43 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -74,8 +74,16 @@ export class TspProvisioningStrategy extends ProvisioningStrategy { } override async apply(data: OauthDataDto): Promise { - if (!data.externalSchool) throw new BadDataLoggableException('External school is missing', { data }); - if (!data.externalClasses) throw new BadDataLoggableException('External classes are missing', { data }); + if (!data.externalSchool) { + throw new BadDataLoggableException('External school is missing for user', { + externalId: data.externalUser.externalId, + }); + } + if (!data.externalClasses) { + throw new BadDataLoggableException('External classes are missing for user', { + externalId: data.externalUser.externalId, + }); + } const school = await this.provisioningService.findSchoolOrFail(data.system, data.externalSchool); const user = await this.provisioningService.provisionUser(data, school); From 7f24da8a3c701ab2ecd61ece6e6e8bb35aed7da7 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Fri, 15 Nov 2024 15:13:51 +0100 Subject: [PATCH 2/4] add missing test coverage. --- .../service/tsp-provisioning.service.spec.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 ab0d4a10832..da637580400 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 @@ -339,5 +339,34 @@ describe('TspProvisioningService', () => { expect(accountServiceMock.saveWithValidation).toHaveBeenCalledTimes(1); }); }); + + describe('when user id is not set after create', () => { + const setup = () => { + const school = schoolFactory.build(); + const data = new OauthDataDto({ + system: setupExternalSystem(), + externalUser: setupExternalUser({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + email: faker.internet.email(), + }), + externalSchool: setupExternalSchool(), + }); + const user = userDoFactory.build({ id: undefined, roles: [] }); + + userServiceMock.findByExternalId.mockResolvedValue(null); + userServiceMock.save.mockResolvedValue(user); + schoolServiceMock.getSchools.mockResolvedValue([school]); + roleServiceMock.findByNames.mockResolvedValue([]); + + return { data, school }; + }; + + it('should throw BadDataLoggableException', async () => { + const { data, school } = setup(); + + await expect(() => sut.provisionUser(data, school)).rejects.toThrow(BadDataLoggableException); + }); + }); }); }); From 1c0821c774f3d8fc00410f9c698ebc5b84e35ea3 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 18 Nov 2024 12:05:19 +0100 Subject: [PATCH 3/4] Handle oauth request error and refactor test. --- .../tsp-client/tsp-client-factory.spec.ts | 137 ++++++++++++++---- .../infra/tsp-client/tsp-client-factory.ts | 29 +++- .../src/infra/tsp-client/tsp-client.module.ts | 3 +- 3 files changed, 130 insertions(+), 39 deletions(-) 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 f4922ebec0b..b8123894961 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 @@ -4,7 +4,9 @@ import { OauthAdapterService } from '@modules/oauth'; import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import axios from 'axios'; +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'; @@ -14,6 +16,7 @@ describe('TspClientFactory', () => { let configServiceMock: DeepMocked>; let oauthAdapterServiceMock: DeepMocked; let encryptionService: DeepMocked; + let logger: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -42,6 +45,10 @@ describe('TspClientFactory', () => { provide: DefaultEncryptionService, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); @@ -49,6 +56,7 @@ describe('TspClientFactory', () => { configServiceMock = module.get(ConfigService); oauthAdapterServiceMock = module.get(OauthAdapterService); encryptionService = module.get(DefaultEncryptionService); + logger = module.get(Logger); }); afterAll(async () => { @@ -57,6 +65,8 @@ describe('TspClientFactory', () => { beforeEach(() => { jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); }); it('should be defined', () => { @@ -76,56 +86,123 @@ describe('TspClientFactory', () => { expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); }); }); + }); + + describe('getAccessToken', () => { + describe('when called successfully', () => { + const setup = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); + + oauthAdapterServiceMock.sendTokenRequest.mockResolvedValue({ + accessToken: faker.string.alpha(), + idToken: faker.string.alpha(), + refreshToken: faker.string.alpha(), + }); + + Reflect.set(sut, 'cachedToken', undefined); + + return { + clientId, + clientSecret, + tokenEndpoint, + }; + }; + + it('should return access token', async () => { + const params = setup(); + + const response = await sut.getAccessToken(params); + + expect(response).toBeDefined(); + expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); + expect(encryptionService.decrypt).toHaveBeenCalled(); + }); + }); describe('when token is cached', () => { const setup = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); const client = sut.createExportClient({ - clientId: faker.string.alpha(), - clientSecret: faker.string.alpha(), - tokenEndpoint: faker.internet.url(), + clientId, + clientSecret, + tokenEndpoint, }); - Reflect.set(sut, 'cachedToken', faker.string.alpha()); + const cached = faker.string.alpha(); + Reflect.set(sut, 'cachedToken', cached); + Reflect.set(sut, 'tokenExpiresAt', Date.now() + 60000); - return client; + return { clientId, clientSecret, tokenEndpoint, client, cached }; }; - it('should return ExportApiInterface', () => { - const result = setup(); + it('should return ExportApiInterface', async () => { + const { clientId, clientSecret, tokenEndpoint, cached } = setup(); - expect(result).toBeDefined(); + const result = await sut.getAccessToken({ clientId, clientSecret, tokenEndpoint }); + + expect(result).toBe(cached); expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); }); }); - }); - describe('getAccessToken', () => { - const setup = () => { - const clientId = faker.string.alpha(); - const clientSecret = faker.string.alpha(); - const tokenEndpoint = faker.internet.url(); + describe('when an AxiosError occurs', () => { + const setup = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); - oauthAdapterServiceMock.sendTokenRequest.mockResolvedValue({ - accessToken: faker.string.alpha(), - idToken: faker.string.alpha(), - refreshToken: faker.string.alpha(), + oauthAdapterServiceMock.sendTokenRequest.mockImplementation(() => { + throw new AxiosError(); + }); + + Reflect.set(sut, 'cachedToken', undefined); + + return { + clientId, + clientSecret, + tokenEndpoint, + }; + }; + + it('should log an AxiosErrorLoggable as warning and reject', async () => { + const params = setup(); + + await expect(() => sut.getAccessToken(params)).rejects.toBeUndefined(); + + expect(logger.warning).toHaveBeenCalledWith(new AxiosErrorLoggable(new AxiosError(), 'TSP_OAUTH_ERROR')); }); + }); - return { - clientId, - clientSecret, - tokenEndpoint, + describe('when a generic error occurs', () => { + const setup = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); + + oauthAdapterServiceMock.sendTokenRequest.mockImplementation(() => { + throw new Error(); + }); + + Reflect.set(sut, 'cachedToken', undefined); + + return { + clientId, + clientSecret, + tokenEndpoint, + }; }; - }; - it('should return access token', async () => { - const params = setup(); + it('should log an ErrorLoggable as warning and reject', async () => { + const params = setup(); - const response = await sut.getAccessToken(params); + await expect(() => sut.getAccessToken(params)).rejects.toBeUndefined(); - expect(response).toBeDefined(); - expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); - expect(encryptionService.decrypt).toHaveBeenCalled(); + expect(logger.warning).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 e9890403541..53d72673aed 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.ts @@ -1,8 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { AxiosErrorLoggable, ErrorLoggable } from '@src/core/error/loggable'; +import { Logger } from '@src/core/logger'; import { OauthAdapterService } from '@src/modules/oauth'; import { OAuthGrantType } from '@src/modules/oauth/interface/oauth-grant-type.enum'; import { ClientCredentialsGrantTokenRequest } from '@src/modules/oauth/service/dto'; +import { AxiosError } from 'axios'; import * as jwt from 'jsonwebtoken'; import { DefaultEncryptionService, EncryptionService } from '../encryption'; import { Configuration, ExportApiFactory, ExportApiInterface } from './generated'; @@ -27,7 +30,8 @@ export class TspClientFactory { constructor( private readonly oauthAdapterService: OauthAdapterService, configService: ConfigService, - @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService + @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, + private readonly logger: Logger ) { this.baseUrl = configService.getOrThrow('TSP_API_CLIENT_BASE_URL'); this.tokenLifetime = configService.getOrThrow('TSP_API_CLIENT_TOKEN_LIFETIME_MS'); @@ -60,13 +64,22 @@ export class TspClientFactory { grant_type: OAuthGrantType.CLIENT_CREDENTIALS_GRANT, }); - const response = await this.oauthAdapterService.sendTokenRequest(params.tokenEndpoint, payload); - - this.cachedToken = response.accessToken; - this.tokenExpiresAt = this.getExpiresAt(now, response.accessToken); - - // We need the Bearer prefix for the generated client, because OAS 2 does not support Bearer token type - return `Bearer ${this.cachedToken}`; + try { + const response = await this.oauthAdapterService.sendTokenRequest(params.tokenEndpoint, payload); + + this.cachedToken = response.accessToken; + this.tokenExpiresAt = this.getExpiresAt(now, response.accessToken); + + // We need the Bearer prefix for the generated client, because OAS 2 does not support Bearer token type + return `Bearer ${this.cachedToken}`; + } catch (e) { + if (e instanceof AxiosError) { + this.logger.warning(new AxiosErrorLoggable(e, 'TSP_OAUTH_ERROR')); + } else { + this.logger.warning(new ErrorLoggable(e)); + } + return Promise.reject(); + } } private getExpiresAt(now: number, token: string): number { 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 2c874fbf7c2..857f2b83dc3 100644 --- a/apps/server/src/infra/tsp-client/tsp-client.module.ts +++ b/apps/server/src/infra/tsp-client/tsp-client.module.ts @@ -1,10 +1,11 @@ import { OauthModule } from '@modules/oauth'; import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; import { EncryptionModule } from '../encryption'; import { TspClientFactory } from './tsp-client-factory'; @Module({ - imports: [OauthModule, EncryptionModule], + imports: [LoggerModule, OauthModule, EncryptionModule], providers: [TspClientFactory], exports: [TspClientFactory], }) From fb5e940b794802570c1c4e701649fddca6272215 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 19 Nov 2024 14:43:15 +0100 Subject: [PATCH 4/4] code review --- .../src/infra/sync/tsp/tsp-fetch.service.ts | 5 +- .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 163 +++++++++--------- 2 files changed, 83 insertions(+), 85 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 4f00e7d81d3..9285fbfd9a8 100644 --- a/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts +++ b/apps/server/src/infra/sync/tsp/tsp-fetch.service.ts @@ -15,8 +15,9 @@ export class TspFetchService { public async fetchTspSchools(system: System, daysToFetch: number) { const lastChangeDate = this.formatChangeDate(daysToFetch); - const test = await this.fetchTsp(system, (client) => client.exportSchuleList(lastChangeDate), []); - return test; + const schools = await this.fetchTsp(system, (client) => client.exportSchuleList(lastChangeDate), []); + + return schools; } public async fetchTspTeachers(system: System, daysToFetch: number) { 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 b71ae0cbd17..f73cda9bcbd 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,21 +1,32 @@ import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { RobjExportLehrerMigration, RobjExportSchuelerMigration, RobjExportSchule } from '@infra/tsp-client'; +import { + RobjExportKlasse, + RobjExportLehrer, + RobjExportLehrerMigration, + 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'; import { SyncStrategyTarget } from '../sync-strategy.types'; +import { TspFetchService } from './tsp-fetch.service'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; import { TspSyncStrategy } from './tsp-sync.strategy'; -import { TspFetchService } from './tsp-fetch.service'; describe(TspSyncStrategy.name, () => { let module: TestingModule; @@ -105,15 +116,47 @@ describe(TspSyncStrategy.name, () => { }); }); + const setupMockServices = (params: { + fetchedSchools?: RobjExportSchule[]; + fetchedClasses?: RobjExportKlasse[]; + fetchedTeachers?: RobjExportLehrer[]; + fetchedStudents?: RobjExportSchueler[]; + fetchedTeacherMigrations?: RobjExportLehrerMigration[]; + fetchedStudentMigrations?: RobjExportSchuelerMigration[]; + foundSchool?: School; + foundSystemSchools?: School[]; + foundTspUidUser?: UserDO | null; + foundTspUidAccount?: Account | null; + mappedOauthDto?: OauthDataDto[]; + foundSystem?: System; + updatedAccount?: Account; + updatedUser?: UserDO; + }) => { + tspFetchService.fetchTspSchools.mockResolvedValueOnce(params.fetchedSchools ?? []); + tspFetchService.fetchTspClasses.mockResolvedValueOnce(params.fetchedClasses ?? []); + tspFetchService.fetchTspStudents.mockResolvedValueOnce(params.fetchedStudents ?? []); + tspFetchService.fetchTspTeachers.mockResolvedValueOnce(params.fetchedTeachers ?? []); + tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce(params.fetchedTeacherMigrations ?? []); + tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce(params.fetchedStudentMigrations ?? []); + + tspSyncService.findSchool.mockResolvedValue(params.foundSchool ?? undefined); + tspSyncService.findSchoolsForSystem.mockResolvedValueOnce(params.foundSystemSchools ?? []); + tspSyncService.findUserByTspUid.mockResolvedValueOnce( + params.foundTspUidUser !== undefined ? params.foundTspUidUser : userDoFactory.build() + ); + tspSyncService.updateUser.mockResolvedValueOnce(params.updatedUser ?? userDoFactory.build()); + tspSyncService.findAccountByTspUid.mockResolvedValueOnce( + params.foundTspUidAccount !== undefined ? params.foundTspUidAccount : accountDoFactory.build() + ); + tspSyncService.updateAccount.mockResolvedValueOnce(params.updatedAccount ?? accountDoFactory.build()); + tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(params.foundSystem ?? systemFactory.build()); + + tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce(params.mappedOauthDto ?? []); + }; + describe('sync', () => { describe('when sync is called', () => { const setup = () => { - tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - const oauthDataDto = new OauthDataDto({ system: new ProvisioningSystemDto({ systemId: faker.string.alpha(), @@ -123,9 +166,6 @@ describe(TspSyncStrategy.name, () => { externalId: faker.string.alpha(), }), }); - - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([oauthDataDto]); - const tspTeacher: RobjExportLehrerMigration = { lehrerUidAlt: faker.string.alpha(), lehrerUidNeu: faker.string.alpha(), @@ -136,14 +176,11 @@ describe(TspSyncStrategy.name, () => { schuelerUidNeu: faker.string.alpha(), }; - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([tspStudent]); - tspSyncService.findUserByTspUid.mockResolvedValueOnce(userDoFactory.build()); - tspSyncService.updateUser.mockResolvedValueOnce(userDoFactory.build()); - tspSyncService.findAccountByTspUid.mockResolvedValueOnce(accountDoFactory.build()); - tspSyncService.updateAccount.mockResolvedValueOnce(accountDoFactory.build()); - - tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); + setupMockServices({ + fetchedStudentMigrations: [tspStudent], + fetchedTeacherMigrations: [tspTeacher], + mappedOauthDto: [oauthDataDto], + }); return { oauthDataDto }; }; @@ -254,17 +291,10 @@ describe(TspSyncStrategy.name, () => { schuleName: faker.string.alpha(), }; const tspSchools = [tspSchool]; - tspFetchService.fetchTspSchools.mockResolvedValueOnce(tspSchools); - - tspSyncService.findSchool.mockResolvedValueOnce(undefined); - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); + setupMockServices({ + fetchedSchools: tspSchools, + }); }; it('should create the school', async () => { @@ -283,18 +313,12 @@ describe(TspSyncStrategy.name, () => { schuleName: faker.string.alpha(), }; const tspSchools = [tspSchool]; - tspFetchService.fetchTspSchools.mockResolvedValueOnce(tspSchools); - const school = schoolFactory.build(); - tspSyncService.findSchool.mockResolvedValueOnce(school); - - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); + + setupMockServices({ + fetchedSchools: tspSchools, + foundSchool: school, + }); }; it('should update the school', async () => { @@ -313,15 +337,10 @@ describe(TspSyncStrategy.name, () => { schuleName: faker.string.alpha(), }; const tspSchools = [tspSchool]; - tspFetchService.fetchTspSchools.mockResolvedValueOnce(tspSchools); - - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.findSchoolsForSystem.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); + + setupMockServices({ + fetchedSchools: tspSchools, + }); }; it('should skip the school', async () => { @@ -341,22 +360,15 @@ describe(TspSyncStrategy.name, () => { lehrerUidAlt: undefined, lehrerUidNeu: faker.string.alpha(), }; - - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); - const tspStudent: RobjExportSchuelerMigration = { schuelerUidAlt: faker.string.alpha(), schuelerUidNeu: undefined, }; - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([tspStudent]); - - tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); + setupMockServices({ + fetchedStudentMigrations: [tspStudent], + fetchedTeacherMigrations: [tspTeacher], + }); }; it('should return false and not call findUserByTspUid', async () => { @@ -375,16 +387,10 @@ describe(TspSyncStrategy.name, () => { lehrerUidNeu: faker.string.alpha(), }; - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); - tspSyncService.findUserByTspUid.mockResolvedValueOnce(null); - - tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); - tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); + setupMockServices({ + fetchedTeacherMigrations: [tspTeacher], + foundTspUidUser: null, + }); return { tspTeacher }; }; @@ -405,19 +411,10 @@ describe(TspSyncStrategy.name, () => { lehrerUidNeu: faker.string.alpha(), }; - tspFetchService.fetchTspTeacherMigrations.mockResolvedValueOnce([tspTeacher]); - tspSyncService.findAccountByTspUid.mockResolvedValueOnce(null); - - tspFetchService.fetchTspSchools.mockResolvedValueOnce([]); - tspFetchService.fetchTspClasses.mockResolvedValueOnce([]); - tspFetchService.fetchTspStudents.mockResolvedValueOnce([]); - tspFetchService.fetchTspTeachers.mockResolvedValueOnce([]); - - tspFetchService.fetchTspStudentMigrations.mockResolvedValueOnce([]); - tspSyncService.findUserByTspUid.mockResolvedValueOnce(userDoFactory.build()); - tspSyncService.updateUser.mockResolvedValueOnce(userDoFactory.build()); - tspSyncService.findTspSystemOrFail.mockResolvedValueOnce(systemFactory.build()); - tspOauthDataMapper.mapTspDataToOauthData.mockReturnValueOnce([]); + setupMockServices({ + fetchedTeacherMigrations: [tspTeacher], + foundTspUidAccount: null, + }); }; it('should throw and not call updateAccount', async () => {