Skip to content

Commit

Permalink
implement task logic
Browse files Browse the repository at this point in the history
* 🚧 implement /tasks routes

* 🚧 WIP

* ✨ add route for creating follow up task

* add logging

* add task.controller tests

---------

Co-authored-by: hafemann <[email protected]>
Co-authored-by: Leona Kuse <[email protected]>
  • Loading branch information
3 people authored Nov 29, 2023
1 parent 5dcdeda commit 461d583
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 0 deletions.
174 changes: 174 additions & 0 deletions src/controllers/task.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Response } from 'express';
import { BridgeRequest, FollowUpWithIntegrationEntities } from '../models';
import { TaskController } from './task.controller';

describe('Task Controller', () => {
const mockAdapter = {
getTasks: jest.fn(),
findAllByQuery: jest.fn(),
createFollowUp: jest.fn(),
};
const mockNext = jest.fn();

describe('findAllByQuery', () => {
beforeEach(() => jest.clearAllMocks());

it('Should check for providerConfig', async () => {
const controller = new TaskController(mockAdapter);

const result = await controller.findAllByQuery(
{} as BridgeRequest<void>,
{} as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockNext).toHaveBeenCalled();
});

it('Should check if adapter.getTasks is implemented', async () => {
const controller = new TaskController({});

const result = await controller.findAllByQuery(
{} as BridgeRequest<void>,
{} as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockNext).toHaveBeenCalled();
});

it('Should handle erroneous adapter.getTasks call', async () => {
const controller = new TaskController(mockAdapter);

mockAdapter.getTasks.mockRejectedValue(null);

const result = await controller.findAllByQuery(
{
providerConfig: {
userId: '123',
apiKey: '123123123',
apiUrl: ':)',
locale: 'de-DE',
},
} as BridgeRequest<void>,
{} as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockNext).toHaveBeenCalled();
});

it('Should resolve happy path', async () => {
const controller = new TaskController(mockAdapter);
const mockResponse = { json: jest.fn() };

mockAdapter.getTasks.mockResolvedValue([]);

const req = {
providerConfig: {
userId: '123',
apiKey: '123123123',
apiUrl: ':)',
locale: 'de-DE',
},
} as BridgeRequest<void>;

const result = await controller.findAllByQuery(
req,
mockResponse as unknown as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockAdapter.getTasks).toHaveBeenCalledWith(
req,
req.providerConfig,
);
expect(mockResponse.json).toHaveBeenCalledWith([]);
});
});

describe('create', () => {
it('Should check for providerConfig', async () => {
const controller = new TaskController(mockAdapter);

const result = await controller.create(
{} as BridgeRequest<FollowUpWithIntegrationEntities>,
{} as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockNext).toHaveBeenCalled();
});

it('Should check if adapter.createFollowUp is implemented', async () => {
const controller = new TaskController({});

const result = await controller.create(
{} as BridgeRequest<FollowUpWithIntegrationEntities>,
{} as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockNext).toHaveBeenCalled();
});

it('Should handle erroneous adapter.createFollowUp call', async () => {
const controller = new TaskController(mockAdapter);

mockAdapter.createFollowUp.mockRejectedValue(null);

const result = await controller.create(
{
providerConfig: {
userId: '123',
apiKey: '123123123',
apiUrl: ':)',
locale: 'de-DE',
},
} as BridgeRequest<FollowUpWithIntegrationEntities>,
{} as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockNext).toHaveBeenCalled();
});

it('Should resolve happy path', async () => {
const controller = new TaskController(mockAdapter);
const mockResponse = { json: jest.fn() };

const followUpId = 123;

mockAdapter.createFollowUp.mockResolvedValue(followUpId);

const req = {
providerConfig: {
userId: '123',
apiKey: '123123123',
apiUrl: ':)',
locale: 'de-DE',
},
} as BridgeRequest<FollowUpWithIntegrationEntities>;

const result = await controller.create(
req,
mockResponse as unknown as Response,
mockNext,
);

expect(result).toBeUndefined();
expect(mockAdapter.createFollowUp).toHaveBeenCalledWith(
req.providerConfig,
undefined,
);
expect(mockResponse.json).toHaveBeenCalledWith({ followUpId });
});
});
});
74 changes: 74 additions & 0 deletions src/controllers/task.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextFunction, Response } from 'express';
import {
Adapter,
BridgeRequest,
FollowUpWithIntegrationEntities,
} from '../models';
import { infoLogger } from '../util';

export class TaskController {
constructor(private readonly adapter: Adapter) {}

async findAllByQuery(
req: BridgeRequest<void>,
res: Response,
next: NextFunction,
) {
const { providerConfig } = req;

if (!providerConfig) {
next(new Error('Provider config not found'));
return;
}

if (!this.adapter.getTasks) {
next(new Error('Method not implemented'));
return;
}

infoLogger('findAllByQuery', 'START', providerConfig.apiKey);

try {
const followUps = await this.adapter.getTasks(req, providerConfig);

infoLogger(
'findAllByQuery',
`Received ${followUps.length} follow ups`,
providerConfig.apiKey,
);

infoLogger('findAllByQuery', 'END', providerConfig.apiKey);
res.json(followUps);
} catch (err) {
next(err);
}
}

async create(
req: BridgeRequest<FollowUpWithIntegrationEntities>,
res: Response,
next: NextFunction,
) {
const { providerConfig } = req;

if (!providerConfig) {
next(new Error('Provider config not found'));
return;
}

if (!this.adapter.createFollowUp) {
next(new Error('Method not implemented'));
return;
}

try {
const followUpId = await this.adapter.createFollowUp(
providerConfig,
req.body,
);
res.json({ followUpId });
} catch (err) {
next(err);
}
}
}
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { CustomRouter } from './models/custom-router.model';
import { CustomRoute } from './models/custom-routes.model';
import { errorLogger, getTokenCache, infoLogger } from './util';
import { getContactCache } from './util/get-contact-cache';
import { TaskController } from './controllers/task.controller';

const PORT: number = Number(process.env.PORT) || 8080;

Expand Down Expand Up @@ -53,6 +54,7 @@ export function start(
tokenCache = getTokenCache();

const controller: Controller = new Controller(adapter, contactCache);
const taskController: TaskController = new TaskController(adapter);

app.get('/contacts', (req, res, next) =>
controller.getContacts(req, res, next),
Expand Down Expand Up @@ -150,6 +152,12 @@ export function start(
controller.handleWebhook(req, res, next),
);

app.get('/tasks', (req, res, next) =>
taskController.findAllByQuery(req, res, next),
);

app.post('/tasks', (req, res, next) => taskController.create(req, res, next));

app.use(errorHandlerMiddleware);

customRouters.forEach(({ path, router }) => app.use(path, router));
Expand Down
7 changes: 7 additions & 0 deletions src/models/adapter.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
ContactDelta,
ContactTemplate,
ContactUpdate,
FollowUpWithIntegrationEntities,
LabeledIntegrationEntity,
LoggedIntegrationEntity,
Task,
} from '.';
import { IntegrationEntityType } from './integration-entity.model';
import { IntegrationsEvent } from './integrations-event.model';
Expand Down Expand Up @@ -91,4 +93,9 @@ export interface Adapter {
) => Promise<{ apiKey: string; apiUrl: string }>;
handleWebhook?: (req: Request) => Promise<IntegrationsEvent[]>;
verifyWebhookRequest?: (req: Request) => Promise<boolean>;
getTasks?: (req: Request, config: Config) => Promise<Task[]>;
createFollowUp?: (
config: Config,
body: FollowUpWithIntegrationEntities,
) => Promise<string>;
}
12 changes: 12 additions & 0 deletions src/models/follow-up.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IntegrationEntity } from './integration-entity.model';

export type FollowUpEvent = {
content: string;
dueAt: number;
title: string;
type: string;
};

export type FollowUpWithIntegrationEntities = FollowUpEvent & {
integrationEntities: IntegrationEntity[];
};
2 changes: 2 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export * from './storage-adapter.model';
export * from './token-cache.model';
export * from './token.model';
export * from './user.model';
export * from './follow-up.model';
export * from './task.model';
8 changes: 8 additions & 0 deletions src/models/task.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Task = {
id: string;
content: string;
createdAt: Date;
dueAt: Date;
link?: string;
title: string;
};

0 comments on commit 461d583

Please sign in to comment.