Skip to content

Commit

Permalink
N21-2103 vidis-sync service unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
GordonNicholasCap committed Dec 13, 2024
1 parent 9651e8a commit 7ce3274
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ 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 { 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 { vidisResponseFactory } from '../testing/vidis.response.factory';
import { VidisSyncService } from './vidis-sync.service';

describe(VidisSyncService.name, () => {
let module: TestingModule;
let vidisSyncService: VidisSyncService;
let httpService: DeepMocked<HttpService>;
let mediaSourceService: DeepMocked<MediaSourceService>;
let mediaSchoolLicenseService: DeepMocked<MediaSchoolLicenseService>;
Expand Down Expand Up @@ -36,6 +44,7 @@ describe(VidisSyncService.name, () => {
],
}).compile();

vidisSyncService = module.get(VidisSyncService);
httpService = module.get(HttpService);
mediaSourceService = module.get(MediaSourceService);
mediaSchoolLicenseService = module.get(MediaSchoolLicenseService);
Expand All @@ -50,21 +59,106 @@ describe(VidisSyncService.name, () => {
jest.clearAllMocks();
});

// TODO: implementation
describe('syncMediaSchoolLicenses', () => {
describe('when the media source with correct configs is found', () => {
const setup = () => {};
describe('when the VIDIS media source is not found', () => {
const setup = () => {
mediaSourceService.findByFormat.mockResolvedValueOnce(null);
};

it('should not throw any error', () => {
it('should throw an MediaSourceForSyncNotFoundLoggableException', async () => {
setup();

const promise = vidisSyncService.syncMediaSchoolLicenses();

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

describe('when there is an error fetching data from media source', () => {
const setup = () => {};
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(),
});

it('should throw an error', () => {
setup();
mediaSourceService.findByFormat.mockResolvedValueOnce(vidisMediaSource);
encryptionService.decrypt.mockReturnValueOnce('username');
encryptionService.decrypt.mockReturnValueOnce('password');
httpService.get.mockReturnValueOnce(of(axiosResponse));
};

it('should not throw any error', async () => {
setup();

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).resolves.not.toThrowError();
});

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

await vidisSyncService.syncMediaSchoolLicenses();

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

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();

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

return { axiosErrorResponse };
};

it('should throw a AxiosErrorLoggable', async () => {
const { axiosErrorResponse } = setup();

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).rejects.toThrowError(
new AxiosErrorLoggable(axiosErrorResponse, 'VIDIS_GET_DATA_FAILED')
);
});
});

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));

return { error };
};

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

const promise = vidisSyncService.syncMediaSchoolLicenses();

await expect(promise).rejects.toThrowError(error);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ 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 { AxiosErrorLoggable } from '@src/core/error/loggable';
import { DefaultEncryptionService, EncryptionService } from '@infra/encryption';
import { HttpService } from '@nestjs/axios';
import { Inject, Injectable } from '@nestjs/common';
import { AxiosResponse } from 'axios';
import { AxiosResponse, isAxiosError } from 'axios';
import { lastValueFrom, Observable } from 'rxjs';
import { VidisItemMapper } from '../mapper/vidis-item.mapper';
import { VidisResponse } from '../response';
Expand Down Expand Up @@ -35,13 +36,11 @@ export class VidisSyncService {
await this.mediaSchoolLicenseService.syncMediaSchoolLicenses(mediasource, itemsDtos);
}

// FIXME: clarify; is there a reason why this is public?
public async fetchData(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
private async fetchData(mediaSource: MediaSource): Promise<VidisItemResponse[]> {
if (!mediaSource.basicAuthConfig || !mediaSource.basicAuthConfig.authEndpoint) {
throw new MediaSourceForSyncNotFoundLoggableException(MediaSourceDataFormat.VIDIS);
}

// TODO: throw a vidis-specific error to make debugging easier
const vidisResponse: VidisResponse = await this.getRequest<VidisResponse>(
new URL(`${mediaSource.basicAuthConfig.authEndpoint}`),
this.encryptionService.decrypt(mediaSource.basicAuthConfig.username),
Expand All @@ -62,8 +61,15 @@ export class VidisSyncService {
},
});

const responseToken: AxiosResponse<T> = await lastValueFrom(observable);

return responseToken.data;
try {
const responseToken: AxiosResponse<T> = await lastValueFrom(observable);
return responseToken.data;
} catch (error: unknown) {
if (isAxiosError(error)) {
throw new AxiosErrorLoggable(error, 'VIDIS_GET_DATA_FAILED');
} else {
throw error;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Factory } from 'fishery';
import { VidisResponse } from '../response';
import { vidisItemResponseFactory } from './vidis-item.response.factory';

export const vidisResponseFactory = Factory.define<VidisResponse>(() => {
return {
items: vidisItemResponseFactory.buildList(3),
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setupEntities } from '@shared/testing';
import { MediaSourceOauthConfig } from '../domain/media-source-oauth-config';
import { MediaSourceOauthConfigEmbeddable } from '../entity/media-source-oauth-config.embeddable';
import { mediaSourceOAuthConfigEmbeddableFactory } from '../testing/media-source-oauth-config.embeddable.factory';
import { mediaSourceConfigFactory } from '../testing/media-source-config.factory';
import { mediaSourceOauthConfigFactory } from '../testing/media-source-oauth-config.factory';
import { MediaSourceConfigMapper } from './media-source-config.mapper';

describe('MediaSourceConfigMapper', () => {
Expand Down Expand Up @@ -47,7 +47,7 @@ describe('MediaSourceConfigMapper', () => {
const setup = async () => {
await setupEntities();

const configDo = mediaSourceConfigFactory.build();
const configDo = mediaSourceOauthConfigFactory.build();
const expected = new MediaSourceOauthConfigEmbeddable({
_id: new ObjectId(configDo.id),
clientId: configDo.getProps().clientId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { BaseFactory } from '@shared/testing';
import { MediaSourceBasicAuthConfig, MediaSourceBasicAuthConfigProps } from '../domain';

export const mediaSourceBasicAuthConfigFactory = BaseFactory.define<
MediaSourceBasicAuthConfig,
MediaSourceBasicAuthConfigProps
>(MediaSourceBasicAuthConfig, ({ sequence }) => {
const config: MediaSourceBasicAuthConfigProps = {
id: new ObjectId().toHexString(),
username: `media-source-user-${sequence}`,
password: `media-source-password-${sequence}`,
authEndpoint: 'https://media-source-endpoint.com/test',
};

return config;
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BaseFactory } from '@shared/testing';
import { MediaSourceOauthConfig, MediaSourceOauthConfigProps } from '../domain/media-source-oauth-config';
import { MediaSourceAuthMethod } from '../enum';

export const mediaSourceConfigFactory = BaseFactory.define<MediaSourceOauthConfig, MediaSourceOauthConfigProps>(
export const mediaSourceOauthConfigFactory = BaseFactory.define<MediaSourceOauthConfig, MediaSourceOauthConfigProps>(
MediaSourceOauthConfig,
({ sequence }) => {
const config = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { BaseFactory } from '@shared/testing';
import { MediaSourceProps, MediaSource } from '@src/modules/media-source/domain';
import { DeepPartial } from 'fishery';
import { MediaSourceProps, MediaSource } from '../domain';
import { MediaSourceDataFormat } from '../enum';
import { mediaSourceOauthConfigFactory } from './media-source-oauth-config.factory';
import { mediaSourceBasicAuthConfigFactory } from './media-source-basic-auth-config.factory';

import { MediaSourceDataFormat } from '@src/modules/media-source/enum';
class MediaSourceFactory extends BaseFactory<MediaSource, MediaSourceProps> {
public withBasicAuthConfig(): this {
const params: DeepPartial<MediaSourceProps> = { basicAuthConfig: mediaSourceBasicAuthConfigFactory.build() };

import { mediaSourceConfigFactory } from './media-source-config.factory';
return this.params(params);
}

export const mediaSourceFactory = BaseFactory.define<MediaSource, MediaSourceProps>(MediaSource, ({ sequence }) => {
public withOauthConfig(): this {
const params: DeepPartial<MediaSourceProps> = { oauthConfig: mediaSourceOauthConfigFactory.build() };

return this.params(params);
}
}

export const mediaSourceFactory = MediaSourceFactory.define(MediaSource, ({ sequence }) => {
return {
id: new ObjectId().toHexString(),
name: `media-source-${sequence}`,
sourceId: `source-id-${sequence}`,
format: MediaSourceDataFormat.BILDUNGSLOGIN,
oauthConfig: mediaSourceConfigFactory.build(),
oauthConfig: mediaSourceOauthConfigFactory.build(),
};
});

0 comments on commit 7ce3274

Please sign in to comment.