mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
test: Add comprehensive unit tests for workflow versioning services
Add 158 unit tests (157 passing, 1 skipped) across 5 new test files to achieve strong coverage of the workflow versioning and auto-update features. New test files: - workflow-versioning-service.test.ts (39 tests) * Version backup, restore, deletion, pruning * Version history and comparison * Storage statistics and auto-pruning * Edge cases: missing API, version not found, restore failures - node-version-service.test.ts (37 tests) * Version discovery and caching (with TTL) * Version comparison and upgrade analysis * Breaking change detection and confidence scoring * Upgrade path suggestions and intermediate versions - node-migration-service.test.ts (32 tests, 1 skipped) * Node parameter migrations (add/remove/rename/set default) * Webhook UUID generation * Nested property migrations * Batch workflow migrations with validation - breaking-change-detector.test.ts (26 tests) * Registry-based and dynamic breaking change detection * Property additions/removals/requirement changes * Severity calculation and change merging * Nested property handling and recommendations - post-update-validator.test.ts (24 tests) * Post-update guidance generation * Required actions and deprecated properties * Behavior change documentation (Execute Workflow, Webhook) * Migration steps, confidence calculation, time estimation Also update README.md to include the new n8n_workflow_versions tool in the Workflow Management tools section. Coverage impact: - Targets services with highest missing coverage from Codecov report - Addresses 1630+ lines of missing coverage in new services - Comprehensive mocking of dependencies (database, API clients) - Follows existing test patterns from workflow-auto-fixer.test.ts All tests use vitest with proper mocking, edge case coverage, and deterministic assertions following project conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
This commit is contained in:
616
tests/unit/services/workflow-versioning-service.test.ts
Normal file
616
tests/unit/services/workflow-versioning-service.test.ts
Normal file
@@ -0,0 +1,616 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { WorkflowVersioningService, type WorkflowVersion, type BackupResult } from '@/services/workflow-versioning-service';
|
||||
import { NodeRepository } from '@/database/node-repository';
|
||||
import { N8nApiClient } from '@/services/n8n-api-client';
|
||||
import { WorkflowValidator } from '@/services/workflow-validator';
|
||||
import type { Workflow } from '@/types/n8n-api';
|
||||
|
||||
vi.mock('@/database/node-repository');
|
||||
vi.mock('@/services/n8n-api-client');
|
||||
vi.mock('@/services/workflow-validator');
|
||||
|
||||
describe('WorkflowVersioningService', () => {
|
||||
let service: WorkflowVersioningService;
|
||||
let mockRepository: NodeRepository;
|
||||
let mockApiClient: N8nApiClient;
|
||||
|
||||
const createMockWorkflow = (id: string, name: string, nodes: any[] = []): Workflow => ({
|
||||
id,
|
||||
name,
|
||||
active: false,
|
||||
nodes,
|
||||
connections: {},
|
||||
settings: {},
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z'
|
||||
});
|
||||
|
||||
const createMockVersion = (versionNumber: number): WorkflowVersion => ({
|
||||
id: versionNumber,
|
||||
workflowId: 'workflow-1',
|
||||
versionNumber,
|
||||
workflowName: 'Test Workflow',
|
||||
workflowSnapshot: createMockWorkflow('workflow-1', 'Test Workflow'),
|
||||
trigger: 'partial_update',
|
||||
createdAt: '2025-01-01T00:00:00.000Z'
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockRepository = new NodeRepository({} as any);
|
||||
mockApiClient = new N8nApiClient('http://test', 'test-key');
|
||||
service = new WorkflowVersioningService(mockRepository, mockApiClient);
|
||||
});
|
||||
|
||||
describe('createBackup', () => {
|
||||
it('should create a backup with version 1 for new workflow', async () => {
|
||||
const workflow = createMockWorkflow('workflow-1', 'Test Workflow');
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(1);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
|
||||
const result = await service.createBackup('workflow-1', workflow, {
|
||||
trigger: 'partial_update'
|
||||
});
|
||||
|
||||
expect(result.versionId).toBe(1);
|
||||
expect(result.versionNumber).toBe(1);
|
||||
expect(result.pruned).toBe(0);
|
||||
expect(result.message).toContain('Backup created (version 1)');
|
||||
});
|
||||
|
||||
it('should increment version number from latest version', async () => {
|
||||
const workflow = createMockWorkflow('workflow-1', 'Test Workflow');
|
||||
const existingVersions = [createMockVersion(3), createMockVersion(2)];
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue(existingVersions);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(4);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
|
||||
const result = await service.createBackup('workflow-1', workflow, {
|
||||
trigger: 'full_update'
|
||||
});
|
||||
|
||||
expect(result.versionNumber).toBe(4);
|
||||
expect(mockRepository.createWorkflowVersion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
versionNumber: 4
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should include context in version metadata', async () => {
|
||||
const workflow = createMockWorkflow('workflow-1', 'Test Workflow');
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(1);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
|
||||
await service.createBackup('workflow-1', workflow, {
|
||||
trigger: 'autofix',
|
||||
operations: [{ type: 'updateNode', nodeId: 'node-1' }],
|
||||
fixTypes: ['expression-format'],
|
||||
metadata: { testKey: 'testValue' }
|
||||
});
|
||||
|
||||
expect(mockRepository.createWorkflowVersion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
trigger: 'autofix',
|
||||
operations: [{ type: 'updateNode', nodeId: 'node-1' }],
|
||||
fixTypes: ['expression-format'],
|
||||
metadata: { testKey: 'testValue' }
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should auto-prune to 10 versions and report pruned count', async () => {
|
||||
const workflow = createMockWorkflow('workflow-1', 'Test Workflow');
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([createMockVersion(1)]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(2);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(3);
|
||||
|
||||
const result = await service.createBackup('workflow-1', workflow, {
|
||||
trigger: 'partial_update'
|
||||
});
|
||||
|
||||
expect(mockRepository.pruneWorkflowVersions).toHaveBeenCalledWith('workflow-1', 10);
|
||||
expect(result.pruned).toBe(3);
|
||||
expect(result.message).toContain('pruned 3 old version(s)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersionHistory', () => {
|
||||
it('should return formatted version history', async () => {
|
||||
const versions = [
|
||||
createMockVersion(3),
|
||||
createMockVersion(2),
|
||||
createMockVersion(1)
|
||||
];
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue(versions);
|
||||
|
||||
const result = await service.getVersionHistory('workflow-1', 10);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].versionNumber).toBe(3);
|
||||
expect(result[0].workflowId).toBe('workflow-1');
|
||||
expect(result[0].size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should include operation count when operations exist', async () => {
|
||||
const versionWithOps: WorkflowVersion = {
|
||||
...createMockVersion(1),
|
||||
operations: [{ type: 'updateNode' }, { type: 'addNode' }]
|
||||
};
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([versionWithOps]);
|
||||
|
||||
const result = await service.getVersionHistory('workflow-1', 10);
|
||||
|
||||
expect(result[0].operationCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should include fixTypes when present', async () => {
|
||||
const versionWithFixes: WorkflowVersion = {
|
||||
...createMockVersion(1),
|
||||
fixTypes: ['expression-format', 'typeversion-correction']
|
||||
};
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([versionWithFixes]);
|
||||
|
||||
const result = await service.getVersionHistory('workflow-1', 10);
|
||||
|
||||
expect(result[0].fixTypesApplied).toEqual(['expression-format', 'typeversion-correction']);
|
||||
});
|
||||
|
||||
it('should respect the limit parameter', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([]);
|
||||
|
||||
await service.getVersionHistory('workflow-1', 5);
|
||||
|
||||
expect(mockRepository.getWorkflowVersions).toHaveBeenCalledWith('workflow-1', 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersion', () => {
|
||||
it('should return the requested version', async () => {
|
||||
const version = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(version);
|
||||
|
||||
const result = await service.getVersion(1);
|
||||
|
||||
expect(result).toEqual(version);
|
||||
});
|
||||
|
||||
it('should return null if version does not exist', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(null);
|
||||
|
||||
const result = await service.getVersion(999);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreVersion', () => {
|
||||
it('should fail if API client is not configured', async () => {
|
||||
const serviceWithoutApi = new WorkflowVersioningService(mockRepository);
|
||||
|
||||
const result = await serviceWithoutApi.restoreVersion('workflow-1', 1);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('API client not configured');
|
||||
expect(result.backupCreated).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail if version does not exist', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(null);
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', 999);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Version 999 not found');
|
||||
expect(result.backupCreated).toBe(false);
|
||||
});
|
||||
|
||||
it('should restore latest version when no versionId provided', async () => {
|
||||
const version = createMockVersion(3);
|
||||
vi.spyOn(mockRepository, 'getLatestWorkflowVersion').mockReturnValue(version);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(4);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
vi.spyOn(mockApiClient, 'getWorkflow').mockResolvedValue(createMockWorkflow('workflow-1', 'Current'));
|
||||
vi.spyOn(mockApiClient, 'updateWorkflow').mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', undefined, false);
|
||||
|
||||
expect(mockRepository.getLatestWorkflowVersion).toHaveBeenCalledWith('workflow-1');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail if no backup versions exist and no versionId provided', async () => {
|
||||
vi.spyOn(mockRepository, 'getLatestWorkflowVersion').mockReturnValue(null);
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', undefined);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('No backup versions found');
|
||||
});
|
||||
|
||||
it('should validate version before restore when validateBefore is true', async () => {
|
||||
const version = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(version);
|
||||
|
||||
const mockValidator = {
|
||||
validateWorkflow: vi.fn().mockResolvedValue({
|
||||
errors: [{ message: 'Validation error' }]
|
||||
})
|
||||
};
|
||||
vi.spyOn(WorkflowValidator.prototype, 'validateWorkflow').mockImplementation(
|
||||
mockValidator.validateWorkflow
|
||||
);
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', 1, true);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('has validation errors');
|
||||
expect(result.validationErrors).toEqual(['Validation error']);
|
||||
expect(result.backupCreated).toBe(false);
|
||||
});
|
||||
|
||||
it('should skip validation when validateBefore is false', async () => {
|
||||
const version = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(version);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(2);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
vi.spyOn(mockApiClient, 'getWorkflow').mockResolvedValue(createMockWorkflow('workflow-1', 'Current'));
|
||||
vi.spyOn(mockApiClient, 'updateWorkflow').mockResolvedValue(undefined);
|
||||
|
||||
const mockValidator = vi.fn();
|
||||
vi.spyOn(WorkflowValidator.prototype, 'validateWorkflow').mockImplementation(mockValidator);
|
||||
|
||||
await service.restoreVersion('workflow-1', 1, false);
|
||||
|
||||
expect(mockValidator).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create backup before restoring', async () => {
|
||||
const versionToRestore = createMockVersion(1);
|
||||
const currentWorkflow = createMockWorkflow('workflow-1', 'Current Workflow');
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(versionToRestore);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([createMockVersion(2)]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(3);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
vi.spyOn(mockApiClient, 'getWorkflow').mockResolvedValue(currentWorkflow);
|
||||
vi.spyOn(mockApiClient, 'updateWorkflow').mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', 1, false);
|
||||
|
||||
expect(mockApiClient.getWorkflow).toHaveBeenCalledWith('workflow-1');
|
||||
expect(mockRepository.createWorkflowVersion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workflowSnapshot: currentWorkflow,
|
||||
metadata: expect.objectContaining({
|
||||
reason: 'Backup before rollback',
|
||||
restoringToVersion: 1
|
||||
})
|
||||
})
|
||||
);
|
||||
expect(result.backupCreated).toBe(true);
|
||||
expect(result.backupVersionId).toBe(3);
|
||||
});
|
||||
|
||||
it('should fail if backup creation fails', async () => {
|
||||
const version = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(version);
|
||||
vi.spyOn(mockApiClient, 'getWorkflow').mockRejectedValue(new Error('Backup failed'));
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', 1, false);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Failed to create backup before restore');
|
||||
expect(result.backupCreated).toBe(false);
|
||||
});
|
||||
|
||||
it('should successfully restore workflow', async () => {
|
||||
const versionToRestore = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(versionToRestore);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([createMockVersion(2)]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(3);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
vi.spyOn(mockApiClient, 'getWorkflow').mockResolvedValue(createMockWorkflow('workflow-1', 'Current'));
|
||||
vi.spyOn(mockApiClient, 'updateWorkflow').mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', 1, false);
|
||||
|
||||
expect(mockApiClient.updateWorkflow).toHaveBeenCalledWith('workflow-1', versionToRestore.workflowSnapshot);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('Successfully restored workflow to version 1');
|
||||
expect(result.fromVersion).toBe(3);
|
||||
expect(result.toVersionId).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle restore API failures', async () => {
|
||||
const version = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(version);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersions').mockReturnValue([]);
|
||||
vi.spyOn(mockRepository, 'createWorkflowVersion').mockReturnValue(2);
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
vi.spyOn(mockApiClient, 'getWorkflow').mockResolvedValue(createMockWorkflow('workflow-1', 'Current'));
|
||||
vi.spyOn(mockApiClient, 'updateWorkflow').mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const result = await service.restoreVersion('workflow-1', 1, false);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Failed to restore workflow');
|
||||
expect(result.backupCreated).toBe(true);
|
||||
expect(result.backupVersionId).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteVersion', () => {
|
||||
it('should delete a specific version', async () => {
|
||||
const version = createMockVersion(1);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(version);
|
||||
vi.spyOn(mockRepository, 'deleteWorkflowVersion').mockReturnValue(undefined);
|
||||
|
||||
const result = await service.deleteVersion(1);
|
||||
|
||||
expect(mockRepository.deleteWorkflowVersion).toHaveBeenCalledWith(1);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('Deleted version 1');
|
||||
});
|
||||
|
||||
it('should fail if version does not exist', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(null);
|
||||
|
||||
const result = await service.deleteVersion(999);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Version 999 not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAllVersions', () => {
|
||||
it('should delete all versions for a workflow', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersionCount').mockReturnValue(5);
|
||||
vi.spyOn(mockRepository, 'deleteWorkflowVersionsByWorkflowId').mockReturnValue(5);
|
||||
|
||||
const result = await service.deleteAllVersions('workflow-1');
|
||||
|
||||
expect(result.deleted).toBe(5);
|
||||
expect(result.message).toContain('Deleted 5 version(s)');
|
||||
});
|
||||
|
||||
it('should return zero if no versions exist', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersionCount').mockReturnValue(0);
|
||||
|
||||
const result = await service.deleteAllVersions('workflow-1');
|
||||
|
||||
expect(result.deleted).toBe(0);
|
||||
expect(result.message).toContain('No versions found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pruneVersions', () => {
|
||||
it('should prune versions and return counts', async () => {
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(3);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersionCount').mockReturnValue(10);
|
||||
|
||||
const result = await service.pruneVersions('workflow-1', 10);
|
||||
|
||||
expect(result.pruned).toBe(3);
|
||||
expect(result.remaining).toBe(10);
|
||||
});
|
||||
|
||||
it('should use custom maxVersions parameter', async () => {
|
||||
vi.spyOn(mockRepository, 'pruneWorkflowVersions').mockReturnValue(0);
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersionCount').mockReturnValue(5);
|
||||
|
||||
await service.pruneVersions('workflow-1', 5);
|
||||
|
||||
expect(mockRepository.pruneWorkflowVersions).toHaveBeenCalledWith('workflow-1', 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateAllVersions', () => {
|
||||
it('should refuse to truncate without confirmation', async () => {
|
||||
const result = await service.truncateAllVersions(false);
|
||||
|
||||
expect(result.deleted).toBe(0);
|
||||
expect(result.message).toContain('not confirmed');
|
||||
});
|
||||
|
||||
it('should truncate all versions when confirmed', async () => {
|
||||
vi.spyOn(mockRepository, 'truncateWorkflowVersions').mockReturnValue(50);
|
||||
|
||||
const result = await service.truncateAllVersions(true);
|
||||
|
||||
expect(result.deleted).toBe(50);
|
||||
expect(result.message).toContain('Truncated workflow_versions table');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStorageStats', () => {
|
||||
it('should return formatted storage statistics', async () => {
|
||||
const mockStats = {
|
||||
totalVersions: 10,
|
||||
totalSize: 1024000,
|
||||
byWorkflow: [
|
||||
{
|
||||
workflowId: 'workflow-1',
|
||||
workflowName: 'Test Workflow',
|
||||
versionCount: 5,
|
||||
totalSize: 512000,
|
||||
lastBackup: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
vi.spyOn(mockRepository, 'getVersionStorageStats').mockReturnValue(mockStats);
|
||||
|
||||
const result = await service.getStorageStats();
|
||||
|
||||
expect(result.totalVersions).toBe(10);
|
||||
expect(result.totalSizeFormatted).toContain('KB');
|
||||
expect(result.byWorkflow).toHaveLength(1);
|
||||
expect(result.byWorkflow[0].totalSizeFormatted).toContain('KB');
|
||||
});
|
||||
|
||||
it('should format bytes correctly', async () => {
|
||||
const mockStats = {
|
||||
totalVersions: 1,
|
||||
totalSize: 0,
|
||||
byWorkflow: []
|
||||
};
|
||||
|
||||
vi.spyOn(mockRepository, 'getVersionStorageStats').mockReturnValue(mockStats);
|
||||
|
||||
const result = await service.getStorageStats();
|
||||
|
||||
expect(result.totalSizeFormatted).toBe('0 Bytes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareVersions', () => {
|
||||
it('should detect added nodes', async () => {
|
||||
const v1 = createMockVersion(1);
|
||||
v1.workflowSnapshot.nodes = [{ id: 'node-1', name: 'Node 1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} }];
|
||||
|
||||
const v2 = createMockVersion(2);
|
||||
v2.workflowSnapshot.nodes = [
|
||||
{ id: 'node-1', name: 'Node 1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} },
|
||||
{ id: 'node-2', name: 'Node 2', type: 'test', typeVersion: 1, position: [100, 0], parameters: {} }
|
||||
];
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion')
|
||||
.mockReturnValueOnce(v1)
|
||||
.mockReturnValueOnce(v2);
|
||||
|
||||
const result = await service.compareVersions(1, 2);
|
||||
|
||||
expect(result.addedNodes).toEqual(['node-2']);
|
||||
expect(result.removedNodes).toEqual([]);
|
||||
expect(result.modifiedNodes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect removed nodes', async () => {
|
||||
const v1 = createMockVersion(1);
|
||||
v1.workflowSnapshot.nodes = [
|
||||
{ id: 'node-1', name: 'Node 1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} },
|
||||
{ id: 'node-2', name: 'Node 2', type: 'test', typeVersion: 1, position: [100, 0], parameters: {} }
|
||||
];
|
||||
|
||||
const v2 = createMockVersion(2);
|
||||
v2.workflowSnapshot.nodes = [{ id: 'node-1', name: 'Node 1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} }];
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion')
|
||||
.mockReturnValueOnce(v1)
|
||||
.mockReturnValueOnce(v2);
|
||||
|
||||
const result = await service.compareVersions(1, 2);
|
||||
|
||||
expect(result.removedNodes).toEqual(['node-2']);
|
||||
expect(result.addedNodes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect modified nodes', async () => {
|
||||
const v1 = createMockVersion(1);
|
||||
v1.workflowSnapshot.nodes = [{ id: 'node-1', name: 'Node 1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} }];
|
||||
|
||||
const v2 = createMockVersion(2);
|
||||
v2.workflowSnapshot.nodes = [{ id: 'node-1', name: 'Node 1', type: 'test', typeVersion: 2, position: [0, 0], parameters: {} }];
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion')
|
||||
.mockReturnValueOnce(v1)
|
||||
.mockReturnValueOnce(v2);
|
||||
|
||||
const result = await service.compareVersions(1, 2);
|
||||
|
||||
expect(result.modifiedNodes).toEqual(['node-1']);
|
||||
});
|
||||
|
||||
it('should detect connection changes', async () => {
|
||||
const v1 = createMockVersion(1);
|
||||
v1.workflowSnapshot.connections = { 'node-1': { main: [[{ node: 'node-2', type: 'main', index: 0 }]] } };
|
||||
|
||||
const v2 = createMockVersion(2);
|
||||
v2.workflowSnapshot.connections = {};
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion')
|
||||
.mockReturnValueOnce(v1)
|
||||
.mockReturnValueOnce(v2);
|
||||
|
||||
const result = await service.compareVersions(1, 2);
|
||||
|
||||
expect(result.connectionChanges).toBe(1);
|
||||
});
|
||||
|
||||
it('should detect settings changes', async () => {
|
||||
const v1 = createMockVersion(1);
|
||||
v1.workflowSnapshot.settings = { executionOrder: 'v0' };
|
||||
|
||||
const v2 = createMockVersion(2);
|
||||
v2.workflowSnapshot.settings = { executionOrder: 'v1' };
|
||||
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion')
|
||||
.mockReturnValueOnce(v1)
|
||||
.mockReturnValueOnce(v2);
|
||||
|
||||
const result = await service.compareVersions(1, 2);
|
||||
|
||||
expect(result.settingChanges).toHaveProperty('executionOrder');
|
||||
expect(result.settingChanges.executionOrder.before).toBe('v0');
|
||||
expect(result.settingChanges.executionOrder.after).toBe('v1');
|
||||
});
|
||||
|
||||
it('should throw error if version not found', async () => {
|
||||
vi.spyOn(mockRepository, 'getWorkflowVersion').mockReturnValue(null);
|
||||
|
||||
await expect(service.compareVersions(1, 2)).rejects.toThrow('One or both versions not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should format bytes to human-readable string', () => {
|
||||
// Access private method through any cast
|
||||
const formatBytes = (service as any).formatBytes.bind(service);
|
||||
|
||||
expect(formatBytes(0)).toBe('0 Bytes');
|
||||
expect(formatBytes(500)).toBe('500 Bytes');
|
||||
expect(formatBytes(1024)).toBe('1 KB');
|
||||
expect(formatBytes(1048576)).toBe('1 MB');
|
||||
expect(formatBytes(1073741824)).toBe('1 GB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('diffObjects', () => {
|
||||
it('should detect object differences', () => {
|
||||
const diffObjects = (service as any).diffObjects.bind(service);
|
||||
|
||||
const obj1 = { a: 1, b: 2 };
|
||||
const obj2 = { a: 1, b: 3, c: 4 };
|
||||
|
||||
const diff = diffObjects(obj1, obj2);
|
||||
|
||||
expect(diff).toHaveProperty('b');
|
||||
expect(diff.b).toEqual({ before: 2, after: 3 });
|
||||
expect(diff).toHaveProperty('c');
|
||||
expect(diff.c).toEqual({ before: undefined, after: 4 });
|
||||
});
|
||||
|
||||
it('should return empty object when no differences', () => {
|
||||
const diffObjects = (service as any).diffObjects.bind(service);
|
||||
|
||||
const obj1 = { a: 1, b: 2 };
|
||||
const obj2 = { a: 1, b: 2 };
|
||||
|
||||
const diff = diffObjects(obj1, obj2);
|
||||
|
||||
expect(Object.keys(diff)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user