This repository has been archived on 2025-08-03. You can view files and clone it, but cannot push or open issues or pull requests.
network-orbit/tests/unit/sharding/ShardManager.test.ts
anonpenguin 1cbca09352 Add unit tests for RelationshipManager and ShardManager
- Implement comprehensive tests for RelationshipManager covering various relationship types (BelongsTo, HasMany, HasOne, ManyToMany) and eager loading functionality.
- Include caching mechanisms and error handling in RelationshipManager tests.
- Create unit tests for ShardManager to validate shard creation, routing, management, global index operations, and query functionalities.
- Ensure tests cover different sharding strategies (hash, range, user) and handle edge cases like errors and non-existent models.
2025-06-19 11:20:13 +03:00

436 lines
17 KiB
TypeScript

import { describe, beforeEach, it, expect, jest } from '@jest/globals';
import { ShardManager, ShardInfo } from '../../../src/framework/sharding/ShardManager';
import { FrameworkOrbitDBService } from '../../../src/framework/services/OrbitDBService';
import { ShardingConfig } from '../../../src/framework/types/framework';
import { createMockServices } from '../../mocks/services';
describe('ShardManager', () => {
let shardManager: ShardManager;
let mockOrbitDBService: FrameworkOrbitDBService;
let mockDatabase: any;
beforeEach(() => {
const mockServices = createMockServices();
mockOrbitDBService = mockServices.orbitDBService;
// Create mock database
mockDatabase = {
address: { toString: () => 'mock-address-123' },
set: jest.fn().mockResolvedValue(undefined),
get: jest.fn().mockResolvedValue(null),
del: jest.fn().mockResolvedValue(undefined),
put: jest.fn().mockResolvedValue('mock-hash'),
add: jest.fn().mockResolvedValue('mock-hash'),
query: jest.fn().mockReturnValue([])
};
// Mock OrbitDB service methods
jest.spyOn(mockOrbitDBService, 'openDatabase').mockResolvedValue(mockDatabase);
shardManager = new ShardManager();
shardManager.setOrbitDBService(mockOrbitDBService);
jest.clearAllMocks();
});
describe('Initialization', () => {
it('should set OrbitDB service correctly', () => {
const newShardManager = new ShardManager();
newShardManager.setOrbitDBService(mockOrbitDBService);
// No direct way to test this, but we can verify it works in other tests
expect(newShardManager).toBeInstanceOf(ShardManager);
});
it('should throw error when OrbitDB service not set', async () => {
const newShardManager = new ShardManager();
const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' };
await expect(newShardManager.createShards('TestModel', config)).rejects.toThrow(
'OrbitDB service not initialized'
);
});
});
describe('Shard Creation', () => {
it('should create shards with hash strategy', async () => {
const config: ShardingConfig = { strategy: 'hash', count: 3, key: 'id' };
await shardManager.createShards('TestModel', config, 'docstore');
// Should create 3 shards
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(3);
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('testmodel-shard-0', 'docstore');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('testmodel-shard-1', 'docstore');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('testmodel-shard-2', 'docstore');
const shards = shardManager.getAllShards('TestModel');
expect(shards).toHaveLength(3);
expect(shards[0]).toMatchObject({
name: 'testmodel-shard-0',
index: 0,
address: 'mock-address-123'
});
});
it('should create shards with range strategy', async () => {
const config: ShardingConfig = { strategy: 'range', count: 2, key: 'name' };
await shardManager.createShards('RangeModel', config, 'keyvalue');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(2);
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('rangemodel-shard-0', 'keyvalue');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('rangemodel-shard-1', 'keyvalue');
});
it('should create shards with user strategy', async () => {
const config: ShardingConfig = { strategy: 'user', count: 4, key: 'userId' };
await shardManager.createShards('UserModel', config);
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(4);
const shards = shardManager.getAllShards('UserModel');
expect(shards).toHaveLength(4);
});
it('should handle shard creation errors', async () => {
const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' };
jest.spyOn(mockOrbitDBService, 'openDatabase').mockRejectedValueOnce(new Error('Database creation failed'));
await expect(shardManager.createShards('FailModel', config)).rejects.toThrow('Database creation failed');
});
});
describe('Shard Routing', () => {
beforeEach(async () => {
const config: ShardingConfig = { strategy: 'hash', count: 4, key: 'id' };
await shardManager.createShards('TestModel', config);
});
it('should route keys to consistent shards with hash strategy', () => {
const key1 = 'user-123';
const key2 = 'user-456';
const key3 = 'user-123'; // Same as key1
const shard1 = shardManager.getShardForKey('TestModel', key1);
const shard2 = shardManager.getShardForKey('TestModel', key2);
const shard3 = shardManager.getShardForKey('TestModel', key3);
// Same keys should route to same shards
expect(shard1.index).toBe(shard3.index);
// Different keys may route to different shards
expect(shard1.index).toBeGreaterThanOrEqual(0);
expect(shard1.index).toBeLessThan(4);
expect(shard2.index).toBeGreaterThanOrEqual(0);
expect(shard2.index).toBeLessThan(4);
});
it('should route keys with range strategy', async () => {
const config: ShardingConfig = { strategy: 'range', count: 3, key: 'name' };
await shardManager.createShards('RangeModel', config);
const shardA = shardManager.getShardForKey('RangeModel', 'apple');
const shardM = shardManager.getShardForKey('RangeModel', 'middle');
const shardZ = shardManager.getShardForKey('RangeModel', 'zebra');
// Keys starting with different letters should potentially route to different shards
expect(shardA.index).toBeGreaterThanOrEqual(0);
expect(shardA.index).toBeLessThan(3);
expect(shardM.index).toBeGreaterThanOrEqual(0);
expect(shardM.index).toBeLessThan(3);
expect(shardZ.index).toBeGreaterThanOrEqual(0);
expect(shardZ.index).toBeLessThan(3);
});
it('should handle user strategy routing', async () => {
const config: ShardingConfig = { strategy: 'user', count: 2, key: 'userId' };
await shardManager.createShards('UserModel', config);
const shard1 = shardManager.getShardForKey('UserModel', 'user-abc');
const shard2 = shardManager.getShardForKey('UserModel', 'user-def');
const shard3 = shardManager.getShardForKey('UserModel', 'user-abc'); // Same as shard1
expect(shard1.index).toBe(shard3.index);
expect(shard1.index).toBeGreaterThanOrEqual(0);
expect(shard1.index).toBeLessThan(2);
});
it('should throw error for unsupported sharding strategy', async () => {
const config: ShardingConfig = { strategy: 'unsupported' as any, count: 2, key: 'id' };
await shardManager.createShards('UnsupportedModel', config);
expect(() => {
shardManager.getShardForKey('UnsupportedModel', 'test-key');
}).toThrow('Unsupported sharding strategy: unsupported');
});
it('should throw error when no shards exist for model', () => {
expect(() => {
shardManager.getShardForKey('NonExistentModel', 'test-key');
}).toThrow('No shards found for model NonExistentModel');
});
it('should throw error when no shard configuration exists', async () => {
// Manually clear the config to simulate this error
const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' };
await shardManager.createShards('ConfigTestModel', config);
// Access private property for testing (not ideal but necessary for this test)
(shardManager as any).shardConfigs.delete('ConfigTestModel');
expect(() => {
shardManager.getShardForKey('ConfigTestModel', 'test-key');
}).toThrow('No shard configuration found for model ConfigTestModel');
});
});
describe('Shard Management', () => {
beforeEach(async () => {
const config: ShardingConfig = { strategy: 'hash', count: 3, key: 'id' };
await shardManager.createShards('TestModel', config);
});
it('should get all shards for a model', () => {
const shards = shardManager.getAllShards('TestModel');
expect(shards).toHaveLength(3);
expect(shards[0].name).toBe('testmodel-shard-0');
expect(shards[1].name).toBe('testmodel-shard-1');
expect(shards[2].name).toBe('testmodel-shard-2');
});
it('should return empty array for non-existent model', () => {
const shards = shardManager.getAllShards('NonExistentModel');
expect(shards).toEqual([]);
});
it('should get shard by index', () => {
const shard0 = shardManager.getShardByIndex('TestModel', 0);
const shard1 = shardManager.getShardByIndex('TestModel', 1);
const shard2 = shardManager.getShardByIndex('TestModel', 2);
const shardInvalid = shardManager.getShardByIndex('TestModel', 5);
expect(shard0?.index).toBe(0);
expect(shard1?.index).toBe(1);
expect(shard2?.index).toBe(2);
expect(shardInvalid).toBeUndefined();
});
it('should get shard count', () => {
const count = shardManager.getShardCount('TestModel');
expect(count).toBe(3);
const nonExistentCount = shardManager.getShardCount('NonExistentModel');
expect(nonExistentCount).toBe(0);
});
it('should get all models with shards', () => {
const models = shardManager.getAllModelsWithShards();
expect(models).toContain('TestModel');
});
});
describe('Global Index Management', () => {
it('should create global index with shards', async () => {
await shardManager.createGlobalIndex('TestModel', 'username-index');
// Should create 4 index shards (default)
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledTimes(4);
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-0', 'keyvalue');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-1', 'keyvalue');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-2', 'keyvalue');
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith('username-index-shard-3', 'keyvalue');
const indexShards = shardManager.getAllShards('username-index');
expect(indexShards).toHaveLength(4);
});
it('should add to global index', async () => {
await shardManager.createGlobalIndex('TestModel', 'email-index');
await shardManager.addToGlobalIndex('email-index', 'user@example.com', 'user-123');
// Should call set on one of the index shards
expect(mockDatabase.set).toHaveBeenCalledWith('user@example.com', 'user-123');
});
it('should get from global index', async () => {
await shardManager.createGlobalIndex('TestModel', 'id-index');
mockDatabase.get.mockResolvedValue('user-456');
const result = await shardManager.getFromGlobalIndex('id-index', 'lookup-key');
expect(result).toBe('user-456');
expect(mockDatabase.get).toHaveBeenCalledWith('lookup-key');
});
it('should remove from global index', async () => {
await shardManager.createGlobalIndex('TestModel', 'remove-index');
await shardManager.removeFromGlobalIndex('remove-index', 'key-to-remove');
expect(mockDatabase.del).toHaveBeenCalledWith('key-to-remove');
});
it('should handle missing global index', async () => {
await expect(
shardManager.addToGlobalIndex('non-existent-index', 'key', 'value')
).rejects.toThrow('Global index non-existent-index not found');
await expect(
shardManager.getFromGlobalIndex('non-existent-index', 'key')
).rejects.toThrow('Global index non-existent-index not found');
await expect(
shardManager.removeFromGlobalIndex('non-existent-index', 'key')
).rejects.toThrow('Global index non-existent-index not found');
});
it('should handle global index operation errors', async () => {
await shardManager.createGlobalIndex('TestModel', 'error-index');
mockDatabase.set.mockRejectedValue(new Error('Database error'));
mockDatabase.get.mockRejectedValue(new Error('Database error'));
mockDatabase.del.mockRejectedValue(new Error('Database error'));
await expect(
shardManager.addToGlobalIndex('error-index', 'key', 'value')
).rejects.toThrow('Database error');
const result = await shardManager.getFromGlobalIndex('error-index', 'key');
expect(result).toBeNull(); // Should return null on error
await expect(
shardManager.removeFromGlobalIndex('error-index', 'key')
).rejects.toThrow('Database error');
});
});
describe('Query Operations', () => {
beforeEach(async () => {
const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' };
await shardManager.createShards('QueryModel', config);
});
it('should query all shards', async () => {
const mockQueryFn = jest.fn()
.mockResolvedValueOnce([{ id: '1', name: 'test1' }])
.mockResolvedValueOnce([{ id: '2', name: 'test2' }]);
const results = await shardManager.queryAllShards('QueryModel', mockQueryFn);
expect(mockQueryFn).toHaveBeenCalledTimes(2);
expect(results).toEqual([
{ id: '1', name: 'test1' },
{ id: '2', name: 'test2' }
]);
});
it('should handle query errors gracefully', async () => {
const mockQueryFn = jest.fn()
.mockResolvedValueOnce([{ id: '1', name: 'test1' }])
.mockRejectedValueOnce(new Error('Query failed'));
const results = await shardManager.queryAllShards('QueryModel', mockQueryFn);
expect(results).toEqual([{ id: '1', name: 'test1' }]);
});
it('should throw error when querying non-existent model', async () => {
const mockQueryFn = jest.fn();
await expect(
shardManager.queryAllShards('NonExistentModel', mockQueryFn)
).rejects.toThrow('No shards found for model NonExistentModel');
});
});
describe('Statistics and Monitoring', () => {
beforeEach(async () => {
const config: ShardingConfig = { strategy: 'hash', count: 3, key: 'id' };
await shardManager.createShards('StatsModel', config);
});
it('should get shard statistics', () => {
const stats = shardManager.getShardStatistics('StatsModel');
expect(stats).toEqual({
modelName: 'StatsModel',
shardCount: 3,
shards: [
{ name: 'statsmodel-shard-0', index: 0, address: 'mock-address-123' },
{ name: 'statsmodel-shard-1', index: 1, address: 'mock-address-123' },
{ name: 'statsmodel-shard-2', index: 2, address: 'mock-address-123' }
]
});
});
it('should return null for non-existent model statistics', () => {
const stats = shardManager.getShardStatistics('NonExistentModel');
expect(stats).toBeNull();
});
it('should list all models with shards', async () => {
const config1: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' };
const config2: ShardingConfig = { strategy: 'range', count: 3, key: 'name' };
await shardManager.createShards('Model1', config1);
await shardManager.createShards('Model2', config2);
const models = shardManager.getAllModelsWithShards();
expect(models).toContain('StatsModel'); // From beforeEach
expect(models).toContain('Model1');
expect(models).toContain('Model2');
expect(models.length).toBeGreaterThanOrEqual(3);
});
});
describe('Hash Function Consistency', () => {
it('should produce consistent hash results', () => {
// Test the hash function directly by creating shards and checking consistency
const testKeys = ['user-123', 'user-456', 'user-789', 'user-abc', 'user-def'];
const shardCount = 4;
// Get shard indices for each key multiple times
const config: ShardingConfig = { strategy: 'hash', count: shardCount, key: 'id' };
return shardManager.createShards('HashTestModel', config).then(() => {
testKeys.forEach(key => {
const shard1 = shardManager.getShardForKey('HashTestModel', key);
const shard2 = shardManager.getShardForKey('HashTestModel', key);
const shard3 = shardManager.getShardForKey('HashTestModel', key);
// Same key should always route to same shard
expect(shard1.index).toBe(shard2.index);
expect(shard2.index).toBe(shard3.index);
// Shard index should be within valid range
expect(shard1.index).toBeGreaterThanOrEqual(0);
expect(shard1.index).toBeLessThan(shardCount);
});
});
});
});
describe('Cleanup', () => {
it('should stop and clear all resources', async () => {
const config: ShardingConfig = { strategy: 'hash', count: 2, key: 'id' };
await shardManager.createShards('CleanupModel', config);
await shardManager.createGlobalIndex('CleanupModel', 'cleanup-index');
expect(shardManager.getAllShards('CleanupModel')).toHaveLength(2);
expect(shardManager.getAllShards('cleanup-index')).toHaveLength(4);
await shardManager.stop();
expect(shardManager.getAllShards('CleanupModel')).toHaveLength(0);
expect(shardManager.getAllShards('cleanup-index')).toHaveLength(0);
expect(shardManager.getAllModelsWithShards()).toHaveLength(0);
});
});
});