diff --git a/openapi3.yaml b/openapi3.yaml index 4797fce..56a13d1 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -141,6 +141,12 @@ paths: application/json: schema: $ref: '#/components/schemas/errorMessage' + '409': + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/errorMessage' '404': description: Job not found content: @@ -236,6 +242,12 @@ paths: application/json: schema: $ref: '#/components/schemas/errorMessage' + '409': + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/errorMessage' '500': description: Internal Server Error content: @@ -550,6 +562,12 @@ paths: application/json: schema: $ref: '#/components/schemas/errorMessage' + '409': + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/errorMessage' tags: - tasksManagement components: diff --git a/src/DAL/repositories/jobRepository.ts b/src/DAL/repositories/jobRepository.ts index 3dcc2ba..f2a374c 100644 --- a/src/DAL/repositories/jobRepository.ts +++ b/src/DAL/repositories/jobRepository.ts @@ -269,14 +269,4 @@ export class JobRepository extends GeneralRepository { return resettableJobsCount > 0; } - - public async isJobHasPendingTasks(jobId: string): Promise { - const pendingTasksCount = await this.createQueryBuilder('job') - .leftJoinAndSelect('job.tasks', 'task') - .where('job.id = :jobId', { jobId }) - .andWhere('task.status = :status', { status: OperationStatus.PENDING }) - .getCount(); - - return pendingTasksCount > 0; - } } diff --git a/src/common/dataModels/jobs.ts b/src/common/dataModels/jobs.ts index 411fdb9..9d9837e 100644 --- a/src/common/dataModels/jobs.ts +++ b/src/common/dataModels/jobs.ts @@ -99,7 +99,7 @@ export interface IGetJobResponse { tasks?: GetTasksResponse; created: Date; updated: Date; - status?: OperationStatus; + status: OperationStatus; percentage?: number; isCleaned: boolean; priority?: number; diff --git a/src/common/middlewares/validateJobStatusMiddleware.ts b/src/common/middlewares/validateJobStatusMiddleware.ts new file mode 100644 index 0000000..32df47c --- /dev/null +++ b/src/common/middlewares/validateJobStatusMiddleware.ts @@ -0,0 +1,28 @@ +import { Request, Response, NextFunction } from 'express'; +import { OperationStatus } from '@map-colonies/mc-priority-queue'; +import { ConflictError } from '@map-colonies/error-types'; +import { Logger } from '@map-colonies/js-logger'; +import { container } from 'tsyringe'; +import { SERVICES } from '../constants'; +import { JobManager } from '../../jobs/models/jobManager'; + +export const validateJobStatusMiddleware = async (req: Request<{ jobId: string }>, res: Response, next: NextFunction): Promise => { + const jobManager = container.resolve(JobManager); + const logger = container.resolve(SERVICES.LOGGER); + + const finalStateStatuses: OperationStatus[] = [OperationStatus.COMPLETED, OperationStatus.ABORTED, OperationStatus.EXPIRED]; + try { + const jobId = req.params.jobId; + const job = await jobManager.getJob({ jobId }, { shouldReturnTasks: false, shouldReturnAvailableActions: false }); + + if (finalStateStatuses.includes(job.status)) { + const errorMessage = `Cannot perform the requested operation on job with final state status: ${job.status}`; + logger.error({ msg: errorMessage, jobId, status: job.status }); + throw new ConflictError(errorMessage); + } + + next(); + } catch (error) { + next(error); + } +}; diff --git a/src/jobs/routes/jobRouter.ts b/src/jobs/routes/jobRouter.ts index c98b076..ae2aa3b 100644 --- a/src/jobs/routes/jobRouter.ts +++ b/src/jobs/routes/jobRouter.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ import { Router } from 'express'; import { FactoryFunction } from 'tsyringe'; import { JobController } from '../controllers/jobController'; import { TaskController } from '../controllers/taskController'; +import { validateJobStatusMiddleware } from '../../common/middlewares/validateJobStatusMiddleware'; const jobRouterFactory: FactoryFunction = (dependencyContainer) => { const router = Router(); @@ -13,13 +15,13 @@ const jobRouterFactory: FactoryFunction = (dependencyContainer) => { router.post('/', jobsController.createResource); router.get('/parameters', jobsController.getJobByJobsParameters); router.get('/:jobId', jobsController.getResource); - router.put('/:jobId', jobsController.updateResource); + router.put('/:jobId', validateJobStatusMiddleware, jobsController.updateResource); router.delete('/:jobId', jobsController.deleteResource); router.post('/:jobId/resettable', jobsController.isResettable); - router.post('/:jobId/reset', jobsController.resetJob); + router.post('/:jobId/reset', validateJobStatusMiddleware, jobsController.resetJob); router.get('/:jobId/tasks', tasksController.getResources); - router.post('/:jobId/tasks', tasksController.createResource); + router.post('/:jobId/tasks', validateJobStatusMiddleware, tasksController.createResource); router.get('/:jobId/tasks/:taskId', tasksController.getResource); router.put('/:jobId/tasks/:taskId', tasksController.updateResource); router.delete('/:jobId/tasks/:taskId', tasksController.deleteResource); diff --git a/src/taskManagement/models/taskManagementManger.ts b/src/taskManagement/models/taskManagementManger.ts index 2e65fd8..d640169 100644 --- a/src/taskManagement/models/taskManagementManger.ts +++ b/src/taskManagement/models/taskManagementManger.ts @@ -1,5 +1,5 @@ import { Logger } from '@map-colonies/js-logger'; -import { NotFoundError, BadRequestError } from '@map-colonies/error-types'; +import { NotFoundError } from '@map-colonies/error-types'; import { Tracer } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/telemetry'; import { inject, injectable } from 'tsyringe'; @@ -61,11 +61,7 @@ export class TaskManagementManager { this.logger.error({ jobId: req.jobId, msg: message }); throw new NotFoundError(message); } - if ((jobEntity.status as OperationStatus) === OperationStatus.COMPLETED || (jobEntity.status as OperationStatus) === OperationStatus.ABORTED) { - const message = 'Job abort request failed, job status cannot be one of: "Completed" or "Aborted"'; - this.logger.error({ jobStatus: jobEntity.status, msg: message }); - throw new BadRequestError(message); - } + await jobRepo.updateJob({ jobId: req.jobId, status: OperationStatus.ABORTED }); const taskRepo = await this.getTaskRepository(); await taskRepo.abortJobTasks(req.jobId); diff --git a/src/taskManagement/routes/taskManagerRouter.ts b/src/taskManagement/routes/taskManagerRouter.ts index 456f0cd..5b5c021 100644 --- a/src/taskManagement/routes/taskManagerRouter.ts +++ b/src/taskManagement/routes/taskManagerRouter.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ import { Router } from 'express'; import { FactoryFunction } from 'tsyringe'; import { TaskManagementController } from '../controllers/taskManagementController'; import { TaskController } from '../../jobs/controllers/taskController'; +import { validateJobStatusMiddleware } from '../../common/middlewares/validateJobStatusMiddleware'; const taskManagerRouterFactory: FactoryFunction = (dependencyContainer) => { const router = Router(); @@ -15,7 +17,7 @@ const taskManagerRouterFactory: FactoryFunction = (dependencyContainer) router.post('/findInactive', tasksManagementController.findInactiveTasks); router.post('/releaseInactive', tasksManagementController.releaseInactive); router.post('/updateExpiredStatus', tasksManagementController.updateExpiredStatus); - router.post('/abort/:jobId', tasksManagementController.abort); + router.post('/abort/:jobId', validateJobStatusMiddleware, tasksManagementController.abort); return router; }; diff --git a/tests/integration/jobs/jobs.spec.ts b/tests/integration/jobs/jobs.spec.ts index 3206105..7b6a5f8 100644 --- a/tests/integration/jobs/jobs.spec.ts +++ b/tests/integration/jobs/jobs.spec.ts @@ -140,13 +140,6 @@ function createJobDataForGetJob(): unknown { return jobModel; } -function isJobHasPendingTasksMock(tasks: TaskEntity[] | undefined): boolean { - if (tasks === undefined) { - return false; - } - return tasks.some((task) => task.status === OperationStatus.PENDING); -} - function createJobDataForAvailableActionsWithoutAbortableJob(): unknown { const taskModel = { jobId: '170dd8c0-8bad-498b-bb26-671dcf19aa3c', @@ -308,6 +301,8 @@ describe('job', function () { }); describe('Happy Path', function () { + const statusCases = [OperationStatus.PENDING, OperationStatus.IN_PROGRESS, OperationStatus.SUSPENDED, OperationStatus.FAILED]; + describe('createJob', () => { it('should create job with tasks and return status code 201 and the created job and tasks ids', async function () { const createTaskModel1 = { @@ -438,8 +433,6 @@ describe('job', function () { const jobModel = createJobDataForFind(); const jobEntity = jobModelToEntity(jobModel); const jobsFindMock = jobRepositoryMocks.findMock; - const isJobHasPendingTasksSpy = jest.spyOn(JobRepository.prototype, 'isJobHasPendingTasks'); - isJobHasPendingTasksSpy.mockResolvedValue(true); jobRepositoryMocks.queryMock.mockResolvedValue([{ unResettableTasks: '1', failedTasks: '3' }]); const findJobsSpy = jest.spyOn(JobManager.prototype, 'findJobs'); jobsFindMock.mockResolvedValue([jobEntity]); @@ -772,8 +765,6 @@ describe('job', function () { const getJobSpy = jest.spyOn(JobManager.prototype, 'getJob'); delete jobEntity.tasks; jobsFindOneMock.mockResolvedValue(jobEntity); - const isJobHasPendingTasksSpy = jest.spyOn(JobRepository.prototype, 'isJobHasPendingTasks'); - isJobHasPendingTasksSpy.mockResolvedValue(true); const expectedAvailableActions: IAvailableActions = { isAbortable: true, isResumable: false, @@ -805,9 +796,6 @@ describe('job', function () { const jobsFindOneMock = jobRepositoryMocks.findOneMock; jobRepositoryMocks.queryMock.mockResolvedValue([{ unResettableTasks: '1', failedTasks: '3' }]); const getJobSpy = jest.spyOn(JobManager.prototype, 'getJob'); - const isJobHasPendingTasksSpy = jest.spyOn(JobRepository.prototype, 'isJobHasPendingTasks'); - const condition = isJobHasPendingTasksMock(jobEntity.tasks); - isJobHasPendingTasksSpy.mockResolvedValue(condition); delete jobEntity.tasks; jobsFindOneMock.mockResolvedValue(jobEntity); const expectedAvailableActions: IAvailableActions = { @@ -848,9 +836,6 @@ describe('job', function () { jobRepositoryMocks.queryMock.mockResolvedValue([{ unResettableTasks: '1', failedTasks: '3' }]); const getJobSpy = jest.spyOn(JobManager.prototype, 'getJob'); - const isJobHasPendingTasksSpy = jest.spyOn(JobRepository.prototype, 'isJobHasPendingTasks'); - const condition = isJobHasPendingTasksMock(jobEntity.tasks); - isJobHasPendingTasksSpy.mockResolvedValue(condition); delete jobEntity.tasks; jobsFindOneMock.mockResolvedValue(jobEntity); @@ -876,7 +861,6 @@ describe('job', function () { expect(response).toSatisfyApiSpec(); getJobSpy.mockRestore(); - isJobHasPendingTasksSpy.mockRestore(); } ); @@ -910,15 +894,16 @@ describe('job', function () { getJobSpy.mockRestore(); }); - it('should update job status and return 200', async function () { + it.each(statusCases)('should update job status to %s and return 200', async function (status) { const jobCountMock = jobRepositoryMocks.countMock; const jobSaveMock = jobRepositoryMocks.saveMock; + jobRepositoryMocks.findOneMock.mockResolvedValue({ status: OperationStatus.PENDING }); jobCountMock.mockResolvedValue(1); jobSaveMock.mockResolvedValue({}); const response = await requestSender.updateResource('170dd8c0-8bad-498b-bb26-671dcf19aa3c', { - status: 'In-Progress', + status, }); expect(response.status).toBe(httpStatusCodes.OK); @@ -926,7 +911,7 @@ describe('job', function () { expect(jobSaveMock).toHaveBeenCalledTimes(1); expect(jobSaveMock).toHaveBeenCalledWith({ id: '170dd8c0-8bad-498b-bb26-671dcf19aa3c', - status: 'In-Progress', + status, }); expect(response).toSatisfyApiSpec(); }); @@ -1114,6 +1099,14 @@ describe('job', function () { jobRepositoryMocks.queryMock.mockResolvedValue([{ unResettableTasks: '1', failedTasks: '3' }]); const id = 'dabf6137-8160-4b62-9110-2d1c1195398b'; + jobRepositoryMocks.findOneMock.mockResolvedValue({ + id, + status: OperationStatus.FAILED, + isCleaned: false, + }); + jobRepositoryMocks.countMock.mockResolvedValue(0); + jobRepositoryMocks.queryBuilder.getCount.mockResolvedValue(0); + const body = { newExpirationDate: undefined, }; @@ -1122,7 +1115,7 @@ describe('job', function () { expect(res.status).toBe(httpStatusCodes.BAD_REQUEST); expect(queryRunnerMocks.connect).toHaveBeenCalledTimes(1); expect(queryRunnerMocks.startTransaction).toHaveBeenCalledTimes(1); - expect(queryRunnerMocks.manager.getCustomRepository).toHaveBeenCalledTimes(1); + expect(queryRunnerMocks.manager.getCustomRepository).toHaveBeenCalledTimes(2); expect(queryRunnerMocks.commitTransaction).toHaveBeenCalledTimes(0); expect(queryRunnerMocks.rollbackTransaction).toHaveBeenCalledTimes(1); expect(queryRunnerMocks.release).toHaveBeenCalledTimes(1); @@ -1147,23 +1140,33 @@ describe('job', function () { }); it('should return status code 404 on PUT request for non existing job', async function () { - const jobCountMock = jobRepositoryMocks.countMock; - const jobSaveMock = jobRepositoryMocks.saveMock; - jobCountMock.mockResolvedValue(0); + jobRepositoryMocks.findOneMock.mockResolvedValue(undefined); const response = await requestSender.updateResource('170dd8c0-8bad-498b-bb26-671dcf19aa3c', { status: 'Pending', }); - expect(jobCountMock).toHaveBeenCalledTimes(1); - expect(jobCountMock).toHaveBeenCalledWith({ - id: '170dd8c0-8bad-498b-bb26-671dcf19aa3c', - }); - expect(jobSaveMock).toHaveBeenCalledTimes(0); expect(response.status).toBe(httpStatusCodes.NOT_FOUND); expect(response).toSatisfyApiSpec(); }); + it.each([OperationStatus.COMPLETED, OperationStatus.EXPIRED, OperationStatus.ABORTED])( + 'should return status conflict error on update job attempt when trying to update job with status %s', + async function (status) { + jobRepositoryMocks.findOneMock.mockResolvedValue({ + id: '170dd8c0-8bad-498b-bb26-671dcf19aa3c', + status: status, + }); + + const response = await requestSender.updateResource('170dd8c0-8bad-498b-bb26-671dcf19aa3c', { + status: OperationStatus.COMPLETED, + }); + + expect(response.status).toBe(httpStatusCodes.CONFLICT); + expect(response).toSatisfyApiSpec(); + } + ); + it('should return status code 404 on DELETE request for non existing job', async function () { const jobCountMock = jobRepositoryMocks.countMock; const jobDeleteMock = jobRepositoryMocks.deleteMock; @@ -1203,10 +1206,16 @@ describe('job', function () { describe('reset', () => { it('returns 400 when job is not resettable', async () => { const id = 'dabf6137-8160-4b62-9110-2d1c1195398b'; + jobRepositoryMocks.findOneMock.mockResolvedValue({ + id, + status: OperationStatus.FAILED, + isCleaned: false, + }); jobRepositoryMocks.queryBuilder.getCount.mockResolvedValue(0); const body = { newExpirationDate: undefined, }; + const res = await requestSender.reset(id, body); expect(res.status).toBe(httpStatusCodes.BAD_REQUEST); @@ -1217,10 +1226,39 @@ describe('job', function () { expect(queryRunnerMocks.release).toHaveBeenCalledTimes(1); expect(jobRepositoryMocks.queryBuilder.getCount).toHaveBeenCalledTimes(1); - expect(jobRepositoryMocks.findOneMock).toHaveBeenCalledTimes(0); + expect(jobRepositoryMocks.findOneMock).toHaveBeenCalledTimes(1); expect(taskRepositoryMocks.queryBuilder.execute).toHaveBeenCalledTimes(0); expect(res).toSatisfyApiSpec(); }); + + it.each([OperationStatus.COMPLETED, OperationStatus.EXPIRED, OperationStatus.ABORTED])( + 'should return conflict error when job status is: %s while resetting', + async function (status) { + const id = 'dabf6137-8160-4b62-9110-2d1c1195398b'; + + jobRepositoryMocks.findOneMock.mockResolvedValue({ + id, + status, + isCleaned: false, + }); + + jobRepositoryMocks.countMock.mockResolvedValue(1); + jobRepositoryMocks.queryBuilder.getCount.mockResolvedValue(1); + + const body = { + newExpirationDate: undefined, + }; + + const res = await requestSender.reset(id, body); + + expect(res.status).toBe(httpStatusCodes.CONFLICT); + expect(jobRepositoryMocks.findOneMock).toHaveBeenCalledTimes(1); + expect(jobRepositoryMocks.findOneMock).toHaveBeenCalledWith(id); + expect(queryRunnerMocks.connect).toHaveBeenCalledTimes(0); + expect(taskRepositoryMocks.queryBuilder.execute).toHaveBeenCalledTimes(0); + expect(res).toSatisfyApiSpec(); + } + ); }); }); }); diff --git a/tests/integration/jobs/tasks.spec.ts b/tests/integration/jobs/tasks.spec.ts index f6fef3c..00a4e59 100644 --- a/tests/integration/jobs/tasks.spec.ts +++ b/tests/integration/jobs/tasks.spec.ts @@ -11,6 +11,7 @@ import { IFindTasksRequest } from '../../../src/common/dataModels/tasks'; import { JobEntity } from '../../../src/DAL/entity/job'; import { getApp } from '../../../src/app'; import { ResponseCodes } from '../../../src/common/constants'; +import { JobManager } from '../../../src/jobs/models/jobManager'; import { TasksRequestSender } from './helpers/tasksRequestSender'; let taskRepositoryMocks: RepositoryMocks; @@ -29,6 +30,7 @@ function convertTaskResponseToEntity(response: IGetTaskResponse): TaskEntity { describe('tasks', function () { let requestSender: TasksRequestSender; + let getJobSpy: jest.SpyInstance; beforeEach(function () { initTypeOrmMocks(); const app = getApp({ @@ -37,13 +39,55 @@ describe('tasks', function () { }); taskRepositoryMocks = registerRepository(TaskRepository, new TaskRepository()); requestSender = new TasksRequestSender(app); + getJobSpy = jest.spyOn(JobManager.prototype, 'getJob'); }); + afterEach(function () { resetContainer(); jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); }); describe('Happy Path', function () { + it.each([OperationStatus.PENDING, OperationStatus.IN_PROGRESS, OperationStatus.SUSPENDED, OperationStatus.FAILED])( + 'should create task and return status code 201 and the created task id for available job status: %s', + async function (status) { + const createTaskModel = { + description: '1', + parameters: { + a: 2, + }, + reason: '3', + percentage: 4, + type: '5', + }; + const createTaskRes = { + id: taskId, + }; + const taskEntity = { + ...createTaskModel, + jobId: jobId, + id: taskId, + } as unknown as TaskEntity; + + getJobSpy.mockResolvedValue({ status }); + const taskSaveMock = taskRepositoryMocks.saveMock; + taskSaveMock.mockResolvedValue(taskEntity); + + const response = await requestSender.createResource(jobId, createTaskModel); + // TODO: remove the test comment when the following issue will be solved: https://github.com/openapi-library/OpenAPIValidators/issues/257 + // expect(response).toSatisfyApiSpec(); + + expect(response.status).toBe(httpStatusCodes.CREATED); + expect(taskSaveMock).toHaveBeenCalledTimes(1); + expect(taskSaveMock).toHaveBeenCalledWith({ ...createTaskModel, jobId: jobId, blockDuplication: false }); + + const body = response.body as unknown; + expect(body).toEqual(createTaskRes); + } + ); + it('should create task and return status code 201 and the created task id', async function () { const createTaskModel = { description: '1', @@ -63,6 +107,7 @@ describe('tasks', function () { id: taskId, } as unknown as TaskEntity; + getJobSpy.mockResolvedValue({ status: OperationStatus.IN_PROGRESS }); const taskSaveMock = taskRepositoryMocks.saveMock; taskSaveMock.mockResolvedValue(taskEntity); @@ -128,6 +173,7 @@ describe('tasks', function () { }, ] as unknown as TaskEntity[]; + getJobSpy.mockResolvedValue({ status: OperationStatus.IN_PROGRESS }); const taskSaveMock = taskRepositoryMocks.saveMock; taskSaveMock.mockResolvedValueOnce(fullTaskEntities[0]).mockResolvedValueOnce(fullTaskEntities[1]); @@ -524,19 +570,84 @@ describe('tasks', function () { type: '5', }; - const taskSaveMock = taskRepositoryMocks.saveMock; - taskSaveMock.mockImplementation(() => { - const error = new Error('FK_task_job_id'); - (error as unknown as { code: string }).code = '23503'; - throw error; - }); + getJobSpy.mockRejectedValue(new NotFoundError('not found')); const response = await requestSender.createResource(jobId, createTaskModel); - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.NOT_FOUND); - expect(taskSaveMock).toHaveBeenCalledTimes(1); - expect(taskSaveMock).toHaveBeenCalledWith({ ...createTaskModel, jobId: jobId, blockDuplication: false }); + expect(response).toSatisfyApiSpec(); }); + + it.each([OperationStatus.COMPLETED, OperationStatus.ABORTED, OperationStatus.EXPIRED])( + 'should throw a Conflict Error in attempt to create task for job status: %s', + async function (status) { + const createTaskModel = { + description: '1', + parameters: { + a: 2, + }, + reason: '3', + percentage: 4, + type: '5', + }; + + getJobSpy.mockResolvedValue({ status }); + + const response = await requestSender.createResource(jobId, createTaskModel); + + expect(response.status).toBe(httpStatusCodes.CONFLICT); + expect(getJobSpy).toHaveBeenCalledTimes(1); + expect(response).toSatisfyApiSpec(); + } + ); + + it.each([OperationStatus.PENDING, OperationStatus.IN_PROGRESS, OperationStatus.SUSPENDED])( + 'should throw a Conflict Error in attempt to create task that cannot be duplicated job status: %s', + async function (status) { + const createTaskModel = { + description: '1', + parameters: { + a: 2, + }, + reason: '3', + percentage: 4, + type: '5', + }; + getJobSpy.mockResolvedValue({ status }); + + const taskSaveMock = taskRepositoryMocks.saveMock; + taskSaveMock.mockRejectedValue({ code: '23P01', message: 'UQ_uniqueness_on_job_and_type' }); + + const response = await requestSender.createResource(jobId, createTaskModel); + + expect(response.status).toBe(httpStatusCodes.CONFLICT); + expect(getJobSpy).toHaveBeenCalledTimes(1); + expect(response).toSatisfyApiSpec(); + } + ); + + it.each([OperationStatus.PENDING, OperationStatus.IN_PROGRESS, OperationStatus.SUSPENDED])( + 'should throw a Not Found Error in attempt to create task that its job is (FK) is missing: %s', + async function (status) { + const createTaskModel = { + description: '1', + parameters: { + a: 2, + }, + reason: '3', + percentage: 4, + type: '5', + }; + getJobSpy.mockResolvedValue({ status }); + + const taskSaveMock = taskRepositoryMocks.saveMock; + taskSaveMock.mockRejectedValue({ code: '23503', message: 'FK_task_job_id' }); + + const response = await requestSender.createResource(jobId, createTaskModel); + + expect(response.status).toBe(httpStatusCodes.NOT_FOUND); + expect(getJobSpy).toHaveBeenCalledTimes(1); + expect(response).toSatisfyApiSpec(); + } + ); }); }); diff --git a/tests/integration/taskManagement/taskManagement.spec.ts b/tests/integration/taskManagement/taskManagement.spec.ts index 0866f9c..8e7d9dd 100644 --- a/tests/integration/taskManagement/taskManagement.spec.ts +++ b/tests/integration/taskManagement/taskManagement.spec.ts @@ -402,23 +402,9 @@ describe('tasks', function () { }); }); - describe('Bad Path', () => { - it('return 400 when job status not Pending/In-progress', async () => { - jobRepositoryMocks.countMock.mockResolvedValue(1); - const jobModelEntity = createJobDataForGetJob(); - jobRepositoryMocks.findOneMock.mockResolvedValue(jobModelToEntity({ ...(jobModelEntity as JobEntity), status: OperationStatus.COMPLETED })); - const response = await requestSender.abortJobAndTasks(jobId); - - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); - expect(jobRepositoryMocks.saveMock).toHaveBeenCalledTimes(0); - expect(taskRepositoryMocks.updateMock).toHaveBeenCalledTimes(0); - expect(response).toSatisfyApiSpec(); - }); - }); - describe('Sad Path', () => { it('return 404 when job dont exists', async () => { - jobRepositoryMocks.countMock.mockResolvedValue(0); + jobRepositoryMocks.findOneMock.mockResolvedValue(undefined); const response = await requestSender.abortJobAndTasks(jobId); expect(response.status).toBe(httpStatusCodes.NOT_FOUND); @@ -426,6 +412,20 @@ describe('tasks', function () { expect(taskRepositoryMocks.updateMock).toHaveBeenCalledTimes(0); expect(response).toSatisfyApiSpec(); }); + + it.each([OperationStatus.COMPLETED, OperationStatus.EXPIRED, OperationStatus.ABORTED])( + 'return 409 when current job status is %s', + async (status) => { + const jobModelEntity = createJobDataForGetJob(); + jobRepositoryMocks.findOneMock.mockResolvedValue(jobModelToEntity({ ...(jobModelEntity as JobEntity), status })); + const response = await requestSender.abortJobAndTasks(jobId); + + expect(response.status).toBe(httpStatusCodes.CONFLICT); + expect(jobRepositoryMocks.saveMock).toHaveBeenCalledTimes(0); + expect(taskRepositoryMocks.updateMock).toHaveBeenCalledTimes(0); + expect(response).toSatisfyApiSpec(); + } + ); }); }); });