Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TM-1531] delayed job with data #25

Merged
merged 19 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 97 additions & 118 deletions apps/job-service/src/jobs/delayed-jobs.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { DelayedJobsController } from './delayed-jobs.controller';
import { Test, TestingModule } from '@nestjs/testing';
import { Logger, NotFoundException } from '@nestjs/common';
import { DelayedJobFactory } from '@terramatch-microservices/database/factories';
import { Resource } from '@terramatch-microservices/common/util';
import { DelayedJobsController } from './delayed-jobs.controller';
import { DelayedJob } from '@terramatch-microservices/database/entities';
import { JobBulkUpdateBodyDto } from './dto/delayed-job-update.dto';
import { DelayedJobBulkUpdateBodyDto } from './dto/delayed-job-update.dto';
import { v4 as uuidv4 } from 'uuid';
import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common';

describe('DelayedJobsController', () => {
let controller: DelayedJobsController;
Expand All @@ -14,6 +13,7 @@ describe('DelayedJobsController', () => {
where: {},
truncate: true
});

const module: TestingModule = await Test.createTestingModule({
controllers: [DelayedJobsController]
}).compile();
Expand All @@ -25,157 +25,136 @@ describe('DelayedJobsController', () => {
jest.restoreAllMocks();
});

it('should throw not found if the delayed job does not exist', async () => {
await expect(controller.findOne('asdf')).rejects
.toThrow(NotFoundException);
});

it('should return the job definition when the delayed job does exist', async () => {
const { uuid, statusCode, payload, totalContent, processedContent, progressMessage } = await DelayedJobFactory.create();
const result = await controller.findOne(uuid);
const resource = result.data as Resource;
expect(resource.type).toBe('delayedJobs');
expect(resource.id).toBe(uuid);
expect(resource.attributes.statusCode).toBe(statusCode);
expect(resource.attributes.payload).toMatchObject(payload);
expect(resource.attributes.totalContent).toBe(totalContent);
expect(resource.attributes.processedContent).toBe(processedContent);
expect(resource.attributes.progressMessage).toBe(progressMessage);
});
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,
it('should successfully clear multiple jobs', async () => {
const authenticatedUserId = 130999;

const job1 = await DelayedJob.create({
uuid: uuidv4(),
createdBy: authenticatedUserId,
isAcknowledged: false,
status: 'completed'
});
const job2 = await DelayedJobFactory.create({
createdBy: 130999,
const job2 = await DelayedJob.create({
uuid: uuidv4(),
createdBy: authenticatedUserId,
isAcknowledged: false,
status: 'failed'
});
const job3 = await DelayedJobFactory.create({
createdBy: 130999,
isAcknowledged: false,
status: 'pending'
});

const request = { authenticatedUserId: 130999 };
const payload: JobBulkUpdateBodyDto = {

const payload: DelayedJobBulkUpdateBodyDto = {
data: [
{
type: 'jobs',
type: 'delayedJobs',
uuid: job1.uuid,
attributes: {
isAcknowledged: true,
},
attributes: { isAcknowledged: true }
},
{
type: 'jobs',
type: 'delayedJobs',
uuid: job2.uuid,
attributes: {
isAcknowledged: true,
},
},
],
attributes: { isAcknowledged: true }
}
]
};


const request = { authenticatedUserId };

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,
},
});
expect(result.data[0].id).toBe(job1.uuid);
expect(result.data[1].id).toBe(job2.uuid);

const updatedJob1 = await DelayedJob.findOne({ where: { uuid: job1.uuid } });
const updatedJob2 = await DelayedJob.findOne({ where: { uuid: job2.uuid } });
expect(updatedJob1.isAcknowledged).toBe(true);
expect(updatedJob2.isAcknowledged).toBe(true);
});

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'
});


it('should throw BadRequestException when no jobs are provided', async () => {
const payload: DelayedJobBulkUpdateBodyDto = { data: [] };
const request = { authenticatedUserId: 130999 };
const payload: JobBulkUpdateBodyDto = {

await expect(controller.bulkClearJobs(payload, request))
.rejects.toThrow(BadRequestException);
});

it('should throw NotFoundException for non-existent job', async () => {
const payload: DelayedJobBulkUpdateBodyDto = {
data: [
{
type: 'jobs',
uuid: job.uuid,
attributes: {
isAcknowledged: true,
},
},
],
type: 'delayedJobs',
uuid: 'non-existent-uuid',
attributes: { isAcknowledged: true }
}
]
};

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

expect(result.data).toHaveLength(0);
});
});

const request = { authenticatedUserId: 130999 };

describe('findOne', () => {
it('should handle non-existent job uuid', async () => {
await expect(controller.findOne('non-existent-uuid')).rejects.toThrow(NotFoundException);
await expect(controller.bulkClearJobs(payload, request))
.rejects.toThrow(NotFoundException);
});

it('should handle null or undefined uuid', async () => {
await expect(controller.findOne(null)).rejects.toThrow();
await expect(controller.findOne(undefined)).rejects.toThrow();
});
});
it('should not update jobs created by other users', async () => {
const authenticatedUserId = 130999;
const otherUserId = 999999;

const otherUserJob = await DelayedJob.create({
uuid: uuidv4(),
createdBy: otherUserId,
isAcknowledged: false,
status: 'completed'
});

describe('findOne', () => {
it('should handle non-existent job uuid', async () => {
await expect(controller.findOne('non-existent-uuid')).rejects.toThrow(NotFoundException);
const payload: DelayedJobBulkUpdateBodyDto = {
data: [
{
type: 'delayedJobs',
uuid: otherUserJob.uuid,
attributes: { isAcknowledged: true }
}
]
};
const request = { authenticatedUserId };

await expect(controller.bulkClearJobs(payload, request))
.rejects.toThrow(NotFoundException);
});

it('should handle null or undefined uuid', async () => {
await expect(controller.findOne(null)).rejects.toThrow();
await expect(controller.findOne(undefined)).rejects.toThrow();
it('should throw UnauthorizedException for missing authenticated id', async () => {
const payload: DelayedJobBulkUpdateBodyDto = {
data: [
{ type: 'delayedJobs', uuid: uuidv4(), attributes: { isAcknowledged: true } }
]
};

await expect(controller.bulkClearJobs(payload, { authenticatedUserId: null }))
.rejects.toThrow(UnauthorizedException);
});
});

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

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

const result = await controller.getRunningJobs(request);
const resources = result.data as Resource[];
const payload: DelayedJobBulkUpdateBodyDto = {
data: [
{
type: 'delayedJobs',
uuid: pendingJob.uuid,
attributes: { isAcknowledged: true }
}
]
};
const request = { authenticatedUserId };

// Should only return non-cleared jobs
expect(resources).toHaveLength(2);
await expect(controller.bulkClearJobs(payload, request))
.rejects.toThrow(NotFoundException);
});
});
});
45 changes: 28 additions & 17 deletions apps/job-service/src/jobs/delayed-jobs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, NotFoundException, Param, UnauthorizedException, Request, Patch, BadRequestException, Body } from '@nestjs/common';
import { Controller, Get, NotFoundException, Param, UnauthorizedException, Request, Patch, BadRequestException, Body, Logger } from '@nestjs/common';
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
import { ApiBody, ApiOperation } from '@nestjs/swagger';
import { Op } from 'sequelize';
Expand All @@ -9,7 +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';
import { DelayedJobBulkUpdateBodyDto } from './dto/delayed-job-update.dto';

@Controller('jobs/v3/delayedJobs')
export class DelayedJobsController {
Expand Down Expand Up @@ -73,7 +73,7 @@ export class DelayedJobsController {
})
@ApiBody({
description: 'JSON:API bulk update payload for jobs',
roguenet marked this conversation as resolved.
Show resolved Hide resolved
type: JobBulkUpdateBodyDto,
type: DelayedJobBulkUpdateBodyDto,
examples: {
example: {
value: {
Expand All @@ -97,19 +97,23 @@ export class DelayedJobsController {
},
},
})
@JsonApiResponse({ data: { type: DelayedJobDto } })
@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,
@Body() bulkClearJobsDto: DelayedJobBulkUpdateBodyDto,
@Request() { authenticatedUserId }
): Promise<{ data: JobData[] }> {
): Promise<JsonApiDocument> {
const jobUpdates = bulkClearJobsDto.data;

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

roguenet marked this conversation as resolved.
Show resolved Hide resolved

if (!authenticatedUserId) {
throw new UnauthorizedException('Authentication failed.');
}
const updatePromises = jobUpdates.map(async (job) => {
roguenet marked this conversation as resolved.
Show resolved Hide resolved
const [updatedCount] = await DelayedJob.update(
{ isAcknowledged: job.attributes.isAcknowledged },
Expand All @@ -119,21 +123,28 @@ export class DelayedJobsController {
createdBy: authenticatedUserId,
status: { [Op.ne]: 'pending' },
},
}
);

});

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

const updatedJob = await DelayedJob.findOne({
where: { uuid: job.uuid },
});

return job;
return updatedJob;
});

roguenet marked this conversation as resolved.
Show resolved Hide resolved
const updatedJobs = await Promise.all(updatePromises);

return {
data: updatedJobs,
};


const jsonApiBuilder = buildJsonApi();
updatedJobs.forEach((job) => {
jsonApiBuilder.addData(job.uuid, new DelayedJobDto(job));
});

return jsonApiBuilder.serialize();

}
roguenet marked this conversation as resolved.
Show resolved Hide resolved

}
Loading
Loading