UNPKG

s3-orm

Version:

Object-Relational Mapping (ORM) interface for Amazon S3, enabling model-based data operations with indexing and querying capabilities

389 lines (308 loc) 14.4 kB
import { AwsEngine } from '../../lib/core/AwsEngine'; import { S3Helper, type S3Options } from '../../lib/services/S3Helper'; import { EngineHelpers } from '../../lib/core/EngineHelpers'; import { Query } from '../../lib/types'; jest.mock('../../lib/services/S3Helper'); describe('AwsEngine', () => { let engine: AwsEngine; let s3Helper: jest.Mocked<S3Helper>; beforeEach(() => { // Clear all mocks jest.clearAllMocks(); // Create a mocked instance of S3Helper s3Helper = new S3Helper({} as S3Options) as jest.Mocked<S3Helper>; const s3Options: S3Options = { bucket: 'test-bucket', accessKeyId: 'test-key', secretAccessKey: 'test-secret', prefix: 'test/' }; engine = new AwsEngine(s3Options); // Replace the real S3Helper with our mock (engine as any).aws = s3Helper; }); afterEach(() => { jest.resetAllMocks(); }); describe('Object Operations', () => { it('should set an object', async () => { const key = 'testKey'; const obj = { test: 'data' }; await engine.setObject(key, obj); expect(s3Helper.uploadString).toHaveBeenCalledTimes(1); expect(s3Helper.uploadString).toHaveBeenCalledWith(JSON.stringify(obj), expect.stringContaining('test/hash/testKey')); }); it('should get an object', async () => { const key = 'testKey'; const obj = { test: 'data' }; s3Helper.get.mockResolvedValue(JSON.stringify(obj)); const result = await engine.getObject(key); expect(s3Helper.get).toHaveBeenCalledTimes(1); expect(result).toEqual(obj); }); it('should check if object exists', async () => { const key = 'testKey'; s3Helper.exists.mockResolvedValue(true); const result = await engine.hasObject(key); expect(s3Helper.exists).toHaveBeenCalledTimes(1); expect(result).toBe(true); }); it('should delete an object', async () => { const key = 'testKey'; await engine.delObject(key); expect(s3Helper.delete).toHaveBeenCalledTimes(1); expect(s3Helper.delete).toHaveBeenCalledWith(expect.stringContaining('test/hash/testKey')); }); }); describe('Set Operations', () => { it('should add to a set', async () => { const setName = 'testSet'; const value = 'testValue'; const meta = 'testMeta'; const encodedValue = EngineHelpers.encode(value); await engine.setAdd(setName, value, meta); expect(s3Helper.uploadString).toHaveBeenCalledTimes(1); expect(s3Helper.uploadString).toHaveBeenCalledWith(meta, expect.stringContaining(`test/sets/${setName}/${encodedValue}`)); }); it('should check set membership', async () => { const setName = 'testSet'; const value = 'testValue'; s3Helper.exists.mockResolvedValue(true); const result = await engine.setIsMember(setName, value); expect(s3Helper.exists).toHaveBeenCalledTimes(1); expect(result).toBe(true); }); it('should remove from a set', async () => { const setName = 'testSet'; const value = 'testValue'; const encodedValue = EngineHelpers.encode(value); await engine.setRemove(setName, value); expect(s3Helper.delete).toHaveBeenCalledTimes(1); expect(s3Helper.delete).toHaveBeenCalledWith(expect.stringContaining(`test/sets/${setName}/${encodedValue}`)); }); it('should clear a set', async () => { const setName = 'testSet'; const items = [ { Key: 'test/sets/testSet/value1' }, { Key: 'test/sets/testSet/value2' } ]; s3Helper.list.mockResolvedValue(items); await engine.setClear(setName); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(s3Helper.deleteAll).toHaveBeenCalledTimes(1); expect(s3Helper.deleteAll).toHaveBeenCalledWith(items); }); }); describe('Key-Value Operations', () => { it('should set a key-value pair', async () => { const key = 'testKey'; const value = 'testValue'; await engine.set(key, value); expect(s3Helper.uploadString).toHaveBeenCalledTimes(1); expect(s3Helper.uploadString).toHaveBeenCalledWith(value, expect.stringContaining('test/keyval/testKey')); }); it('should get a value by key', async () => { const key = 'testKey'; const value = 'testValue'; s3Helper.get.mockResolvedValue(value); const result = await engine.get(key); expect(s3Helper.get).toHaveBeenCalledTimes(1); expect(result).toBe(value); }); it('should delete a key-value pair', async () => { const key = 'testKey'; await engine.del(key); expect(s3Helper.delete).toHaveBeenCalledTimes(1); expect(s3Helper.delete).toHaveBeenCalledWith(expect.stringContaining('test/keyval/testKey')); }); it('should delete multiple keys in batch', async () => { const keys = ['key1', 'key2']; const expectedDeleteList = keys.map(key => ({ Key: `test/keyval/${key}` })); await engine.delBatch(keys); expect(s3Helper.deleteAll).toHaveBeenCalledTimes(1); expect(s3Helper.deleteAll).toHaveBeenCalledWith(expectedDeleteList); }); it('should handle empty batch delete', async () => { await engine.delBatch([]); expect(s3Helper.deleteAll).not.toHaveBeenCalled(); }); }); describe('Set Member Operations', () => { it('should get set members', async () => { const setName = 'testSet'; const items = [ { Key: `test/sets/${setName}/dGVzdFZhbHVlMQ` }, { Key: `test/sets/${setName}/dGVzdFZhbHVlMg` } ]; s3Helper.list.mockResolvedValue(items); const result = await engine.setMembers(setName); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toEqual(['testValue1', 'testValue2']); }); it('should get intersection of sets', async () => { const setNames = ['set1', 'set2']; s3Helper.list.mockResolvedValueOnce([ { Key: 'test/sets/set1/dGVzdFZhbHVlMQ' }, { Key: 'test/sets/set1/dGVzdFZhbHVlMg' } ]); s3Helper.list.mockResolvedValueOnce([ { Key: 'test/sets/set2/dGVzdFZhbHVlMQ' }, { Key: 'test/sets/set2/dGVzdFZhbHVlMw' } ]); const result = await engine.setIntersection(setNames); expect(s3Helper.list).toHaveBeenCalledTimes(2); expect(result).toEqual(['testValue1']); }); }); describe('Sorted Set Operations', () => { it('should add to sorted set', async () => { const setName = 'testSet'; const score = 100; const value = 'testValue'; const meta = 'testMeta'; await engine.zSetAdd(setName, score, value, meta); expect(s3Helper.uploadString).toHaveBeenCalledTimes(1); expect(s3Helper.uploadString).toHaveBeenCalledWith( meta, expect.stringContaining(`test/zsets/${setName}/${score}###${EngineHelpers.encode(value)}`) ); }); it('should handle boolean meta in sorted set', async () => { const setName = 'testSet'; const score = 100; const value = 'testValue'; const meta = false; await engine.zSetAdd(setName, score, value, meta); expect(s3Helper.uploadString).toHaveBeenCalledTimes(1); expect(s3Helper.uploadString).toHaveBeenCalledWith( 'false', expect.stringContaining(`test/zsets/${setName}/${score}###${EngineHelpers.encode(value)}`) ); }); it('should remove from sorted set', async () => { const setName = 'testSet'; const score = 100; const value = 'testValue'; await engine.zSetRemove(setName, score, value); expect(s3Helper.delete).toHaveBeenCalledTimes(1); expect(s3Helper.delete).toHaveBeenCalledWith( expect.stringContaining(`test/zsets/${setName}/${score}###${EngineHelpers.encode(value)}`) ); }); it('should get max value from sorted set without scores', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` } ]; s3Helper.list.mockResolvedValue(items); const result = await engine.zGetMax(setName); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toBe('value2'); }); it('should get max value from sorted set with scores', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` } ]; s3Helper.list.mockResolvedValue(items); const result = await engine.zGetMax(setName, true); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toEqual({ score: 200, val: 'value2' }); }); it('should get sorted set members without scores', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` } ]; s3Helper.list.mockResolvedValue(items); const result = await engine.zSetMembers(setName); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toEqual(['value1', 'value2']); }); it('should get sorted set members with scores', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` } ]; s3Helper.list.mockResolvedValue(items); const result = await engine.zSetMembers(setName, true); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toEqual([ { score: 100, val: 'value1' }, { score: 200, val: 'value2' } ]); }); it('should clear sorted set', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` } ]; const expireItems = [ { Key: `test/zsets/${setName}/expires/item1` }, { Key: `test/zsets/${setName}/expires/item2` } ]; s3Helper.list.mockResolvedValueOnce(items); s3Helper.list.mockResolvedValueOnce(expireItems); await engine.zSetClear(setName); expect(s3Helper.list).toHaveBeenCalledTimes(2); expect(s3Helper.deleteAll).toHaveBeenCalledTimes(2); expect(s3Helper.deleteAll).toHaveBeenNthCalledWith(1, items); expect(s3Helper.deleteAll).toHaveBeenNthCalledWith(2, expireItems); }); it('should get range from sorted set', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` }, { Key: `test/zsets/${setName}/300###${EngineHelpers.encode('value3')}` } ]; s3Helper.list.mockResolvedValue(items); const query: Query = { $gt: 100, $lt: 300 }; const result = await engine.zRange(setName, query); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toEqual([ { score: 200, val: 'value2' } ]); }); it('should throw error when no range specifiers provided', async () => { const setName = 'testSet'; const query: Query = {}; await expect(engine.zRange(setName, query)).rejects.toThrow( 'You need to set at least one range specifier ($lt, $lte, $gt, $gte)!' ); }); it('should handle inclusive range bounds', async () => { const setName = 'testSet'; const items = [ { Key: `test/zsets/${setName}/100###${EngineHelpers.encode('value1')}` }, { Key: `test/zsets/${setName}/200###${EngineHelpers.encode('value2')}` }, { Key: `test/zsets/${setName}/300###${EngineHelpers.encode('value3')}` } ]; s3Helper.list.mockResolvedValue(items); const query: Query = { $gte: 100, $lte: 300 }; const result = await engine.zRange(setName, query); expect(s3Helper.list).toHaveBeenCalledTimes(1); expect(result).toEqual([ { score: 100, val: 'value1' }, { score: 200, val: 'value2' }, { score: 300, val: 'value3' } ]); }); }); });