/*! * @license * Copyright Squiz Australia Pty Ltd. All Rights Reserved. */ import { AttributeValue, DeleteItemCommandOutput, DynamoDBClient, GetItemCommandOutput, PutItemCommandOutput, ScanCommandOutput, } from '@aws-sdk/client-dynamodb'; import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils'; import { faker } from '@faker-js/faker/locale/en'; import { JobContextService } from './JobContextService'; import { PAGINATION_DIRECTION_TYPE_AFTER } from '../../core/pagination'; import { JobContextRepository } from '../../repository'; import { createMockCreateJobContextRequest, createMockJobContext, createMockJobContextDto, createMockJobContextDtos, createMockJobContextRecord, createMockJobContexts, JobContext, } from '../../model/JobContext'; import { createMockClientGetResponse, createMockClientPutResponse, createMockClientScanResponse, } from '../../core/dynamoClient.mock'; import { ENTITY_TYPES } from '../../constants/Repository.constants'; import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc'; import { testLogger } from '../../../../jest-global-setup'; // Mock DynamoDB Client const mockSend = jest.spyOn(DynamoDBClient.prototype, 'send'); initIocContainer(testLogger); describe('JobContextService', (): void => { let jobContextService: JobContextService; beforeAll((): void => { jobContextService = iocContainer.get(JobContextService); }); describe(`createJobContext`, (): void => { it(`should return a response for createJobContext`, async (): Promise => { const newJobContextToCreate = createMockCreateJobContextRequest({ environment: { secret1: faker.string.uuid(), secret2: faker.string.uuid(), }, }); const environmentMap: Record = {}; for (const key of Object.keys(newJobContextToCreate.environment)) { environmentMap[key] = { S: newJobContextToCreate.environment[key] }; } const mockResponse = createMockClientPutResponse({ Attributes: { pk: { S: ENTITY_TYPES.context }, sk: { S: newJobContextToCreate.contextName }, name: { S: newJobContextToCreate.contextName }, version: { M: { secret1: { S: newJobContextToCreate.environment.secret1 }, secret2: { S: newJobContextToCreate.environment.secret2 }, }, }, }, }); mockSend.mockImplementationOnce((): PutItemCommandOutput => mockResponse); const response = await jobContextService.createJobContext(newJobContextToCreate); expect(mockSend).toBeCalledTimes(1); expect(response).toStrictEqual({ ...newJobContextToCreate, type: ENTITY_TYPES.context, }); expect(testLogger.audit).toHaveBeenCalledWith(`Created job context ${newJobContextToCreate.contextName}`, { context: expect.any(String), processable: true, remote: true, details: { context: { contextName: newJobContextToCreate.contextName, description: newJobContextToCreate.description, environment: { secret1: '*redacted*', secret2: '*redacted*', }, }, }, }); }); }); describe(`deleteJobContext`, (): void => { it(`should return an empty response for job context deletion`, async (): Promise => { mockSend.mockImplementationOnce((): DeleteItemCommandOutput => { return { $metadata: {}, }; }); const context = createMockJobContext({ environment: { secret1: faker.string.uuid(), }, }); const response = await jobContextService.deleteJobContext(context); expect(response).toBe(undefined); expect(mockSend).toBeCalledTimes(1); expect(testLogger.audit).toHaveBeenCalledWith(`Deleted job context ${context.contextName}`, { context: expect.any(String), processable: true, remote: true, details: { context: { contextName: context.contextName, description: context.description, environment: { secret1: '*redacted*', }, }, }, }); }); }); describe(`findJobContext`, (): void => { it(`should return job context by name`, async (): Promise => { const mockJobContextRecord = createMockJobContextDto({ environment: { key: 'value' }, }); mockSend.mockImplementationOnce( (): GetItemCommandOutput => createMockClientGetResponse({ Item: mockJobContextRecord, }), ); const response = await jobContextService.findJobContext(mockJobContextRecord.sk.S as string); expect(response).toStrictEqual({ contextName: mockJobContextRecord.sk.S as string, environment: { key: 'value' }, description: mockJobContextRecord.description.S as string, type: `context`, } as JobContext); expect(mockSend).toBeCalledTimes(1); }); it(`should return null if job context does not exist`, async (): Promise => { mockSend.mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: [], }), ); const response = await jobContextService.findJobContext(`null`); expect(mockSend).toBeCalledTimes(1); expect(response).toBe(null); }); }); describe(`listAllJobContexts`, (): void => { it(`should return an empty list`, async (): Promise => { mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse()); const response = await jobContextService.listAllJobContexts(); expect(mockSend).toBeCalled(); expect(response).toStrictEqual([]); }); it(`should return a list of existing job contexts`, async (): Promise => { const mockRecords = createMockJobContextDtos(); mockSend.mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockRecords, LastEvaluatedKey: undefined, }), ); const response = await jobContextService.listAllJobContexts(); expect(mockSend).toBeCalledTimes(1); expect(response).toHaveLength(mockRecords.length); }); it(`should paginate results to return a list of existing job context`, async (): Promise => { const mockRecordsPage1 = createMockJobContextDtos(); const mockRecordsPage2 = createMockJobContextDtos(); mockSend .mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockRecordsPage1, LastEvaluatedKey: mockRecordsPage1.slice(-1)[0], }), ) .mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockRecordsPage2, LastEvaluatedKey: undefined, }), ); const response = await jobContextService.listAllJobContexts(); expect(mockSend).toBeCalledTimes(2); expect(response).toHaveLength(mockRecordsPage1.length + mockRecordsPage2.length); }); }); describe(`listJobContexts`, (): void => { let mockJobContexts: Array; let mockPaginationInfo: any; let mockPaginationLinks: any; let mockRequestData: any; let getContextsSpy: any; beforeEach(() => { mockJobContexts = createMockJobContexts(); mockPaginationInfo = { direction: PAGINATION_DIRECTION_TYPE_AFTER, limit: 100, requestUrl: faker.internet.url(), token: null, }; mockPaginationLinks = { next: null, prev: null, }; mockRequestData = { mockPaginationInfo }; getContextsSpy = jest.spyOn(JobContextRepository.prototype, 'getContexts'); getContextsSpy.mockResolvedValue({ items: mockJobContexts, lastEvaluatedKey: undefined, }); jest.spyOn(PaginationUtils, 'getPaginationLinks').mockReturnValue(mockPaginationLinks); }); it(`should return all contexts if there are less than the specified limit`, async (): Promise => { const result = await jobContextService.listJobContexts(mockRequestData); expect(result).toStrictEqual({ data: mockJobContexts }); expect(getContextsSpy).toHaveBeenCalledWith(mockRequestData); }); }); describe(`updateJobContext`, (): void => { it(`should update a job context record`, async (): Promise => { const jobContextToUpdate = createMockJobContext(); const updatedJobContext: JobContext = { ...jobContextToUpdate, environment: { name: 'Alice', age: '30' }, contextName: `new-name`, description: `this is description`, }; const updatedJobContextRecord = createMockJobContextRecord({ sk: updatedJobContext.contextName, environment: updatedJobContext.environment, description: updatedJobContext.description, }); const updatedJobContextRecordDto = createMockJobContextDto({ ...updatedJobContextRecord, }); const putResponse = createMockClientPutResponse({ Attributes: { ...updatedJobContextRecordDto }, }); mockSend.mockImplementation((): PutItemCommandOutput => putResponse); const response = await jobContextService.updateJobContext( jobContextToUpdate.contextName, updatedJobContextRecord, ); expect(response).toStrictEqual(updatedJobContext); expect(mockSend).toBeCalledTimes(1); expect(testLogger.audit).toHaveBeenCalledWith(`Updated job context ${updatedJobContext.contextName}`, { context: expect.any(String), processable: true, remote: true, details: { context: { contextName: updatedJobContext.contextName, description: updatedJobContext.description, environment: { name: '*redacted*', age: '*redacted*', }, }, }, }); }); }); });