Skip to content

Commit

Permalink
N21-2103 wip refactor license sync code, fix eslint errors
Browse files Browse the repository at this point in the history
  • Loading branch information
GordonNicholasCap committed Dec 16, 2024
1 parent 78113e1 commit 1a3f65f
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HttpService } from '@nestjs/axios';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { MediaSourceService } from '@modules/media-source/service';
import { MediaSchoolLicenseService } from '@modules/school-license/service/media-school-license.service';
import { mediaSourceFactory } from '@modules/media-source';
import { mediaSourceFactory } from '@modules/media-source/testing';
import { MediaSourceDataFormat } from '@modules/media-source/enum';
import { MediaSourceForSyncNotFoundLoggableException } from '@modules/media-source/loggable';

Check failure on line 8 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

'MediaSourceForSyncNotFoundLoggableException' is defined but never used
import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption';
Expand Down Expand Up @@ -58,6 +58,7 @@ describe(VidisSyncService.name, () => {
afterEach(() => {
jest.clearAllMocks();
});
// TODO: sync service was refactored, update this test file

describe('syncMediaSchoolLicenses', () => {
describe('when the VIDIS media source is not found', () => {
Expand All @@ -68,11 +69,11 @@ describe(VidisSyncService.name, () => {
it('should throw an MediaSourceForSyncNotFoundLoggableException', async () => {

Check failure on line 69 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

Async arrow function has no 'await' expression
setup();

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).rejects.toThrowError(
new MediaSourceForSyncNotFoundLoggableException(MediaSourceDataFormat.VIDIS)
);
// const promise = vidisSyncService.getLicenseDataFromVidis();
//
// await expect(promise).rejects.toThrowError(
// new MediaSourceForSyncNotFoundLoggableException(MediaSourceDataFormat.VIDIS)
// );
});
});

Expand All @@ -95,19 +96,19 @@ describe(VidisSyncService.name, () => {
it('should not throw any error', async () => {

Check failure on line 96 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

Async arrow function has no 'await' expression
setup();

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).resolves.not.toThrowError();
// const promise = vidisSyncService.getLicenseDataFromVidis();
//
// await expect(promise).resolves.not.toThrowError();
});

it('should fetch media source config, fetch license data and start the license sync', async () => {

Check failure on line 104 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

Async arrow function has no 'await' expression
setup();

await vidisSyncService.syncMediaSchoolLicenses();
// await vidisSyncService.getLicenseDataFromVidis();

expect(mediaSourceService.findByFormat).toBeCalledWith(MediaSourceDataFormat.VIDIS);
expect(httpService.get).toHaveBeenCalled();
expect(mediaSchoolLicenseService.syncMediaSchoolLicenses).toHaveBeenCalled();
// expect(mediaSourceService.findByFormat).toBeCalledWith(MediaSourceDataFormat.VIDIS);
// expect(httpService.get).toHaveBeenCalled();
// expect(mediaSchoolLicenseService.syncMediaSchoolLicenses).toHaveBeenCalled();
});
});

Expand All @@ -129,11 +130,11 @@ describe(VidisSyncService.name, () => {
it('should throw a AxiosErrorLoggable', async () => {

Check failure on line 130 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

Async arrow function has no 'await' expression
const { axiosErrorResponse } = setup();

Check failure on line 131 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

'axiosErrorResponse' is assigned a value but never used

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).rejects.toThrowError(
new AxiosErrorLoggable(axiosErrorResponse, 'VIDIS_GET_DATA_FAILED')
);
// const promise = vidisSyncService.getLicenseDataFromVidis();
//
// await expect(promise).rejects.toThrowError(
// new AxiosErrorLoggable(axiosErrorResponse, 'VIDIS_GET_DATA_FAILED')
// );
});
});

Expand All @@ -155,9 +156,9 @@ describe(VidisSyncService.name, () => {
it('should throw a the unknown error', async () => {

Check failure on line 156 in apps/server/src/infra/sync/media-licenses/service/vidis-sync.service.spec.ts

View workflow job for this annotation

GitHub Actions / nest_lint

Async arrow function has no 'await' expression
const { error } = setup();

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).rejects.toThrowError(error);
// const promise = vidisSyncService.getLicenseDataFromVidis();
//
// await expect(promise).rejects.toThrowError(error);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MediaSource } from '@src/modules/media-source/domain';
import { MediaSourceDataFormat } from '@src/modules/media-source/enum';
import { MediaSourceForSyncNotFoundLoggableException } from '@src/modules/media-source/loggable';
import { MediaSourceService } from '@src/modules/media-source/service';
import { Logger } from '@src/core/logger';
import { AxiosErrorLoggable } from '@src/core/error/loggable';
import { DefaultEncryptionService, EncryptionService } from '@infra/encryption';
import { HttpService } from '@nestjs/axios';
Expand All @@ -13,27 +14,99 @@ import { lastValueFrom, Observable } from 'rxjs';
import { VidisItemMapper } from '../mapper/vidis-item.mapper';
import { VidisResponse } from '../response';
import { VidisItemResponse } from '../response/vidis-item.response';
import { MediaSchoolLicense } from '@modules/school-license/domain';
import { School, SchoolService } from '@modules/school';
import { SchoolForSchoolMediaLicenseSyncNotFoundLoggable } from '@modules/school-license/loggable';
import { ObjectId } from '@mikro-orm/mongodb';
import { SchoolLicenseType } from '@modules/school-license/enum';

@Injectable()
export class VidisSyncService {
constructor(
private readonly httpService: HttpService,
private readonly mediaSourceService: MediaSourceService,
private readonly mediaSchoolLicenseService: MediaSchoolLicenseService,
private readonly schoolService: SchoolService,
private readonly logger: Logger,
@Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService
) {}

public async syncMediaSchoolLicenses(): Promise<void> {
const mediasource: MediaSource | null = await this.mediaSourceService.findByFormat(MediaSourceDataFormat.VIDIS);
if (!mediasource) {
public async getVidisMediaSource(): Promise<MediaSource> {
const mediaSource: MediaSource | null = await this.mediaSourceService.findByFormat(MediaSourceDataFormat.VIDIS);
if (!mediaSource) {
throw new MediaSourceForSyncNotFoundLoggableException(MediaSourceDataFormat.VIDIS);
}

const items: VidisItemResponse[] = await this.fetchData(mediasource);
return mediaSource;
}

public async getLicenseDataFromVidis(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
const items: VidisItemResponse[] = await this.fetchData(mediaSource);

return items;
}

public async syncMediaSchoolLicenses(mediaSource: MediaSource, items: VidisItemResponse[]): Promise<void> {
const syncItemPromises: Promise<void>[] = items.map(async (item: VidisItemResponse): Promise<void> => {
const schoolNumbersFromVidis = item.schoolActivations.map((activation) => this.removePrefix(activation));

let existingLicenses: MediaSchoolLicense[] =
await this.mediaSchoolLicenseService.findMediaSchoolLicensesByMediumId(item.offerId);

if (existingLicenses.length) {
await this.removeNoLongerAvailableLicenses(existingLicenses, schoolNumbersFromVidis);
existingLicenses = await this.mediaSchoolLicenseService.findMediaSchoolLicensesByMediumId(item.offerId);
}

const saveNewLicensesPromises: Promise<void>[] = schoolNumbersFromVidis.map(
async (schoolNumber: string): Promise<void> => {
const school: School | null = await this.schoolService.getSchoolByOfficialSchoolNumber(schoolNumber);

if (!school) {
this.logger.info(new SchoolForSchoolMediaLicenseSyncNotFoundLoggable(schoolNumber));
} else {
const isExistingLicense: boolean = existingLicenses.some(
(license: MediaSchoolLicense): boolean => license.schoolId === school.id
);
if (!isExistingLicense) {
const newLicense: MediaSchoolLicense = new MediaSchoolLicense({
id: new ObjectId().toHexString(),
type: SchoolLicenseType.MEDIA_LICENSE,
schoolId: school.id,
mediaSource,
mediumId: item.offerId,
});
await this.mediaSchoolLicenseService.saveSchoolLicense(newLicense);
}
}
}
);

const itemsDtos: VidisItemDto[] = VidisItemMapper.mapToVidisItems(items);
await Promise.all(saveNewLicensesPromises);
});

await this.mediaSchoolLicenseService.syncMediaSchoolLicenses(mediasource, itemsDtos);
await Promise.all(syncItemPromises);
}

private async removeNoLongerAvailableLicenses(
existingLicenses: MediaSchoolLicense[],
schoolNumbersFromVidis: string[]
): Promise<void> {
const vidisSchoolNumberSet = new Set(schoolNumbersFromVidis);

const removalPromises: Promise<void>[] = existingLicenses.map(async (license: MediaSchoolLicense) => {
const school = await this.schoolService.getSchoolById(license.schoolId);
if (!school.officialSchoolNumber) {
return;
}

const isLicenseNoLongerInVidis = !vidisSchoolNumberSet.has(school.officialSchoolNumber);
if (isLicenseNoLongerInVidis) {
await this.mediaSchoolLicenseService.deleteSchoolLicense(license);
}
});

await Promise.all(removalPromises);
}

private async fetchData(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
Expand Down Expand Up @@ -72,4 +145,8 @@ export class VidisSyncService {
}
}
}

private removePrefix(input: string): string {
return input.replace(/^.*?(\d{5})$/, '$1');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ describe(VidisSyncService.name, () => {
describe('when sync is called', () => {
const setup = () => {};

it('should find the vidis system', async () => {});
it('should find the vidis system', async () => {
setup();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Logger } from '@src/core/logger';
import { SyncStrategy } from '../../strategy/sync-strategy';
import { SyncStrategyTarget } from '../../sync-strategy.types';
import { VidisSyncService } from '../service/vidis-sync.service';
Expand All @@ -15,6 +14,8 @@ export class VidisSyncStrategy extends SyncStrategy {
}

public async sync(): Promise<void> {
return await this.vidisSyncService.syncMediaSchoolLicenses();
const mediaSource = await this.vidisSyncService.getVidisMediaSource();
const vidisItems = await this.vidisSyncService.getLicenseDataFromVidis(mediaSource);
await this.vidisSyncService.syncMediaSchoolLicenses(mediaSource, vidisItems);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { contextExternalToolEntityFactory } from '@modules/tool/context-external
import { externalToolEntityFactory } from '@modules/tool/external-tool/testing';
import { schoolExternalToolEntityFactory } from '@modules/tool/school-external-tool/testing';
import { MediaUserLicenseEntity } from '@modules/user-license/entity';
import { mediaSourceEntityFactory, mediaUserLicenseEntityFactory } from '@modules/user-license/testing';
import { mediaUserLicenseEntityFactory } from '@modules/user-license/testing';
import { mediaSourceEntityFactory } from '@modules/media-source/testing';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { DateToString, fileRecordFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MediaSourceForSyncNotFoundLoggableException } from './media-source-for-sync-not-found-loggable.exception';

describe(MediaSourceForSyncNotFoundLoggableException.name, () => {
describe('getLogMessage', () => {
const setup = () => {
const mediaSourceName = 'test-media-source';
const exception = new MediaSourceForSyncNotFoundLoggableException(mediaSourceName);

return {
exception,
mediaSourceName,
};
};

it('should return the correct log message', () => {
const { exception, mediaSourceName } = setup();

const logMessage = exception.getLogMessage();

expect(logMessage).toEqual({
message: 'Unable to sync media school license, because media source cannot be found.',
data: {
mediaSourceName,
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import { cleanupCollections } from '@shared/testing';
import { MediaSource } from '../domain';
import { MediaSourceEntity } from '../entity';
import { MediaSourceRepo } from './media-source.repo';
import { MediaSourceOauthConfigEmbeddable } from '../entity/media-source-oauth-config.embeddable';
import { mediaSourceOAuthConfigEmbeddableFactory } from '../testing/media-source-oauth-config.embeddable.factory';
import { MediaSourceConfigMapper } from './media-source-config.mapper';
import { mediaSourceEntityFactory } from '../testing/media-source-entity.factory';
import { mediaSourceFactory } from '../testing/media-source.factory';
import { MediaSourceRepo } from './media-source.repo';
import { MediaSourceConfigMapper } from './media-source-config.mapper';

describe(MediaSourceRepo.name, () => {
let module: TestingModule;
Expand Down Expand Up @@ -39,7 +39,10 @@ describe(MediaSourceRepo.name, () => {
const setup = async () => {
const config: MediaSourceOauthConfigEmbeddable = mediaSourceOAuthConfigEmbeddableFactory.build();

const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build({ oauthConfig: config });
const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build({
oauthConfig: config,
basicAuthConfig: undefined,
});

await em.persistAndFlush([mediaSource]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
MediaUserLicenseService,
UserLicenseType,
} from '@modules/user-license';
import { MediaSource, mediaSourceFactory, MediaSourceService } from '@modules/media-source';
import { MediaSource, MediaSourceService } from '@modules/media-source';
import { mediaSourceFactory } from '@modules/media-source/testing';
import { Test, TestingModule } from '@nestjs/testing';
import { User as UserEntity } from '@shared/domain/entity';
import { setupEntities, userFactory } from '@shared/testing';
Expand Down
Loading

0 comments on commit 1a3f65f

Please sign in to comment.