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

feat: added task status api #28

Merged
merged 8 commits into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
55 changes: 55 additions & 0 deletions openapi3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,44 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/internalError'
/taskStatus/{jobId}:
get:
tags:
- tasks
summary: Get task status by job id
operationId: getTaskStatusByJobId
parameters:
- name: jobId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/getTaskStatusResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/error'
'404':
description: Could not find task with matched jobId
content:
application/json:
schema:
$ref: '#/components/schemas/error'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/internalError'
components:
requestBodies:
ExportGetmapBody:
Expand Down Expand Up @@ -202,3 +240,20 @@ components:
description: >-
Bounding box corners (lower left, upper right)=[minx,miny,maxx,maxy] in
crs units as array
getTaskStatusResponse:
type: object
properties:
percentage:
type: number
minimum: 0
maximum: 100
description: percentage of task by job id
status:
type: string
enum:
- Completed
- In-Progress
- Pending
- Failed
- Expired
- Aborted
6 changes: 6 additions & 0 deletions src/clients/jobManagerWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
IWorkerInput,
JobDuplicationParams,
JobResponse,
TaskResponse,
} from '../common/interfaces';

@injectable()
Expand Down Expand Up @@ -135,6 +136,11 @@ export class JobManagerWrapper extends JobManagerClient {
return undefined;
}

public async getTasksByJobId(jobId: string): Promise<TaskResponse[]> {
const tasks = await this.get<TaskResponse[]>(`/jobs/${jobId}/tasks`);
return tasks;
}

private async getJobs(queryParams: IFindJob): Promise<JobResponse[] | undefined> {
this.logger.info(`Getting jobs that match these parameters: ${JSON.stringify(queryParams)}`);
const jobs = await this.get<JobResponse[] | undefined>('/jobs', queryParams as unknown as Record<string, unknown>);
Expand Down
3 changes: 2 additions & 1 deletion src/common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ICreateJobBody, IJobResponse, OperationStatus } from '@map-colonies/mc-priority-queue';
import { ICreateJobBody, IJobResponse, ITaskResponse, OperationStatus } from '@map-colonies/mc-priority-queue';
import { Polygon, MultiPolygon } from '@turf/helpers';
import { BBox2d } from '@turf/helpers/dist/js/lib/geojson';

Expand Down Expand Up @@ -104,4 +104,5 @@ export interface ITaskParameters {
}

export type JobResponse = IJobResponse<IJobParameters, ITaskParameters>;
export type TaskResponse = ITaskResponse<ITaskParameters>;
export type CreateJobBody = ICreateJobBody<IJobParameters, ITaskParameters>;
2 changes: 2 additions & 0 deletions src/containerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SERVICES, SERVICE_NAME } from './common/constants';
import { tracing } from './common/tracing';
import { createPackageRouterFactory, CREATE_PACKAGE_ROUTER_SYMBOL } from './createPackage/routes/createPackageRouter';
import { InjectionObject, registerDependencies } from './common/dependencyRegistration';
import { tasksRouterFactory, TASKS_ROUTER_SYMBOL } from './createPackage/routes/tasksRouter';

export interface RegisterOptions {
override?: InjectionObject<unknown>[];
Expand All @@ -31,6 +32,7 @@ export const registerExternalValues = (options?: RegisterOptions): DependencyCon
{ token: SERVICES.TRACER, provider: { useValue: tracer } },
{ token: SERVICES.METER, provider: { useValue: meter } },
{ token: CREATE_PACKAGE_ROUTER_SYMBOL, provider: { useFactory: createPackageRouterFactory } },
{ token: TASKS_ROUTER_SYMBOL, provider: { useFactory: tasksRouterFactory } },
{
token: 'onSignal',
provider: {
Expand Down
23 changes: 23 additions & 0 deletions src/createPackage/controllers/tasksController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Logger } from '@map-colonies/js-logger';
import { RequestHandler } from 'express';
import httpStatus from 'http-status-codes';
import { injectable, inject } from 'tsyringe';
import { SERVICES } from '../../common/constants';
import { ITaskStatusResponse, TasksManager } from '../models/tasksManager';

type GetTaskByJobIdHandler = RequestHandler<{ jobId: string }, ITaskStatusResponse, string>;

@injectable()
export class TasksController {
public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger, @inject(TasksManager) private readonly taskManager: TasksManager) {}

public getTaskStatusByJobId: GetTaskByJobIdHandler = async (req, res, next) => {
const jobId: string = req.params.jobId;
try {
const taskStatus = await this.taskManager.getTaskStatusByJobId(jobId);
return res.status(httpStatus.OK).json(taskStatus);
} catch (err) {
next(err);
}
};
}
34 changes: 34 additions & 0 deletions src/createPackage/models/tasksManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Logger } from '@map-colonies/js-logger';
import { inject, injectable } from 'tsyringe';
import { OperationStatus } from '@map-colonies/mc-priority-queue';
import { NotFoundError } from '@map-colonies/error-types';
import { SERVICES } from '../../common/constants';
import { JobManagerWrapper } from '../../clients/jobManagerWrapper';

export interface ITaskStatusResponse {
percentage: number | undefined;
status: OperationStatus;
}

@injectable()
export class TasksManager {
public constructor(
@inject(SERVICES.LOGGER) private readonly logger: Logger,
@inject(JobManagerWrapper) private readonly jobManagerClient: JobManagerWrapper
) {}

public async getTaskStatusByJobId(jobId: string): Promise<ITaskStatusResponse> {
this.logger.info(`Getting task status by jobId: ${jobId}`);
const tasks = await this.jobManagerClient.getTasksByJobId(jobId);

if (tasks.length === 0) {
throw new NotFoundError(`jobId: ${jobId} is not exists`);
}
const task = tasks[0];
const statusResponse: ITaskStatusResponse = {
percentage: task.percentage,
status: task.status,
};
return statusResponse;
}
}
16 changes: 16 additions & 0 deletions src/createPackage/routes/tasksRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Router } from 'express';
import { FactoryFunction } from 'tsyringe';
import { TasksController } from '../controllers/tasksController';

const tasksRouterFactory: FactoryFunction<Router> = (dependencyContainer) => {
const router = Router();
const controller = dependencyContainer.resolve(TasksController);

router.get('/taskStatus/:jobId', controller.getTaskStatusByJobId);

return router;
};

export const TASKS_ROUTER_SYMBOL = Symbol('tasksFactory');

export { tasksRouterFactory };
5 changes: 4 additions & 1 deletion src/serverBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import httpLogger from '@map-colonies/express-access-log-middleware';
import { SERVICES } from './common/constants';
import { IConfig } from './common/interfaces';
import { CREATE_PACKAGE_ROUTER_SYMBOL } from './createPackage/routes/createPackageRouter';
import { TASKS_ROUTER_SYMBOL } from './createPackage/routes/tasksRouter';

@injectable()
export class ServerBuilder {
Expand All @@ -18,7 +19,8 @@ export class ServerBuilder {
public constructor(
@inject(SERVICES.CONFIG) private readonly config: IConfig,
@inject(SERVICES.LOGGER) private readonly logger: Logger,
@inject(CREATE_PACKAGE_ROUTER_SYMBOL) private readonly createPackageRouter: Router
@inject(CREATE_PACKAGE_ROUTER_SYMBOL) private readonly createPackageRouter: Router,
@inject(TASKS_ROUTER_SYMBOL) private readonly tasksRouter: Router
) {
this.serverInstance = express();
}
Expand All @@ -39,6 +41,7 @@ export class ServerBuilder {

private buildRoutes(): void {
this.serverInstance.use('/create', this.createPackageRouter);
this.serverInstance.use('/', this.tasksRouter);
this.buildDocsRoutes();
}

Expand Down
9 changes: 9 additions & 0 deletions tests/integration/createPackage/helpers/tasksSender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as supertest from 'supertest';

export class TasksSender {
public constructor(private readonly app: Express.Application) {}

public async getTasksByJobId(jobId: string): Promise<supertest.Response> {
return supertest.agent(this.app).get(`/taskStatus/${jobId}`).set('Content-Type', 'application/json').send();
}
}
89 changes: 89 additions & 0 deletions tests/integration/createPackage/tasks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import jsLogger from '@map-colonies/js-logger';
import { trace } from '@opentelemetry/api';
import httpStatusCodes from 'http-status-codes';
import { container } from 'tsyringe';
import { OperationStatus } from '@map-colonies/mc-priority-queue';
import { getApp } from '../../../src/app';
import { SERVICES } from '../../../src/common/constants';
import { ITaskParameters, TaskResponse } from '../../../src/common/interfaces';
import { JobManagerWrapper } from '../../../src/clients/jobManagerWrapper';
import { TasksSender } from './helpers/tasksSender';

describe('tasks', function () {
let requestSender: TasksSender;
let getTasksByJobIdSpy: jest.SpyInstance;

beforeEach(function () {
const app = getApp({
override: [
{ token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } },
{ token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } },
],
useChild: true,
});
requestSender = new TasksSender(app);
getTasksByJobIdSpy = jest.spyOn(JobManagerWrapper.prototype, 'getTasksByJobId');
});

afterEach(function () {
container.clearInstances();
jest.resetAllMocks();
});

describe('Happy Path', function () {
it('should return 200 status code and the tasks matched the jobId', async function () {
const tasksResponse: TaskResponse[] = [
{
id: '0a5552f7-01eb-40af-a912-eed8fa9e1561',
jobId: '0a5552f7-01eb-40af-a912-eed8fa9e1568',
type: 'rasterTilesExporterd',
description: '',
parameters: {} as unknown as ITaskParameters,
status: OperationStatus.IN_PROGRESS,
percentage: 23,
reason: '',
attempts: 0,
resettable: true,
created: '2022-08-02T13:02:18.475Z',
updated: '2022-08-02T15:01:56.658Z',
},
];
const jobId = '09e29fa8-7283-4334-b3a4-99f75922de59';
getTasksByJobIdSpy.mockResolvedValue(tasksResponse);

const resposne = await requestSender.getTasksByJobId(jobId);

expect(resposne).toSatisfyApiSpec();
expect(getTasksByJobIdSpy).toHaveBeenCalledTimes(1);
expect(resposne.status).toBe(httpStatusCodes.OK);
});
});

describe('Sad Path', function () {
it('should return 404 status code due to invalid string format (uuid)', async function () {
const tasksResponse: TaskResponse[] = [];
const jobId = '09e29fa8-7283-4334-b3a4-99f75922de59';
getTasksByJobIdSpy.mockResolvedValue(tasksResponse);

const resposne = await requestSender.getTasksByJobId(jobId);

expect(resposne).toSatisfyApiSpec();
expect(getTasksByJobIdSpy).toHaveBeenCalledTimes(1);
expect(resposne.status).toBe(httpStatusCodes.NOT_FOUND);
});
});

describe('Bad Path', function () {
it('should return 400 status code when jobId is not exists', async function () {
const tasksResponse: TaskResponse[] = [];
const jobId = 'string';
getTasksByJobIdSpy.mockResolvedValue(tasksResponse);

const resposne = await requestSender.getTasksByJobId(jobId);

expect(resposne).toSatisfyApiSpec();
expect(getTasksByJobIdSpy).toHaveBeenCalledTimes(0);
expect(resposne.status).toBe(httpStatusCodes.BAD_REQUEST);
});
});
});
68 changes: 68 additions & 0 deletions tests/unit/createPackage/models/tasksModel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import jsLogger from '@map-colonies/js-logger';
import { OperationStatus } from '@map-colonies/mc-priority-queue';
import { NotFoundError } from '@map-colonies/error-types';
import { JobManagerWrapper } from '../../../../src/clients/jobManagerWrapper';
import { ITaskStatusResponse, TasksManager } from '../../../../src/createPackage/models/tasksManager';
import { ITaskParameters, TaskResponse } from '../../../../src/common/interfaces';

let jobManagerWrapper: JobManagerWrapper;
let tasksManager: TasksManager;
let getTasksByJobIdStub: jest.Mock;

describe('TasksManager', () => {
beforeEach(() => {
const logger = jsLogger({ enabled: false });
jobManagerWrapper = new JobManagerWrapper(logger);
tasksManager = new TasksManager(logger, jobManagerWrapper);
});

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

describe('#getTaskStatusByJobId', () => {
it('should throw NotFoundError when jobId is not exists', async () => {
const emptyTasksResponse: TaskResponse[] = [];

getTasksByJobIdStub = jest.fn();
jobManagerWrapper.getTasksByJobId = getTasksByJobIdStub.mockResolvedValue(emptyTasksResponse);

const action = async () => tasksManager.getTaskStatusByJobId('09e29fa8-7283-4334-b3a4-99f75922de59');

await expect(action).rejects.toThrow(NotFoundError);
expect(getTasksByJobIdStub).toHaveBeenCalledTimes(1);
});

it('should successfully return task status by jobId', async () => {
const tasksResponse: TaskResponse[] = [
{
id: '0a5552f7-01eb-40af-a912-eed8fa9e1561',
jobId: '0a5552f7-01eb-40af-a912-eed8fa9e1568',
type: 'rasterTilesExporterd',
description: '',
parameters: {} as unknown as ITaskParameters,
status: OperationStatus.IN_PROGRESS,
percentage: 23,
reason: '',
attempts: 0,
resettable: true,
created: '2022-08-02T13:02:18.475Z',
updated: '2022-08-02T15:01:56.658Z',
},
];

getTasksByJobIdStub = jest.fn();
jobManagerWrapper.getTasksByJobId = getTasksByJobIdStub.mockResolvedValue(tasksResponse);

const result = tasksManager.getTaskStatusByJobId('0a5552f7-01eb-40af-a912-eed8fa9e1568');
const expectedResult: ITaskStatusResponse = {
percentage: tasksResponse[0].percentage,
status: tasksResponse[0].status,
};
await expect(result).resolves.not.toThrow();
await expect(result).resolves.toEqual(expectedResult);
expect(getTasksByJobIdStub).toHaveBeenCalledTimes(1);
});
});
});