Skip to content

Commit

Permalink
test(workflow): add integration tests for workflow controller
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
tomer-shvadron committed Dec 11, 2024
1 parent 3750829 commit 65b069c
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
import request from 'supertest';
import { Request } from 'express';
import { INestApplication } from '@nestjs/common';
import { Business, Project, User } from '@prisma/client';

import { UserService } from '@/user/user.service';
import { AlertService } from '@/alert/alert.service';
import { PrismaModule } from '@/prisma/prisma.module';
import { FilterService } from '@/filter/filter.service';
import { NotionService } from '@/notion/notion.service';
import { PrismaService } from '@/prisma/prisma.service';
import { SentryService } from '@/sentry/sentry.service';
import { UserRepository } from '@/user/user.repository';
import { StorageService } from '@/storage/storage.service';
import { AlertRepository } from '@/alert/alert.repository';
import { FileService } from '@/providers/file/file.service';
import { EndUserService } from '@/end-user/end-user.service';
import { FileRepository } from '@/storage/storage.repository';
import { BusinessService } from '@/business/business.service';
import { FilterRepository } from '@/filter/filter.repository';
import { createProject } from '@/test/helpers/create-project';
import { WorkflowService } from '@/workflow/workflow.service';
import { createCustomer } from '@/test/helpers/create-customer';
import { RiskRuleService } from '@/rule-engine/risk-rule.service';
import { PasswordService } from '@/auth/password/password.service';
import { EndUserRepository } from '@/end-user/end-user.repository';
import { BusinessRepository } from '@/business/business.repository';
import { SalesforceService } from '@/salesforce/salesforce.service';
import { EntityRepository } from '@/common/entity/entity.repository';
import { RuleEngineService } from '@/rule-engine/rule-engine.service';
import { ProjectScopeService } from '@/project/project-scope.service';
import { UiDefinitionService } from '@/ui-definition/ui-definition.service';
import { DataAnalyticsService } from '@/data-analytics/data-analytics.service';
import { BusinessReportService } from '@/business-report/business-report.service';
import { SecretsManagerFactory } from '@/secrets-manager/secrets-manager.factory';
import { UiDefinitionRepository } from '@/ui-definition/ui-definition.repository';
import { cleanupDatabase, tearDownDatabase } from '@/test/helpers/database-helper';
import { WorkflowTokenService } from '@/auth/workflow-token/workflow-token.service';
import { WorkflowControllerExternal } from '@/workflow/workflow.controller.external';
import { HookCallbackHandlerService } from '@/workflow/hook-callback-handler.service';
import { DataInvestigationService } from '@/data-analytics/data-investigation.service';
import { MerchantMonitoringClient } from '@/business-report/merchant-monitoring-client';
import { WorkflowEventEmitterService } from '@/workflow/workflow-event-emitter.service';
import { fetchServiceFromModule, initiateNestApp } from '@/test/helpers/nest-app-helper';
import { WorkflowTokenRepository } from '@/auth/workflow-token/workflow-token.repository';
import { AlertDefinitionRepository } from '@/alert-definition/alert-definition.repository';
import { WorkflowRuntimeDataRepository } from '@/workflow/workflow-runtime-data.repository';
import { WorkflowDefinitionService } from '@/workflow-defintion/workflow-definition.service';
import { SalesforceIntegrationRepository } from '@/salesforce/salesforce-integration.repository';
import { WorkflowDefinitionRepository } from '@/workflow-defintion/workflow-definition.repository';

describe('/api/v1/external/workflows #api #integration', () => {
let app: INestApplication;

let assignee: User;
let project: Project;
let business: Business;

const API_KEY = 'secret';
const WORKFLOW_ID = 'workflow-id';

afterEach(tearDownDatabase);

beforeAll(async () => {
await cleanupDatabase();

const servicesProviders = [
FileService,
UserService,
AlertService,
FilterService,
NotionService,
PrismaService,
SentryService,
EndUserService,
FileRepository,
StorageService,
UserRepository,
AlertRepository,
BusinessService,
PasswordService,
RiskRuleService,
WorkflowService,
EntityRepository,
FilterRepository,
EndUserRepository,
RuleEngineService,
SalesforceService,
BusinessRepository,
ProjectScopeService,
UiDefinitionService,
DataAnalyticsService,
WorkflowTokenService,
BusinessReportService,
SecretsManagerFactory,
UiDefinitionRepository,
WorkflowTokenRepository,
DataInvestigationService,
MerchantMonitoringClient,
AlertDefinitionRepository,
WorkflowDefinitionService,
HookCallbackHandlerService,
WorkflowEventEmitterService,
WorkflowDefinitionRepository,
WorkflowRuntimeDataRepository,
SalesforceIntegrationRepository,
];

const userAuthOverrideMiddleware = (req: Request, res: any, next: any) => {
req.user = {
// @ts-ignore
user: assignee,
type: 'user',
projectIds: [project.id],
};

next();
};

app = await initiateNestApp(
app,
servicesProviders,
[WorkflowControllerExternal],
[PrismaModule],
[userAuthOverrideMiddleware],
);

const workflowDefinitionRepository = (await fetchServiceFromModule(
WorkflowDefinitionRepository,
servicesProviders,
[PrismaModule],
)) as unknown as WorkflowDefinitionRepository;

const businessRepository = (await fetchServiceFromModule(
BusinessRepository,
servicesProviders,
[PrismaModule],
)) as unknown as BusinessRepository;

const customer = await createCustomer(
await app.get(PrismaService),
String(Date.now()),
API_KEY,
'',
'',
'webhook-shared-secret',
);

project = await createProject(await app.get(PrismaService), customer, '4');

business = await businessRepository.create({
data: {
companyName: 'Test Company',
project: {
connect: {
id: project.id,
},
},
},
});

await workflowDefinitionRepository.create({
data: {
id: WORKFLOW_ID,
name: 'workflow-name',
definitionType: 'statechart-json',
definition: {},
project: {
connect: {
id: project.id,
},
},
},
});
});

describe('when unauthenticated', () => {
it('should return 401 when not recieving authorization token', async () => {
// Arrange

// Act
const res = await request(app.getHttpServer()).post('/external/workflows/run').send({});

// Assert
expect(res.statusCode).toEqual(401);
});

it('should return 401 when API key is invalid', async () => {
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', 'Bearer INVALID_API_KEY')

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

The hard-coded value "Bearer INVALID_API_KEY" is used as
authorization header
.
.send({
workflowDefinitionId: 'test-id',
context: { entityId: 'test-entity' },
});

expect(res.statusCode).toEqual(401);
});
});

describe('when authenticated', () => {
describe('POST /run', () => {
describe('workflow should not be created', () => {
it('should return 400 when there is no context', async () => {
// Arrange
const data = {};

// Act
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', `Bearer ${API_KEY}`)
.send(data);

// Assert
expect(res.statusCode).toEqual(400);
expect(res.body.message).toEqual('Context is required');
});

it('should return 400 when there is no entity in context', async () => {
// Arrange
const data = { context: {} };

// Act
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', `Bearer ${API_KEY}`)
.send(data);

// Assert
expect(res.statusCode).toEqual(400);
expect(res.body.message).toEqual('Entity id is required');
});

it('should return 400 when there is no workflowId in the payload', async () => {
// Arrange
const data = {
context: { entity: { id: 'some-entity' } },
};

// Act
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', `Bearer ${API_KEY}`)
.send(data);

// Assert
expect(res.statusCode).toEqual(400);
expect(res.body.message).toContain('Workflow id is required');
});

it('should return 400 when the provided workflowId does not exist in the DB', async () => {
// Arrange
const workflowId = 'NON_EXISTANT_WORKFLOW_ID';
const data = {
workflowId,
context: { entity: { id: 'some-entity' } },
};

// Act
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', `Bearer ${API_KEY}`)
.send(data);

// Assert
expect(res.statusCode).toEqual(400);
expect(res.body.message).toContain(`Workflow Defintion ${workflowId} was not found`);
});

it('should return 400 when there is no entity data in the payload', async () => {
// Arrange
const data = {
workflowId: WORKFLOW_ID,
context: { entity: { id: 'some-entity' } },
};

// Act
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', `Bearer ${API_KEY}`)
.send(data);

// Assert
expect(res.statusCode).toEqual(400);
expect(res.body.message).toEqual('Entity data is required');
});
});

describe('workflow should be created', () => {
it('should return 200 when workflow is successfully created', async () => {
// Arrange
const data = {
workflowId: WORKFLOW_ID,
context: {
entity: {
id: 'some-entity',
type: 'business',
data: { ballerineEntityId: business.id },
},
},
};

// Act
const res = await request(app.getHttpServer())
.post('/external/workflows/run')
.set('authorization', `Bearer ${API_KEY}`)
.send(data);

// Assert
expect(res.statusCode).toEqual(200);
expect(res.body.workflowDefinitionId).toEqual(WORKFLOW_ID);
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { WorkflowService } from './workflow.service';
import { Validate } from 'ballerine-nestjs-typebox';
import { PutWorkflowExtensionSchema, WorkflowExtensionSchema } from './schemas/extensions.schemas';
import { type Static, Type } from '@sinclair/typebox';
import { DefaultContextSchema, defaultContextSchema } from '@ballerine/common';
import { DefaultContextSchema, defaultContextSchema, isObject } from '@ballerine/common';
import { WorkflowRunSchema } from './schemas/workflow-run';
import { ValidationError } from '@/errors';
import { WorkflowRuntimeListItemModel } from '@/workflow/workflow-runtime-list-item.model';
Expand Down Expand Up @@ -343,19 +343,35 @@ export class WorkflowControllerExternal {
@CurrentProject() currentProjectId: TProjectId,
): Promise<unknown> {
const { workflowId, context, config } = body;
const { entity } = context;

if (!('id' in entity) && !('ballerineEntityId' in entity)) {
if (!context || !isObject(context)) {
throw new common.BadRequestException('Context is required');
}

if (
!isObject(context.entity) ||
(!('id' in context.entity) && !('ballerineEntityId' in context.entity))
) {
throw new common.BadRequestException('Entity id is required');
}

if (!workflowId) {
throw new common.BadRequestException('Workflow id is required');
}

const hasSalesforceRecord =
Boolean(body.salesforceObjectName) && Boolean(body.salesforceRecordId);

const latestDefinitionVersion = await this.workflowDefinitionService.getLatestVersion(
workflowId,
projectIds,
);
let latestDefinitionVersion;

try {
latestDefinitionVersion = await this.workflowDefinitionService.getLatestVersion(
workflowId,
projectIds,
);
} catch (e) {
throw new common.BadRequestException(`Workflow Defintion ${workflowId} was not found`);
}

const actionResult = await this.workflowService.createOrUpdateWorkflowRuntime({
workflowDefinitionId: latestDefinitionVersion.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1923,7 +1923,7 @@ export class WorkflowService {
const isValid = validate({
...context,
// Validation should not include the documents' 'propertiesSchema' prop.
documents: context?.documents?.map(
documents: (context?.documents || []).map(
({
// @ts-ignore
propertiesSchema: _propertiesSchema,
Expand Down

0 comments on commit 65b069c

Please sign in to comment.