diff --git a/package-lock.json b/package-lock.json index d32e378d2d..5afe9f27aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@pyroscope/nodejs": "^0.2.6", "@rudderstack/integrations-lib": "^0.2.2", "@rudderstack/workflow-engine": "^0.7.2", + "@shopify/jest-koa-mocks": "^5.1.1", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", @@ -4575,6 +4576,18 @@ "tslib": "^2.6.2" } }, + "node_modules/@shopify/jest-koa-mocks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.1.1.tgz", + "integrity": "sha512-H1dRznXIK03ph1l/VDBQ5ef+A9kkEn3ikNfk70zwm9auW15MfHfY9gekE99VecxUSekws7sbFte0i8ltWCS4/g==", + "dependencies": { + "koa": "^2.13.4", + "node-mocks-http": "^1.11.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -16138,6 +16151,14 @@ "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", "dev": true }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -16720,6 +16741,47 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-mocks-http": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.14.1.tgz", + "integrity": "sha512-mfXuCGonz0A7uG1FEjnypjm34xegeN5+HI6xeGhYKecfgaZhjsmYoLE9LEFmT+53G1n8IuagPZmVnEL/xNsFaA==", + "dependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.6", + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/node-mocks-http/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-mocks-http/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/node-notifier": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz", @@ -18172,6 +18234,14 @@ "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", "dev": true }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", diff --git a/package.json b/package.json index 0d8e528342..dc90574854 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@pyroscope/nodejs": "^0.2.6", "@rudderstack/integrations-lib": "^0.2.2", "@rudderstack/workflow-engine": "^0.7.2", + "@shopify/jest-koa-mocks": "^5.1.1", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", diff --git a/src/controllers/__tests__/delivery.test.ts b/src/controllers/__tests__/delivery.test.ts new file mode 100644 index 0000000000..b601ff756c --- /dev/null +++ b/src/controllers/__tests__/delivery.test.ts @@ -0,0 +1,187 @@ +import request from 'supertest'; +import { createHttpTerminator } from 'http-terminator'; +import Koa from 'koa'; +import bodyParser from 'koa-bodyparser'; +import { applicationRoutes } from '../../routes'; +import { NativeIntegrationDestinationService } from '../../services/destination/nativeIntegration'; +import { ServiceSelector } from '../../helpers/serviceSelector'; + +let server: any; +const OLD_ENV = process.env; + +beforeAll(async () => { + process.env = { ...OLD_ENV }; // Make a copy + const app = new Koa(); + app.use( + bodyParser({ + jsonLimit: '200mb', + }), + ); + applicationRoutes(app); + server = app.listen(9090); +}); + +afterAll(async () => { + process.env = OLD_ENV; // Restore old environment + const httpTerminator = createHttpTerminator({ + server, + }); + await httpTerminator.terminate(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +const getData = () => { + return { body: { JSON: { a: 'b' } }, metadata: [{ a1: 'b1' }], destinationConfig: { a2: 'b2' } }; +}; + +describe('Delivery controller tests', () => { + describe('Delivery V0 tests', () => { + test('successful delivery', async () => { + const testOutput = { status: 200, message: 'success' }; + const mockDestinationService = new NativeIntegrationDestinationService(); + mockDestinationService.deliver = jest + .fn() + .mockImplementation((event, destinationType, requestMetadata, version) => { + expect(event).toEqual(getData()); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v0'); + return testOutput; + }); + const getNativeDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + const response = await request(server) + .post('/v0/destinations/__rudder_test__/proxy') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ output: testOutput }); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.deliver).toHaveBeenCalledTimes(1); + }); + + test('delivery failure', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + mockDestinationService.deliver = jest + .fn() + .mockImplementation((event, destinationType, requestMetadata, version) => { + expect(event).toEqual(getData()); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v0'); + throw new Error('test error'); + }); + const getNativeDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + const response = await request(server) + .post('/v0/destinations/__rudder_test__/proxy') + .set('Accept', 'application/json') + .send(getData()); + + const expectedResp = { + output: { + message: 'test error', + statTags: { + errorCategory: 'transformation', + }, + destinationResponse: '', + status: 500, + }, + }; + expect(response.status).toEqual(500); + expect(response.body).toEqual(expectedResp); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.deliver).toHaveBeenCalledTimes(1); + }); + }); + + describe('Delivery V1 tests', () => { + test('successful delivery', async () => { + const testOutput = { status: 200, message: 'success' }; + const mockDestinationService = new NativeIntegrationDestinationService(); + mockDestinationService.deliver = jest + .fn() + .mockImplementation((event, destinationType, requestMetadata, version) => { + expect(event).toEqual(getData()); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v1'); + return testOutput; + }); + const getNativeDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + const response = await request(server) + .post('/v1/destinations/__rudder_test__/proxy') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ output: testOutput }); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.deliver).toHaveBeenCalledTimes(1); + }); + + test('delivery failure', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + mockDestinationService.deliver = jest + .fn() + .mockImplementation((event, destinationType, requestMetadata, version) => { + expect(event).toEqual(getData()); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v1'); + throw new Error('test error'); + }); + const getNativeDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + const response = await request(server) + .post('/v1/destinations/__rudder_test__/proxy') + .set('Accept', 'application/json') + .send(getData()); + + const expectedResp = { + output: { + message: 'test error', + statTags: { + errorCategory: 'transformation', + }, + authErrorCategory: '', + status: 500, + response: [{ error: 'test error', metadata: { a1: 'b1' }, statusCode: 500 }], + }, + }; + expect(response.status).toEqual(500); + expect(response.body).toEqual(expectedResp); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.deliver).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/controllers/__tests__/destination.test.ts b/src/controllers/__tests__/destination.test.ts new file mode 100644 index 0000000000..3c49a9a0af --- /dev/null +++ b/src/controllers/__tests__/destination.test.ts @@ -0,0 +1,337 @@ +import request from 'supertest'; +import { createHttpTerminator } from 'http-terminator'; +import Koa from 'koa'; +import bodyParser from 'koa-bodyparser'; +import { applicationRoutes } from '../../routes'; +import { ServiceSelector } from '../../helpers/serviceSelector'; +import { DynamicConfigParser } from '../../util/dynamicConfigParser'; +import { NativeIntegrationDestinationService } from '../../services/destination/nativeIntegration'; + +let server: any; +const OLD_ENV = process.env; + +beforeAll(async () => { + process.env = { ...OLD_ENV }; // Make a copy + const app = new Koa(); + app.use( + bodyParser({ + jsonLimit: '200mb', + }), + ); + applicationRoutes(app); + server = app.listen(9090); +}); + +afterAll(async () => { + process.env = OLD_ENV; // Restore old environment + const httpTerminator = createHttpTerminator({ + server, + }); + await httpTerminator.terminate(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +const getData = () => { + return [{ event: { a: 'b1' } }, { event: { a: 'b2' } }]; +}; + +const getRouterTransformInputData = () => { + return { + input: [ + { message: { a: 'b1' }, destination: {}, metadata: { jobId: 1 } }, + { message: { a: 'b2' }, destination: {}, metadata: { jobId: 2 } }, + ], + destType: '__rudder_test__', + }; +}; + +describe('Destination controller tests', () => { + describe('Destination processor transform tests', () => { + test('successful transformation at processor', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + const expectedOutput = [ + { + event: { a: 'b1' }, + request: { query: {} }, + message: {}, + }, + { + event: { a: 'b2' }, + request: { query: {} }, + message: {}, + }, + ]; + mockDestinationService.doProcessorTransformation = jest + .fn() + .mockImplementation((events, destinationType, version, requestMetadata) => { + expect(events).toEqual(expectedOutput); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v0'); + + return events; + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + DynamicConfigParser.process = jest.fn().mockImplementation((events) => { + return events; + }); + + const response = await request(server) + .post('/v0/destinations/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedOutput); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.doProcessorTransformation).toHaveBeenCalledTimes(1); + }); + + test('transformation at processor failure', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + const expectedOutput = [ + { + statusCode: 500, + error: 'Processor transformation failed', + statTags: { errorCategory: 'transformation' }, + }, + { + statusCode: 500, + error: 'Processor transformation failed', + statTags: { errorCategory: 'transformation' }, + }, + ]; + + mockDestinationService.doProcessorTransformation = jest + .fn() + .mockImplementation((events, destinationType, version, requestMetadata) => { + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v0'); + + throw new Error('Processor transformation failed'); + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + DynamicConfigParser.process = jest.fn().mockImplementation((events) => { + return events; + }); + + const response = await request(server) + .post('/v0/destinations/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedOutput); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.doProcessorTransformation).toHaveBeenCalledTimes(1); + }); + }); + + describe('Destination router transform tests', () => { + test('successful transformation at router', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + const expectedOutput = [ + { + message: { a: 'b1' }, + destination: {}, + metadata: { jobId: 1 }, + request: { query: {} }, + }, + { + message: { a: 'b2' }, + destination: {}, + metadata: { jobId: 2 }, + request: { query: {} }, + }, + ]; + + mockDestinationService.doRouterTransformation = jest + .fn() + .mockImplementation((events, destinationType, version, requestMetadata) => { + expect(events).toEqual(expectedOutput); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v0'); + + return events; + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + DynamicConfigParser.process = jest.fn().mockImplementation((events) => { + return events; + }); + + const response = await request(server) + .post('/routerTransform') + .set('Accept', 'application/json') + .send(getRouterTransformInputData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ output: expectedOutput }); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.doRouterTransformation).toHaveBeenCalledTimes(1); + }); + + test('transformation at router failure', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + mockDestinationService.doRouterTransformation = jest + .fn() + .mockImplementation((events, destinationType, version, requestMetadata) => { + throw new Error('Router transformation failed'); + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + DynamicConfigParser.process = jest.fn().mockImplementation((events) => { + return events; + }); + + const response = await request(server) + .post('/routerTransform') + .set('Accept', 'application/json') + .send(getRouterTransformInputData()); + + const expectedOutput = [ + { + metadata: [{ jobId: 1 }, { jobId: 2 }], + batched: false, + statusCode: 500, + error: 'Router transformation failed', + statTags: { errorCategory: 'transformation' }, + }, + ]; + expect(response.status).toEqual(200); + expect(response.body).toEqual({ output: expectedOutput }); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.doRouterTransformation).toHaveBeenCalledTimes(1); + }); + }); + + describe('Batch transform tests', () => { + test('successful batching at router', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + const expectedOutput = [ + { + message: { a: 'b1' }, + destination: {}, + metadata: { jobId: 1 }, + request: { query: {} }, + }, + { + message: { a: 'b2' }, + destination: {}, + metadata: { jobId: 2 }, + request: { query: {} }, + }, + ]; + + mockDestinationService.doBatchTransformation = jest + .fn() + .mockImplementation((events, destinationType, version, requestMetadata) => { + expect(events).toEqual(expectedOutput); + expect(destinationType).toEqual('__rudder_test__'); + expect(version).toEqual('v0'); + + return events; + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + DynamicConfigParser.process = jest.fn().mockImplementation((events) => { + return events; + }); + + const response = await request(server) + .post('/batch') + .set('Accept', 'application/json') + .send(getRouterTransformInputData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedOutput); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.doBatchTransformation).toHaveBeenCalledTimes(1); + }); + + test('batch transformation failure', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + mockDestinationService.doBatchTransformation = jest + .fn() + .mockImplementation((events, destinationType, version, requestMetadata) => { + throw new Error('Batch transformation failed'); + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + DynamicConfigParser.process = jest.fn().mockImplementation((events) => { + return events; + }); + + const response = await request(server) + .post('/batch') + .set('Accept', 'application/json') + .send(getRouterTransformInputData()); + + const expectedOutput = [ + { + metadata: [{ jobId: 1 }, { jobId: 2 }], + batched: false, + statusCode: 500, + error: 'Batch transformation failed', + statTags: { errorCategory: 'transformation' }, + }, + ]; + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedOutput); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.doBatchTransformation).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/controllers/__tests__/regulation.test.ts b/src/controllers/__tests__/regulation.test.ts new file mode 100644 index 0000000000..55cd8f2d37 --- /dev/null +++ b/src/controllers/__tests__/regulation.test.ts @@ -0,0 +1,107 @@ +import request from 'supertest'; +import { createHttpTerminator } from 'http-terminator'; +import Koa from 'koa'; +import bodyParser from 'koa-bodyparser'; +import { applicationRoutes } from '../../routes'; +import { ServiceSelector } from '../../helpers/serviceSelector'; +import { NativeIntegrationDestinationService } from '../../services/destination/nativeIntegration'; + +let server: any; +const OLD_ENV = process.env; + +beforeAll(async () => { + process.env = { ...OLD_ENV }; // Make a copy + const app = new Koa(); + app.use( + bodyParser({ + jsonLimit: '200mb', + }), + ); + applicationRoutes(app); + server = app.listen(9090); +}); + +afterAll(async () => { + process.env = OLD_ENV; // Restore old environment + const httpTerminator = createHttpTerminator({ + server, + }); + await httpTerminator.terminate(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +const getDeletionData = () => { + return [ + { userAttributes: [{ a: 'b1' }], destType: '__rudder_test__' }, + { userAttributes: [{ a: 'b1' }], destType: '__rudder_test__' }, + ]; +}; + +describe('Regulation controller tests', () => { + describe('Delete users tests', () => { + test('successful delete users request', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + const expectedOutput = [{ statusCode: 400 }, { statusCode: 200 }]; + + mockDestinationService.processUserDeletion = jest + .fn() + .mockImplementation((reqs, destInfo) => { + expect(reqs).toEqual(getDeletionData()); + expect(destInfo).toEqual({ a: 'test' }); + + return expectedOutput; + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + const response = await request(server) + .post('/deleteUsers') + .set('Accept', 'application/json') + .set('x-rudder-dest-info', '{"a": "test"}') + .send(getDeletionData()); + + expect(response.status).toEqual(400); + expect(response.body).toEqual(expectedOutput); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.processUserDeletion).toHaveBeenCalledTimes(1); + }); + + test('delete users request failure', async () => { + const mockDestinationService = new NativeIntegrationDestinationService(); + + mockDestinationService.processUserDeletion = jest + .fn() + .mockImplementation((reqs, destInfo) => { + expect(reqs).toEqual(getDeletionData()); + expect(destInfo).toEqual({ a: 'test' }); + + throw new Error('processUserDeletion error'); + }); + const getDestinationServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeDestinationService') + .mockImplementation(() => { + return mockDestinationService; + }); + + const response = await request(server) + .post('/deleteUsers') + .set('Accept', 'application/json') + .set('x-rudder-dest-info', '{"a": "test"}') + .send(getDeletionData()); + + expect(response.status).toEqual(500); + expect(response.body).toEqual([{ error: {}, statusCode: 500 }]); + + expect(getDestinationServiceSpy).toHaveBeenCalledTimes(1); + expect(mockDestinationService.processUserDeletion).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/controllers/__tests__/source.test.ts b/src/controllers/__tests__/source.test.ts new file mode 100644 index 0000000000..565f39d559 --- /dev/null +++ b/src/controllers/__tests__/source.test.ts @@ -0,0 +1,220 @@ +import request from 'supertest'; +import { createHttpTerminator } from 'http-terminator'; +import Koa from 'koa'; +import bodyParser from 'koa-bodyparser'; +import { applicationRoutes } from '../../routes'; +import { NativeIntegrationSourceService } from '../../services/source/nativeIntegration'; +import { ServiceSelector } from '../../helpers/serviceSelector'; +import { ControllerUtility } from '../util/index'; + +let server: any; +const OLD_ENV = process.env; + +beforeAll(async () => { + process.env = { ...OLD_ENV }; // Make a copy + const app = new Koa(); + app.use( + bodyParser({ + jsonLimit: '200mb', + }), + ); + applicationRoutes(app); + server = app.listen(9090); +}); + +afterAll(async () => { + process.env = OLD_ENV; // Restore old environment + const httpTerminator = createHttpTerminator({ + server, + }); + await httpTerminator.terminate(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +const getData = () => { + return [{ event: { a: 'b1' } }, { event: { a: 'b2' } }]; +}; + +describe('Source controller tests', () => { + describe('V0 Source transform tests', () => { + test('successful source transform', async () => { + const sourceType = '__rudder_test__'; + const version = 'v0'; + const testOutput = [{ event: { a: 'b' } }]; + + const mockSourceService = new NativeIntegrationSourceService(); + mockSourceService.sourceTransformRoutine = jest + .fn() + .mockImplementation((i, s, v, requestMetadata) => { + expect(i).toEqual(getData()); + expect(s).toEqual(sourceType); + expect(v).toEqual(version); + return testOutput; + }); + const getNativeSourceServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeSourceService') + .mockImplementation(() => { + return mockSourceService; + }); + + const adaptInputToVersionSpy = jest + .spyOn(ControllerUtility, 'adaptInputToVersion') + .mockImplementation((s, v, e) => { + expect(s).toEqual(sourceType); + expect(v).toEqual(version); + expect(e).toEqual(getData()); + return { implementationVersion: version, input: e }; + }); + + const response = await request(server) + .post('/v0/sources/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(testOutput); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeSourceServiceSpy).toHaveBeenCalledTimes(1); + expect(adaptInputToVersionSpy).toHaveBeenCalledTimes(1); + expect(mockSourceService.sourceTransformRoutine).toHaveBeenCalledTimes(1); + }); + + test('failing source transform', async () => { + const sourceType = '__rudder_test__'; + const version = 'v0'; + + const mockSourceService = new NativeIntegrationSourceService(); + const getNativeSourceServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeSourceService') + .mockImplementation(() => { + return mockSourceService; + }); + + const adaptInputToVersionSpy = jest + .spyOn(ControllerUtility, 'adaptInputToVersion') + .mockImplementation((s, v, e) => { + expect(s).toEqual(sourceType); + expect(v).toEqual(version); + expect(e).toEqual(getData()); + throw new Error('test error'); + }); + + const response = await request(server) + .post('/v0/sources/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + const expectedResp = [ + { + error: 'test error', + statTags: { + errorCategory: 'transformation', + }, + statusCode: 500, + }, + ]; + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedResp); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeSourceServiceSpy).toHaveBeenCalledTimes(1); + expect(adaptInputToVersionSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('V1 Source transform tests', () => { + test('successful source transform', async () => { + const sourceType = '__rudder_test__'; + const version = 'v1'; + const testOutput = [{ event: { a: 'b' }, source: { id: 'id' } }]; + + const mockSourceService = new NativeIntegrationSourceService(); + mockSourceService.sourceTransformRoutine = jest + .fn() + .mockImplementation((i, s, v, requestMetadata) => { + expect(i).toEqual(getData()); + expect(s).toEqual(sourceType); + expect(v).toEqual(version); + return testOutput; + }); + const getNativeSourceServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeSourceService') + .mockImplementation(() => { + return mockSourceService; + }); + + const adaptInputToVersionSpy = jest + .spyOn(ControllerUtility, 'adaptInputToVersion') + .mockImplementation((s, v, e) => { + expect(s).toEqual(sourceType); + expect(v).toEqual(version); + expect(e).toEqual(getData()); + return { implementationVersion: version, input: e }; + }); + + const response = await request(server) + .post('/v1/sources/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(testOutput); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeSourceServiceSpy).toHaveBeenCalledTimes(1); + expect(adaptInputToVersionSpy).toHaveBeenCalledTimes(1); + expect(mockSourceService.sourceTransformRoutine).toHaveBeenCalledTimes(1); + }); + + test('failing source transform', async () => { + const sourceType = '__rudder_test__'; + const version = 'v1'; + const mockSourceService = new NativeIntegrationSourceService(); + const getNativeSourceServiceSpy = jest + .spyOn(ServiceSelector, 'getNativeSourceService') + .mockImplementation(() => { + return mockSourceService; + }); + + const adaptInputToVersionSpy = jest + .spyOn(ControllerUtility, 'adaptInputToVersion') + .mockImplementation((s, v, e) => { + expect(s).toEqual(sourceType); + expect(v).toEqual(version); + expect(e).toEqual(getData()); + throw new Error('test error'); + }); + + const response = await request(server) + .post('/v1/sources/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + const expectedResp = [ + { + error: 'test error', + statTags: { + errorCategory: 'transformation', + }, + statusCode: 500, + }, + ]; + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedResp); + + expect(response.header['apiversion']).toEqual('2'); + + expect(getNativeSourceServiceSpy).toHaveBeenCalledTimes(1); + expect(adaptInputToVersionSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/controllers/obs.delivery.js b/src/controllers/obs.delivery.js index 4a93afe1dc..8e99650af6 100644 --- a/src/controllers/obs.delivery.js +++ b/src/controllers/obs.delivery.js @@ -1,7 +1,7 @@ /** * -------------------------------------- * -------------------------------------- - * ---------TO BE DEPRICIATED------------ + * ---------TO BE DEPRECATED------------- * -------------------------------------- * -------------------------------------- */ @@ -96,11 +96,15 @@ const DestProxyController = { destination, }); - response = generateErrorObject(err, { - [tags.TAG_NAMES.DEST_TYPE]: destination.toUpperCase(), - [tags.TAG_NAMES.MODULE]: tags.MODULES.DESTINATION, - [tags.TAG_NAMES.FEATURE]: tags.FEATURES.DATA_DELIVERY, - }, false); + response = generateErrorObject( + err, + { + [tags.TAG_NAMES.DEST_TYPE]: destination.toUpperCase(), + [tags.TAG_NAMES.MODULE]: tags.MODULES.DESTINATION, + [tags.TAG_NAMES.FEATURE]: tags.FEATURES.DATA_DELIVERY, + }, + false, + ); response.message = `[TransformerProxyTest] Error occurred while testing proxy for destination ("${destination}"): "${err.message}"`; logger.error(response.message); logger.error(err); diff --git a/src/controllers/regulation.ts b/src/controllers/regulation.ts index a50541780d..318b5ed4e7 100644 --- a/src/controllers/regulation.ts +++ b/src/controllers/regulation.ts @@ -34,7 +34,7 @@ export class RegulationController { rudderDestInfo, ); ctx.body = resplist; - ctx.status = resplist[0].statusCode; + ctx.status = resplist[0].statusCode; // TODO: check if this is the right way to set status } catch (error: CatchErr) { const metaTO = integrationService.getTags( userDeletionRequests[0].destType, @@ -46,8 +46,8 @@ export class RegulationController { const errResp = DestinationPostTransformationService.handleUserDeletionFailureEvents( error, metaTO, - ); - ctx.body = [{ error, statusCode: 500 }] as UserDeletionResponse[]; + ); // TODO: this is not used. Fix it. + ctx.body = [{ error, statusCode: 500 }] as UserDeletionResponse[]; // TODO: responses array length is always 1. Is that okay? ctx.status = 500; } stats.timing('dest_transform_request_latency', startTime, { diff --git a/src/helpers/__tests__/fetchHandlers.test.ts b/src/helpers/__tests__/fetchHandlers.test.ts new file mode 100644 index 0000000000..2135317caf --- /dev/null +++ b/src/helpers/__tests__/fetchHandlers.test.ts @@ -0,0 +1,36 @@ +import { FetchHandler } from '../fetchHandlers'; +import { MiscService } from '../../services/misc'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('FetchHandlers Service', () => { + test('should save the handlers in the respective maps', async () => { + const dest = 'dest'; + const source = 'source'; + const version = 'version'; + + MiscService.getDestHandler = jest.fn().mockImplementation((dest, version) => { + return {}; + }); + MiscService.getSourceHandler = jest.fn().mockImplementation((source, version) => { + return {}; + }); + MiscService.getDeletionHandler = jest.fn().mockImplementation((source, version) => { + return {}; + }); + + expect(FetchHandler['sourceHandlerMap'].get(dest)).toBeUndefined(); + FetchHandler.getSourceHandler(dest, version); + expect(FetchHandler['sourceHandlerMap'].get(dest)).toBeDefined(); + + expect(FetchHandler['destHandlerMap'].get(dest)).toBeUndefined(); + FetchHandler.getDestHandler(dest, version); + expect(FetchHandler['destHandlerMap'].get(dest)).toBeDefined(); + + expect(FetchHandler['deletionHandlerMap'].get(dest)).toBeUndefined(); + FetchHandler.getDeletionHandler(dest, version); + expect(FetchHandler['deletionHandlerMap'].get(dest)).toBeDefined(); + }); +}); diff --git a/src/helpers/__tests__/serviceSelector.test.ts b/src/helpers/__tests__/serviceSelector.test.ts new file mode 100644 index 0000000000..c48d6bbe8b --- /dev/null +++ b/src/helpers/__tests__/serviceSelector.test.ts @@ -0,0 +1,105 @@ +import { ServiceSelector } from '../serviceSelector'; +import { INTEGRATION_SERVICE } from '../../routes/utils/constants'; +import { ProcessorTransformationRequest } from '../../types/index'; +import { CDKV1DestinationService } from '../../services/destination/cdkV1Integration'; +import { CDKV2DestinationService } from '../../services/destination/cdkV2Integration'; +import { NativeIntegrationDestinationService } from '../../services/destination/nativeIntegration'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('ServiceSelector Service', () => { + test('should save the service in the cache', async () => { + expect(ServiceSelector['serviceMap'].get(INTEGRATION_SERVICE.NATIVE_DEST)).toBeUndefined(); + expect(ServiceSelector['serviceMap'].get(INTEGRATION_SERVICE.NATIVE_SOURCE)).toBeUndefined(); + + ServiceSelector.getNativeDestinationService(); + ServiceSelector.getNativeSourceService(); + + expect(ServiceSelector['serviceMap'].get(INTEGRATION_SERVICE.NATIVE_DEST)).toBeDefined(); + expect(ServiceSelector['serviceMap'].get(INTEGRATION_SERVICE.NATIVE_SOURCE)).toBeDefined(); + }); + + test('fetchCachedService should throw error for invalidService', async () => { + expect(() => ServiceSelector['fetchCachedService']('invalidService')).toThrow( + 'Invalid Service', + ); + }); + + test('isCdkDestination should return true', async () => { + const destinationDefinitionConfig = { + cdkEnabled: true, + }; + expect(ServiceSelector['isCdkDestination'](destinationDefinitionConfig)).toBe(true); + }); + + test('isCdkDestination should return false', async () => { + const destinationDefinitionConfig = { + cdkEnabledXYZ: true, + }; + expect(ServiceSelector['isCdkDestination'](destinationDefinitionConfig)).toBe(false); + }); + + test('isCdkV2Destination should return true', async () => { + const destinationDefinitionConfig = { + cdkV2Enabled: true, + }; + expect(ServiceSelector['isCdkV2Destination'](destinationDefinitionConfig)).toBe(true); + }); + + test('isCdkV2Destination should return false', async () => { + const destinationDefinitionConfig = { + cdkV2EnabledXYZ: true, + }; + expect(ServiceSelector['isCdkV2Destination'](destinationDefinitionConfig)).toBe(false); + }); + + test('getPrimaryDestinationService should return cdk v1 dest service', async () => { + const events = [ + { + destination: { + DestinationDefinition: { + Config: { + cdkEnabled: true, + }, + }, + }, + }, + ] as ProcessorTransformationRequest[]; + expect(ServiceSelector['getPrimaryDestinationService'](events)).toBeInstanceOf( + CDKV1DestinationService, + ); + }); + + test('getPrimaryDestinationService should return cdk v2 dest service', async () => { + const events = [ + { + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + }, + }, + ] as ProcessorTransformationRequest[]; + expect(ServiceSelector['getPrimaryDestinationService'](events)).toBeInstanceOf( + CDKV2DestinationService, + ); + }); + + test('getPrimaryDestinationService should return native dest service', async () => { + const events = [{}] as ProcessorTransformationRequest[]; + expect(ServiceSelector['getPrimaryDestinationService'](events)).toBeInstanceOf( + NativeIntegrationDestinationService, + ); + }); + + test('getDestinationService should return native dest service', async () => { + const events = [{}] as ProcessorTransformationRequest[]; + expect(ServiceSelector.getDestinationService(events)).toBeInstanceOf( + NativeIntegrationDestinationService, + ); + }); +}); diff --git a/src/helpers/serviceSelector.ts b/src/helpers/serviceSelector.ts index 89678e9407..faa1c58240 100644 --- a/src/helpers/serviceSelector.ts +++ b/src/helpers/serviceSelector.ts @@ -79,7 +79,7 @@ export class ServiceSelector { // eslint-disable-next-line @typescript-eslint/no-unused-vars public static getSourceService(arg: unknown) { - // Implement source event based descision logic for selecting service + // Implement source event based decision logic for selecting service } public static getDestinationService( diff --git a/src/services/__tests__/misc.test.ts b/src/services/__tests__/misc.test.ts new file mode 100644 index 0000000000..5dcd948b34 --- /dev/null +++ b/src/services/__tests__/misc.test.ts @@ -0,0 +1,26 @@ +import { DestHandlerMap } from '../../constants/destinationCanonicalNames'; +import { MiscService } from '../misc'; + +describe('Misc tests', () => { + test('should return the right transform', async () => { + const version = 'v0'; + + Object.keys(DestHandlerMap).forEach((key) => { + expect(MiscService.getDestHandler(key, version)).toEqual( + require(`../../${version}/destinations/${DestHandlerMap[key]}/transform`), + ); + }); + + expect(MiscService.getDestHandler('am', version)).toEqual( + require(`../../${version}/destinations/am/transform`), + ); + + expect(MiscService.getSourceHandler('shopify', version)).toEqual( + require(`../../${version}/sources/shopify/transform`), + ); + + expect(MiscService.getDeletionHandler('intercom', version)).toEqual( + require(`../../${version}/destinations/intercom/deleteUsers`), + ); + }); +}); diff --git a/src/services/destination/__tests__/nativeIntegration.test.ts b/src/services/destination/__tests__/nativeIntegration.test.ts new file mode 100644 index 0000000000..59c8b41881 --- /dev/null +++ b/src/services/destination/__tests__/nativeIntegration.test.ts @@ -0,0 +1,100 @@ +import { NativeIntegrationDestinationService } from '../nativeIntegration'; +import { DestinationPostTransformationService } from '../postTransformation'; +import { + ProcessorTransformationRequest, + ProcessorTransformationOutput, + ProcessorTransformationResponse, +} from '../../../types/index'; +import { FetchHandler } from '../../../helpers/fetchHandlers'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('NativeIntegration Service', () => { + test('doProcessorTransformation - success', async () => { + const destType = '__rudder_test__'; + const version = 'v0'; + const requestMetadata = {}; + const event = { message: { a: 'b' } } as ProcessorTransformationRequest; + const events: ProcessorTransformationRequest[] = [event, event]; + + const tevent = { version: 'v0', endpoint: 'http://abc' } as ProcessorTransformationOutput; + const tresp = { output: tevent, statusCode: 200 } as ProcessorTransformationResponse; + const tresponse: ProcessorTransformationResponse[] = [tresp, tresp]; + + FetchHandler.getDestHandler = jest.fn().mockImplementation((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + process: jest.fn(() => { + return tevent; + }), + }; + }); + + const postTransformSpy = jest + .spyOn(DestinationPostTransformationService, 'handleProcessorTransformSucessEvents') + .mockImplementation((e, p, d) => { + expect(e).toEqual(event); + expect(p).toEqual(tevent); + return [tresp]; + }); + + const service = new NativeIntegrationDestinationService(); + const resp = await service.doProcessorTransformation( + events, + destType, + version, + requestMetadata, + ); + + expect(resp).toEqual(tresponse); + + expect(postTransformSpy).toHaveBeenCalledTimes(2); + }); + + test('doProcessorTransformation - failure', async () => { + const destType = '__rudder_test__'; + const version = 'v0'; + const requestMetadata = {}; + const event = { message: { a: 'b' } } as ProcessorTransformationRequest; + const events: ProcessorTransformationRequest[] = [event, event]; + + FetchHandler.getDestHandler = jest.fn().mockImplementation((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + process: jest.fn(() => { + throw new Error('test error'); + }), + }; + }); + + const service = new NativeIntegrationDestinationService(); + const resp = await service.doProcessorTransformation( + events, + destType, + version, + requestMetadata, + ); + + const expected = [ + { + metadata: undefined, + statusCode: 500, + error: 'test error', + statTags: { errorCategory: 'transformation' }, + }, + { + metadata: undefined, + statusCode: 500, + error: 'test error', + statTags: { errorCategory: 'transformation' }, + }, + ]; + + console.log('resp:', resp); + expect(resp).toEqual(expected); + }); +}); diff --git a/src/services/destination/__tests__/postTransformation.test.ts b/src/services/destination/__tests__/postTransformation.test.ts new file mode 100644 index 0000000000..f961dcbce7 --- /dev/null +++ b/src/services/destination/__tests__/postTransformation.test.ts @@ -0,0 +1,22 @@ +import { MetaTransferObject, ProcessorTransformationRequest } from '../../../types/index'; +import { DestinationPostTransformationService } from '../postTransformation'; +import { ProcessorTransformationResponse } from '../../../types'; + +describe('PostTransformation Service', () => { + test('should handleProcessorTransformFailureEvents', async () => { + const e = new Error('test error'); + const metaTo = { errorContext: 'error Context' } as MetaTransferObject; + const resp = DestinationPostTransformationService.handleProcessorTransformFailureEvents( + e, + metaTo, + ); + + const expected = { + statusCode: 500, + error: 'test error', + statTags: { errorCategory: 'transformation' }, + } as ProcessorTransformationResponse; + + expect(resp).toEqual(expected); + }); +}); diff --git a/src/services/destination/__tests__/preTransformation.test.ts b/src/services/destination/__tests__/preTransformation.test.ts new file mode 100644 index 0000000000..c10bab78ac --- /dev/null +++ b/src/services/destination/__tests__/preTransformation.test.ts @@ -0,0 +1,23 @@ +import { createMockContext } from '@shopify/jest-koa-mocks'; +import { ProcessorTransformationRequest } from '../../../types/index'; +import { DestinationPreTransformationService } from '../../destination/preTransformation'; + +describe('PreTransformation Service', () => { + test('should enhance events with query params', async () => { + const ctx = createMockContext(); + ctx.request.query = { cycle: 'true', x: 'y' }; + + const events: ProcessorTransformationRequest[] = [ + { message: { a: 'b' } } as ProcessorTransformationRequest, + ]; + const expected: ProcessorTransformationRequest[] = [ + { + message: { a: 'b' }, + request: { query: { cycle: 'true', x: 'y' } }, + } as ProcessorTransformationRequest, + ]; + + const resp = DestinationPreTransformationService.preProcess(events, ctx); + expect(resp).toEqual(expected); + }); +}); diff --git a/src/services/source/__tests__/nativeIntegration.test.ts b/src/services/source/__tests__/nativeIntegration.test.ts new file mode 100644 index 0000000000..bb40438811 --- /dev/null +++ b/src/services/source/__tests__/nativeIntegration.test.ts @@ -0,0 +1,89 @@ +import { NativeIntegrationSourceService } from '../nativeIntegration'; +import { SourcePostTransformationService } from '../postTransformation'; +import { SourceTransformationResponse, RudderMessage } from '../../../types/index'; +import stats from '../../../util/stats'; +import { FetchHandler } from '../../../helpers/fetchHandlers'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('NativeIntegration Source Service', () => { + test('sourceTransformRoutine - success', async () => { + const sourceType = '__rudder_test__'; + const version = 'v0'; + const requestMetadata = {}; + + const event = { message: { a: 'b' } }; + const events = [event, event]; + + const tevent = { anonymousId: 'test' } as RudderMessage; + const tresp = { output: { batch: [tevent] }, statusCode: 200 } as SourceTransformationResponse; + + const tresponse = [ + { output: { batch: [{ anonymousId: 'test' }] }, statusCode: 200 }, + { output: { batch: [{ anonymousId: 'test' }] }, statusCode: 200 }, + ]; + + FetchHandler.getSourceHandler = jest.fn().mockImplementationOnce((d, v) => { + expect(d).toEqual(sourceType); + expect(v).toEqual(version); + return { + process: jest.fn(() => { + return tevent; + }), + }; + }); + + const postTransformSpy = jest + .spyOn(SourcePostTransformationService, 'handleSuccessEventsSource') + .mockImplementation((e) => { + expect(e).toEqual(tevent); + return tresp; + }); + + const service = new NativeIntegrationSourceService(); + const resp = await service.sourceTransformRoutine(events, sourceType, version, requestMetadata); + + expect(resp).toEqual(tresponse); + + expect(postTransformSpy).toHaveBeenCalledTimes(2); + }); + + test('sourceTransformRoutine - failure', async () => { + const sourceType = '__rudder_test__'; + const version = 'v0'; + const requestMetadata = {}; + + const event = { message: { a: 'b' } }; + const events = [event, event]; + + const tresp = { error: 'error' } as SourceTransformationResponse; + + const tresponse = [{ error: 'error' }, { error: 'error' }]; + + FetchHandler.getSourceHandler = jest.fn().mockImplementationOnce((d, v) => { + expect(d).toEqual(sourceType); + expect(v).toEqual(version); + return { + process: jest.fn(() => { + throw new Error('test error'); + }), + }; + }); + + const postTransformSpy = jest + .spyOn(SourcePostTransformationService, 'handleFailureEventsSource') + .mockImplementation((e, m) => { + return tresp; + }); + jest.spyOn(stats, 'increment').mockImplementation(() => {}); + + const service = new NativeIntegrationSourceService(); + const resp = await service.sourceTransformRoutine(events, sourceType, version, requestMetadata); + + expect(resp).toEqual(tresponse); + + expect(postTransformSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/services/source/__tests__/postTransformation.test.ts b/src/services/source/__tests__/postTransformation.test.ts new file mode 100644 index 0000000000..e5efbe8194 --- /dev/null +++ b/src/services/source/__tests__/postTransformation.test.ts @@ -0,0 +1,49 @@ +import { + MetaTransferObject, + RudderMessage, + SourceTransformationResponse, +} from '../../../types/index'; +import { SourcePostTransformationService } from '../../source/postTransformation'; + +describe('Source PostTransformation Service', () => { + test('should handleFailureEventsSource', async () => { + const e = new Error('test error'); + const metaTo = { errorContext: 'error Context' } as MetaTransferObject; + const resp = SourcePostTransformationService.handleFailureEventsSource(e, metaTo); + + const expected = { + statusCode: 500, + error: 'test error', + statTags: { errorCategory: 'transformation' }, + } as SourceTransformationResponse; + + expect(resp).toEqual(expected); + }); + + test('should return the event as SourceTransformationResponse if it has outputToSource property', () => { + const event = { + outputToSource: {}, + output: { batch: [{ anonymousId: 'test' }] }, + } as SourceTransformationResponse; + + const result = SourcePostTransformationService.handleSuccessEventsSource(event); + + expect(result).toEqual(event); + }); + + test('should return the events as batch in SourceTransformationResponse if it is an array', () => { + const events = [{ anonymousId: 'test' }, { anonymousId: 'test' }] as RudderMessage[]; + + const result = SourcePostTransformationService.handleSuccessEventsSource(events); + + expect(result).toEqual({ output: { batch: events } }); + }); + + test('should return the event as batch in SourceTransformationResponse if it is a single object', () => { + const event = { anonymousId: 'test' } as RudderMessage; + + const result = SourcePostTransformationService.handleSuccessEventsSource(event); + + expect(result).toEqual({ output: { batch: [event] } }); + }); +}); diff --git a/test/apitests/service.api.test.ts b/test/apitests/service.api.test.ts index cbc2abb3b2..f68b627e9b 100644 --- a/test/apitests/service.api.test.ts +++ b/test/apitests/service.api.test.ts @@ -6,6 +6,8 @@ import Koa from 'koa'; import bodyParser from 'koa-bodyparser'; import setValue from 'set-value'; import { applicationRoutes } from '../../src/routes'; +import { FetchHandler } from '../../src/helpers/fetchHandlers'; +import networkHandlerFactory from '../../src/adapters/networkHandlerFactory'; let server: any; const OLD_ENV = process.env; @@ -30,6 +32,10 @@ afterAll(async () => { await httpTerminator.terminate(); }); +afterEach(() => { + jest.clearAllMocks(); +}); + const getDataFromPath = (pathInput) => { const testDataFile = fs.readFileSync(path.resolve(__dirname, pathInput)); return JSON.parse(testDataFile.toString()); @@ -76,6 +82,330 @@ describe('features tests', () => { }); }); +describe('Api tests with a mock source/destination', () => { + test('(mock destination) Processor transformation scenario with single event', async () => { + const destType = '__rudder_test__'; + const version = 'v0'; + + const getInputData = () => { + return [ + { message: { a: 'b1' }, destination: {}, metadata: { jobId: 1 } }, + { message: { a: 'b2' }, destination: {}, metadata: { jobId: 2 } }, + ]; + }; + const tevent = { version: 'v0', endpoint: 'http://abc' }; + + const getDestHandlerSpy = jest + .spyOn(FetchHandler, 'getDestHandler') + .mockImplementationOnce((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + process: jest.fn(() => { + return tevent; + }), + }; + }); + + const expected = [ + { + output: { version: 'v0', endpoint: 'http://abc', userId: '' }, + metadata: { jobId: 1 }, + statusCode: 200, + }, + { + output: { version: 'v0', endpoint: 'http://abc', userId: '' }, + metadata: { jobId: 2 }, + statusCode: 200, + }, + ]; + + const response = await request(server) + .post('/v0/destinations/__rudder_test__') + .set('Accept', 'application/json') + .send(getInputData()); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual(expected); + expect(getDestHandlerSpy).toHaveBeenCalledTimes(1); + }); + + test('(mock destination) Batching', async () => { + const destType = '__rudder_test__'; + const version = 'v0'; + + const getBatchInputData = () => { + return { + input: [ + { message: { a: 'b1' }, destination: {}, metadata: { jobId: 1 } }, + { message: { a: 'b2' }, destination: {}, metadata: { jobId: 2 } }, + ], + destType: destType, + }; + }; + const tevent = [ + { + batchedRequest: { version: 'v0', endpoint: 'http://abc' }, + metadata: [{ jobId: 1 }, { jobId: 2 }], + statusCode: 200, + }, + ]; + + const getDestHandlerSpy = jest + .spyOn(FetchHandler, 'getDestHandler') + .mockImplementationOnce((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + batch: jest.fn(() => { + return tevent; + }), + }; + }); + + const response = await request(server) + .post('/batch') + .set('Accept', 'application/json') + .send(getBatchInputData()); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual(tevent); + expect(getDestHandlerSpy).toHaveBeenCalledTimes(1); + }); + + test('(mock destination) Router transformation', async () => { + const destType = '__rudder_test__'; + const version = 'v0'; + + const getRouterTransformInputData = () => { + return { + input: [ + { message: { a: 'b1' }, destination: {}, metadata: { jobId: 1 } }, + { message: { a: 'b2' }, destination: {}, metadata: { jobId: 2 } }, + ], + destType: destType, + }; + }; + const tevent = [ + { + batchedRequest: { version: 'v0', endpoint: 'http://abc' }, + metadata: [{ jobId: 1 }, { jobId: 2 }], + statusCode: 200, + }, + ]; + + const getDestHandlerSpy = jest + .spyOn(FetchHandler, 'getDestHandler') + .mockImplementationOnce((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + processRouterDest: jest.fn(() => { + return tevent; + }), + }; + }); + + const response = await request(server) + .post('/routerTransform') + .set('Accept', 'application/json') + .send(getRouterTransformInputData()); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual({ output: tevent }); + expect(getDestHandlerSpy).toHaveBeenCalledTimes(1); + }); + + test('(mock destination) v0 proxy', async () => { + const destType = '__rudder_test__'; + const version = 'v0'; + + const getData = () => { + return { + body: { JSON: { a: 'b' } }, + metadata: { a1: 'b1' }, + destinationConfig: { a2: 'b2' }, + }; + }; + + const proxyResponse = { success: true, response: { response: 'response', code: 200 } }; + + const mockNetworkHandler = { + proxy: jest.fn((r, d) => { + expect(r).toEqual(getData()); + expect(d).toEqual(destType); + return proxyResponse; + }), + processAxiosResponse: jest.fn((r) => { + expect(r).toEqual(proxyResponse); + return { response: 'response', status: 200 }; + }), + responseHandler: jest.fn((o, d) => { + expect(o).toEqual({ response: 'response', status: 200, rudderJobMetadata: { a1: 'b1' } }); + expect(d).toEqual(destType); + return { status: 200, message: 'response', destinationResponse: 'response' }; + }), + }; + + const getNetworkHandlerSpy = jest + .spyOn(networkHandlerFactory, 'getNetworkHandler') + .mockImplementationOnce((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + networkHandler: mockNetworkHandler, + handlerVersion: version, + }; + }); + + const response = await request(server) + .post('/v0/destinations/__rudder_test__/proxy') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual({ + output: { status: 200, message: 'response', destinationResponse: 'response' }, + }); + expect(getNetworkHandlerSpy).toHaveBeenCalledTimes(1); + }); + + test('(mock destination) v1 proxy', async () => { + const destType = '__rudder_test__'; + const version = 'v1'; + + const getData = () => { + return { + body: { JSON: { a: 'b' } }, + metadata: [{ a1: 'b1' }], + destinationConfig: { a2: 'b2' }, + }; + }; + + const proxyResponse = { success: true, response: { response: 'response', code: 200 } }; + const respHandlerResponse = { + status: 200, + message: 'response', + destinationResponse: 'response', + response: [{ statusCode: 200, metadata: { a1: 'b1' } }], + }; + + const mockNetworkHandler = { + proxy: jest.fn((r, d) => { + expect(r).toEqual(getData()); + expect(d).toEqual(destType); + return proxyResponse; + }), + processAxiosResponse: jest.fn((r) => { + expect(r).toEqual(proxyResponse); + return { response: 'response', status: 200 }; + }), + responseHandler: jest.fn((o, d) => { + expect(o).toEqual({ response: 'response', status: 200, rudderJobMetadata: [{ a1: 'b1' }] }); + expect(d).toEqual(destType); + return respHandlerResponse; + }), + }; + + const getNetworkHandlerSpy = jest + .spyOn(networkHandlerFactory, 'getNetworkHandler') + .mockImplementationOnce((d, v) => { + expect(d).toEqual(destType); + expect(v).toEqual(version); + return { + networkHandler: mockNetworkHandler, + handlerVersion: version, + }; + }); + + const response = await request(server) + .post('/v1/destinations/__rudder_test__/proxy') + .set('Accept', 'application/json') + .send(getData()); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual({ + output: respHandlerResponse, + }); + expect(getNetworkHandlerSpy).toHaveBeenCalledTimes(1); + }); + + test('(mock source) v0 source transformation', async () => { + const sourceType = '__rudder_test__'; + const version = 'v0'; + + const getData = () => { + return [{ event: { a: 'b1' } }, { event: { a: 'b2' } }]; + }; + + const tevent = { event: 'clicked', type: 'track' }; + + const getSourceHandlerSpy = jest + .spyOn(FetchHandler, 'getSourceHandler') + .mockImplementationOnce((s, v) => { + expect(s).toEqual(sourceType); + return { + process: jest.fn(() => { + return tevent; + }), + }; + }); + + const response = await request(server) + .post('/v0/sources/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + const expected = [ + { output: { batch: [{ event: 'clicked', type: 'track' }] } }, + { output: { batch: [{ event: 'clicked', type: 'track' }] } }, + ]; + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual(expected); + expect(getSourceHandlerSpy).toHaveBeenCalledTimes(1); + }); + + test('(mock source) v1 source transformation', async () => { + const sourceType = '__rudder_test__'; + const version = 'v1'; + + const getData = () => { + return [ + { event: { a: 'b1' }, source: { id: 'id' } }, + { event: { a: 'b2' }, source: { id: 'id' } }, + ]; + }; + + const tevent = { event: 'clicked', type: 'track' }; + + const getSourceHandlerSpy = jest + .spyOn(FetchHandler, 'getSourceHandler') + .mockImplementationOnce((s, v) => { + expect(s).toEqual(sourceType); + return { + process: jest.fn(() => { + return tevent; + }), + }; + }); + + const response = await request(server) + .post('/v1/sources/__rudder_test__') + .set('Accept', 'application/json') + .send(getData()); + + const expected = [ + { output: { batch: [{ event: 'clicked', type: 'track' }] } }, + { output: { batch: [{ event: 'clicked', type: 'track' }] } }, + ]; + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual(expected); + expect(getSourceHandlerSpy).toHaveBeenCalledTimes(1); + }); +}); + describe('Destination api tests', () => { describe('Processor transform tests', () => { test('(webhook) success scenario with single event', async () => { @@ -183,6 +513,7 @@ describe('Destination api tests', () => { expect(response.status).toEqual(200); expect(JSON.parse(response.text)).toEqual(data.output); }); + test('(pinterest_tag) failure router transform(partial failure)', async () => { const data = getDataFromPath('./data_scenarios/destination/router/failure_test.json'); const response = await request(server)