Skip to content

Commit

Permalink
[TM-1531] update attrib to delayed job, change tests and update jobs …
Browse files Browse the repository at this point in the history
…endpoint
  • Loading branch information
egrojMonroy committed Dec 10, 2024
1 parent 2a1a911 commit ed8a7ff
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 98 deletions.
167 changes: 94 additions & 73 deletions apps/job-service/src/jobs/delayed-jobs.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { Logger, NotFoundException } from '@nestjs/common';
import { DelayedJobFactory } from '@terramatch-microservices/database/factories';
import { Resource } from '@terramatch-microservices/common/util';
import { DelayedJob } from '@terramatch-microservices/database/entities';

import { JobBulkUpdateBodyDto } from './dto/delayed-job-update.dto';

describe('DelayedJobsController', () => {
let controller: DelayedJobsController;

beforeEach(async () => {
await DelayedJob.destroy({
where: {},
truncate: true
truncate: true
});
const module: TestingModule = await Test.createTestingModule({
controllers: [DelayedJobsController]
Expand All @@ -23,7 +23,7 @@ describe('DelayedJobsController', () => {

afterEach(() => {
jest.restoreAllMocks();
})
});

it('should throw not found if the delayed job does not exist', async () => {
await expect(controller.findOne('asdf')).rejects
Expand All @@ -42,81 +42,102 @@ describe('DelayedJobsController', () => {
expect(resource.attributes.processedContent).toBe(processedContent);
expect(resource.attributes.progressMessage).toBe(progressMessage);
});

describe('getRunningJobs', () => {
it('should return only running jobs for the authenticated user', async () => {
const authenticatedUserId = '130999';
const request = { authenticatedUserId };

await DelayedJobFactory.create({
createdBy: authenticatedUserId,
isAknowledged: false
describe('bulkClearJobs', () => {
it('should clear non-pending jobs in bulk for the authenticated user and return the updated jobs', async () => {
const job1 = await DelayedJobFactory.create({
createdBy: 130999,
isAcknowledged: false,
status: 'completed'
});
await DelayedJobFactory.create({
createdBy: authenticatedUserId,
isAknowledged: false
const job2 = await DelayedJobFactory.create({
createdBy: 130999,
isAcknowledged: false,
status: 'failed'
});
await DelayedJobFactory.create({
createdBy: '1388',
isAknowledged: false
const job3 = await DelayedJobFactory.create({
createdBy: 130999,
isAcknowledged: false,
status: 'pending'
});

const request = { authenticatedUserId: 130999 };
const payload: JobBulkUpdateBodyDto = {
data: [
{
type: 'jobs',
uuid: job1.uuid,
attributes: {
isAcknowledged: true,
},
},
{
type: 'jobs',
uuid: job2.uuid,
attributes: {
isAcknowledged: true,
},
},
],
};

const result = await controller.bulkClearJobs(payload, request);

expect(result.data).toHaveLength(2);
expect(result.data[0]).toMatchObject({
type: 'jobs',
uuid: job1.uuid,
attributes: {
isAcknowledged: true,
},
});
expect(result.data[1]).toMatchObject({
type: 'jobs',
uuid: job2.uuid,
attributes: {
isAcknowledged: true,
},
});

const result = await controller.getRunningJobs(request);
const resources = result.data as Resource[];

expect(resources).toHaveLength(2);
});

it('should return an empty array when no running jobs exist', async () => {
const request = { authenticatedUserId: '181818' };
const result = await controller.getRunningJobs(request);

it('should return an empty array when no jobs can be cleared in bulk', async () => {
const job = await DelayedJobFactory.create({
createdBy: 130999,
isAcknowledged: false,
status: 'pending'
});

const request = { authenticatedUserId: 130999 };
const payload: JobBulkUpdateBodyDto = {
data: [
{
type: 'jobs',
uuid: job.uuid,
attributes: {
isAcknowledged: true,
},
},
],
};

const result = await controller.bulkClearJobs(payload, request);

expect(result.data).toHaveLength(0);
});
});
describe('clearNonPendingJobs', () => {
it('should clear non-pending jobs for the authenticated user', async () => {
// Create some jobs with different statuses
await DelayedJobFactory.create({
createdBy: '130999',
isAknowledged: false,
status: 'completed'
});
await DelayedJobFactory.create({
createdBy: '130999',
isAknowledged: false,
status: 'failed'
});
await DelayedJobFactory.create({
createdBy: '130999',
isAknowledged: false,
status: 'pending'
});
await DelayedJobFactory.create({
createdBy: '999999',
isAknowledged: false,
status: 'completed'
});


const request = { authenticatedUserId: '130999' };
const result = await controller.clearNonPendingJobs(request);

expect(result.message).toBe('2 jobs have been cleared.');
describe('findOne', () => {
it('should handle non-existent job uuid', async () => {
await expect(controller.findOne('non-existent-uuid')).rejects.toThrow(NotFoundException);
});

it('should return 0 when no jobs can be cleared', async () => {
await DelayedJobFactory.create({
createdBy: '130999',
isAknowledged: false,
status: 'pending'
});

const request = { authenticatedUserId: '130999' };
const result = await controller.clearNonPendingJobs(request);

expect(result.message).toBe('0 jobs have been cleared.');
it('should handle null or undefined uuid', async () => {
await expect(controller.findOne(null)).rejects.toThrow();
await expect(controller.findOne(undefined)).rejects.toThrow();
});
});


describe('findOne', () => {
it('should handle non-existent job uuid', async () => {
await expect(controller.findOne('non-existent-uuid')).rejects.toThrow(NotFoundException);
Expand All @@ -130,23 +151,23 @@ describe('DelayedJobsController', () => {

describe('getRunningJobs', () => {
it('should handle jobs with different statuses', async () => {
const authenticatedUserId = '130999';
const authenticatedUserId = 130999;
const request = { authenticatedUserId };

// Create jobs with different statuses but same user
await DelayedJobFactory.create({
await DelayedJobFactory.create({
createdBy: authenticatedUserId,
isAknowledged: false,
isAcknowledged: false,
status: 'completed'
});
await DelayedJobFactory.create({
await DelayedJobFactory.create({
createdBy: authenticatedUserId,
isAknowledged: false,
isAcknowledged: false,
status: 'pending'
});
await DelayedJobFactory.create({
await DelayedJobFactory.create({
createdBy: authenticatedUserId,
isAknowledged: true,
isAcknowledged: true,
status: 'failed'
});

Expand All @@ -157,4 +178,4 @@ describe('DelayedJobsController', () => {
expect(resources).toHaveLength(2);
});
});
});
});
88 changes: 69 additions & 19 deletions apps/job-service/src/jobs/delayed-jobs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Controller, Get, NotFoundException, Param, UnauthorizedException, Request, Patch } from '@nestjs/common';
import { Controller, Get, NotFoundException, Param, UnauthorizedException, Request, Patch, BadRequestException, Body } from '@nestjs/common';
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
import { ApiOperation } from '@nestjs/swagger';
import { ApiBody, ApiOperation } from '@nestjs/swagger';
import { Op } from 'sequelize';
import { JsonApiResponse } from '@terramatch-microservices/common/decorators';
import {
Expand All @@ -9,6 +9,7 @@ import {
} from '@terramatch-microservices/common/util';
import { DelayedJobDto } from './dto/delayed-job.dto';
import { DelayedJob } from '@terramatch-microservices/database/entities';
import { JobBulkUpdateBodyDto, JobData } from './dto/delayed-job-update.dto';

@Controller('jobs/v3/delayedJobs')
export class DelayedJobsController {
Expand All @@ -26,7 +27,7 @@ export class DelayedJobsController {
): Promise<JsonApiDocument> {
const runningJobs = await DelayedJob.findAll({
where: {
isAknowledged: false,
isAcknowledged: false,
createdBy: authenticatedUserId
},
order: [['createdAt', 'DESC']],
Expand Down Expand Up @@ -64,26 +65,75 @@ export class DelayedJobsController {
.document.serialize();
}

@Patch('clear')
@Patch('bulk-clear')
@ApiOperation({
operationId: 'clearNonPendingJobs',
description: 'Set isAknowledged to true for all jobs where status is not pending.',
operationId: 'bulkClearJobs',
summary: 'Bulk update jobs to modify isAcknowledged for specified job IDs',
description: `Accepts a JSON:API-compliant payload to bulk update jobs, allowing each job's isAcknowledged attribute to be set to true or false.`,
})
@ApiException(() => UnauthorizedException, {
description: 'Authentication failed.',
})
async clearNonPendingJobs(@Request() { authenticatedUserId }): Promise<{ message: string }> {
const updatedCount = await DelayedJob.update(
{ isAknowledged: true },
{
where: {
isAknowledged: false,
status: { [Op.ne]: 'pending' },
createdBy: authenticatedUserId,
@ApiBody({
description: 'JSON:API bulk update payload for jobs',
type: JobBulkUpdateBodyDto,
examples: {
example: {
value: {
data: [
{
type: 'jobs',
uuid: 'uuid-1',
attributes: {
isAcknowledged: true,
},
},
{
type: 'jobs',
uuid: 'uuid-2',
attributes: {
isAcknowledged: false,
},
},
],
},
},
},
})
@ApiException(() => UnauthorizedException, { description: 'Authentication failed.' })
@ApiException(() => BadRequestException, { description: 'Invalid payload or IDs provided.' })
@ApiException(() => NotFoundException, { description: 'One or more jobs specified in the payload could not be found.' })
async bulkClearJobs(
@Body() bulkClearJobsDto: JobBulkUpdateBodyDto,
@Request() { authenticatedUserId }
): Promise<{ data: JobData[] }> {
const jobUpdates = bulkClearJobsDto.data;

if (!jobUpdates || jobUpdates.length === 0) {
throw new BadRequestException('No jobs provided in the payload.');
}

const updatePromises = jobUpdates.map(async (job) => {
const [updatedCount] = await DelayedJob.update(
{ isAcknowledged: job.attributes.isAcknowledged },
{
where: {
uuid: job.uuid,
createdBy: authenticatedUserId,
status: { [Op.ne]: 'pending' },
},
}
);

if (updatedCount === 0) {
throw new NotFoundException(`Job with UUID ${job.uuid} could not be updated.`);
}
);

return { message: `${updatedCount[0]} jobs have been cleared.` };
return job;
});

const updatedJobs = await Promise.all(updatePromises);

return {
data: updatedJobs,
};
}

}
31 changes: 31 additions & 0 deletions apps/job-service/src/jobs/dto/delayed-job-update.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsBoolean, IsUUID, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class JobAttributes {
@IsBoolean()
@ApiProperty({ description: 'Value to set for isAcknowledged', example: true })
isAcknowledged: boolean;
}

export class JobData {
@ApiProperty({ enum: ['jobs'], description: 'Type of the resource', example: 'jobs' })
type: 'jobs';

@IsUUID()
@ApiProperty({ format: 'uuid', description: 'UUID of the job', example: 'uuid-1' })
uuid: string;

@ValidateNested()
@Type(() => JobAttributes)
@ApiProperty({ description: 'Attributes to update for the job', type: JobAttributes })
attributes: JobAttributes;
}

export class JobBulkUpdateBodyDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => JobData)
@ApiProperty({ description: 'List of jobs to update', type: [JobData] })
data: JobData[];
}
2 changes: 1 addition & 1 deletion apps/job-service/src/jobs/dto/delayed-job.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ export class DelayedJobDto extends JsonApiAttributes<DelayedJobDto> {
description: 'Indicates whether the jobs have been cleared',
nullable: true
})
isAknowledged: boolean | null
isAcknowledged: boolean | null
}
Loading

0 comments on commit ed8a7ff

Please sign in to comment.