@gp_jcisneros/aws-utils
Version:
AWS SDK utilities for GreenPay microservices
682 lines (567 loc) • 21.3 kB
JavaScript
const { DynamoUtils } = require('../src/DynamoUtils');
const { DatabaseError, AWSError } = require('@gp_jcisneros/errors');
const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');
// Mock functions
const mockSend = jest.fn();
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
// Setup default mock implementation
DynamoDBDocumentClient.from.mockReturnValue({
send: mockSend,
destroy: jest.fn().mockResolvedValue(undefined),
});
});
describe('DynamoUtils', () => {
describe('constructor', () => {
it('should create DynamoUtils instance', () => {
const dynamoUtils = new DynamoUtils();
expect(dynamoUtils).toBeInstanceOf(DynamoUtils);
expect(dynamoUtils.docClient).toBeDefined();
});
it('should create DynamoUtils instance with custom region', () => {
const dynamoUtils = new DynamoUtils('us-west-2');
expect(dynamoUtils).toBeInstanceOf(DynamoUtils);
});
});
describe('getItem', () => {
it('should return item when found', async () => {
const mockItem = { id: '123', name: 'test' };
mockSend.mockResolvedValue({ Item: mockItem });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.getItem('test-table', { id: '123' });
expect(result).toEqual(mockItem);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should return null when item not found', async () => {
mockSend.mockResolvedValue({});
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.getItem('test-table', { id: '123' });
expect(result).toBeNull();
});
it('should throw DatabaseError on failure', async () => {
mockSend.mockRejectedValue(new Error('DynamoDB error'));
const dynamoUtils = new DynamoUtils();
await expect(
dynamoUtils.getItem('test-table', { id: '123' })
).rejects.toThrow(DatabaseError);
});
});
describe('putItem', () => {
it('should put item successfully', async () => {
mockSend.mockResolvedValue({});
const item = { id: '123', name: 'test' };
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.putItem('test-table', item);
expect(result).toEqual({
success: true,
item,
});
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should throw DatabaseError on failure', async () => {
mockSend.mockRejectedValue(new Error('Put failed'));
const dynamoUtils = new DynamoUtils();
await expect(
dynamoUtils.putItem('test-table', { id: '123' })
).rejects.toThrow(DatabaseError);
});
});
describe('updateItem', () => {
it('should update item successfully', async () => {
const updatedItem = { id: '123', name: 'updated' };
mockSend.mockResolvedValue({ Attributes: updatedItem });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.updateItem(
'test-table',
{ id: '123' },
'SET #name = :name',
{ ':name': 'updated' },
{ '#name': 'name' }
);
expect(result).toEqual({
success: true,
item: updatedItem,
});
});
});
describe('deleteItem', () => {
it('should delete item successfully', async () => {
mockSend.mockResolvedValue({});
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.deleteItem('test-table', { id: '123' });
expect(result).toEqual({ success: true });
});
});
describe('queryItems', () => {
it('should query items with basic parameters', async () => {
const items = [{ id: '123', name: 'test' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems('test-table', 'id = :id', {
':id': '123',
});
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should query items with GSI index', async () => {
const items = [
{ id: '123', gsi1pk: 'USER#123', name: 'test' },
{ id: '124', gsi1pk: 'USER#123', name: 'test2' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'gsi1pk = :gsi1pk',
{ ':gsi1pk': 'USER#123' },
{},
'GSI1', // indexName
100 // limit
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
// Verify the command was called (basic verification without internal structure)
expect(mockSend).toHaveBeenCalled();
});
it('should query items with expression attribute names', async () => {
const items = [{ id: '123', status: 'active', name: 'test' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'id = :id AND #status = :status',
{ ':id': '123', ':status': 'active' },
{ '#status': 'status' } // expressionAttributeNames
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should handle pagination automatically', async () => {
const items1 = [{ id: '123' }];
const items2 = [{ id: '456' }];
mockSend
.mockResolvedValueOnce({
Items: items1,
LastEvaluatedKey: { id: '123' },
})
.mockResolvedValueOnce({
Items: items2,
});
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems('test-table', 'id = :id', {
':id': '123',
});
expect(result).toEqual([...items1, ...items2]);
expect(mockSend).toHaveBeenCalledTimes(2);
});
it('should query with custom limit', async () => {
const items = [{ id: '123' }, { id: '124' }, { id: '125' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'gsi1pk = :gsi1pk',
{ ':gsi1pk': 'USER#123' },
{},
null, // no index
5 // custom limit
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should query with LSI index and sort key condition', async () => {
const items = [
{ id: '123', lsi1sk: '2023-01-01', data: 'old' },
{ id: '123', lsi1sk: '2023-12-01', data: 'new' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'id = :id AND lsi1sk BETWEEN :start AND :end',
{
':id': '123',
':start': '2023-01-01',
':end': '2023-12-31',
},
{},
'LSI1' // Local Secondary Index
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should handle multiple pages with large datasets', async () => {
const page1 = Array.from({ length: 100 }, (_, i) => ({ id: `${i}` }));
const page2 = Array.from({ length: 100 }, (_, i) => ({
id: `${i + 100}`,
}));
const page3 = Array.from({ length: 50 }, (_, i) => ({
id: `${i + 200}`,
}));
mockSend
.mockResolvedValueOnce({
Items: page1,
LastEvaluatedKey: { id: '99' },
})
.mockResolvedValueOnce({
Items: page2,
LastEvaluatedKey: { id: '199' },
})
.mockResolvedValueOnce({
Items: page3,
});
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'gsi1pk = :gsi1pk',
{ ':gsi1pk': 'LARGE_DATASET' }
);
expect(result).toEqual([...page1, ...page2, ...page3]);
expect(result).toHaveLength(250);
expect(mockSend).toHaveBeenCalledTimes(3);
});
it('should query with filter expression on GSI', async () => {
const items = [
{ id: '123', gsi1pk: 'USER#123', status: 'active', score: 95 },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'gsi1pk = :gsi1pk',
{ ':gsi1pk': 'USER#123', ':minScore': 90 },
{ '#score': 'score' },
'GSI1'
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should query with sort key conditions', async () => {
const items = [
{ id: '123', sk: 'PROFILE#2023', data: 'profile' },
{ id: '123', sk: 'SETTINGS#2023', data: 'settings' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'id = :id AND begins_with(sk, :prefix)',
{ ':id': '123', ':prefix': 'PROFILE' }
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should query with FilterExpression for additional filtering', async () => {
const items = [
{ id: '123', sk: 'USER#001', status: 'active', type: 'premium' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'id = :id', // KeyConditionExpression (solo partition key)
{ ':id': '123', ':status': 'active' }, // Values para ambos
{ '#status': 'status' }, // Names para FilterExpression
null, // No index
1000, // Default limit
'#status = :status' // FilterExpression
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should query with FilterExpression and complex conditions', async () => {
const items = [
{
id: '123',
sk: 'USER#001',
status: 'active',
score: 95,
type: 'premium',
},
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.queryItems(
'test-table',
'id = :id AND begins_with(sk, :prefix)',
{
':id': '123',
':prefix': 'USER',
':status': 'active',
':minScore': 90,
},
{
'#status': 'status',
'#score': 'score',
},
null,
1000,
'#status = :status AND #score > :minScore'
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
});
describe('scanItems', () => {
it('should scan items without filters', async () => {
const items = [
{ id: '123', name: 'test1' },
{ id: '124', name: 'test2' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems('test-table');
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with basic filter', async () => {
const items = [{ id: '123', status: 'active', name: 'test' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'#status = :status',
{ ':status': 'active' },
{ '#status': 'status' }
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with complex filter conditions', async () => {
const items = [
{ id: '123', age: 25, status: 'active', type: 'premium' },
{ id: '124', age: 30, status: 'active', type: 'premium' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'#status = :status AND #age BETWEEN :minAge AND :maxAge AND #type = :type',
{
':status': 'active',
':minAge': 20,
':maxAge': 35,
':type': 'premium',
},
{
'#status': 'status',
'#age': 'age',
'#type': 'type',
}
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with attribute_exists filter', async () => {
const items = [{ id: '123', name: 'test', optionalField: 'exists' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'attribute_exists(optionalField)'
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with IN filter', async () => {
const items = [
{ id: '123', status: 'active' },
{ id: '124', status: 'pending' },
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'#status IN (:status1, :status2)',
{
':status1': 'active',
':status2': 'pending',
},
{ '#status': 'status' }
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with contains filter for strings', async () => {
const items = [{ id: '123', description: 'This contains the word test' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'contains(description, :searchTerm)',
{ ':searchTerm': 'test' }
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with size filter', async () => {
const items = [{ id: '123', tags: ['tag1', 'tag2', 'tag3'] }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'size(tags) > :minSize',
{ ':minSize': 2 }
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with empty filter parameters', async () => {
const items = [{ id: '123', name: 'test' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
null, // no filter
{}, // empty values
{} // empty names
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with attribute_not_exists filter', async () => {
const items = [{ id: '123', name: 'test' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'attribute_not_exists(deletedAt)'
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with type function filter', async () => {
const items = [{ id: '123', data: 'string_value' }];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'attribute_type(#data, :dataType)',
{ ':dataType': 'S' },
{ '#data': 'data' }
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
it('should scan items with complex nested conditions', async () => {
const items = [
{
id: '123',
profile: { name: 'John', age: 30 },
tags: ['premium', 'active'],
createdAt: '2023-01-01',
},
];
mockSend.mockResolvedValue({ Items: items });
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.scanItems(
'test-table',
'(contains(tags, :tag1) OR contains(tags, :tag2)) AND #profile.#age > :minAge',
{
':tag1': 'premium',
':tag2': 'vip',
':minAge': 25,
},
{
'#profile': 'profile',
'#age': 'age',
}
);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
});
describe('batchGetItems', () => {
it('should batch get items successfully', async () => {
const items = [{ id: '123', name: 'test' }];
mockSend.mockResolvedValue({
Responses: { 'test-table': items },
});
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.batchGetItems('test-table', [
{ id: '123' },
]);
expect(result).toEqual(items);
});
it('should handle empty keys array', async () => {
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.batchGetItems('test-table', []);
expect(result).toEqual([]);
expect(mockSend).not.toHaveBeenCalled();
});
it('should filter null/undefined keys', async () => {
const items = [{ id: '123' }];
mockSend.mockResolvedValue({
Responses: { 'test-table': items },
});
const dynamoUtils = new DynamoUtils();
const result = await dynamoUtils.batchGetItems('test-table', [
{ id: '123' },
null,
undefined,
]);
expect(result).toEqual(items);
expect(mockSend).toHaveBeenCalledTimes(1);
});
});
describe('static methods', () => {
it('should have static getItem method', () => {
expect(typeof DynamoUtils.getItem).toBe('function');
});
it('should have static putItem method', () => {
expect(typeof DynamoUtils.putItem).toBe('function');
});
it('should have static updateItem method', () => {
expect(typeof DynamoUtils.updateItem).toBe('function');
});
it('should have static deleteItem method', () => {
expect(typeof DynamoUtils.deleteItem).toBe('function');
});
it('should have static queryItems method', () => {
expect(typeof DynamoUtils.queryItems).toBe('function');
});
it('should have static scanItems method', () => {
expect(typeof DynamoUtils.scanItems).toBe('function');
});
it('should have static batchGetItems method', () => {
expect(typeof DynamoUtils.batchGetItems).toBe('function');
});
});
describe('error handling', () => {
it('should create DatabaseError with correct properties', () => {
const error = new DatabaseError('Test error', 'GET', 'users');
expect(error).toBeInstanceOf(DatabaseError);
expect(error.message).toBe('Test error');
expect(error.operation).toBe('GET');
expect(error.table).toBe('users');
expect(error.statusCode).toBe(500);
expect(error.errorCode).toBe('DB_GET');
expect(error.description).toBe('Test error');
expect(error.integration).toBe('database-service');
});
it('should create AWSError with correct properties', () => {
const error = new AWSError('Test error', 'DYNAMODB', 'ERROR');
expect(error).toBeInstanceOf(AWSError);
expect(error.message).toBe('Test error');
expect(error.service).toBe('DYNAMODB');
expect(error.awsErrorCode).toBe('ERROR');
expect(error.statusCode).toBe(500);
expect(error.errorCode).toBe('AWS_DYNAMODB');
expect(error.description).toBe('Test error');
expect(error.integration).toBe('aws-service');
});
it('should re-throw custom errors', async () => {
const customError = new DatabaseError(
'Custom error',
'GET',
'test-table'
);
mockSend.mockRejectedValue(customError);
const dynamoUtils = new DynamoUtils();
await expect(
dynamoUtils.getItem('test-table', { id: '123' })
).rejects.toBe(customError);
});
});
});