- 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.
440 lines
15 KiB
TypeScript
440 lines
15 KiB
TypeScript
import { describe, beforeEach, it, expect, jest } from '@jest/globals';
|
|
import { DatabaseManager, UserMappingsData } from '../../../src/framework/core/DatabaseManager';
|
|
import { FrameworkOrbitDBService } from '../../../src/framework/services/OrbitDBService';
|
|
import { ModelRegistry } from '../../../src/framework/core/ModelRegistry';
|
|
import { createMockServices } from '../../mocks/services';
|
|
import { BaseModel } from '../../../src/framework/models/BaseModel';
|
|
import { Model, Field } from '../../../src/framework/models/decorators';
|
|
|
|
// Test models for DatabaseManager testing
|
|
@Model({
|
|
scope: 'global',
|
|
type: 'docstore'
|
|
})
|
|
class GlobalTestModel extends BaseModel {
|
|
@Field({ type: 'string', required: true })
|
|
title: string;
|
|
}
|
|
|
|
@Model({
|
|
scope: 'user',
|
|
type: 'keyvalue'
|
|
})
|
|
class UserTestModel extends BaseModel {
|
|
@Field({ type: 'string', required: true })
|
|
name: string;
|
|
}
|
|
|
|
describe('DatabaseManager', () => {
|
|
let databaseManager: DatabaseManager;
|
|
let mockOrbitDBService: FrameworkOrbitDBService;
|
|
let mockDatabase: any;
|
|
let mockOrbitDB: 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),
|
|
put: jest.fn().mockResolvedValue('mock-hash'),
|
|
add: jest.fn().mockResolvedValue('mock-hash'),
|
|
del: jest.fn().mockResolvedValue(undefined),
|
|
query: jest.fn().mockReturnValue([]),
|
|
iterator: jest.fn().mockReturnValue({
|
|
collect: jest.fn().mockReturnValue([])
|
|
}),
|
|
all: jest.fn().mockReturnValue({}),
|
|
value: 0,
|
|
id: 'mock-counter-id',
|
|
inc: jest.fn().mockResolvedValue(undefined)
|
|
};
|
|
|
|
mockOrbitDB = {
|
|
open: jest.fn().mockResolvedValue(mockDatabase)
|
|
};
|
|
|
|
// Mock OrbitDB service methods
|
|
jest.spyOn(mockOrbitDBService, 'openDatabase').mockResolvedValue(mockDatabase);
|
|
jest.spyOn(mockOrbitDBService, 'getOrbitDB').mockReturnValue(mockOrbitDB);
|
|
|
|
// Mock ModelRegistry
|
|
jest.spyOn(ModelRegistry, 'getGlobalModels').mockReturnValue([
|
|
{ modelName: 'GlobalTestModel', dbType: 'docstore' }
|
|
]);
|
|
jest.spyOn(ModelRegistry, 'getUserScopedModels').mockReturnValue([
|
|
{ modelName: 'UserTestModel', dbType: 'keyvalue' }
|
|
]);
|
|
|
|
databaseManager = new DatabaseManager(mockOrbitDBService);
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Initialization', () => {
|
|
it('should initialize all databases correctly', async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
|
|
// Should create global databases
|
|
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith(
|
|
'global-globaltestmodel',
|
|
'docstore'
|
|
);
|
|
|
|
// Should create system directory shards
|
|
for (let i = 0; i < 4; i++) {
|
|
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith(
|
|
`global-user-directory-shard-${i}`,
|
|
'keyvalue'
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should not initialize databases twice', async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
const firstCallCount = (mockOrbitDBService.openDatabase as jest.Mock).mock.calls.length;
|
|
|
|
await databaseManager.initializeAllDatabases();
|
|
const secondCallCount = (mockOrbitDBService.openDatabase as jest.Mock).mock.calls.length;
|
|
|
|
expect(secondCallCount).toBe(firstCallCount);
|
|
});
|
|
|
|
it('should handle database creation errors', async () => {
|
|
jest.spyOn(mockOrbitDBService, 'openDatabase').mockRejectedValueOnce(new Error('Creation failed'));
|
|
|
|
await expect(databaseManager.initializeAllDatabases()).rejects.toThrow('Creation failed');
|
|
});
|
|
});
|
|
|
|
describe('User Database Management', () => {
|
|
beforeEach(async () => {
|
|
// Initialize global databases first
|
|
await databaseManager.initializeAllDatabases();
|
|
});
|
|
|
|
it('should create user databases correctly', async () => {
|
|
const userId = 'test-user-123';
|
|
|
|
const userMappings = await databaseManager.createUserDatabases(userId);
|
|
|
|
expect(userMappings).toBeInstanceOf(UserMappingsData);
|
|
expect(userMappings.userId).toBe(userId);
|
|
expect(userMappings.databases).toHaveProperty('usertestmodelDB');
|
|
|
|
// Should create mappings database
|
|
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith(
|
|
`${userId}-mappings`,
|
|
'keyvalue'
|
|
);
|
|
|
|
// Should create user model database
|
|
expect(mockOrbitDBService.openDatabase).toHaveBeenCalledWith(
|
|
`${userId}-usertestmodel`,
|
|
'keyvalue'
|
|
);
|
|
|
|
// Should store mappings in database
|
|
expect(mockDatabase.set).toHaveBeenCalledWith('mappings', expect.any(Object));
|
|
});
|
|
|
|
it('should retrieve user mappings from cache', async () => {
|
|
const userId = 'test-user-456';
|
|
|
|
// Create user databases first
|
|
const originalMappings = await databaseManager.createUserDatabases(userId);
|
|
jest.clearAllMocks();
|
|
|
|
// Get mappings again - should come from cache
|
|
const cachedMappings = await databaseManager.getUserMappings(userId);
|
|
|
|
expect(cachedMappings).toBe(originalMappings);
|
|
expect(mockDatabase.get).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should retrieve user mappings from global directory', async () => {
|
|
const userId = 'test-user-789';
|
|
const mappingsAddress = 'mock-mappings-address';
|
|
const mappingsData = { usertestmodelDB: 'mock-db-address' };
|
|
|
|
// Mock directory shard return
|
|
mockDatabase.get
|
|
.mockResolvedValueOnce(mappingsAddress) // From directory shard
|
|
.mockResolvedValueOnce(mappingsData); // From mappings DB
|
|
|
|
const userMappings = await databaseManager.getUserMappings(userId);
|
|
|
|
expect(userMappings).toBeInstanceOf(UserMappingsData);
|
|
expect(userMappings.userId).toBe(userId);
|
|
expect(userMappings.databases).toEqual(mappingsData);
|
|
|
|
// Should open mappings database
|
|
expect(mockOrbitDB.open).toHaveBeenCalledWith(mappingsAddress);
|
|
});
|
|
|
|
it('should handle user not found in directory', async () => {
|
|
const userId = 'nonexistent-user';
|
|
|
|
// Mock directory shard returning null
|
|
mockDatabase.get.mockResolvedValue(null);
|
|
|
|
await expect(databaseManager.getUserMappings(userId)).rejects.toThrow(
|
|
`User ${userId} not found in directory`
|
|
);
|
|
});
|
|
|
|
it('should get user database correctly', async () => {
|
|
const userId = 'test-user-db';
|
|
const modelName = 'UserTestModel';
|
|
|
|
// Create user databases first
|
|
await databaseManager.createUserDatabases(userId);
|
|
|
|
const userDB = await databaseManager.getUserDatabase(userId, modelName);
|
|
|
|
expect(userDB).toBe(mockDatabase);
|
|
});
|
|
|
|
it('should handle missing user database', async () => {
|
|
const userId = 'test-user-missing';
|
|
const modelName = 'NonExistentModel';
|
|
|
|
// Create user databases first
|
|
await databaseManager.createUserDatabases(userId);
|
|
|
|
await expect(databaseManager.getUserDatabase(userId, modelName)).rejects.toThrow(
|
|
`Database not found for user ${userId} and model ${modelName}`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Global Database Management', () => {
|
|
beforeEach(async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
});
|
|
|
|
it('should get global database correctly', async () => {
|
|
const globalDB = await databaseManager.getGlobalDatabase('GlobalTestModel');
|
|
|
|
expect(globalDB).toBe(mockDatabase);
|
|
});
|
|
|
|
it('should handle missing global database', async () => {
|
|
await expect(databaseManager.getGlobalDatabase('NonExistentModel')).rejects.toThrow(
|
|
'Global database not found for model: NonExistentModel'
|
|
);
|
|
});
|
|
|
|
it('should get global directory shards', async () => {
|
|
const shards = await databaseManager.getGlobalDirectoryShards();
|
|
|
|
expect(shards).toHaveLength(4);
|
|
expect(shards.every(shard => shard === mockDatabase)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Database Operations', () => {
|
|
beforeEach(async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
});
|
|
|
|
describe('getAllDocuments', () => {
|
|
it('should get all documents from eventlog', async () => {
|
|
const mockDocs = [{ id: '1', data: 'test' }];
|
|
mockDatabase.iterator.mockReturnValue({
|
|
collect: jest.fn().mockReturnValue(mockDocs)
|
|
});
|
|
|
|
const docs = await databaseManager.getAllDocuments(mockDatabase, 'eventlog');
|
|
|
|
expect(docs).toEqual(mockDocs);
|
|
expect(mockDatabase.iterator).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should get all documents from keyvalue', async () => {
|
|
const mockData = { key1: { id: '1' }, key2: { id: '2' } };
|
|
mockDatabase.all.mockReturnValue(mockData);
|
|
|
|
const docs = await databaseManager.getAllDocuments(mockDatabase, 'keyvalue');
|
|
|
|
expect(docs).toEqual([{ id: '1' }, { id: '2' }]);
|
|
expect(mockDatabase.all).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should get all documents from docstore', async () => {
|
|
const mockDocs = [{ id: '1' }, { id: '2' }];
|
|
mockDatabase.query.mockReturnValue(mockDocs);
|
|
|
|
const docs = await databaseManager.getAllDocuments(mockDatabase, 'docstore');
|
|
|
|
expect(docs).toEqual(mockDocs);
|
|
expect(mockDatabase.query).toHaveBeenCalledWith(expect.any(Function));
|
|
});
|
|
|
|
it('should get documents from counter', async () => {
|
|
mockDatabase.value = 42;
|
|
mockDatabase.id = 'counter-123';
|
|
|
|
const docs = await databaseManager.getAllDocuments(mockDatabase, 'counter');
|
|
|
|
expect(docs).toEqual([{ value: 42, id: 'counter-123' }]);
|
|
});
|
|
|
|
it('should handle unsupported database type', async () => {
|
|
await expect(
|
|
databaseManager.getAllDocuments(mockDatabase, 'unsupported' as any)
|
|
).rejects.toThrow('Unsupported database type: unsupported');
|
|
});
|
|
});
|
|
|
|
describe('addDocument', () => {
|
|
it('should add document to eventlog', async () => {
|
|
const data = { content: 'test' };
|
|
mockDatabase.add.mockResolvedValue('hash123');
|
|
|
|
const result = await databaseManager.addDocument(mockDatabase, 'eventlog', data);
|
|
|
|
expect(result).toBe('hash123');
|
|
expect(mockDatabase.add).toHaveBeenCalledWith(data);
|
|
});
|
|
|
|
it('should add document to keyvalue', async () => {
|
|
const data = { id: 'key1', content: 'test' };
|
|
|
|
const result = await databaseManager.addDocument(mockDatabase, 'keyvalue', data);
|
|
|
|
expect(result).toBe('key1');
|
|
expect(mockDatabase.set).toHaveBeenCalledWith('key1', data);
|
|
});
|
|
|
|
it('should add document to docstore', async () => {
|
|
const data = { id: 'doc1', content: 'test' };
|
|
mockDatabase.put.mockResolvedValue('hash123');
|
|
|
|
const result = await databaseManager.addDocument(mockDatabase, 'docstore', data);
|
|
|
|
expect(result).toBe('hash123');
|
|
expect(mockDatabase.put).toHaveBeenCalledWith(data);
|
|
});
|
|
|
|
it('should increment counter', async () => {
|
|
const data = { amount: 5 };
|
|
mockDatabase.id = 'counter-123';
|
|
|
|
const result = await databaseManager.addDocument(mockDatabase, 'counter', data);
|
|
|
|
expect(result).toBe('counter-123');
|
|
expect(mockDatabase.inc).toHaveBeenCalledWith(5);
|
|
});
|
|
});
|
|
|
|
describe('updateDocument', () => {
|
|
it('should update document in keyvalue', async () => {
|
|
const data = { id: 'key1', content: 'updated' };
|
|
|
|
await databaseManager.updateDocument(mockDatabase, 'keyvalue', 'key1', data);
|
|
|
|
expect(mockDatabase.set).toHaveBeenCalledWith('key1', data);
|
|
});
|
|
|
|
it('should update document in docstore', async () => {
|
|
const data = { id: 'doc1', content: 'updated' };
|
|
|
|
await databaseManager.updateDocument(mockDatabase, 'docstore', 'doc1', data);
|
|
|
|
expect(mockDatabase.put).toHaveBeenCalledWith(data);
|
|
});
|
|
|
|
it('should add new entry for append-only stores', async () => {
|
|
const data = { id: 'event1', content: 'updated' };
|
|
mockDatabase.add.mockResolvedValue('hash123');
|
|
|
|
await databaseManager.updateDocument(mockDatabase, 'eventlog', 'event1', data);
|
|
|
|
expect(mockDatabase.add).toHaveBeenCalledWith(data);
|
|
});
|
|
});
|
|
|
|
describe('deleteDocument', () => {
|
|
it('should delete document from keyvalue', async () => {
|
|
await databaseManager.deleteDocument(mockDatabase, 'keyvalue', 'key1');
|
|
|
|
expect(mockDatabase.del).toHaveBeenCalledWith('key1');
|
|
});
|
|
|
|
it('should delete document from docstore', async () => {
|
|
await databaseManager.deleteDocument(mockDatabase, 'docstore', 'doc1');
|
|
|
|
expect(mockDatabase.del).toHaveBeenCalledWith('doc1');
|
|
});
|
|
|
|
it('should add deletion marker for append-only stores', async () => {
|
|
mockDatabase.add.mockResolvedValue('hash123');
|
|
|
|
await databaseManager.deleteDocument(mockDatabase, 'eventlog', 'event1');
|
|
|
|
expect(mockDatabase.add).toHaveBeenCalledWith({
|
|
_deleted: true,
|
|
id: 'event1',
|
|
deletedAt: expect.any(Number)
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Shard Index Calculation', () => {
|
|
it('should calculate consistent shard indices', async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
|
|
const userId1 = 'user-123';
|
|
const userId2 = 'user-456';
|
|
|
|
// Create users and verify they're stored in shards
|
|
await databaseManager.createUserDatabases(userId1);
|
|
await databaseManager.createUserDatabases(userId2);
|
|
|
|
// The shard index should be consistent for the same user
|
|
const calls = (mockDatabase.set as jest.Mock).mock.calls;
|
|
const user1Calls = calls.filter(call => call[0] === userId1);
|
|
const user2Calls = calls.filter(call => call[0] === userId2);
|
|
|
|
expect(user1Calls).toHaveLength(1);
|
|
expect(user2Calls).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle database operation errors', async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
|
|
mockDatabase.put.mockRejectedValue(new Error('Database error'));
|
|
|
|
await expect(
|
|
databaseManager.addDocument(mockDatabase, 'docstore', { id: 'test' })
|
|
).rejects.toThrow('Database error');
|
|
});
|
|
|
|
it('should handle missing global directory', async () => {
|
|
// Don't initialize databases
|
|
const userId = 'test-user';
|
|
|
|
await expect(databaseManager.getUserMappings(userId)).rejects.toThrow(
|
|
'Global directory not initialized'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Cleanup', () => {
|
|
it('should stop and clear all resources', async () => {
|
|
await databaseManager.initializeAllDatabases();
|
|
await databaseManager.createUserDatabases('test-user');
|
|
|
|
await databaseManager.stop();
|
|
|
|
// After stopping, initialization should be required again
|
|
await expect(databaseManager.getGlobalDatabase('GlobalTestModel')).rejects.toThrow();
|
|
});
|
|
});
|
|
}); |