From 461d5837869bd91ec4f30c9883e8efc76eee7927 Mon Sep 17 00:00:00 2001 From: vogelskamp <103994401+vogelskamp@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:37:40 +0100 Subject: [PATCH] implement task logic * :construction: implement /tasks routes * :construction: WIP * :sparkles: add route for creating follow up task * add logging * add task.controller tests --------- Co-authored-by: hafemann Co-authored-by: Leona Kuse --- src/controllers/task.controller.test.ts | 174 ++++++++++++++++++++++++ src/controllers/task.controller.ts | 74 ++++++++++ src/index.ts | 8 ++ src/models/adapter.model.ts | 7 + src/models/follow-up.model.ts | 12 ++ src/models/index.ts | 2 + src/models/task.model.ts | 8 ++ 7 files changed, 285 insertions(+) create mode 100644 src/controllers/task.controller.test.ts create mode 100644 src/controllers/task.controller.ts create mode 100644 src/models/follow-up.model.ts create mode 100644 src/models/task.model.ts diff --git a/src/controllers/task.controller.test.ts b/src/controllers/task.controller.test.ts new file mode 100644 index 00000000..9aa237e6 --- /dev/null +++ b/src/controllers/task.controller.test.ts @@ -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, + {} 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, + {} 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, + {} 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; + + 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, + {} 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, + {} 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, + {} 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; + + 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 }); + }); + }); +}); diff --git a/src/controllers/task.controller.ts b/src/controllers/task.controller.ts new file mode 100644 index 00000000..8a4882b4 --- /dev/null +++ b/src/controllers/task.controller.ts @@ -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, + 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, + 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); + } + } +} diff --git a/src/index.ts b/src/index.ts index b8186a3f..6f734295 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; @@ -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), @@ -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)); diff --git a/src/models/adapter.model.ts b/src/models/adapter.model.ts index 2884c861..abe108e6 100644 --- a/src/models/adapter.model.ts +++ b/src/models/adapter.model.ts @@ -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'; @@ -91,4 +93,9 @@ export interface Adapter { ) => Promise<{ apiKey: string; apiUrl: string }>; handleWebhook?: (req: Request) => Promise; verifyWebhookRequest?: (req: Request) => Promise; + getTasks?: (req: Request, config: Config) => Promise; + createFollowUp?: ( + config: Config, + body: FollowUpWithIntegrationEntities, + ) => Promise; } diff --git a/src/models/follow-up.model.ts b/src/models/follow-up.model.ts new file mode 100644 index 00000000..1b635f4f --- /dev/null +++ b/src/models/follow-up.model.ts @@ -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[]; +}; diff --git a/src/models/index.ts b/src/models/index.ts index 748138a6..c92ef4e8 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -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'; diff --git a/src/models/task.model.ts b/src/models/task.model.ts new file mode 100644 index 00000000..7dcd4cc1 --- /dev/null +++ b/src/models/task.model.ts @@ -0,0 +1,8 @@ +export type Task = { + id: string; + content: string; + createdAt: Date; + dueAt: Date; + link?: string; + title: string; +};