@defra-fish/connectors-lib
Version:
Shared connectors
226 lines (203 loc) • 8.57 kB
JavaScript
import { createDocumentClient } from '../documentclient-decorator'
import { DynamoDB } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocument, QueryCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'
jest.mock('@aws-sdk/client-dynamodb')
jest.mock('@aws-sdk/lib-dynamodb')
describe('document client decorations', () => {
beforeAll(() => {
jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
DynamoDBDocument.from.mockReturnValue({
send: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
query: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
scan: jest.fn().mockResolvedValue({ Items: [], lastEvaluatedKey: false }),
batchWrite: jest.fn().mockResolvedValue({ UnprocessedItems: {} })
})
})
afterEach(jest.clearAllMocks)
it('passes options to DynamoDB constructor', () => {
const options = {
option1: '1',
option2: 2,
option3: Symbol('option3')
}
createDocumentClient(options)
expect(DynamoDB).toHaveBeenCalledWith(options)
})
it('creates DynamoDBDocument using client', () => {
createDocumentClient()
const [mockClient] = DynamoDB.mock.instances
expect(DynamoDBDocument.from).toHaveBeenCalledWith(mockClient, expect.any(Object))
})
it('Sets options to strip empty and undefined values when marshalling to DynamoDB lists, sets and values', () => {
createDocumentClient()
expect(DynamoDBDocument.from).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
marshallOptions: {
convertEmptyValues: true,
removeUndefinedValues: true
}
})
)
})
describe.each`
aggregateMethod | commandType
${'queryAllPromise'} | ${QueryCommand}
${'scanAllPromise'} | ${ScanCommand}
`('$aggregateMethod', ({ aggregateMethod, commandType }) => {
it('is added to document client', () => {
const docClient = createDocumentClient()
expect(docClient[aggregateMethod]).toBeDefined()
})
it(`passes arguments provided for ${aggregateMethod} to ${commandType.name}`, async () => {
const params = { TableName: 'TEST', KeyConditionExpression: 'id = :id', ExpressionAttributeValues: { ':id': 1 } }
const docClient = createDocumentClient()
await docClient[aggregateMethod](params)
expect(commandType).toHaveBeenCalledWith(params)
})
it(`passes created command ${commandType.name} to docClient.send`, async () => {
const docClient = createDocumentClient()
await docClient[aggregateMethod]()
const [command] = commandType.mock.instances
expect(docClient.send).toHaveBeenCalledWith(command)
})
it('calls send repeatedly until LastEvaluatedKey evaluates to false, concatenating all returned items', async () => {
const expectedItems = [
{ id: 1, data: Symbol('data1') },
{ id: 2, data: Symbol('data2') },
{ id: 3, data: Symbol('data3') },
{ id: 4, data: Symbol('data4') },
{ id: 5, data: Symbol('data5') }
]
const docClient = createDocumentClient()
docClient.send
.mockResolvedValueOnce({ Items: expectedItems.slice(0, 2), LastEvaluatedKey: true })
.mockResolvedValueOnce({ Items: expectedItems.slice(2, 4), LastEvaluatedKey: true })
.mockResolvedValueOnce({ Items: expectedItems.slice(4), LastEvaluatedKey: false })
const actualItems = await docClient[aggregateMethod]()
expect(actualItems).toEqual(expectedItems)
})
it(`whilst concatenating ${commandType.name} results, passes ExclusiveStartKey param`, async () => {
const expectedKey = Symbol('🔑')
const docClient = createDocumentClient()
docClient.send.mockResolvedValueOnce({ Items: [], LastEvaluatedKey: expectedKey }).mockResolvedValueOnce({ Items: [] })
await docClient[aggregateMethod]()
expect(commandType).toHaveBeenLastCalledWith(
expect.objectContaining({
ExclusiveStartKey: expectedKey
})
)
})
it("omits ExclusiveStartKey if previous LastEvaluatedKey isn't available", async () => {
const docClient = createDocumentClient()
await docClient[aggregateMethod]()
expect(docClient.send).toHaveBeenNthCalledWith(
1,
expect.not.objectContaining({
ExclusiveStartKey: expect.anything()
})
)
})
})
describe('batchWriteAllPromise', () => {
it('is added to document client', () => {
const docClient = createDocumentClient()
expect(docClient.batchWriteAllPromise).toBeDefined()
})
it('passes arguments provided for batchWriteAllPromise to batchWrite', async () => {
const params = { RequestItems: { TEST: [{ PutRequest: { Item: { id: 1, data: Symbol('data1') } } }] } }
const docClient = createDocumentClient()
await docClient.batchWriteAllPromise(params)
expect(docClient.batchWrite).toHaveBeenCalledWith(params)
})
it('calls batchWrite repeatedly until UnprocessedItems is empty', async () => {
const docClient = createDocumentClient()
docClient.batchWrite
.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
.mockResolvedValueOnce({ UnprocessedItems: {} })
await docClient.batchWriteAllPromise({ RequestItems: { key: true } })
expect(docClient.batchWrite).toHaveBeenCalledTimes(4)
})
it.each([
[1, 500],
[2, 750],
[3, 1125],
[4, 1687.5],
[5, 2500],
[6, 2500],
[7, 2500],
[8, 2500],
[9, 2500],
[10, 2500]
])('retries %i times with %i ms delay on final retry', async (retries, delay) => {
const docClient = createDocumentClient()
for (let i = 0; i < retries; i++) {
docClient.batchWrite.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
}
await docClient.batchWriteAllPromise({ RequestItems: { key: true } })
expect(setTimeout).toHaveBeenNthCalledWith(retries, expect.any(Function), delay)
})
it('throws an error on the eleventh retry', async () => {
const docClient = createDocumentClient()
for (let i = 0; i < 11; i++) {
docClient.batchWrite.mockResolvedValueOnce({ UnprocessedItems: { key: true } })
}
await expect(() => docClient.batchWriteAllPromise({ RequestItems: { key: true } })).rejects.toThrow()
})
it('adds unprocessed items to batchWrite request', async () => {
const token = Symbol('token')
const firstCallSymbol = Symbol('first call')
const secondCallSymbol = Symbol('second call')
const docClient = createDocumentClient()
docClient.batchWrite.mockResolvedValueOnce({ UnprocessedItems: { secondCallSymbol } })
await docClient.batchWriteAllPromise({ token, RequestItems: { firstCallSymbol } })
expect(docClient.batchWrite).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
token,
RequestItems: { secondCallSymbol }
})
)
})
})
describe('createUpdateExpression', () => {
it('is added to document client', () => {
const docClient = createDocumentClient()
expect(docClient.createUpdateExpression).toBeDefined()
})
it('returns an object with UpdateExpression, ExpressionAttributeNames and ExpressionAttributeValues', () => {
const docClient = createDocumentClient()
const actual = docClient.createUpdateExpression({ id: 1, data: Symbol('data1') })
expect(actual).toEqual(
expect.objectContaining({
UpdateExpression: expect.any(String),
ExpressionAttributeNames: expect.any(Object),
ExpressionAttributeValues: expect.any(Object)
})
)
})
it('transforms object to an update expression, with provided attribute names and values', () => {
const permission = {
id: 'abc-123',
name: 'ABCDE-123JJ-ABK12',
type: 'ddd-111-ggg-888'
}
const transaction = {
payload: permission,
permissions: [permission],
status: { id: 'finalised' },
payment: {
amount: 16.32,
method: 'barter',
source: 'credit',
timestamp: '2025-04-09T11:53:17.854Z'
}
}
const docClient = createDocumentClient()
const actual = docClient.createUpdateExpression(transaction)
expect(actual).toMatchSnapshot()
})
})
})