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/migrations/MigrationManager.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

652 lines
23 KiB
TypeScript

import { describe, beforeEach, it, expect, jest } from '@jest/globals';
import {
MigrationManager,
Migration,
MigrationOperation,
MigrationResult,
MigrationValidator,
MigrationLogger
} from '../../../src/framework/migrations/MigrationManager';
import { FieldConfig } from '../../../src/framework/types/models';
import { createMockServices } from '../../mocks/services';
describe('MigrationManager', () => {
let migrationManager: MigrationManager;
let mockDatabaseManager: any;
let mockShardManager: any;
let mockLogger: MigrationLogger;
const createTestMigration = (overrides: Partial<Migration> = {}): Migration => ({
id: 'test-migration-1',
version: '1.0.0',
name: 'Test Migration',
description: 'A test migration for unit testing',
targetModels: ['TestModel'],
up: [
{
type: 'add_field',
modelName: 'TestModel',
fieldName: 'newField',
fieldConfig: {
type: 'string',
required: false,
default: 'default-value'
} as FieldConfig
}
],
down: [
{
type: 'remove_field',
modelName: 'TestModel',
fieldName: 'newField'
}
],
createdAt: Date.now(),
...overrides
});
beforeEach(() => {
const mockServices = createMockServices();
mockDatabaseManager = {
getAllDocuments: jest.fn().mockResolvedValue([]),
addDocument: jest.fn().mockResolvedValue('mock-id'),
updateDocument: jest.fn().mockResolvedValue(undefined),
deleteDocument: jest.fn().mockResolvedValue(undefined),
};
mockShardManager = {
getAllShards: jest.fn().mockReturnValue([]),
getShardForKey: jest.fn().mockReturnValue({ name: 'shard-0', database: {} }),
};
mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn()
};
migrationManager = new MigrationManager(mockDatabaseManager, mockShardManager, mockLogger);
jest.clearAllMocks();
});
describe('Migration Registration', () => {
it('should register a valid migration', () => {
const migration = createTestMigration();
migrationManager.registerMigration(migration);
const registered = migrationManager.getMigration(migration.id);
expect(registered).toEqual(migration);
expect(mockLogger.info).toHaveBeenCalledWith(
`Registered migration: ${migration.name} (${migration.version})`,
expect.objectContaining({
migrationId: migration.id,
targetModels: migration.targetModels
})
);
});
it('should throw error for invalid migration structure', () => {
const invalidMigration = createTestMigration({
id: '', // Invalid - empty ID
});
expect(() => migrationManager.registerMigration(invalidMigration)).toThrow(
'Migration must have id, version, and name'
);
});
it('should throw error for migration without target models', () => {
const invalidMigration = createTestMigration({
targetModels: [] // Invalid - empty target models
});
expect(() => migrationManager.registerMigration(invalidMigration)).toThrow(
'Migration must specify target models'
);
});
it('should throw error for migration without up operations', () => {
const invalidMigration = createTestMigration({
up: [] // Invalid - no up operations
});
expect(() => migrationManager.registerMigration(invalidMigration)).toThrow(
'Migration must have at least one up operation'
);
});
it('should throw error for duplicate version with different ID', () => {
const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' });
const migration2 = createTestMigration({ id: 'migration-2', version: '1.0.0' });
migrationManager.registerMigration(migration1);
expect(() => migrationManager.registerMigration(migration2)).toThrow(
'Migration version 1.0.0 already exists with different ID'
);
});
it('should allow registering same migration with same ID', () => {
const migration = createTestMigration();
migrationManager.registerMigration(migration);
migrationManager.registerMigration(migration); // Should not throw
expect(migrationManager.getMigrations()).toHaveLength(1);
});
});
describe('Migration Retrieval', () => {
beforeEach(() => {
const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' });
const migration2 = createTestMigration({ id: 'migration-2', version: '2.0.0' });
const migration3 = createTestMigration({ id: 'migration-3', version: '1.5.0' });
migrationManager.registerMigration(migration1);
migrationManager.registerMigration(migration2);
migrationManager.registerMigration(migration3);
});
it('should get all migrations sorted by version', () => {
const migrations = migrationManager.getMigrations();
expect(migrations).toHaveLength(3);
expect(migrations[0].version).toBe('1.0.0');
expect(migrations[1].version).toBe('1.5.0');
expect(migrations[2].version).toBe('2.0.0');
});
it('should get migration by ID', () => {
const migration = migrationManager.getMigration('migration-2');
expect(migration).toBeDefined();
expect(migration?.version).toBe('2.0.0');
});
it('should return null for non-existent migration', () => {
const migration = migrationManager.getMigration('non-existent');
expect(migration).toBeNull();
});
it('should get pending migrations', () => {
// Mock applied migrations (empty for this test)
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
const pending = migrationManager.getPendingMigrations();
expect(pending).toHaveLength(3);
});
it('should filter pending migrations by model', () => {
const migration4 = createTestMigration({
id: 'migration-4',
version: '3.0.0',
targetModels: ['OtherModel']
});
migrationManager.registerMigration(migration4);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
const pending = migrationManager.getPendingMigrations('TestModel');
expect(pending).toHaveLength(3); // Should exclude migration-4
expect(pending.every(m => m.targetModels.includes('TestModel'))).toBe(true);
});
});
describe('Migration Operations', () => {
it('should validate add_field operation', () => {
const operation: MigrationOperation = {
type: 'add_field',
modelName: 'TestModel',
fieldName: 'newField',
fieldConfig: { type: 'string', required: false }
};
expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow();
});
it('should validate remove_field operation', () => {
const operation: MigrationOperation = {
type: 'remove_field',
modelName: 'TestModel',
fieldName: 'oldField'
};
expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow();
});
it('should validate rename_field operation', () => {
const operation: MigrationOperation = {
type: 'rename_field',
modelName: 'TestModel',
fieldName: 'oldField',
newFieldName: 'newField'
};
expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow();
});
it('should validate transform_data operation', () => {
const operation: MigrationOperation = {
type: 'transform_data',
modelName: 'TestModel',
transformer: (data: any) => data
};
expect(() => (migrationManager as any).validateOperation(operation)).not.toThrow();
});
it('should reject invalid operation type', () => {
const operation: MigrationOperation = {
type: 'invalid_type' as any,
modelName: 'TestModel'
};
expect(() => (migrationManager as any).validateOperation(operation)).toThrow(
'Invalid operation type: invalid_type'
);
});
it('should reject operation without model name', () => {
const operation: MigrationOperation = {
type: 'add_field',
modelName: ''
};
expect(() => (migrationManager as any).validateOperation(operation)).toThrow(
'Operation must specify modelName'
);
});
});
describe('Migration Execution', () => {
let migration: Migration;
beforeEach(() => {
migration = createTestMigration();
migrationManager.registerMigration(migration);
// Mock helper methods
jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([
{ id: 'record-1', name: 'Test 1' },
{ id: 'record-2', name: 'Test 2' }
]);
jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined);
});
it('should run migration successfully', async () => {
const result = await migrationManager.runMigration(migration.id);
expect(result.success).toBe(true);
expect(result.migrationId).toBe(migration.id);
expect(result.recordsProcessed).toBe(2);
expect(result.rollbackAvailable).toBe(true);
expect(mockLogger.info).toHaveBeenCalledWith(
`Migration completed: ${migration.name}`,
expect.objectContaining({
migrationId: migration.id,
recordsProcessed: 2
})
);
});
it('should perform dry run without modifying data', async () => {
jest.spyOn(migrationManager as any, 'countRecordsForModel').mockResolvedValue(2);
const result = await migrationManager.runMigration(migration.id, { dryRun: true });
expect(result.success).toBe(true);
expect(result.warnings).toContain('This was a dry run - no data was actually modified');
expect(migrationManager as any).not.toHaveProperty('updateRecord');
expect(mockLogger.info).toHaveBeenCalledWith(
`Performing dry run for migration: ${migration.name}`
);
});
it('should throw error for non-existent migration', async () => {
await expect(migrationManager.runMigration('non-existent')).rejects.toThrow(
'Migration non-existent not found'
);
});
it('should throw error for already running migration', async () => {
// Start first migration (don't await)
const promise1 = migrationManager.runMigration(migration.id);
// Try to start same migration again
await expect(migrationManager.runMigration(migration.id)).rejects.toThrow(
`Migration ${migration.id} is already running`
);
// Clean up first migration
await promise1;
});
it('should handle migration with dependencies', async () => {
const dependentMigration = createTestMigration({
id: 'dependent-migration',
version: '2.0.0',
dependencies: ['test-migration-1']
});
migrationManager.registerMigration(dependentMigration);
// Mock that dependency is not applied
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
await expect(migrationManager.runMigration(dependentMigration.id)).rejects.toThrow(
'Migration dependency not satisfied: test-migration-1'
);
});
});
describe('Migration Rollback', () => {
let migration: Migration;
beforeEach(() => {
migration = createTestMigration();
migrationManager.registerMigration(migration);
jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([
{ id: 'record-1', name: 'Test 1', newField: 'default-value' },
{ id: 'record-2', name: 'Test 2', newField: 'default-value' }
]);
jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined);
jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined);
});
it('should rollback applied migration', async () => {
// Mock that migration was applied
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([
{ migrationId: migration.id, success: true }
]);
const result = await migrationManager.rollbackMigration(migration.id);
expect(result.success).toBe(true);
expect(result.migrationId).toBe(migration.id);
expect(result.rollbackAvailable).toBe(false);
expect(mockLogger.info).toHaveBeenCalledWith(
`Rollback completed: ${migration.name}`,
expect.objectContaining({ migrationId: migration.id })
);
});
it('should throw error for non-existent migration rollback', async () => {
await expect(migrationManager.rollbackMigration('non-existent')).rejects.toThrow(
'Migration non-existent not found'
);
});
it('should throw error for unapplied migration rollback', async () => {
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
await expect(migrationManager.rollbackMigration(migration.id)).rejects.toThrow(
`Migration ${migration.id} has not been applied`
);
});
it('should handle migration without rollback operations', async () => {
const migrationWithoutRollback = createTestMigration({
id: 'no-rollback',
down: []
});
migrationManager.registerMigration(migrationWithoutRollback);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([
{ migrationId: 'no-rollback', success: true }
]);
await expect(migrationManager.rollbackMigration('no-rollback')).rejects.toThrow(
'Migration has no rollback operations defined'
);
});
});
describe('Batch Migration Operations', () => {
beforeEach(() => {
const migration1 = createTestMigration({ id: 'migration-1', version: '1.0.0' });
const migration2 = createTestMigration({ id: 'migration-2', version: '2.0.0' });
const migration3 = createTestMigration({ id: 'migration-3', version: '3.0.0' });
migrationManager.registerMigration(migration1);
migrationManager.registerMigration(migration2);
migrationManager.registerMigration(migration3);
jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([]);
jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined);
});
it('should run all pending migrations', async () => {
const results = await migrationManager.runPendingMigrations();
expect(results).toHaveLength(3);
expect(results.every(r => r.success)).toBe(true);
expect(mockLogger.info).toHaveBeenCalledWith(
'Running 3 pending migrations',
expect.objectContaining({ dryRun: false })
);
});
it('should run pending migrations for specific model', async () => {
const migration4 = createTestMigration({
id: 'migration-4',
version: '4.0.0',
targetModels: ['OtherModel']
});
migrationManager.registerMigration(migration4);
const results = await migrationManager.runPendingMigrations({ modelName: 'TestModel' });
expect(results).toHaveLength(3); // Should exclude migration-4
});
it('should stop on error when specified', async () => {
// Make second migration fail
jest.spyOn(migrationManager, 'runMigration')
.mockResolvedValueOnce({ success: true } as MigrationResult)
.mockRejectedValueOnce(new Error('Migration failed'));
await expect(
migrationManager.runPendingMigrations({ stopOnError: true })
).rejects.toThrow('Migration failed');
});
it('should continue on error when not specified', async () => {
// Make second migration fail
jest.spyOn(migrationManager, 'runMigration')
.mockResolvedValueOnce({ success: true } as MigrationResult)
.mockRejectedValueOnce(new Error('Migration failed'))
.mockResolvedValueOnce({ success: true } as MigrationResult);
const results = await migrationManager.runPendingMigrations({ stopOnError: false });
expect(results).toHaveLength(2); // Only successful migrations
expect(mockLogger.error).toHaveBeenCalledWith(
'Skipping failed migration: migration-2',
expect.objectContaining({ error: expect.any(Error) })
);
});
});
describe('Migration Validation', () => {
it('should run pre-migration validators', async () => {
const validator: MigrationValidator = {
name: 'Test Validator',
description: 'Tests migration validity',
validate: jest.fn().mockResolvedValue({
valid: true,
errors: [],
warnings: ['Test warning']
})
};
const migration = createTestMigration({
validators: [validator]
});
migrationManager.registerMigration(migration);
jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([]);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined);
await migrationManager.runMigration(migration.id);
expect(validator.validate).toHaveBeenCalled();
expect(mockLogger.debug).toHaveBeenCalledWith(
`Running pre-migration validator: ${validator.name}`
);
});
it('should fail migration on validation error', async () => {
const validator: MigrationValidator = {
name: 'Failing Validator',
description: 'Always fails',
validate: jest.fn().mockResolvedValue({
valid: false,
errors: ['Validation failed'],
warnings: []
})
};
const migration = createTestMigration({
validators: [validator]
});
migrationManager.registerMigration(migration);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
await expect(migrationManager.runMigration(migration.id)).rejects.toThrow(
'Pre-migration validation failed: Validation failed'
);
});
});
describe('Migration Progress and Monitoring', () => {
it('should track migration progress', async () => {
const migration = createTestMigration();
migrationManager.registerMigration(migration);
jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([
{ id: 'record-1' }
]);
jest.spyOn(migrationManager as any, 'updateRecord').mockResolvedValue(undefined);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined);
const migrationPromise = migrationManager.runMigration(migration.id);
// Check progress while migration is running
const progress = migrationManager.getMigrationProgress(migration.id);
expect(progress).toBeDefined();
expect(progress?.status).toBe('running');
await migrationPromise;
// Progress should be cleared after completion
const finalProgress = migrationManager.getMigrationProgress(migration.id);
expect(finalProgress).toBeNull();
});
it('should get active migrations', async () => {
const migration1 = createTestMigration({ id: 'migration-1' });
const migration2 = createTestMigration({ id: 'migration-2' });
migrationManager.registerMigration(migration1);
migrationManager.registerMigration(migration2);
jest.spyOn(migrationManager as any, 'getAllRecordsForModel').mockResolvedValue([]);
jest.spyOn(migrationManager as any, 'getAppliedMigrations').mockReturnValue([]);
jest.spyOn(migrationManager as any, 'recordMigrationResult').mockResolvedValue(undefined);
// Start migrations but don't await
const promise1 = migrationManager.runMigration(migration1.id);
const promise2 = migrationManager.runMigration(migration2.id);
const activeMigrations = migrationManager.getActiveMigrations();
expect(activeMigrations).toHaveLength(2);
expect(activeMigrations.every(p => p.status === 'running')).toBe(true);
await Promise.all([promise1, promise2]);
});
it('should get migration history', () => {
// Manually add some history
const result1: MigrationResult = {
migrationId: 'migration-1',
success: true,
duration: 1000,
recordsProcessed: 10,
recordsModified: 5,
warnings: [],
errors: [],
rollbackAvailable: true
};
const result2: MigrationResult = {
migrationId: 'migration-2',
success: false,
duration: 500,
recordsProcessed: 5,
recordsModified: 0,
warnings: [],
errors: ['Test error'],
rollbackAvailable: false
};
(migrationManager as any).migrationHistory.set('migration-1', [result1]);
(migrationManager as any).migrationHistory.set('migration-2', [result2]);
const allHistory = migrationManager.getMigrationHistory();
expect(allHistory).toHaveLength(2);
const specificHistory = migrationManager.getMigrationHistory('migration-1');
expect(specificHistory).toEqual([result1]);
});
});
describe('Version Comparison', () => {
it('should compare versions correctly', () => {
const compareVersions = (migrationManager as any).compareVersions.bind(migrationManager);
expect(compareVersions('1.0.0', '2.0.0')).toBe(-1);
expect(compareVersions('2.0.0', '1.0.0')).toBe(1);
expect(compareVersions('1.0.0', '1.0.0')).toBe(0);
expect(compareVersions('1.2.0', '1.1.0')).toBe(1);
expect(compareVersions('1.0.1', '1.0.0')).toBe(1);
expect(compareVersions('1.0', '1.0.0')).toBe(0);
});
});
describe('Field Value Conversion', () => {
it('should convert field values correctly', () => {
const convertFieldValue = (migrationManager as any).convertFieldValue.bind(migrationManager);
expect(convertFieldValue('123', { type: 'number' })).toBe(123);
expect(convertFieldValue(123, { type: 'string' })).toBe('123');
expect(convertFieldValue('true', { type: 'boolean' })).toBe(true);
expect(convertFieldValue('test', { type: 'array' })).toEqual(['test']);
expect(convertFieldValue(['test'], { type: 'array' })).toEqual(['test']);
expect(convertFieldValue(null, { type: 'string' })).toBeNull();
});
});
describe('Cleanup', () => {
it('should cleanup resources', async () => {
await migrationManager.cleanup();
expect(migrationManager.getActiveMigrations()).toHaveLength(0);
expect(mockLogger.info).toHaveBeenCalledWith('Cleaning up migration manager');
});
});
});