409e92d6ae
Build and Push Docker Images / Build Docker Image (push) Has been cancelled
Build and Push Docker Images / Build Railway Docker Image (push) Has been cancelled
Build and Publish n8n Docker Image / test-image (push) Has been cancelled
Dependency Compatibility Check / Fresh Install Dependency Check (push) Has been cancelled
Build and Publish n8n Docker Image / build-and-push (push) Has been cancelled
Build and Publish n8n Docker Image / create-release (push) Has been cancelled
Automated Release / Detect Version Change (push) Has been cancelled
Automated Release / Generate Release Notes (push) Has been cancelled
Automated Release / Create GitHub Release (push) Has been cancelled
Automated Release / Package MCPB Bundle (push) Has been cancelled
Automated Release / Build and Verify (push) Has been cancelled
Automated Release / Publish to NPM (push) Has been cancelled
Automated Release / Build and Push Docker Images (push) Has been cancelled
Automated Release / Update Documentation (push) Has been cancelled
Automated Release / Notify Release Completion (push) Has been cancelled
Secret Scan / secretlint (push) Has been cancelled
Test Suite / test (push) Has been cancelled
Test Suite / cjs-runtime (push) Has been cancelled
Test Suite / publish-results (push) Has been cancelled
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
// Mock dependencies at module level
|
|
const mockDb = {
|
|
prepare: vi.fn().mockReturnValue({
|
|
get: vi.fn(),
|
|
all: vi.fn(),
|
|
run: vi.fn()
|
|
}),
|
|
exec: vi.fn(),
|
|
close: vi.fn(),
|
|
pragma: vi.fn(),
|
|
inTransaction: false,
|
|
transaction: vi.fn(),
|
|
checkFTS5Support: vi.fn()
|
|
};
|
|
|
|
vi.mock('../../../src/database/database-adapter', () => ({
|
|
createDatabaseAdapter: vi.fn().mockResolvedValue(mockDb)
|
|
}));
|
|
|
|
vi.mock('../../../src/database/node-repository', () => ({
|
|
NodeRepository: vi.fn().mockImplementation(() => ({
|
|
getNodeTypes: vi.fn().mockReturnValue([]),
|
|
pruneExpiredWorkflowVersions: vi.fn()
|
|
}))
|
|
}));
|
|
|
|
vi.mock('../../../src/database/migrations/add-workflow-versions-instance-id', () => ({
|
|
migrateWorkflowVersionsInstanceId: vi.fn().mockReturnValue(false)
|
|
}));
|
|
|
|
vi.mock('../../../src/templates/template-service', () => ({
|
|
TemplateService: vi.fn().mockImplementation(() => ({}))
|
|
}));
|
|
|
|
vi.mock('../../../src/services/enhanced-config-validator', () => ({
|
|
EnhancedConfigValidator: {
|
|
initializeSimilarityServices: vi.fn()
|
|
}
|
|
}));
|
|
|
|
vi.mock('../../../src/utils/logger', () => ({
|
|
logger: {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn()
|
|
}
|
|
}));
|
|
|
|
describe('Shared Database Module', () => {
|
|
let sharedDbModule: typeof import('../../../src/database/shared-database');
|
|
let createDatabaseAdapter: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(async () => {
|
|
// Reset all mocks
|
|
vi.clearAllMocks();
|
|
mockDb.close.mockReset();
|
|
|
|
// Reset modules to get fresh state
|
|
vi.resetModules();
|
|
|
|
// Import fresh module
|
|
sharedDbModule = await import('../../../src/database/shared-database');
|
|
|
|
// Get the mocked function
|
|
const adapterModule = await import('../../../src/database/database-adapter');
|
|
createDatabaseAdapter = adapterModule.createDatabaseAdapter as ReturnType<typeof vi.fn>;
|
|
createDatabaseAdapter.mockResolvedValue(mockDb);
|
|
|
|
// Re-establish constructor mock implementation. The global afterEach runs
|
|
// vi.restoreAllMocks(), which clears vi.fn implementations between tests.
|
|
const { NodeRepository } = await import('../../../src/database/node-repository');
|
|
(NodeRepository as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
|
getNodeTypes: vi.fn().mockReturnValue([]),
|
|
pruneExpiredWorkflowVersions: vi.fn()
|
|
}));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up any shared state by closing
|
|
try {
|
|
await sharedDbModule.closeSharedDatabase();
|
|
} catch {
|
|
// Ignore errors during cleanup
|
|
}
|
|
});
|
|
|
|
describe('getSharedDatabase', () => {
|
|
it('should initialize database on first call', async () => {
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
|
|
expect(state).toBeDefined();
|
|
expect(state.db).toBe(mockDb);
|
|
expect(state.dbPath).toBe('/path/to/db');
|
|
expect(state.refCount).toBe(1);
|
|
expect(state.initialized).toBe(true);
|
|
expect(createDatabaseAdapter).toHaveBeenCalledWith('/path/to/db');
|
|
});
|
|
|
|
it('should reuse existing connection and increment refCount', async () => {
|
|
// First call initializes
|
|
const state1 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(state1.refCount).toBe(1);
|
|
|
|
// Second call reuses
|
|
const state2 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(state2.refCount).toBe(2);
|
|
|
|
// Same object
|
|
expect(state1).toBe(state2);
|
|
|
|
// Only initialized once
|
|
expect(createDatabaseAdapter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should throw error when called with different path', async () => {
|
|
await sharedDbModule.getSharedDatabase('/path/to/db1');
|
|
|
|
await expect(sharedDbModule.getSharedDatabase('/path/to/db2'))
|
|
.rejects.toThrow('Shared database already initialized with different path');
|
|
});
|
|
|
|
it('should handle concurrent initialization requests', async () => {
|
|
// Start two requests concurrently
|
|
const [state1, state2] = await Promise.all([
|
|
sharedDbModule.getSharedDatabase('/path/to/db'),
|
|
sharedDbModule.getSharedDatabase('/path/to/db')
|
|
]);
|
|
|
|
// Both should get the same state
|
|
expect(state1).toBe(state2);
|
|
|
|
// RefCount should be 2 (one for each call)
|
|
expect(state1.refCount).toBe(2);
|
|
|
|
// Only one actual initialization
|
|
expect(createDatabaseAdapter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle initialization failure', async () => {
|
|
createDatabaseAdapter.mockRejectedValueOnce(new Error('DB error'));
|
|
|
|
await expect(sharedDbModule.getSharedDatabase('/path/to/db'))
|
|
.rejects.toThrow('DB error');
|
|
|
|
// After failure, should not be initialized
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
|
});
|
|
|
|
it('should allow retry after initialization failure', async () => {
|
|
// First call fails
|
|
createDatabaseAdapter.mockRejectedValueOnce(new Error('DB error'));
|
|
await expect(sharedDbModule.getSharedDatabase('/path/to/db'))
|
|
.rejects.toThrow('DB error');
|
|
|
|
// Reset mock for successful call
|
|
createDatabaseAdapter.mockResolvedValueOnce(mockDb);
|
|
|
|
// Second call succeeds
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
|
|
expect(state).toBeDefined();
|
|
expect(state.initialized).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('releaseSharedDatabase', () => {
|
|
it('should decrement refCount', async () => {
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(state.refCount).toBe(1);
|
|
|
|
sharedDbModule.releaseSharedDatabase(state);
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
});
|
|
|
|
it('should not decrement below 0', async () => {
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
|
|
// Release once (refCount: 1 -> 0)
|
|
sharedDbModule.releaseSharedDatabase(state);
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
|
|
// Release again (should stay at 0, not go negative)
|
|
sharedDbModule.releaseSharedDatabase(state);
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
});
|
|
|
|
it('should handle null state gracefully', () => {
|
|
// Should not throw
|
|
sharedDbModule.releaseSharedDatabase(null as any);
|
|
});
|
|
|
|
it('should not close database when refCount hits 0', async () => {
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
sharedDbModule.releaseSharedDatabase(state);
|
|
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
expect(mockDb.close).not.toHaveBeenCalled();
|
|
|
|
// Database should still be accessible
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('closeSharedDatabase', () => {
|
|
it('should close database and clear state', async () => {
|
|
// Get state
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(1);
|
|
|
|
await sharedDbModule.closeSharedDatabase();
|
|
|
|
// State should be cleared
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
});
|
|
|
|
it('should handle close error gracefully', async () => {
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
mockDb.close.mockImplementationOnce(() => {
|
|
throw new Error('Close error');
|
|
});
|
|
|
|
// Should not throw
|
|
await sharedDbModule.closeSharedDatabase();
|
|
|
|
// State should still be cleared
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
|
});
|
|
|
|
it('should be idempotent when already closed', async () => {
|
|
// Close without ever initializing
|
|
await sharedDbModule.closeSharedDatabase();
|
|
|
|
// Should not throw
|
|
await sharedDbModule.closeSharedDatabase();
|
|
});
|
|
|
|
it('should allow re-initialization after close', async () => {
|
|
// Initialize
|
|
const state1 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(state1.refCount).toBe(1);
|
|
|
|
// Close
|
|
await sharedDbModule.closeSharedDatabase();
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
|
|
|
// Re-initialize
|
|
const state2 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(state2.refCount).toBe(1);
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
|
|
|
// Should be a new state object
|
|
expect(state1).not.toBe(state2);
|
|
});
|
|
});
|
|
|
|
describe('isSharedDatabaseInitialized', () => {
|
|
it('should return false before initialization', () => {
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
|
});
|
|
|
|
it('should return true after initialization', async () => {
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
|
});
|
|
|
|
it('should return false after close', async () => {
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
await sharedDbModule.closeSharedDatabase();
|
|
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getSharedDatabaseRefCount', () => {
|
|
it('should return 0 before initialization', () => {
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
});
|
|
|
|
it('should return correct refCount after multiple operations', async () => {
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(1);
|
|
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(2);
|
|
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(3);
|
|
|
|
sharedDbModule.releaseSharedDatabase(state);
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(2);
|
|
});
|
|
|
|
it('should return 0 after close', async () => {
|
|
await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
await sharedDbModule.closeSharedDatabase();
|
|
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('SharedDatabaseState interface', () => {
|
|
it('should expose correct properties', async () => {
|
|
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
|
|
|
expect(state).toHaveProperty('db');
|
|
expect(state).toHaveProperty('repository');
|
|
expect(state).toHaveProperty('templateService');
|
|
expect(state).toHaveProperty('dbPath');
|
|
expect(state).toHaveProperty('refCount');
|
|
expect(state).toHaveProperty('initialized');
|
|
});
|
|
});
|
|
});
|