UNPKG

jinaga

Version:

Data management for web and mobile applications.

498 lines (436 loc) 16.2 kB
import 'source-map-support/register'; import { FactEnvelope, FactRecord, FactReference } from '../../src/storage'; import { IndexedDBQueue } from '../../src/indexeddb/indexeddb-queue'; import * as driver from '../../src/indexeddb/driver'; import { TopologicalSorter } from '../../src/fact/sorter'; // Mock the IndexedDB driver functions jest.mock('../../src/indexeddb/driver', () => { const originalModule = jest.requireActual('../../src/indexeddb/driver'); return { ...originalModule, withDatabase: jest.fn(), withTransaction: jest.fn(), execRequest: jest.fn(), factKey: jest.fn((fact: FactReference) => `${fact.type}:${fact.hash}`) }; }); describe('IndexedDBQueue', () => { let queue: IndexedDBQueue; // Mock data const mockQueuedFact1: FactRecord = { type: 'TestType1', hash: 'hash1', predecessors: { role1: { type: 'PredType1', hash: 'predhash1' } }, fields: { field1: 'value1' } }; const mockQueuedFact2: FactRecord = { type: 'TestType2', hash: 'hash2', predecessors: { role2: { type: 'PredType2', hash: 'predhash2' } }, fields: { field2: 'value2' } }; const mockPredecessorFact1: FactRecord = { type: 'PredType1', hash: 'predhash1', predecessors: {}, fields: { predField1: 'predValue1' } }; const mockPredecessorFact2: FactRecord = { type: 'PredType2', hash: 'predhash2', predecessors: {}, fields: { predField2: 'predValue2' } }; const mockQueuedEnvelope1: FactEnvelope = { fact: mockQueuedFact1, signatures: [] }; const mockQueuedEnvelope2: FactEnvelope = { fact: mockQueuedFact2, signatures: [] }; beforeEach(() => { jest.clearAllMocks(); queue = new IndexedDBQueue('test-index'); }); describe('peek', () => { it('should return an empty array when the queue is empty', async () => { // Mock the execRequest function to return an empty array for the queue (driver.execRequest as jest.Mock).mockResolvedValueOnce([]); // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => callback({ objectStore: () => ({ getAll: () => ({}) }) })); const result = await queue.peek(); expect(result).toEqual([]); expect(driver.withTransaction).toHaveBeenCalledWith( expect.anything(), ['queue', 'fact', 'ancestor'], 'readonly', expect.any(Function) ); }); it('should return queued facts and their ancestors', async () => { // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => { const mockObjectStores: { [key: string]: any } = { queue: { getAll: () => ({}) }, fact: { get: (key: string) => { if (key === 'PredType1:predhash1') { return mockPredecessorFact1; } else if (key === 'PredType2:predhash2') { return mockPredecessorFact2; } else if (key === 'TestType1:hash1') { return mockQueuedFact1; } else if (key === 'TestType2:hash2') { return mockQueuedFact2; } return {}; } }, ancestor: { get: (key: string) => { if (key === 'TestType1:hash1') { return {}; } else if (key === 'TestType2:hash2') { return {}; } return {}; } } }; return callback({ objectStore: (name: string) => mockObjectStores[name] }); }); // Mock the execRequest function for different calls (driver.execRequest as jest.Mock) // First call - get queued envelopes .mockResolvedValueOnce([mockQueuedEnvelope1, mockQueuedEnvelope2]) // Second and third calls - get ancestors for queued facts .mockResolvedValueOnce(['TestType1:hash1', 'PredType1:predhash1']) .mockResolvedValueOnce(['TestType2:hash2', 'PredType2:predhash2']) // Fourth and fifth calls - get fact records for ancestors .mockResolvedValueOnce(mockPredecessorFact1) .mockResolvedValueOnce(mockPredecessorFact2); const result = await queue.peek(); // Should return both queued facts and their ancestors expect(result).toHaveLength(4); expect(result[0]).toEqual({ fact: mockPredecessorFact1, signatures: [] }); expect(result[1]).toEqual({ fact: mockPredecessorFact2, signatures: [] }); expect(result[2]).toEqual(mockQueuedEnvelope1); expect(result[3]).toEqual(mockQueuedEnvelope2); }); it('should handle facts with no ancestors', async () => { // Create a fact with no predecessors const mockFactNoAncestors: FactRecord = { type: 'TestTypeNoAncestors', hash: 'hashNoAncestors', predecessors: {}, fields: { field: 'value' } }; const mockEnvelopeNoAncestors: FactEnvelope = { fact: mockFactNoAncestors, signatures: [] }; // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => { const mockObjectStores: { [key: string]: any } = { queue: { getAll: () => ({}) }, fact: { get: () => ({}) }, ancestor: { get: () => ({}) } }; return callback({ objectStore: (name: string) => mockObjectStores[name] }); }); // Mock the execRequest function (driver.execRequest as jest.Mock) // First call - get queued envelopes .mockResolvedValueOnce([mockEnvelopeNoAncestors]) // Second call - get ancestors (empty array) .mockResolvedValueOnce([]); const result = await queue.peek(); // Should return only the queued fact expect(result).toHaveLength(1); expect(result).toContainEqual(mockEnvelopeNoAncestors); }); it('should sort facts in topological order', async () => { // Create a chain of facts where each depends on the previous one const mockFact1: FactRecord = { type: 'Type1', hash: 'hash1', predecessors: {}, fields: { field1: 'value1' } }; const mockFact2: FactRecord = { type: 'Type2', hash: 'hash2', predecessors: { role1: { type: 'Type1', hash: 'hash1' } }, fields: { field2: 'value2' } }; const mockFact3: FactRecord = { type: 'Type3', hash: 'hash3', predecessors: { role2: { type: 'Type2', hash: 'hash2' } }, fields: { field3: 'value3' } }; const mockEnvelope1: FactEnvelope = { fact: mockFact1, signatures: [] }; const mockEnvelope2: FactEnvelope = { fact: mockFact2, signatures: [] }; const mockEnvelope3: FactEnvelope = { fact: mockFact3, signatures: [] }; // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => { const mockObjectStores: { [key: string]: any } = { queue: { getAll: () => ({}) }, fact: { get: (key: string) => { if (key === 'Type1:hash1') { return mockFact1; } else if (key === 'Type2:hash2') { return mockFact2; } else if (key === 'Type3:hash3') { return mockFact3; } return {}; } }, ancestor: { get: () => ({}) } }; return callback({ objectStore: (name: string) => mockObjectStores[name] }); }); // Mock the execRequest function to return the envelopes in reverse order // This tests that the function sorts them correctly (driver.execRequest as jest.Mock) // First call - get queued envelopes (in reverse order) .mockResolvedValueOnce([mockEnvelope3, mockEnvelope2, mockEnvelope1]) // Second call - get ancestors (empty array) .mockResolvedValueOnce([]); const result = await queue.peek(); // Should return the facts in topological order expect(result).toHaveLength(3); expect(result[0]).toEqual(mockEnvelope1); // Type1 should be first expect(result[1]).toEqual(mockEnvelope2); // Type2 should be second expect(result[2]).toEqual(mockEnvelope3); // Type3 should be third }); it('should detect circular dependencies', async () => { // Create a circular dependency: A depends on B, B depends on C, C depends on A const mockFactA: FactRecord = { type: 'TypeA', hash: 'hashA', predecessors: { roleC: { type: 'TypeC', hash: 'hashC' } }, fields: { fieldA: 'valueA' } }; const mockFactB: FactRecord = { type: 'TypeB', hash: 'hashB', predecessors: { roleA: { type: 'TypeA', hash: 'hashA' } }, fields: { fieldB: 'valueB' } }; const mockFactC: FactRecord = { type: 'TypeC', hash: 'hashC', predecessors: { roleB: { type: 'TypeB', hash: 'hashB' } }, fields: { fieldC: 'valueC' } }; const mockEnvelopeA: FactEnvelope = { fact: mockFactA, signatures: [] }; const mockEnvelopeB: FactEnvelope = { fact: mockFactB, signatures: [] }; const mockEnvelopeC: FactEnvelope = { fact: mockFactC, signatures: [] }; // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => { const mockObjectStores: { [key: string]: any } = { queue: { getAll: () => ({}) }, fact: { get: () => ({}) }, ancestor: { get: () => ({}) } }; return callback({ objectStore: (name: string) => mockObjectStores[name] }); }); // Mock the execRequest function (driver.execRequest as jest.Mock) // First call - get queued envelopes .mockResolvedValueOnce([mockEnvelopeA, mockEnvelopeB, mockEnvelopeC]) // Second call - get ancestors (empty array) .mockResolvedValueOnce([]); // Should throw an error about circular dependencies await expect(queue.peek()).rejects.toThrow('Circular dependencies detected'); }); it('should correctly sort facts in topological order', async () => { // Create facts with a predecessor relationship const mockFact1: FactRecord = { type: 'Type1', hash: 'hash1', predecessors: { role2: { type: 'Type2', hash: 'hash2' } // This creates a dependency on mockFact2 }, fields: { field1: 'value1' } }; const mockFact2: FactRecord = { type: 'Type2', hash: 'hash2', predecessors: {}, fields: { field2: 'value2' } }; const mockEnvelope1: FactEnvelope = { fact: mockFact1, signatures: [] }; const mockEnvelope2: FactEnvelope = { fact: mockFact2, signatures: [] }; // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => { const mockObjectStores: { [key: string]: any } = { queue: { getAll: () => ({}) }, fact: { get: (key: string) => { if (key === 'Type1:hash1') { return mockFact1; } else if (key === 'Type2:hash2') { return mockFact2; } return {}; } }, ancestor: { get: () => ({}) } }; return callback({ objectStore: (name: string) => mockObjectStores[name] }); }); // Mock the execRequest function (driver.execRequest as jest.Mock) // First call - get queued envelopes .mockResolvedValueOnce([mockEnvelope1, mockEnvelope2]) // Second call - get ancestors (empty array) .mockResolvedValueOnce([]); // Should correctly sort the facts in topological order const result = await queue.peek(); expect(result).toEqual([mockEnvelope2, mockEnvelope1]); }); it('should not return duplicate ancestors', async () => { // Create a fact with multiple predecessors that share an ancestor const mockFactWithSharedAncestor: FactRecord = { type: 'TestType3', hash: 'hash3', predecessors: { role1: { type: 'PredType1', hash: 'predhash1' }, role2: { type: 'PredType2', hash: 'predhash2' } }, fields: { field3: 'value3' } }; const mockEnvelopeWithSharedAncestor: FactEnvelope = { fact: mockFactWithSharedAncestor, signatures: [] }; // Mock the withDatabase and withTransaction functions (driver.withDatabase as jest.Mock).mockImplementation((_, callback) => callback({})); (driver.withTransaction as jest.Mock).mockImplementation((_, __, ___, callback) => { const mockObjectStores: { [key: string]: any } = { queue: { getAll: () => ({}) }, fact: { get: (key: string) => { if (key === 'PredType1:predhash1') { return mockPredecessorFact1; } else if (key === 'PredType2:predhash2') { return mockPredecessorFact2; } else if (key === 'TestType3:hash3') { return mockFactWithSharedAncestor; } return {}; } }, ancestor: { get: () => ({}) } }; return callback({ objectStore: (name: string) => mockObjectStores[name] }); }); // Mock the execRequest function for different calls (driver.execRequest as jest.Mock) // First call - get queued envelopes .mockResolvedValueOnce([mockEnvelopeWithSharedAncestor]) // Second call - get ancestors with duplicates .mockResolvedValueOnce(['PredType1:predhash1', 'PredType2:predhash2', 'PredType1:predhash1']) // Third and fourth calls - get fact records for ancestors .mockResolvedValueOnce(mockPredecessorFact1) .mockResolvedValueOnce(mockPredecessorFact2); const result = await queue.peek(); // Should return the queued fact and its two distinct ancestors expect(result).toHaveLength(3); expect(result[0]).toEqual({ fact: mockPredecessorFact1, signatures: [] }); expect(result[1]).toEqual({ fact: mockPredecessorFact2, signatures: [] }); expect(result[2]).toEqual(mockEnvelopeWithSharedAncestor); }); }); });