diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cef57af73..bdd76d916c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,6 @@ See the project's [README](README.md) for further information about working in t - Include instructions on how to test your changes. 3. Your branch may be merged once all configured checks pass, including: - A review from appropriate maintainers -4. Along with the PR in transformer raise a PR against [config-generator][config-generator] with the configurations. ## Committing diff --git a/package-lock.json b/package-lock.json index 0f44dfce3d..05bab904aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@pyroscope/nodejs": "^0.2.6", "@rudderstack/integrations-lib": "^0.2.4", "@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", @@ -4529,6 +4530,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", @@ -16013,6 +16026,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", @@ -16591,6 +16612,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", @@ -18041,6 +18103,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 f6ab6bc1dd..558160207c 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@pyroscope/nodejs": "^0.2.6", "@rudderstack/integrations-lib": "^0.2.4", "@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..0f91913f9d --- /dev/null +++ b/src/controllers/__tests__/delivery.test.ts @@ -0,0 +1,186 @@ +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', + }, + status: 500, + response: [{ error: 'test error', metadata: { a1: 'b1' }, statusCode: 500 }], + }, + }; + expect(response.status).toEqual(200); + 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 5aa3ca5862..8e99650af6 100644 --- a/src/controllers/obs.delivery.js +++ b/src/controllers/obs.delivery.js @@ -1,7 +1,7 @@ /** * -------------------------------------- * -------------------------------------- - * ---------TO BE DEPRICIATED------------ + * ---------TO BE DEPRECATED------------- * -------------------------------------- * -------------------------------------- */ 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/src/util/prometheus.js b/src/util/prometheus.js index eec480bbff..0fa17dc9bd 100644 --- a/src/util/prometheus.js +++ b/src/util/prometheus.js @@ -497,7 +497,7 @@ class Prometheus { name: 'shopify_anon_id_resolve', help: 'shopify_anon_id_resolve', type: 'counter', - labelNames: ['method', 'writeKey', 'shopifyTopic'], + labelNames: ['method', 'writeKey', 'shopifyTopic', 'source'], }, { name: 'shopify_redis_calls', diff --git a/test/apitests/service.api.test.ts b/test/apitests/service.api.test.ts index cbc2abb3b2..266619b6ac 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,332 @@ 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.destinationResponse).toEqual({ response: 'response', status: 200 }); + expect(o.rudderJobMetadata).toEqual({ a1: 'b1' }); + expect(o.destType).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.destinationResponse).toEqual({ response: 'response', status: 200 }); + expect(o.rudderJobMetadata).toEqual([{ a1: 'b1' }]); + expect(o.destType).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 +515,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) diff --git a/test/integrations/destinations/snapchat_custom_audience/dataDelivery/business.ts b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/business.ts new file mode 100644 index 0000000000..4ee646bedb --- /dev/null +++ b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/business.ts @@ -0,0 +1,118 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { + generateMetadata, + generateProxyV0Payload, + generateProxyV1Payload, +} from '../../../testUtils'; + +const commonHeaders = { + Authorization: 'Bearer abcd123', + 'Content-Type': 'application/json', + 'User-Agent': 'RudderLabs', +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: { + users: [ + { + schema: ['EMAIL_SHA256'], + data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], + }, + ], + }, +}; + +export const businessV0TestScenarios = [ + { + id: 'snapchat_custom_audience_v0_oauth_scenario_1', + name: 'snapchat_custom_audience', + description: '[Proxy v0 API] :: successfull call', + successCriteria: 'Proper response from destination is received', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://adsapi.snapchat.com/v1/segments/123/users', + params: { + destination: 'snapchat_custom_audience', + }, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + destinationResponse: { + response: { + request_status: 'SUCCESS', + request_id: '12345', + users: [ + { + sub_request_status: 'SUCCESS', + user: { + number_uploaded_users: 1, + }, + }, + ], + }, + status: 200, + }, + }, + }, + }, + }, + }, +]; + +export const businessV1TestScenarios: ProxyV1TestData[] = [ + { + id: 'snapchat_custom_audience_v1_oauth_scenario_1', + name: 'snapchat_custom_audience', + description: '[Proxy v1 API] :: successfull oauth', + successCriteria: 'Proper response from destination is received', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://adsapi.snapchat.com/v1/segments/123/users', + params: { + destination: 'snapchat_custom_audience', + }, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: `{\"request_status\":\"SUCCESS\",\"request_id\":\"12345\",\"users\":[{\"sub_request_status\":\"SUCCESS\",\"user\":{\"number_uploaded_users\":1}}]}`, + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/snapchat_custom_audience/dataDelivery/data.ts b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/data.ts index d8ec365a82..4991ed1d38 100644 --- a/test/integrations/destinations/snapchat_custom_audience/dataDelivery/data.ts +++ b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/data.ts @@ -1,206 +1,11 @@ +import { businessV0TestScenarios, businessV1TestScenarios } from './business'; +import { v0OauthScenarios, v1OauthScenarios } from './oauth'; +import { otherScenariosV1 } from './other'; + export const data = [ - { - name: 'snapchat_custom_audience', - description: 'Test 0', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://adsapi.snapchat.com/v1/segments/123/users', - headers: { - Authorization: 'Bearer abcd123', - 'Content-Type': 'application/json', - }, - body: { - JSON: { - users: [ - { - schema: ['EMAIL_SHA256'], - data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'snapchat_custom_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: 'Request Processed Successfully', - destinationResponse: { - response: { - request_status: 'SUCCESS', - request_id: '12345', - users: [ - { - sub_request_status: 'SUCCESS', - user: { - number_uploaded_users: 1, - }, - }, - ], - }, - status: 200, - }, - }, - }, - }, - }, - }, - { - name: 'snapchat_custom_audience', - description: 'Test 1', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://adsapi.snapchat.com/v1/segments/456/users', - headers: { - Authorization: 'Bearer abcd123', - 'Content-Type': 'application/json', - }, - body: { - JSON: { - users: [ - { - schema: ['EMAIL_SHA256'], - data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'snapchat_custom_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 500, - body: { - output: { - status: 500, - destinationResponse: { - response: 'unauthorized', - status: 401, - }, - message: - 'Failed with unauthorized during snapchat_custom_audience response transformation', - statTags: { - destType: 'SNAPCHAT_CUSTOM_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'retryable', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - authErrorCategory: 'REFRESH_TOKEN', - }, - }, - }, - }, - }, - { - name: 'snapchat_custom_audience', - description: 'Test 2', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'DELETE', - endpoint: 'https://adsapi.snapchat.com/v1/segments/789/users', - headers: { - Authorization: 'Bearer abcd123', - 'Content-Type': 'application/json', - }, - body: { - JSON: { - users: [ - { - id: '123456', - schema: ['EMAIL_SHA256'], - data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'snapchat_custom_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 400, - body: { - output: { - authErrorCategory: 'AUTH_STATUS_INACTIVE', - status: 400, - destinationResponse: { - response: { - request_status: 'ERROR', - request_id: '98e2a602-3cf4-4596-a8f9-7f034161f89a', - debug_message: 'Caller does not have permission', - display_message: - "We're sorry, but the requested resource is not available at this time", - error_code: 'E3002', - }, - status: 403, - }, - message: 'undefined during snapchat_custom_audience response transformation', - statTags: { - destType: 'SNAPCHAT_CUSTOM_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - }, - }, - }, - }, - }, + ...v0OauthScenarios, + ...v1OauthScenarios, + ...businessV0TestScenarios, + ...businessV1TestScenarios, + ...otherScenariosV1, ]; diff --git a/test/integrations/destinations/snapchat_custom_audience/dataDelivery/oauth.ts b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/oauth.ts new file mode 100644 index 0000000000..e4bf5d4588 --- /dev/null +++ b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/oauth.ts @@ -0,0 +1,244 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { + generateMetadata, + generateProxyV0Payload, + generateProxyV1Payload, +} from '../../../testUtils'; + +const commonHeaders = { + Authorization: 'Bearer abcd123', + 'Content-Type': 'application/json', + 'User-Agent': 'RudderLabs', +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: { + users: [ + { + schema: ['EMAIL_SHA256'], + data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], + }, + ], + }, +}; + +const commonDeleteRequestParameters = { + headers: commonHeaders, + JSON: { + users: [ + { + id: '123456', + schema: ['EMAIL_SHA256'], + data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], + }, + ], + }, +}; + +const retryStatTags = { + destType: 'SNAPCHAT_CUSTOM_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +const abortStatTags = { + destType: 'SNAPCHAT_CUSTOM_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const v0OauthScenarios = [ + { + id: 'snapchat_custom_audience_v0_oauth_scenario_2', + name: 'snapchat_custom_audience', + description: + '[Proxy v0 API] :: Oauth where valid credentials are missing as mock response from destination', + successCriteria: + 'Since the error from the destination is 401 - the proxy should return 500 with authErrorCategory as REFRESH_TOKEN', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://adsapi.snapchat.com/v1/segments/456/users', + params: { + destination: 'snapchat_custom_audience', + }, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + destinationResponse: { + response: 'unauthorized', + status: 401, + }, + message: + 'Failed with unauthorized during snapchat_custom_audience response transformation', + statTags: retryStatTags, + authErrorCategory: 'REFRESH_TOKEN', + }, + }, + }, + }, + }, + { + id: 'snapchat_custom_audience_v0_oauth_scenario_3', + name: 'snapchat_custom_audience', + description: + '[Proxy v0 API] :: Oauth where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', + successCriteria: + 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://adsapi.snapchat.com/v1/segments/999/users', + params: { + destination: 'snapchat_custom_audience', + }, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + authErrorCategory: 'AUTH_STATUS_INACTIVE', + status: 400, + destinationResponse: { + response: { + request_status: 'ERROR', + request_id: '98e2a602-3cf4-4596-a8f9-7f034161f89a', + debug_message: 'Caller does not have permission', + display_message: + "We're sorry, but the requested resource is not available at this time", + error_code: 'E3002', + }, + status: 403, + }, + message: 'undefined during snapchat_custom_audience response transformation', + statTags: abortStatTags, + }, + }, + }, + }, + }, +]; + +export const v1OauthScenarios: ProxyV1TestData[] = [ + { + id: 'snapchat_custom_audience_v1_oauth_scenario_1', + name: 'snapchat_custom_audience', + description: + '[Proxy v1 API] :: Oauth where valid credentials are missing as mock response from destination', + successCriteria: + 'Since the error from the destination is 401 - the proxy should return 500 with authErrorCategory as REFRESH_TOKEN', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://adsapi.snapchat.com/v1/segments/456/users', + params: { + destination: 'snapchat_custom_audience', + }, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: '"unauthorized"', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: retryStatTags, + authErrorCategory: 'REFRESH_TOKEN', + message: + 'Failed with unauthorized during snapchat_custom_audience response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'snapchat_custom_audience_v1_oauth_scenario_2', + name: 'snapchat_custom_audience', + description: + '[Proxy v1 API] :: Oauth where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', + successCriteria: + 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://adsapi.snapchat.com/v1/segments/999/users', + params: { + destination: 'snapchat_custom_audience', + }, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + response: [ + { + error: `{"request_status":"ERROR","request_id":"98e2a602-3cf4-4596-a8f9-7f034161f89a","debug_message":"Caller does not have permission","display_message":"We're sorry, but the requested resource is not available at this time","error_code":"E3002"}`, + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + statTags: abortStatTags, + message: 'undefined during snapchat_custom_audience response transformation', + status: 400, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/snapchat_custom_audience/dataDelivery/other.ts b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/other.ts new file mode 100644 index 0000000000..90508c2481 --- /dev/null +++ b/test/integrations/destinations/snapchat_custom_audience/dataDelivery/other.ts @@ -0,0 +1,204 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; + +const expectedStatTags = { + destType: 'SNAPCHAT_CUSTOM_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const otherScenariosV1: ProxyV1TestData[] = [ + { + id: 'snapchat_custom_audience_v1_other_scenario_1', + name: 'snapchat_custom_audience', + description: + '[Proxy v1 API] :: Scenario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_service_not_available', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 503, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + status: 503, + message: 'Service Unavailable during snapchat_custom_audience response transformation', + }, + }, + }, + }, + }, + { + id: 'snapchat_custom_audience_v1_other_scenario_2', + name: 'snapchat_custom_audience', + description: '[Proxy v1 API] :: Scenario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_internal_server_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Internal Server Error"', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + status: 500, + message: 'undefined during snapchat_custom_audience response transformation', + }, + }, + }, + }, + }, + { + id: 'snapchat_custom_audience_v1_other_scenario_3', + name: 'snapchat_custom_audience', + description: '[Proxy v1 API] :: Scenario for testing Gateway Time Out error from destination', + successCriteria: 'Should return 504 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_gateway_time_out', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Gateway Timeout"', + statusCode: 504, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + status: 504, + message: 'undefined during snapchat_custom_audience response transformation', + }, + }, + }, + }, + }, + { + id: 'snapchat_custom_audience_v1_other_scenario_4', + name: 'snapchat_custom_audience', + description: '[Proxy v1 API] :: Scenario for testing null response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_null_response', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + status: 500, + message: 'undefined during snapchat_custom_audience response transformation', + }, + }, + }, + }, + }, + { + id: 'snapchat_custom_audience_v1_other_scenario_5', + name: 'snapchat_custom_audience', + description: + '[Proxy v1 API] :: Scenario for testing null and no status response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_null_and_no_status', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + status: 500, + message: 'undefined during snapchat_custom_audience response transformation', + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/snapchat_custom_audience/network.ts b/test/integrations/destinations/snapchat_custom_audience/network.ts index 9be134c202..39bd46122d 100644 --- a/test/integrations/destinations/snapchat_custom_audience/network.ts +++ b/test/integrations/destinations/snapchat_custom_audience/network.ts @@ -81,4 +81,35 @@ export const networkCallsData = [ statusText: 'Forbidden', }, }, + { + httpReq: { + url: 'https://adsapi.snapchat.com/v1/segments/999/users', + data: { + users: [ + { + schema: ['EMAIL_SHA256'], + data: [['938758751f5af66652a118e26503af824404bc13acd1cb7642ddff99916f0e1c']], + }, + ], + }, + params: { destination: 'snapchat_custom_audience' }, + headers: { + Authorization: 'Bearer abcd123', + 'Content-Type': 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: { + request_status: 'ERROR', + request_id: '98e2a602-3cf4-4596-a8f9-7f034161f89a', + debug_message: 'Caller does not have permission', + display_message: "We're sorry, but the requested resource is not available at this time", + error_code: 'E3002', + }, + status: 403, + statusText: 'Forbidden', + }, + }, ];