/*! * @license * Copyright Squiz Australia Pty Ltd. All Rights Reserved. */ import { AttributeValue, DeleteItemCommandOutput, DynamoDBClient, DynamoDBClientConfig, GetItemCommandOutput, PutItemCommandOutput, QueryCommandInput, ScanCommandOutput, } from '@aws-sdk/client-dynamodb'; import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc'; import { createMockClientPutResponse, createMockClientScanResponse, createMockClientGetResponse, } from '../../core/dynamoClient.mock'; import { ENTITY_TYPES } from '../../constants/Repository.constants'; import { JobContext, createMockJobContext, createMockJobContextDto, createMockJobContextDtos, createMockJobContextRecord, createMockCreateJobContextRequest, } from '../../model/JobContext'; import { JobContextRepository } from './JobContextRepository'; import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils'; import { PaginationInfo, PAGINATION_DIRECTION_TYPE_BEFORE } from '../../core/pagination/'; import { InjectTokens } from '../../constants/InjectTokens'; initIocContainer(); // Mock DynamoDB Client const mockOptions = {} as DynamoDBClientConfig; const mockClient = new DynamoDBClient(mockOptions); const mockSend = jest.fn(); mockClient.send = mockSend; // Inject JobContextRepository with a mocked client iocContainer.rebind(DynamoDBClient).toConstantValue(mockClient); describe('JobContextRepository', (): void => { let jobContextRepository: JobContextRepository; let dynamoTableName: string; let tenant: string; beforeAll(() => { jobContextRepository = iocContainer.get(JobContextRepository); dynamoTableName = iocContainer.get(InjectTokens.DynamoTableName); tenant = iocContainer.get(InjectTokens.Tenant); }); describe('find', () => { it(`should return an empty list`, async (): Promise => { mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse()); const response = await jobContextRepository.list(); expect(mockSend).toBeCalled(); expect(response).toStrictEqual([]); }); it(`should return a list of existing job context`, async (): Promise => { const mockContextItems = createMockJobContextDtos(); mockSend.mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockContextItems, LastEvaluatedKey: undefined, }), ); const response = await jobContextRepository.list(); expect(mockSend).toBeCalledTimes(1); expect(response).toHaveLength(mockContextItems.length); }); it(`should paginate results to return a list of existing job contexts`, async (): Promise => { const mockContextItemsPage1 = createMockJobContextDtos(2); const mockContextItemsPage2 = createMockJobContextDtos(2); mockSend .mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockContextItemsPage1, LastEvaluatedKey: mockContextItemsPage1.slice(-1)[0], }), ) .mockImplementationOnce( (): ScanCommandOutput => createMockClientScanResponse({ Items: mockContextItemsPage2, LastEvaluatedKey: undefined, }), ); const response = await jobContextRepository.list({ limit: 3, }); expect(mockSend).toBeCalledTimes(2); expect(response).toHaveLength(3); }); }); describe('findOne', () => { it(`should return job context by name`, async (): Promise => { const mockJobContextItem = createMockJobContextDto({ environment: { key: 'value' }, }); mockSend.mockImplementationOnce( (): GetItemCommandOutput => createMockClientGetResponse({ Item: mockJobContextItem, }), ); const response = await jobContextRepository.findOne({ contextName: mockJobContextItem.sk.S as string }); expect(response).toStrictEqual({ contextName: mockJobContextItem.sk.S as string, description: mockJobContextItem.description.S as string, environment: { key: 'value' }, 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 jobContextRepository.findOne({ contextName: `null` }); expect(mockSend).toBeCalledTimes(1); expect(response).toBe(null); }); }); describe('create', () => { it(`should create a new job context record and return job context in the response`, async (): Promise => { const newJobContextToCreate = createMockCreateJobContextRequest(); const mockResponse = createMockClientPutResponse({ Attributes: createMockJobContextDto({ pk: `${ENTITY_TYPES.context}~${tenant}`, sk: newJobContextToCreate.contextName, }), }); mockSend.mockImplementationOnce((): PutItemCommandOutput => mockResponse); const response = await jobContextRepository.create(newJobContextToCreate); expect(mockSend).toBeCalledTimes(1); expect(response).toStrictEqual({ ...newJobContextToCreate, type: ENTITY_TYPES.context, }); }); }); describe('update', () => { it(`should update a job context record`, async (): Promise => { const jobContextToUpdate = createMockJobContext(); const updatedJobContext: JobContext = { ...jobContextToUpdate, contextName: `new-name`, description: `this is description`, environment: { name: 'Bob', age: '35' }, }; 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 jobContextRepository.update( { contextName: jobContextToUpdate.contextName }, updatedJobContextRecord, ); expect(response).toStrictEqual(updatedJobContext); expect(mockSend).toBeCalledTimes(1); }); }); describe('delete', () => { it(`should return undefined when calling the delete operation`, async (): Promise => { mockSend.mockImplementationOnce((): DeleteItemCommandOutput => { return { $metadata: {}, }; }); const response = await jobContextRepository.delete({ contextName: `name` }); expect(response).toBe(undefined); expect(mockSend).toBeCalledTimes(1); }); }); describe('getContexts', () => { let mockExclusiveStartKey: Record; let mockPaginationInfo: PaginationInfo; let mockParams: QueryCommandInput; beforeEach(() => { mockExclusiveStartKey = { pk: { S: ENTITY_TYPES.context }, sk: { S: 'mockSk' }, }; mockPaginationInfo = { direction: PAGINATION_DIRECTION_TYPE_BEFORE, limit: 100, requestUrl: 'mockUrl', token: 'mockToken', }; mockParams = { ExclusiveStartKey: mockExclusiveStartKey, ExpressionAttributeValues: { ':pk': { S: `${ENTITY_TYPES.context}~${tenant}` }, }, KeyConditionExpression: 'pk = :pk', ScanIndexForward: false, TableName: dynamoTableName, }; jest.spyOn(PaginationUtils, 'decodeTokenString').mockReturnValue(mockExclusiveStartKey); }); it('should return an empty array if no contexts are found', async () => { mockSend.mockImplementationOnce(() => Promise.resolve({ Items: [], LastEvaluatedKey: null, }), ); const mockRequestData: PaginationInfo = mockPaginationInfo; const result = await jobContextRepository.getContexts(mockRequestData); expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ input: { ...mockParams }, }), ); expect(result.items).toEqual([]); expect(result.lastEvaluatedKey).toBe(null); }); it('should return an array of contexts if found', async () => { const mockJobContextRecords = createMockJobContextDtos(); const mockLastEvaluatedKey = { pk: { S: ENTITY_TYPES.job }, sk: { S: 'mockSk' }, }; mockSend.mockImplementationOnce(() => Promise.resolve({ Items: mockJobContextRecords, LastEvaluatedKey: mockLastEvaluatedKey, }), ); const mockRequestData: PaginationInfo = mockPaginationInfo; const result = await jobContextRepository.getContexts(mockRequestData); expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ input: { ...mockParams }, }), ); expect(result.items).toHaveLength(mockJobContextRecords.length); expect(result.lastEvaluatedKey).toStrictEqual(mockLastEvaluatedKey); }); }); });