Skip to content

Commit

Permalink
Merge pull request #780 from recurly/multi-form-fraud-detection
Browse files Browse the repository at this point in the history
Multi form fraud detection
  • Loading branch information
CJ-Howard authored Nov 29, 2022
2 parents 46daa5b + 59cfeff commit 3e23267
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 55 deletions.
12 changes: 8 additions & 4 deletions lib/recurly.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,14 @@ export class Recurly extends Emitter {
return;
}

if (!this.fraud) this.fraud = new Fraud(this);
this.fraud.on('error', (...args) => {
this.emit('error', ...args);
});
if (this.fraud) {
this.ready(this.fraud.activateProfiles);
} else {
this.fraud = new Fraud(this);
this.fraud.on('error', (...args) => {
this.emit('error', ...args);
});
}

if (!this.bus) {
this.bus = new Bus({ api: this.config.api, role: 'recurly' });
Expand Down
121 changes: 72 additions & 49 deletions lib/recurly/fraud.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export class Fraud extends Emitter {
if (this.shouldCollectData) {
recurly.ready(this.attachDataCollector.bind(this));
}
// make sure activateProfiles can be passed as a callback
this.activateProfiles = this.activateProfiles.bind(this);
}

get shouldCollectData () {
Expand Down Expand Up @@ -73,54 +75,68 @@ export class Fraud extends Emitter {
debug('risk info', error, response);

if (error) {
return this.emit('error', errors('fraud-data-collector-request-failed', { error }));
return this.emit(
'error',
errors('fraud-data-collector-request-failed', { error })
);
}

const { profiles } = response;
this.profiles = profiles;
this.activateProfiles();
},
});
}

profiles.forEach(profile => {
const { processor, params } = profile;
if (processor === 'kount') {
const configuredForm = this.recurly.config.fraud.kount.form;
const scriptElement = document.createElement('script');
const initializerElement = document.createElement('div');
const sessionIdInputElement = dom.createHiddenInput({
'data-recurly': 'fraud_session_id',
'value': params.session_id,
'type': 'hidden'
});
const initialize = () => {
if (!window.ka) {
return this.emit('error', errors('fraud-data-collector-request-failed', {
error: 'Kount SDK failed to load.'
}));
}
// eslint-disable-next-line no-undef
const client = new ka.ClientSDK();
client.autoLoadEvents();
};

scriptElement.setAttribute('src', params.script_url);
scriptElement.onload = initialize;

initializerElement.className = 'kaxsdc';
initializerElement.setAttribute('data-event', 'load');

const form = dom.element(configuredForm) || this.getHostedFieldParentForm();
if (form) {
const nodes = [
sessionIdInputElement,
scriptElement,
initializerElement
];

nodes.forEach(node => form.appendChild(node));
this.attachedNodes = nodes;
}

this.emit('ready');
}
activateProfiles () {
if (!this.profiles) return;

this.profiles.forEach((profile) => {
const { processor, params } = profile;
if (processor === 'kount') {
const configuredForm = this.recurly.config.fraud.kount.form;
const sessionIdInputElement = dom.createHiddenInput({
'data-recurly': 'fraud_session_id',
value: params.session_id,
'type': 'hidden',
});

const initialize = () => {
if (!window.ka) {
return this.emit(
'error',
errors('fraud-data-collector-request-failed', {
error: 'Kount SDK failed to load.',
})
);
}
// eslint-disable-next-line no-undef
const client = new ka.ClientSDK();
client.autoLoadEvents();
};

const scriptElement = document.createElement('script');
scriptElement.setAttribute('src', params.script_url);
scriptElement.onload = initialize;

const initializerElement = document.createElement('div');
initializerElement.className = 'kaxsdc';
initializerElement.setAttribute('data-event', 'load');

const form =
dom.element(configuredForm) || this.getHostedFieldParentForm();
if (form) {
const nodes = [
sessionIdInputElement,
scriptElement,
initializerElement,
];

nodes.forEach((node) => form.appendChild(node));
this.attachedNodes = nodes;
}

this.emit('ready');
}
});
}
Expand All @@ -135,17 +151,24 @@ export class Fraud extends Emitter {
getHostedFieldParentForm () {
if (this._form) return this._form;
const fields = this.recurly.config.fields;
const selectors = Object.keys(fields).map(name => fields[name].selector).filter(Boolean);
const selectors = Object.keys(fields)
.map((name) => fields[name].selector)
.filter(Boolean);
let form;
find(selectors, selector => {
const node = dom.findNodeInParents(window.document.querySelector(selector), 'form');
find(selectors, (selector) => {
const node = dom.findNodeInParents(
window.document.querySelector(selector),
'form'
);
if (dom.element(node)) form = node;
});

if (form) {
return this._form = form;
return (this._form = form);
} else {
const missingFormError = errors('fraud-data-collector-missing-form', { selectors });
const missingFormError = errors('fraud-data-collector-missing-form', {
selectors,
});
this.emit('error', missingFormError);
}
}
Expand All @@ -154,7 +177,7 @@ export class Fraud extends Emitter {
* Removes any attached data collectors
*/
destroy () {
this.attachedNodes.forEach(node => {
this.attachedNodes.forEach((node) => {
if (!node.parentElement) return;
node.parentElement.removeChild(node);
});
Expand Down
52 changes: 52 additions & 0 deletions test/unit/fraud.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,58 @@ describe('Recurly.fraud', function () {
});
});

describe('with more than one payment form', function () {
this.ctx.fixture = 'multipleEmptyForms';

it('creates a data collector repeatedly', function (done) {
let recurly = null;

const firstForm = () => {
const form = testBed().querySelector('#test-form-1');
assert.strictEqual(form.children.length, 0);

recurly = initRecurly({
fraud: {
kount: { ...kountConfiguration.kount, form }
}
});

recurly.fraud.once('ready', () => {
assert.strictEqual(form.children.length, 3, 'test form 1 should have Kount elements');
assert.strictEqual(form.children[0].getAttribute('data-recurly'), 'fraud_session_id');
assert.strictEqual(form.children[1].getAttribute('src'), '/api/mock-200');
assert.strictEqual(form.children[2].className, 'kaxsdc');
secondForm();
});
};

const secondForm = () => {
const form = testBed().querySelector('#test-form-2');
assert.strictEqual(form.children.length, 0);

// on the second pass, we need to set up additional checks BEFORE
// calling recurly.configuration, because recurly.fraud already
// exists
recurly.fraud.once('ready', () => {
assert.strictEqual(form.children.length, 3, 'test form 2 should have Kount elements');
assert.strictEqual(form.children[0].getAttribute('data-recurly'), 'fraud_session_id');
assert.strictEqual(form.children[1].getAttribute('src'), '/api/mock-200');
assert.strictEqual(form.children[2].className, 'kaxsdc');
done();
});

initRecurly(recurly, {
fraud: {
kount: { ...kountConfiguration.kount, form }
}
});

};

firstForm();
});
});

it('emits an error when the Kount SDK encounters an error', function (done) {
const form = testBed().querySelector('#test-form');
assert.strictEqual(form.children.length, 0);
Expand Down
11 changes: 9 additions & 2 deletions test/unit/support/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,14 @@ const iframe = opts => `
></iframe>
`;

const threeDSecure = () => `<div id="three-d-secure-container"></div>`;
const threeDSecure = () => '<div id="three-d-secure-container"></div>';

const emptyForm = () => `<form id="test-form"></form>`;
const emptyForm = () => '<form id="test-form"></form>';

const multipleEmptyForms = () => `
<form id="test-form-1"></form>
<form id="test-form-2"></form>
`;

const selectLists = (name) => `<select id="${name}" name="${name}"></select>`;

Expand All @@ -207,6 +212,7 @@ const FIXTURES = {
threeDSecure,
empty,
emptyForm,
multipleEmptyForms,
selectLists,
selectListsFull,
};
Expand Down Expand Up @@ -243,5 +249,6 @@ export function clearFixture () {
* @return {Mixed} value of property on object or default if none found
*/
function fetch (object, prop, def = '') {
// eslint-disable-next-line no-prototype-builtins
return object.hasOwnProperty(prop) ? object[prop] : def;
}

0 comments on commit 3e23267

Please sign in to comment.