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

test(workflow): add integration tests for workflow controller #2888

Merged
merged 7 commits into from
Dec 15, 2024
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')
.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
Loading