UNPKG

@datadome/fraud-sdk-node

Version:

Fraud Protection - Node.js SDK

516 lines 23.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const ava_1 = tslib_1.__importDefault(require("ava")); const nock_1 = tslib_1.__importDefault(require("nock")); const index_1 = require("./index"); const ENDPOINT_HOST = 'account-api.datadome.co'; const ENDPOINT_URL = `https://${ENDPOINT_HOST}/v1/`; ava_1.default.afterEach((t) => { nock_1.default.abortPendingRequests(); nock_1.default.cleanAll(); }); ava_1.default.serial('Return an allow response after the API times out', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/login') .reply((uri, requestBody, cb) => { setTimeout(() => cb(null, [ 200, { action: 'allow', ip: '::1', reasons: [], location: 'Lyon', }, ]), 1000); }); const datadomeClient = new index_1.DataDome('key', { timeout: 100 }); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'timeout', status: 'succeeded' })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.TIMEOUT); t.true('message' in result); t.regex(result.message, /timed out/); })); ava_1.default.serial('Return an allow response when using an invalid key', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/login') .reply(403, { message: 'Invalid header', errors: [ { field: 'x-api-key', error: 'Invalid or blocked API key', }, ], }); const datadomeClient = new index_1.DataDome('key', { logger: console }); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'accountName', status: 'succeeded' })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.FAILURE); t.assert(result.errors.length > 0); t.is(result.errors[0].field, 'x-api-key'); t.is(result.errors[0].error, 'Invalid or blocked API key'); t.true(scope.isDone()); })); (0, ava_1.default)('Do not fail to instantiate the DataDome class if the timeout is negative', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const datadomeClient = new index_1.DataDome('key', { timeout: -2, logger: console }); t.pass(); })); (0, ava_1.default)('Do not fail to instantiate the DataDome class if the endpoint URL is invalid', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const datadomeClient = new index_1.DataDome('key', { endpoint: 'invalid-url' }); t.pass(); })); ava_1.default.serial('Properly handle timeout with manually triggered abort', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/login') .delay(500) .reply(200, { action: 'allow' }); const datadomeClient = new index_1.DataDome('key', { timeout: 50 }); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'abort-timeout', status: 'succeeded' })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.TIMEOUT); t.true('message' in result); t.is(result.message, 'Request timed out after 50 milliseconds'); })); ava_1.default.serial('Return an allow response on validating login [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/login') .reply(200, { action: 'allow', ip: '192.168.1.1', reasons: [], location: { countryCode: 'FR', country: 'France', city: 'Lyon' }, }); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.validate(mockCompleteRequest(), new index_1.LoginEvent({ account: 'happy', status: 'succeeded' })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.OK); t.pass(); })); ava_1.default.serial('Return an allow response on validating registration', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/validate/registration').reply(200, { action: 'allow', reasons: [], }); const datadomeClient = new index_1.DataDome('key', { timeout: 1000, logger: console }); const result = yield datadomeClient.validate(mockRequest(), new index_1.RegistrationEvent({ account: 'happy', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.OK); t.pass(); })); ava_1.default.serial('Return a failure on validating registration [500]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/registration') .reply(500, 'Internal Server Error'); const datadomeClient = new index_1.DataDome('key', { timeout: 1000, logger: console }); const result = yield datadomeClient.validate(mockRequest(), new index_1.RegistrationEvent({ account: '500-internal-server-error', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.FAILURE); t.pass(); })); ava_1.default.serial('Return status ok on collect login [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/collect/login').reply(201); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.LoginEvent({ account: 'collect', status: 'succeeded' })); t.is(result.status, index_1.ResponseStatus.OK); })); ava_1.default.serial('Return status ok on collect failed login', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/collect/login').reply(201); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.LoginEvent({ account: 'collect', status: 'failed' })); t.is(result.status, index_1.ResponseStatus.OK); })); ava_1.default.serial('Return status ok on collect registration [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/collect/registration').reply(201); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.RegistrationEvent({ account: 'collect', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.status, index_1.ResponseStatus.OK); })); ava_1.default.serial('Return an allow response on validating account update [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/account/update') .reply(200, { action: 'allow', ip: '192.168.1.1', reasons: [], location: { countryCode: 'FR', country: 'France', city: 'Lyon' }, }); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.validate(mockCompleteRequest(), new index_1.AccountUpdateEvent({ account: 'happy', session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.OK); t.pass(); })); ava_1.default.serial('Return status ok on collect account update [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/collect/account/update').reply(201); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.AccountUpdateEvent({ account: 'collect', session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.status, index_1.ResponseStatus.OK); })); ava_1.default.serial('Return an allow response on validating password update [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/password/update') .reply(200, { action: 'allow', ip: '192.168.1.1', reasons: [], location: { countryCode: 'FR', country: 'France', city: 'Lyon' }, }); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.validate(mockCompleteRequest(), new index_1.PasswordUpdateEvent({ account: 'happy', reason: 'userUpdate', status: 'succeeded', session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.OK); t.pass(); })); ava_1.default.serial('Return status ok on collect password update [Happy path]', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/collect/password/update').reply(201); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.PasswordUpdateEvent({ account: 'collect', reason: 'forgotPassword', status: 'succeeded', user: { id: 'userId' }, })); t.is(result.status, index_1.ResponseStatus.OK); })); ava_1.default.serial('Return status ok on collect failed password update', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/collect/password/update').reply(201); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.PasswordUpdateEvent({ account: 'collect', reason: 'forcedReset', status: 'failed', user: { id: 'userId' }, })); t.is(result.status, index_1.ResponseStatus.OK); })); ava_1.default.serial('Return status fail on collect registration with invalid key', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/collect/registration') .reply(403, { message: 'Invalid header', errors: [ { field: 'x-api-key', error: 'Invalid or blocked API key', }, ], }); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.collect(mockRequest(), new index_1.RegistrationEvent({ account: 'collect', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.status, index_1.ResponseStatus.FAILURE); t.assert(result.errors.length > 0); t.is(result.errors[0].field, 'x-api-key'); t.is(result.errors[0].error, 'Invalid or blocked API key'); })); ava_1.default.serial('Ignore result on collect registration with invalid key', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/collect/registration') .reply(403, { message: 'Invalid header', errors: [ { field: 'x-api-key', error: 'Invalid or blocked API key', }, ], }); const datadomeClient = new index_1.DataDome('key'); const result = datadomeClient.collect(mockRequest(), new index_1.RegistrationEvent({ account: 'collect', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); return result .then((r) => { t.pass(); }) .catch((ex) => t.fail()); })); ava_1.default.serial('Return a deny response on validate login', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/login') .reply(200, { action: 'deny', reasons: ['brute_force'], ip: '192.167.1.1', location: { countryCode: 'IT', country: 'Italy', city: 'Chieti', }, }); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'deny', status: 'succeeded' })); t.is(result.status, index_1.ResponseStatus.OK); t.is(result.action, index_1.ResponseAction.DENY); t.assert(result.reasons !== undefined); t.assert(result.reasons.length > 0); t.assert(result.location); t.assert(result.reasons[0], 'brute_force'); })); ava_1.default.serial('Return a deny response on validate registration', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/registration') .reply(200, { action: 'deny', reasons: ['brute_force'], }); const datadomeClient = new index_1.DataDome('key'); const result = yield datadomeClient.validate(mockRequest(), new index_1.RegistrationEvent({ account: 'deny', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); t.is(result.status, index_1.ResponseStatus.OK); t.is(result.action, index_1.ResponseAction.DENY); t.assert(result.reasons !== undefined); t.assert(result.reasons.length > 0); t.assert(result.reasons[0], 'brute_force'); })); ava_1.default.serial('Force Https on endpoint configuration', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/validate/login').reply(200, { action: 'allow', reason: [], }); let options = { endpoint: 'account-api.datadome.co', }; const datadomeClient = new index_1.DataDome('key', options); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'deny', status: 'failed' })); t.is(result.status, index_1.ResponseStatus.OK); t.is(result.action, index_1.ResponseAction.ALLOW); })); ava_1.default.serial('Allow http on endpoint configuration', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)('http://account-api.datadome.co/v1').post('/validate/login').reply(200, { action: 'allow', reason: [], }); let options = { endpoint: 'http://account-api.datadome.co', }; const datadomeClient = new index_1.DataDome('key', options); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'deny', status: 'failed' })); t.is(result.status, index_1.ResponseStatus.OK); t.is(result.action, index_1.ResponseAction.ALLOW); })); ava_1.default.serial('Allow https on endpoint configuration', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL).post('/validate/login').reply(200, { action: 'allow', reason: [], }); let options = { endpoint: 'https://account-api.datadome.co', }; const datadomeClient = new index_1.DataDome('key', options); const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'deny', status: 'failed' })); t.is(result.status, index_1.ResponseStatus.OK); t.is(result.action, index_1.ResponseAction.ALLOW); })); ava_1.default.serial('Confirms headers are set', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/registration') .reply((uri, requestBody) => { t.is(requestBody.header.acceptLanguage, 'en-US,en;q=0.9'); t.is(requestBody.header.secCHUA, 'ua'); t.is(requestBody.header.secCHUAMobile, 'mobile'); t.is(requestBody.header.secCHUAArch, 'arch'); t.is(requestBody.header.secCHUAModel, 'model'); t.is(requestBody.header.secCHUAFullVersionList, 'fullVersionList'); t.is(requestBody.header.secCHDeviceMemory, 'deviceMe'); t.is(requestBody.header.secCHUAPlatform, 'platform'); t.is(requestBody.header.clientID, 'blablablaClientId%1=blabla='); return [ 200, { action: 'allow', ip: '192.168.1.1', reasons: [], location: { countryCode: 'FR', country: 'France', city: 'Lyon' }, }, ]; }); const datadomeClient = new index_1.DataDome('key', { timeout: 100 }); const result = yield datadomeClient.validate(mockCompleteRequest(), new index_1.RegistrationEvent({ account: 'accept-language', authentication: { type: 'other', mode: 'password' }, session: { id: 'sessionId', createdAt: new Date() }, user: { id: 'userId' }, })); })); ava_1.default.serial('Use requestMetadata parameter in validate method', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/validate/login') .reply((uri, requestBody) => { t.is(requestBody.header.addr, '8.8.8.8'); t.is(requestBody.header.secCHUAPlatform, 'macOS'); t.is(requestBody.header.contentType, 'application/special'); t.is(requestBody.header.secCHUAArch, 'arm64'); t.is(requestBody.header.secCHUAModel, 'MacBook Pro'); t.is(requestBody.header.secCHUAFullVersionList, 'Chrome;v="91"'); t.is(requestBody.header.secCHDeviceMemory, '8GB'); t.is(requestBody.header.secCHUA, 'Chrome'); t.is(requestBody.header.secCHUAMobile, '?0'); t.is(requestBody.header.protocol, 'https'); t.is(requestBody.header.port, 8443); t.is(requestBody.header.method, 'PUT'); t.is(requestBody.header.xRealIp, '10.0.0.1'); t.is(requestBody.header.xForwardedForIp, '10.0.0.2, 10.0.0.3'); t.is(requestBody.header.acceptEncoding, 'gzip, deflate'); t.is(requestBody.header.serverHostname, 'api.example.com'); t.is(requestBody.header.from, 'user@example.com'); t.is(requestBody.header.acceptCharset, 'utf-8, iso-8859-1'); t.is(requestBody.header.clientID, 'custom-client-id'); t.is(requestBody.header.request, '/custom-path'); return [ 200, { action: 'allow', ip: '8.8.8.8', reasons: [], location: { countryCode: 'US', country: 'United States', city: 'Mountain View', }, }, ]; }); const datadomeClient = new index_1.DataDome('key'); const customMetadata = { addr: '8.8.8.8', contentType: 'application/special', protocol: 'https', port: 8443, method: 'PUT', secCHUAPlatform: 'macOS', secCHUAArch: 'arm64', secCHUAModel: 'MacBook Pro', secCHUAFullVersionList: 'Chrome;v="91"', secCHDeviceMemory: '8GB', secCHUA: 'Chrome', secCHUAMobile: '?0', xRealIp: '10.0.0.1', xForwardedForIp: '10.0.0.2, 10.0.0.3', acceptEncoding: 'gzip, deflate', serverHostname: 'api.example.com', from: 'user@example.com', acceptCharset: 'utf-8, iso-8859-1', clientID: 'custom-client-id', request: '/custom-path', }; const result = yield datadomeClient.validate(mockRequest(), new index_1.LoginEvent({ account: 'metadata-test', status: 'succeeded' }), customMetadata); t.is(result.action, index_1.ResponseAction.ALLOW); t.is(result.status, index_1.ResponseStatus.OK); t.pass(); })); ava_1.default.serial('Use requestMetadata parameter in collect method', (t) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const scope = (0, nock_1.default)(ENDPOINT_URL) .post('/collect/registration') .reply((uri, requestBody) => { t.is(requestBody.header.host, 'custom.host.com'); t.is(requestBody.header.origin, 'https://app.custom.com'); t.is(requestBody.header.userAgent, 'Custom User Agent'); t.is(requestBody.header.referer, 'https://app.custom.com/login'); t.is(requestBody.header.accept, 'application/custom+json'); t.is(requestBody.header.acceptLanguage, 'fr-FR,fr;q=0.9'); t.is(requestBody.header.connection, 'close'); return [201]; }); const datadomeClient = new index_1.DataDome('key'); const customMetadata = { host: 'custom.host.com', origin: 'https://app.custom.com', userAgent: 'Custom User Agent', referer: 'https://app.custom.com/login', accept: 'application/custom+json', acceptLanguage: 'fr-FR,fr;q=0.9', connection: 'close', }; const result = yield datadomeClient.collect(mockRequest(), new index_1.RegistrationEvent({ account: 'metadata-collect-test', session: { id: 'customSessionId', createdAt: new Date() }, user: { id: 'customUserId' }, }), customMetadata); t.is(result.status, index_1.ResponseStatus.OK); t.pass(); })); const http_1 = require("http"); function mockRequest() { const req = Object.create(http_1.IncomingMessage.prototype); req.socket = { localAddress: '::1' }; req.headers = {}; req.method = 'POST'; req.url = ''; return req; } function mockCompleteRequest() { const req = Object.create(http_1.IncomingMessage.prototype); req.socket = { localAddress: '192.168.1.5', localPort: 443, }; req.headers = { accept: 'application/json', 'accept-language': 'en-US,en;q=0.9', host: 'datashield.co', referer: 'datashield.co', 'x-real-ip': '192.168.1.10', 'x-forwarded-for': '192.168.1.11', 'accept-encoding': 'application/json', 'accept-charset': 'utf8', from: 'datashield', 'user-agent': 'Mozilla', connection: 'keep-alive', origin: 'datashield.co', cookie: 'datadome=blablablaClientId%1=blabla=;other="bla"', 'sec-ch-ua': 'ua', 'sec-ch-ua-arch': 'arch', 'sec-ch-ua-mobile': 'mobile', 'sec-ch-ua-model': 'model', 'sec-ch-ua-full-version-list': 'fullVersionList', 'sec-ch-device-memory': 'deviceMemory', 'sec-ch-ua-platform': 'platform', }; req.method = 'POST'; req.url = '/login'; req.protocol = 'https'; return req; } //# sourceMappingURL=datadome.test.js.map