mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
281 lines
8.9 KiB
TypeScript
281 lines
8.9 KiB
TypeScript
import type { IExecutionResponse } from '@n8n/db';
|
|
import type { ExecutionRepository } from '@n8n/db';
|
|
import { mock } from 'jest-mock-extended';
|
|
import { WorkflowOperationError } from 'n8n-workflow';
|
|
|
|
import type { ActiveExecutions } from '@/active-executions';
|
|
import type { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
|
|
import config from '@/config';
|
|
import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error';
|
|
import { MissingExecutionStopError } from '@/errors/missing-execution-stop.error';
|
|
import { ExecutionService } from '@/executions/execution.service';
|
|
import type { ExecutionRequest } from '@/executions/execution.types';
|
|
import { ScalingService } from '@/scaling/scaling.service';
|
|
import type { Job } from '@/scaling/scaling.types';
|
|
import type { WaitTracker } from '@/wait-tracker';
|
|
import { mockInstance } from '@test/mocking';
|
|
|
|
describe('ExecutionService', () => {
|
|
const scalingService = mockInstance(ScalingService);
|
|
const activeExecutions = mock<ActiveExecutions>();
|
|
const executionRepository = mock<ExecutionRepository>();
|
|
const waitTracker = mock<WaitTracker>();
|
|
const concurrencyControl = mock<ConcurrencyControlService>();
|
|
|
|
const executionService = new ExecutionService(
|
|
mock(),
|
|
mock(),
|
|
activeExecutions,
|
|
mock(),
|
|
mock(),
|
|
executionRepository,
|
|
mock(),
|
|
mock(),
|
|
waitTracker,
|
|
mock(),
|
|
concurrencyControl,
|
|
mock(),
|
|
mock(),
|
|
);
|
|
|
|
beforeEach(() => {
|
|
config.set('executions.mode', 'regular');
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('retry', () => {
|
|
it('should error on retrying a execution that was aborted before starting', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
executionRepository.findWithUnflattenedData.mockResolvedValue(
|
|
mock<IExecutionResponse>({ data: { executionData: undefined } }),
|
|
);
|
|
const req = mock<ExecutionRequest.Retry>();
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
const retry = executionService.retry(req, []);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
await expect(retry).rejects.toThrow(AbortedExecutionRetryError);
|
|
});
|
|
});
|
|
|
|
describe('stop', () => {
|
|
it('should throw when stopping a missing execution', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
executionRepository.findSingleExecution.mockResolvedValue(undefined);
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
const stop = executionService.stop('inexistent-123');
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
await expect(stop).rejects.toThrowError(MissingExecutionStopError);
|
|
});
|
|
|
|
it('should throw when stopping a not-in-progress execution', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'success' });
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
const stop = executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
await expect(stop).rejects.toThrowError(WorkflowOperationError);
|
|
});
|
|
|
|
describe('regular mode', () => {
|
|
it('should stop a `running` execution in regular mode', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'running' });
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
concurrencyControl.has.mockReturnValue(false);
|
|
activeExecutions.has.mockReturnValue(true);
|
|
waitTracker.has.mockReturnValue(false);
|
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
await executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
expect(concurrencyControl.remove).not.toHaveBeenCalled();
|
|
expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id);
|
|
expect(waitTracker.stopExecution).not.toHaveBeenCalled();
|
|
expect(executionRepository.stopDuringRun).toHaveBeenCalledWith(execution);
|
|
});
|
|
|
|
it('should stop a `waiting` execution in regular mode', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'waiting' });
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
concurrencyControl.has.mockReturnValue(false);
|
|
activeExecutions.has.mockReturnValue(true);
|
|
waitTracker.has.mockReturnValue(true);
|
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
await executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
expect(concurrencyControl.remove).not.toHaveBeenCalled();
|
|
expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id);
|
|
expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id);
|
|
expect(executionRepository.stopDuringRun).toHaveBeenCalledWith(execution);
|
|
});
|
|
|
|
it('should stop a concurrency-controlled `new` execution in regular mode', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'new', mode: 'trigger' });
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
concurrencyControl.has.mockReturnValue(true);
|
|
activeExecutions.has.mockReturnValue(false);
|
|
waitTracker.has.mockReturnValue(false);
|
|
executionRepository.stopBeforeRun.mockResolvedValue(mock<IExecutionResponse>());
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
await executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
expect(concurrencyControl.remove).toHaveBeenCalledWith({
|
|
mode: execution.mode,
|
|
executionId: execution.id,
|
|
});
|
|
expect(activeExecutions.stopExecution).not.toHaveBeenCalled();
|
|
expect(waitTracker.stopExecution).not.toHaveBeenCalled();
|
|
expect(executionRepository.stopDuringRun).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('scaling mode', () => {
|
|
describe('manual execution', () => {
|
|
it('should stop a `running` execution in scaling mode', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
config.set('executions.mode', 'queue');
|
|
const execution = mock<IExecutionResponse>({
|
|
id: '123',
|
|
mode: 'manual',
|
|
status: 'running',
|
|
});
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
concurrencyControl.has.mockReturnValue(false);
|
|
activeExecutions.has.mockReturnValue(true);
|
|
waitTracker.has.mockReturnValue(false);
|
|
const job = mock<Job>({ data: { executionId: '123' } });
|
|
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
|
// @ts-expect-error Private method
|
|
const stopInRegularModeSpy = jest.spyOn(executionService, 'stopInRegularMode');
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
await executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
expect(stopInRegularModeSpy).not.toHaveBeenCalled();
|
|
expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id);
|
|
expect(executionRepository.stopDuringRun).toHaveBeenCalledWith(execution);
|
|
|
|
expect(concurrencyControl.remove).not.toHaveBeenCalled();
|
|
expect(waitTracker.stopExecution).not.toHaveBeenCalled();
|
|
expect(scalingService.stopJob).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('production execution', () => {
|
|
it('should stop a `running` execution in scaling mode', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
config.set('executions.mode', 'queue');
|
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'running' });
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
waitTracker.has.mockReturnValue(false);
|
|
const job = mock<Job>({ data: { executionId: '123' } });
|
|
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
await executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
expect(waitTracker.stopExecution).not.toHaveBeenCalled();
|
|
expect(activeExecutions.stopExecution).toHaveBeenCalled();
|
|
expect(scalingService.findJobsByStatus).not.toHaveBeenCalled();
|
|
expect(scalingService.stopJob).not.toHaveBeenCalled();
|
|
expect(executionRepository.stopDuringRun).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should stop a `waiting` execution in scaling mode', async () => {
|
|
/**
|
|
* Arrange
|
|
*/
|
|
config.set('executions.mode', 'queue');
|
|
const execution = mock<IExecutionResponse>({ id: '123', status: 'waiting' });
|
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
|
waitTracker.has.mockReturnValue(true);
|
|
const job = mock<Job>({ data: { executionId: '123' } });
|
|
scalingService.findJobsByStatus.mockResolvedValue([job]);
|
|
executionRepository.stopDuringRun.mockResolvedValue(mock<IExecutionResponse>());
|
|
|
|
/**
|
|
* Act
|
|
*/
|
|
await executionService.stop(execution.id);
|
|
|
|
/**
|
|
* Assert
|
|
*/
|
|
expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id);
|
|
expect(scalingService.findJobsByStatus).not.toHaveBeenCalled();
|
|
expect(scalingService.stopJob).not.toHaveBeenCalled();
|
|
expect(executionRepository.stopDuringRun).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|