/*! * @license * Copyright Squiz Australia Pty Ltd. All Rights Reserved. */ import { DeleteItemCommandOutput, DynamoDBClient, DynamoDBClientConfig, GetItemCommandOutput, PutItemCommandOutput, ScanCommandOutput, } from '@aws-sdk/client-dynamodb'; import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc'; import { ENTITY_TYPES } from '../../constants/Repository.constants'; import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants'; import { createMockClientGetResponse, createMockClientPutResponse, createMockClientScanResponse, } from '../../core/dynamoClient.mock'; import { JobManifest, createMockCreateJobManifestRequest, createMockJobManifest, createMockJobManifestDto, createMockJobManifestDtos, createMockJobManifestRecord, } from '../../manifest'; import { JobManifestRepository } from './JobManifestRepository'; initIocContainer(); // Mock DynamoDB Client const mockOptions = {} as DynamoDBClientConfig; const mockClient = new DynamoDBClient(mockOptions); const mockSend = jest.fn(); mockClient.send = mockSend; // Inject JobManifestRepository with a mocked client iocContainer.rebind(DynamoDBClient).toConstantValue(mockClient); let jobManifestRepository: JobManifestRepository; describe('JobManifestRepository', (): void => { beforeAll(() => { jobManifestRepository = iocContainer.get(JobManifestRepository); }); describe('find', () => { it(`should return an empty list`, async (): Promise => { mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse()); const response = await jobManifestRepository.list(); expect(mockSend).toBeCalled(); expect(response).toStrictEqual([]); }); it(`should return a list of existing job manifests`, async (): Promise => { const mockManifestItems = createMockJobManifestDtos(); mockSend.mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockManifestItems, LastEvaluatedKey: undefined, }), ); const response = await jobManifestRepository.list(); expect(mockSend).toBeCalledTimes(1); expect(response).toHaveLength(mockManifestItems.length); }); it(`should paginate results to return a list of existing job manifests`, async (): Promise => { const mockManifestItemsPage1 = createMockJobManifestDtos(); const mockManifestItemsPage2 = createMockJobManifestDtos(); mockSend .mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockManifestItemsPage1, LastEvaluatedKey: mockManifestItemsPage1.slice(-1)[0], }), ) .mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockManifestItemsPage2, LastEvaluatedKey: undefined, }), ); const response = await jobManifestRepository.list(); expect(mockSend).toBeCalledTimes(2); expect(response).toHaveLength(mockManifestItemsPage1.length + mockManifestItemsPage2.length); }); }); describe('findOne', () => { it(`should return job manifest`, async (): Promise => { const mockJobManifestItem = createMockJobManifestDto({ image: 'node:24' }); mockSend.mockImplementationOnce( (): GetItemCommandOutput => createMockClientGetResponse({ Item: mockJobManifestItem, }), ); const response = await jobManifestRepository.findOne({ name: mockJobManifestItem.name.S as string, version: mockJobManifestItem.version.S as string, }); expect(response).toStrictEqual({ concurrency: parseInt(mockJobManifestItem.concurrency.N as string), entryFile: mockJobManifestItem.entryFile.S as string, name: mockJobManifestItem.name.S as string, version: mockJobManifestItem.version.S as string, entryFunction: JSON.parse(mockJobManifestItem.entryFunction.S as string), timeoutSeconds: parseInt(mockJobManifestItem.timeoutSeconds.N as string), image: 'node:24', autoRetry: false, type: `manifest`, } as JobManifest); expect(mockSend).toBeCalledTimes(1); }); it(`should default missing image to legacy runtime`, async (): Promise => { const mockJobManifestItem = createMockJobManifestDto(); delete (mockJobManifestItem as Partial).image; mockSend.mockImplementationOnce( (): GetItemCommandOutput => createMockClientGetResponse({ Item: mockJobManifestItem, }), ); const response = await jobManifestRepository.findOne({ name: mockJobManifestItem.name.S as string, version: mockJobManifestItem.version.S as string, }); expect(response).toEqual( expect.objectContaining({ image: LEGACY_RUNTIME_IMAGE, autoRetry: false, }), ); }); it(`should default missing autoRetry to false`, async (): Promise => { const mockJobManifestItem = createMockJobManifestDto(); delete (mockJobManifestItem as Partial).autoRetry; mockSend.mockImplementationOnce( (): GetItemCommandOutput => createMockClientGetResponse({ Item: mockJobManifestItem, }), ); const response = await jobManifestRepository.findOne({ name: mockJobManifestItem.name.S as string, version: mockJobManifestItem.version.S as string, }); expect(response).toEqual( expect.objectContaining({ autoRetry: false, }), ); }); it(`should preserve an explicit unsupported runtime image`, async (): Promise => { const mockJobManifestItem = createMockJobManifestDto({ image: 'node:26' }); mockSend.mockImplementationOnce( (): GetItemCommandOutput => createMockClientGetResponse({ Item: mockJobManifestItem, }), ); const response = await jobManifestRepository.findOne({ name: mockJobManifestItem.name.S as string, version: mockJobManifestItem.version.S as string, }); expect(response).toEqual( expect.objectContaining({ image: 'node:26', }), ); }); it(`should return null if job manifest does not exist`, async (): Promise => { mockSend.mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: [], }), ); const response = await jobManifestRepository.findOne({ name: 'null', version: '1.0.0' }); expect(mockSend).toBeCalledTimes(1); expect(response).toBe(null); }); }); describe('create', () => { it(`should create a new job manifest record and return job manifest in the response`, async (): Promise => { const newJobManifestToCreate = createMockCreateJobManifestRequest(); const mockReponse = createMockClientPutResponse({ Attributes: createMockJobManifestDto({ pk: ENTITY_TYPES.manifest, sk: newJobManifestToCreate.name, }), }); mockSend.mockImplementationOnce((): PutItemCommandOutput => mockReponse); const response = await jobManifestRepository.create(newJobManifestToCreate); expect(mockSend).toBeCalledTimes(1); expect(response).toStrictEqual({ ...newJobManifestToCreate, type: ENTITY_TYPES.manifest, }); }); }); describe('update', () => { it(`should update a job manifest record`, async (): Promise => { const jobManifestToUpdate = createMockJobManifest(); const updatedJobManifest: JobManifest = { ...jobManifestToUpdate, concurrency: 10, timeoutSeconds: 100, }; const updatedJobManifestRecord = createMockJobManifestRecord({ ...updatedJobManifest, sk: `${updatedJobManifest.name}~${updatedJobManifest.version}`, }); const updatedJobManifestRecordDto = createMockJobManifestDto({ ...updatedJobManifestRecord, }); const putResponse = createMockClientPutResponse({ Attributes: { ...updatedJobManifestRecordDto }, }); mockSend.mockImplementation((): PutItemCommandOutput => putResponse); const response = await jobManifestRepository.update( { name: jobManifestToUpdate.name, version: jobManifestToUpdate.version, }, updatedJobManifestRecord, ); expect(response).toStrictEqual(updatedJobManifest); expect(mockSend).toBeCalledTimes(1); }); }); describe('convertToDynamoDbItem', () => { it(`should persist image on the DynamoDB item`, () => { const manifest = createMockJobManifest({ image: 'node:24' }); const dto = jobManifestRepository.convertToDynamoDbItem(manifest); expect(dto.image).toEqual({ S: 'node:24' }); }); it(`should persist autoRetry on the DynamoDB item`, () => { const manifest = createMockJobManifest({ autoRetry: true }); const dto = jobManifestRepository.convertToDynamoDbItem(manifest); expect(dto.autoRetry).toEqual({ BOOL: true }); }); }); describe('delete', () => { it(`should return undefined when calling the delete operation`, async (): Promise => { mockSend.mockImplementationOnce((): DeleteItemCommandOutput => { return { $metadata: {}, }; }); const response = await jobManifestRepository.delete({ name: `name`, version: '1.0.0' }); expect(response).toBe(undefined); expect(mockSend).toBeCalledTimes(1); }); }); });