UNPKG

@gp_jcisneros/aws-utils

Version:

AWS SDK utilities for GreenPay microservices

682 lines (567 loc) 21.3 kB
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); }); }); });