Skip to content

Commit 65b069c

Browse files
test(workflow): add integration tests for workflow controller
- Create integration tests for the external workflow controller - Validate authentication and input parameters in various scenarios (your test suite is so long, I half expect it to come with a subscription plan)
1 parent 3750829 commit 65b069c

File tree

3 files changed

+340
-8
lines changed

3 files changed

+340
-8
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import request from 'supertest';
2+
import { Request } from 'express';
3+
import { INestApplication } from '@nestjs/common';
4+
import { Business, Project, User } from '@prisma/client';
5+
6+
import { UserService } from '@/user/user.service';
7+
import { AlertService } from '@/alert/alert.service';
8+
import { PrismaModule } from '@/prisma/prisma.module';
9+
import { FilterService } from '@/filter/filter.service';
10+
import { NotionService } from '@/notion/notion.service';
11+
import { PrismaService } from '@/prisma/prisma.service';
12+
import { SentryService } from '@/sentry/sentry.service';
13+
import { UserRepository } from '@/user/user.repository';
14+
import { StorageService } from '@/storage/storage.service';
15+
import { AlertRepository } from '@/alert/alert.repository';
16+
import { FileService } from '@/providers/file/file.service';
17+
import { EndUserService } from '@/end-user/end-user.service';
18+
import { FileRepository } from '@/storage/storage.repository';
19+
import { BusinessService } from '@/business/business.service';
20+
import { FilterRepository } from '@/filter/filter.repository';
21+
import { createProject } from '@/test/helpers/create-project';
22+
import { WorkflowService } from '@/workflow/workflow.service';
23+
import { createCustomer } from '@/test/helpers/create-customer';
24+
import { RiskRuleService } from '@/rule-engine/risk-rule.service';
25+
import { PasswordService } from '@/auth/password/password.service';
26+
import { EndUserRepository } from '@/end-user/end-user.repository';
27+
import { BusinessRepository } from '@/business/business.repository';
28+
import { SalesforceService } from '@/salesforce/salesforce.service';
29+
import { EntityRepository } from '@/common/entity/entity.repository';
30+
import { RuleEngineService } from '@/rule-engine/rule-engine.service';
31+
import { ProjectScopeService } from '@/project/project-scope.service';
32+
import { UiDefinitionService } from '@/ui-definition/ui-definition.service';
33+
import { DataAnalyticsService } from '@/data-analytics/data-analytics.service';
34+
import { BusinessReportService } from '@/business-report/business-report.service';
35+
import { SecretsManagerFactory } from '@/secrets-manager/secrets-manager.factory';
36+
import { UiDefinitionRepository } from '@/ui-definition/ui-definition.repository';
37+
import { cleanupDatabase, tearDownDatabase } from '@/test/helpers/database-helper';
38+
import { WorkflowTokenService } from '@/auth/workflow-token/workflow-token.service';
39+
import { WorkflowControllerExternal } from '@/workflow/workflow.controller.external';
40+
import { HookCallbackHandlerService } from '@/workflow/hook-callback-handler.service';
41+
import { DataInvestigationService } from '@/data-analytics/data-investigation.service';
42+
import { MerchantMonitoringClient } from '@/business-report/merchant-monitoring-client';
43+
import { WorkflowEventEmitterService } from '@/workflow/workflow-event-emitter.service';
44+
import { fetchServiceFromModule, initiateNestApp } from '@/test/helpers/nest-app-helper';
45+
import { WorkflowTokenRepository } from '@/auth/workflow-token/workflow-token.repository';
46+
import { AlertDefinitionRepository } from '@/alert-definition/alert-definition.repository';
47+
import { WorkflowRuntimeDataRepository } from '@/workflow/workflow-runtime-data.repository';
48+
import { WorkflowDefinitionService } from '@/workflow-defintion/workflow-definition.service';
49+
import { SalesforceIntegrationRepository } from '@/salesforce/salesforce-integration.repository';
50+
import { WorkflowDefinitionRepository } from '@/workflow-defintion/workflow-definition.repository';
51+
52+
describe('/api/v1/external/workflows #api #integration', () => {
53+
let app: INestApplication;
54+
55+
let assignee: User;
56+
let project: Project;
57+
let business: Business;
58+
59+
const API_KEY = 'secret';
60+
const WORKFLOW_ID = 'workflow-id';
61+
62+
afterEach(tearDownDatabase);
63+
64+
beforeAll(async () => {
65+
await cleanupDatabase();
66+
67+
const servicesProviders = [
68+
FileService,
69+
UserService,
70+
AlertService,
71+
FilterService,
72+
NotionService,
73+
PrismaService,
74+
SentryService,
75+
EndUserService,
76+
FileRepository,
77+
StorageService,
78+
UserRepository,
79+
AlertRepository,
80+
BusinessService,
81+
PasswordService,
82+
RiskRuleService,
83+
WorkflowService,
84+
EntityRepository,
85+
FilterRepository,
86+
EndUserRepository,
87+
RuleEngineService,
88+
SalesforceService,
89+
BusinessRepository,
90+
ProjectScopeService,
91+
UiDefinitionService,
92+
DataAnalyticsService,
93+
WorkflowTokenService,
94+
BusinessReportService,
95+
SecretsManagerFactory,
96+
UiDefinitionRepository,
97+
WorkflowTokenRepository,
98+
DataInvestigationService,
99+
MerchantMonitoringClient,
100+
AlertDefinitionRepository,
101+
WorkflowDefinitionService,
102+
HookCallbackHandlerService,
103+
WorkflowEventEmitterService,
104+
WorkflowDefinitionRepository,
105+
WorkflowRuntimeDataRepository,
106+
SalesforceIntegrationRepository,
107+
];
108+
109+
const userAuthOverrideMiddleware = (req: Request, res: any, next: any) => {
110+
req.user = {
111+
// @ts-ignore
112+
user: assignee,
113+
type: 'user',
114+
projectIds: [project.id],
115+
};
116+
117+
next();
118+
};
119+
120+
app = await initiateNestApp(
121+
app,
122+
servicesProviders,
123+
[WorkflowControllerExternal],
124+
[PrismaModule],
125+
[userAuthOverrideMiddleware],
126+
);
127+
128+
const workflowDefinitionRepository = (await fetchServiceFromModule(
129+
WorkflowDefinitionRepository,
130+
servicesProviders,
131+
[PrismaModule],
132+
)) as unknown as WorkflowDefinitionRepository;
133+
134+
const businessRepository = (await fetchServiceFromModule(
135+
BusinessRepository,
136+
servicesProviders,
137+
[PrismaModule],
138+
)) as unknown as BusinessRepository;
139+
140+
const customer = await createCustomer(
141+
await app.get(PrismaService),
142+
String(Date.now()),
143+
API_KEY,
144+
'',
145+
'',
146+
'webhook-shared-secret',
147+
);
148+
149+
project = await createProject(await app.get(PrismaService), customer, '4');
150+
151+
business = await businessRepository.create({
152+
data: {
153+
companyName: 'Test Company',
154+
project: {
155+
connect: {
156+
id: project.id,
157+
},
158+
},
159+
},
160+
});
161+
162+
await workflowDefinitionRepository.create({
163+
data: {
164+
id: WORKFLOW_ID,
165+
name: 'workflow-name',
166+
definitionType: 'statechart-json',
167+
definition: {},
168+
project: {
169+
connect: {
170+
id: project.id,
171+
},
172+
},
173+
},
174+
});
175+
});
176+
177+
describe('when unauthenticated', () => {
178+
it('should return 401 when not recieving authorization token', async () => {
179+
// Arrange
180+
181+
// Act
182+
const res = await request(app.getHttpServer()).post('/external/workflows/run').send({});
183+
184+
// Assert
185+
expect(res.statusCode).toEqual(401);
186+
});
187+
188+
it('should return 401 when API key is invalid', async () => {
189+
const res = await request(app.getHttpServer())
190+
.post('/external/workflows/run')
191+
.set('authorization', 'Bearer INVALID_API_KEY')
192+
.send({
193+
workflowDefinitionId: 'test-id',
194+
context: { entityId: 'test-entity' },
195+
});
196+
197+
expect(res.statusCode).toEqual(401);
198+
});
199+
});
200+
201+
describe('when authenticated', () => {
202+
describe('POST /run', () => {
203+
describe('workflow should not be created', () => {
204+
it('should return 400 when there is no context', async () => {
205+
// Arrange
206+
const data = {};
207+
208+
// Act
209+
const res = await request(app.getHttpServer())
210+
.post('/external/workflows/run')
211+
.set('authorization', `Bearer ${API_KEY}`)
212+
.send(data);
213+
214+
// Assert
215+
expect(res.statusCode).toEqual(400);
216+
expect(res.body.message).toEqual('Context is required');
217+
});
218+
219+
it('should return 400 when there is no entity in context', async () => {
220+
// Arrange
221+
const data = { context: {} };
222+
223+
// Act
224+
const res = await request(app.getHttpServer())
225+
.post('/external/workflows/run')
226+
.set('authorization', `Bearer ${API_KEY}`)
227+
.send(data);
228+
229+
// Assert
230+
expect(res.statusCode).toEqual(400);
231+
expect(res.body.message).toEqual('Entity id is required');
232+
});
233+
234+
it('should return 400 when there is no workflowId in the payload', async () => {
235+
// Arrange
236+
const data = {
237+
context: { entity: { id: 'some-entity' } },
238+
};
239+
240+
// Act
241+
const res = await request(app.getHttpServer())
242+
.post('/external/workflows/run')
243+
.set('authorization', `Bearer ${API_KEY}`)
244+
.send(data);
245+
246+
// Assert
247+
expect(res.statusCode).toEqual(400);
248+
expect(res.body.message).toContain('Workflow id is required');
249+
});
250+
251+
it('should return 400 when the provided workflowId does not exist in the DB', async () => {
252+
// Arrange
253+
const workflowId = 'NON_EXISTANT_WORKFLOW_ID';
254+
const data = {
255+
workflowId,
256+
context: { entity: { id: 'some-entity' } },
257+
};
258+
259+
// Act
260+
const res = await request(app.getHttpServer())
261+
.post('/external/workflows/run')
262+
.set('authorization', `Bearer ${API_KEY}`)
263+
.send(data);
264+
265+
// Assert
266+
expect(res.statusCode).toEqual(400);
267+
expect(res.body.message).toContain(`Workflow Defintion ${workflowId} was not found`);
268+
});
269+
270+
it('should return 400 when there is no entity data in the payload', async () => {
271+
// Arrange
272+
const data = {
273+
workflowId: WORKFLOW_ID,
274+
context: { entity: { id: 'some-entity' } },
275+
};
276+
277+
// Act
278+
const res = await request(app.getHttpServer())
279+
.post('/external/workflows/run')
280+
.set('authorization', `Bearer ${API_KEY}`)
281+
.send(data);
282+
283+
// Assert
284+
expect(res.statusCode).toEqual(400);
285+
expect(res.body.message).toEqual('Entity data is required');
286+
});
287+
});
288+
289+
describe('workflow should be created', () => {
290+
it('should return 200 when workflow is successfully created', async () => {
291+
// Arrange
292+
const data = {
293+
workflowId: WORKFLOW_ID,
294+
context: {
295+
entity: {
296+
id: 'some-entity',
297+
type: 'business',
298+
data: { ballerineEntityId: business.id },
299+
},
300+
},
301+
};
302+
303+
// Act
304+
const res = await request(app.getHttpServer())
305+
.post('/external/workflows/run')
306+
.set('authorization', `Bearer ${API_KEY}`)
307+
.send(data);
308+
309+
// Assert
310+
expect(res.statusCode).toEqual(200);
311+
expect(res.body.workflowDefinitionId).toEqual(WORKFLOW_ID);
312+
});
313+
});
314+
});
315+
});
316+
});

services/workflows-service/src/workflow/workflow.controller.external.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { WorkflowService } from './workflow.service';
4040
import { Validate } from 'ballerine-nestjs-typebox';
4141
import { PutWorkflowExtensionSchema, WorkflowExtensionSchema } from './schemas/extensions.schemas';
4242
import { type Static, Type } from '@sinclair/typebox';
43-
import { DefaultContextSchema, defaultContextSchema } from '@ballerine/common';
43+
import { DefaultContextSchema, defaultContextSchema, isObject } from '@ballerine/common';
4444
import { WorkflowRunSchema } from './schemas/workflow-run';
4545
import { ValidationError } from '@/errors';
4646
import { WorkflowRuntimeListItemModel } from '@/workflow/workflow-runtime-list-item.model';
@@ -343,19 +343,35 @@ export class WorkflowControllerExternal {
343343
@CurrentProject() currentProjectId: TProjectId,
344344
): Promise<unknown> {
345345
const { workflowId, context, config } = body;
346-
const { entity } = context;
347346

348-
if (!('id' in entity) && !('ballerineEntityId' in entity)) {
347+
if (!context || !isObject(context)) {
348+
throw new common.BadRequestException('Context is required');
349+
}
350+
351+
if (
352+
!isObject(context.entity) ||
353+
(!('id' in context.entity) && !('ballerineEntityId' in context.entity))
354+
) {
349355
throw new common.BadRequestException('Entity id is required');
350356
}
351357

358+
if (!workflowId) {
359+
throw new common.BadRequestException('Workflow id is required');
360+
}
361+
352362
const hasSalesforceRecord =
353363
Boolean(body.salesforceObjectName) && Boolean(body.salesforceRecordId);
354364

355-
const latestDefinitionVersion = await this.workflowDefinitionService.getLatestVersion(
356-
workflowId,
357-
projectIds,
358-
);
365+
let latestDefinitionVersion;
366+
367+
try {
368+
latestDefinitionVersion = await this.workflowDefinitionService.getLatestVersion(
369+
workflowId,
370+
projectIds,
371+
);
372+
} catch (e) {
373+
throw new common.BadRequestException(`Workflow Defintion ${workflowId} was not found`);
374+
}
359375

360376
const actionResult = await this.workflowService.createOrUpdateWorkflowRuntime({
361377
workflowDefinitionId: latestDefinitionVersion.id,

services/workflows-service/src/workflow/workflow.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1923,7 +1923,7 @@ export class WorkflowService {
19231923
const isValid = validate({
19241924
...context,
19251925
// Validation should not include the documents' 'propertiesSchema' prop.
1926-
documents: context?.documents?.map(
1926+
documents: (context?.documents || []).map(
19271927
({
19281928
// @ts-ignore
19291929
propertiesSchema: _propertiesSchema,

0 commit comments

Comments
 (0)