Skip to content

Commit

Permalink
N21-2103 wip sync service test
Browse files Browse the repository at this point in the history
  • Loading branch information
GordonNicholasCap committed Dec 17, 2024
1 parent 1a3f65f commit 33b06d6
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 118 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { MediaSourceService, MediaSourceDataFormat } from '@modules/media-source';
import {
MediaSourceBasicAuthConfigNotFoundLoggableException,
MediaSourceForSyncNotFoundLoggableException,
} from '@modules/media-source/loggable';
import { mediaSourceFactory } from '@modules/media-source/testing';
import { MediaSchoolLicenseService } from '@modules/school-license/service/media-school-license.service';
import { SchoolService } from '@modules/school';
import { axiosResponseFactory } from '@shared/testing';
import { Logger } from '@src/core/logger';
import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption';
import { Test, TestingModule } from '@nestjs/testing';
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/testing';
import { MediaSourceDataFormat } from '@modules/media-source/enum';
import { MediaSourceForSyncNotFoundLoggableException } from '@modules/media-source/loggable';
import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption';
import { AxiosErrorLoggable } from '@src/core/error/loggable';
import { axiosErrorFactory, axiosResponseFactory } from '@shared/testing';
import { of, throwError } from 'rxjs';
import { of } from 'rxjs';
import { AxiosResponse } from 'axios';
import { vidisResponseFactory } from '../testing/vidis.response.factory';
import { VidisResponse } from '../response';
import { VidisSyncService } from './vidis-sync.service';

describe(VidisSyncService.name, () => {
Expand All @@ -19,6 +24,8 @@ describe(VidisSyncService.name, () => {
let httpService: DeepMocked<HttpService>;
let mediaSourceService: DeepMocked<MediaSourceService>;
let mediaSchoolLicenseService: DeepMocked<MediaSchoolLicenseService>;

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

View workflow job for this annotation

GitHub Actions / nest_lint

'mediaSchoolLicenseService' is assigned a value but never used
let schoolService: DeepMocked<SchoolService>;

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

View workflow job for this annotation

GitHub Actions / nest_lint

'schoolService' is assigned a value but never used
let logger: DeepMocked<Logger>;

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

View workflow job for this annotation

GitHub Actions / nest_lint

'logger' is assigned a value but never used
let encryptionService: DeepMocked<SymetricKeyEncryptionService>;

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

View workflow job for this annotation

GitHub Actions / nest_lint

'encryptionService' is assigned a value but never used

beforeAll(async () => {
Expand All @@ -37,6 +44,14 @@ describe(VidisSyncService.name, () => {
provide: MediaSchoolLicenseService,
useValue: createMock<MediaSchoolLicenseService>(),
},
{
provide: SchoolService,
useValue: createMock<SchoolService>(),
},
{
provide: Logger,
useValue: createMock<Logger>(),
},
{
provide: DefaultEncryptionService,
useValue: createMock<EncryptionService>(),
Expand All @@ -48,6 +63,8 @@ describe(VidisSyncService.name, () => {
httpService = module.get(HttpService);
mediaSourceService = module.get(MediaSourceService);
mediaSchoolLicenseService = module.get(MediaSchoolLicenseService);
schoolService = module.get(SchoolService);
logger = module.get(Logger);
encryptionService = module.get(DefaultEncryptionService);
});

Expand All @@ -58,109 +75,99 @@ 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', () => {
// TODO: incomplete
describe('getVidisMediaSource', () => {
describe('when the vidis media source exists', () => {
const setup = () => {
mediaSourceService.findByFormat.mockResolvedValueOnce(null);
const vidisMediaSource = mediaSourceFactory.build({ format: MediaSourceDataFormat.VIDIS });

mediaSourceService.findByFormat.mockResolvedValueOnce(vidisMediaSource);

return {
vidisMediaSource,
};
};

it('should throw an MediaSourceForSyncNotFoundLoggableException', async () => {
it('should find the media source by format', async () => {
setup();

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

expect(mediaSourceService.findByFormat).toBeCalledWith(MediaSourceDataFormat.VIDIS);
});
});

describe('when the VIDIS media source is found', () => {
describe('when VIDIS returns valid licenses', () => {
const setup = () => {
const vidisMediaSource = mediaSourceFactory.withBasicAuthConfig().build({
format: MediaSourceDataFormat.VIDIS,
});
const axiosResponse = axiosResponseFactory.build({
data: vidisResponseFactory.build(),
});

mediaSourceService.findByFormat.mockResolvedValueOnce(vidisMediaSource);
encryptionService.decrypt.mockReturnValueOnce('username');
encryptionService.decrypt.mockReturnValueOnce('password');
httpService.get.mockReturnValueOnce(of(axiosResponse));
};
it('should return the vidis media source', async () => {
const { vidisMediaSource } = setup();

it('should not throw any error', async () => {
setup();
const result = await vidisSyncService.getVidisMediaSource();

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

it('should fetch media source config, fetch license data and start the license sync', async () => {
setup();
describe('when the vidis media source does not exist', () => {
const setup = () => {
mediaSourceService.findByFormat.mockResolvedValueOnce(null);
};

// await vidisSyncService.getLicenseDataFromVidis();
it('should throw an MediaSourceForSyncNotFoundLoggableException', async () => {
setup();

// expect(mediaSourceService.findByFormat).toBeCalledWith(MediaSourceDataFormat.VIDIS);
// expect(httpService.get).toHaveBeenCalled();
// expect(mediaSchoolLicenseService.syncMediaSchoolLicenses).toHaveBeenCalled();
});
const promise = vidisSyncService.getVidisMediaSource();

await expect(promise).rejects.toThrow(
new MediaSourceForSyncNotFoundLoggableException(MediaSourceDataFormat.VIDIS)
);
});
});
});

describe('when there is a axios error fetching licenses from VIDIS', () => {
const setup = () => {
const vidisMediaSource = mediaSourceFactory.withBasicAuthConfig().build({
format: MediaSourceDataFormat.VIDIS,
});
const axiosErrorResponse = axiosErrorFactory.build();
describe('getSchoolActivationsFromVidis', () => {
describe('when the provided media source has a valid basic auth config', () => {
const setup = () => {
const mediaSource = mediaSourceFactory.withBasicAuthConfig().build();
const axiosResponse: AxiosResponse<VidisResponse> = axiosResponseFactory.build({
data: vidisResponseFactory.build(),
});

mediaSourceService.findByFormat.mockResolvedValueOnce(vidisMediaSource);
encryptionService.decrypt.mockReturnValueOnce('username');
encryptionService.decrypt.mockReturnValueOnce('password');
httpService.get.mockReturnValue(throwError(() => axiosErrorResponse));
httpService.get.mockReturnValueOnce(of(axiosResponse));

return { axiosErrorResponse };
return {
mediaSource,
axiosResponse,
};
};

it('should throw a AxiosErrorLoggable', async () => {
const { axiosErrorResponse } = setup();
it('should return school activations from vidis', async () => {

Check failure on line 142 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 { mediaSource, axiosResponse } = setup();

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

View workflow job for this annotation

GitHub Actions / nest_lint

'mediaSource' is assigned a value but never used

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

View workflow job for this annotation

GitHub Actions / nest_lint

'axiosResponse' is assigned a value but never used

// const promise = vidisSyncService.getLicenseDataFromVidis();
//
// await expect(promise).rejects.toThrowError(
// new AxiosErrorLoggable(axiosErrorResponse, 'VIDIS_GET_DATA_FAILED')
// );
});
// const result = await vidisSyncService.getSchoolActivationsFromVidis(mediaSource);
//
// expect(result).toEqual(axiosResponse.data);
});
});

describe('when there is an unknown error fetching licenses from VIDIS', () => {
const setup = () => {
const vidisMediaSource = mediaSourceFactory.withBasicAuthConfig().build({
format: MediaSourceDataFormat.VIDIS,
});
const error = new Error('test-unknown-error');

mediaSourceService.findByFormat.mockResolvedValueOnce(vidisMediaSource);
encryptionService.decrypt.mockReturnValueOnce('username');
encryptionService.decrypt.mockReturnValueOnce('password');
httpService.get.mockReturnValue(throwError(() => error));
describe('when the provided media source has no basic auth config', () => {
const setup = () => {
const mediaSource = mediaSourceFactory.build({ basicAuthConfig: undefined });

return { error };
return {
mediaSource,
};
};

it('should throw a the unknown error', async () => {
const { error } = setup();
it('should throw an MediaSourceBasicAuthConfigNotFoundLoggableException', async () => {
const { mediaSource } = setup();

// const promise = vidisSyncService.getLicenseDataFromVidis();
//
// await expect(promise).rejects.toThrowError(error);
});
const promise = vidisSyncService.getSchoolActivationsFromVidis(mediaSource);

await expect(promise).rejects.toThrow(
new MediaSourceBasicAuthConfigNotFoundLoggableException(mediaSource.id, MediaSourceDataFormat.VIDIS)
);
});
});
});

// describe('syncMediaSchoolLicenses', () => {});
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { VidisItemDto } from '@src/modules/school-license/dto';
import { MediaSchoolLicenseService } from '@src/modules/school-license/service/media-school-license.service';
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 { MediaSource, MediaSourceDataFormat, MediaSourceService } from '@modules/media-source';
import {
MediaSourceForSyncNotFoundLoggableException,
MediaSourceBasicAuthConfigNotFoundLoggableException,
} from '@modules/media-source/loggable';
import { MediaSchoolLicenseService, MediaSchoolLicense, SchoolLicenseType } from '@modules/school-license';
import { SchoolForSchoolMediaLicenseSyncNotFoundLoggable } from '@modules/school-license/loggable';
import { School, SchoolService } from '@modules/school';
import { DefaultEncryptionService, EncryptionService } from '@infra/encryption';
import { Logger } from '@src/core/logger';
import { AxiosErrorLoggable } from '@src/core/error/loggable';
import { DefaultEncryptionService, EncryptionService } from '@infra/encryption';
import { ObjectId } from '@mikro-orm/mongodb';
import { HttpService } from '@nestjs/axios';
import { Inject, Injectable } from '@nestjs/common';
import { AxiosResponse, isAxiosError } from 'axios';
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 {
Expand All @@ -40,8 +37,18 @@ export class VidisSyncService {
return mediaSource;
}

public async getLicenseDataFromVidis(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
const items: VidisItemResponse[] = await this.fetchData(mediaSource);
public async getSchoolActivationsFromVidis(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
if (!mediaSource.basicAuthConfig || !mediaSource.basicAuthConfig.authEndpoint) {
throw new MediaSourceBasicAuthConfigNotFoundLoggableException(mediaSource.id, MediaSourceDataFormat.VIDIS);
}

const vidisResponse: VidisResponse = await this.getRequest<VidisResponse>(
new URL(`${mediaSource.basicAuthConfig.authEndpoint}`),
this.encryptionService.decrypt(mediaSource.basicAuthConfig.username),
this.encryptionService.decrypt(mediaSource.basicAuthConfig.password)
);

const { items } = vidisResponse;

return items;
}
Expand Down Expand Up @@ -109,22 +116,6 @@ export class VidisSyncService {
await Promise.all(removalPromises);
}

private async fetchData(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
if (!mediaSource.basicAuthConfig || !mediaSource.basicAuthConfig.authEndpoint) {
throw new MediaSourceForSyncNotFoundLoggableException(MediaSourceDataFormat.VIDIS);
}

const vidisResponse: VidisResponse = await this.getRequest<VidisResponse>(
new URL(`${mediaSource.basicAuthConfig.authEndpoint}`),
this.encryptionService.decrypt(mediaSource.basicAuthConfig.username),
this.encryptionService.decrypt(mediaSource.basicAuthConfig.password)
);

const { items } = vidisResponse;

return items;
}

private async getRequest<T>(url: URL, username: string, password: string): Promise<T> {
const encodedCredentials = btoa(`${username}:${password}`);
const observable: Observable<AxiosResponse<T>> = this.httpService.get(url.toString(), {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class VidisSyncStrategy extends SyncStrategy {

public async sync(): Promise<void> {
const mediaSource = await this.vidisSyncService.getVidisMediaSource();
const vidisItems = await this.vidisSyncService.getLicenseDataFromVidis(mediaSource);
const vidisItems = await this.vidisSyncService.getSchoolActivationsFromVidis(mediaSource);
await this.vidisSyncService.syncMediaSchoolLicenses(mediaSource, vidisItems);
}
}
1 change: 1 addition & 0 deletions apps/server/src/modules/media-source/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { MediaSource } from './domain';
export { MediaSourceDataFormat } from './enum';
export { MediaSourceService } from './service';
1 change: 1 addition & 0 deletions apps/server/src/modules/media-source/loggable/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { MediaSourceForSyncNotFoundLoggableException } from './media-source-for-sync-not-found-loggable.exception';
export { MediaSourceBasicAuthConfigNotFoundLoggableException } from './media-source-basic-auth-config-not-found-loggable.exception';
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { mediaSourceFactory } from '@modules/media-source/testing';
import { MediaSourceBasicAuthConfigNotFoundLoggableException } from './media-source-basic-auth-config-not-found-loggable.exception';

describe(MediaSourceBasicAuthConfigNotFoundLoggableException.name, () => {
describe('getLogMessage', () => {
const setup = () => {
const mediaSource = mediaSourceFactory.build();
const exception = new MediaSourceBasicAuthConfigNotFoundLoggableException(
mediaSource.id,
mediaSource.name as string
);

return {
mediaSource,
exception,
};
};

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

const logMessage = exception.getLogMessage();

const mediaSourceName = mediaSource.name as string;
expect(logMessage).toEqual({
message: `Required basic auth config of media source ${mediaSourceName} is missing.`,
data: {
mediaSourceId: mediaSource.id,
mediaSourceName,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NotFoundException } from '@nestjs/common';
import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class MediaSourceBasicAuthConfigNotFoundLoggableException extends NotFoundException {
constructor(private readonly mediaSourceId: string, private readonly mediaSourceName: string) {
super();
}

public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
message: `Required basic auth config of media source ${this.mediaSourceName} is missing.`,
data: {
mediaSourceId: this.mediaSourceId,
mediaSourceName: this.mediaSourceName,
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class MediaSourceRepo extends BaseDomainObjectRepo<MediaSource, MediaSour
const entity: MediaSourceEntity | null = await this.em.findOne(
MediaSourceEntity,
{ format },
{ populate: ['oauthConfig', 'basicAuthConfig'] }
{ populate: ['oauthConfig'] }
);

if (!entity) {
Expand Down
Loading

0 comments on commit 33b06d6

Please sign in to comment.