From fd7c73d861a8526885f1239398f0bccd05e4fdd7 Mon Sep 17 00:00:00 2001 From: Russell Hart Date: Mon, 7 Nov 2022 18:07:48 -0500 Subject: [PATCH] Marketo Associate Web Activity Step --- package-lock.json | 138 +++++++++++++++++- package.json | 1 + src/client/client-wrapper.ts | 21 ++- src/client/mixins/basic-interaction.ts | 12 ++ src/client/mixins/marketo.ts | 83 +++++++++++ .../marketo-associate-web-cookie-to-lead.ts | 93 ++++++++++++ .../marketo-associate-web-cookie-to-lead.ts | 120 +++++++++++++++ 7 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 src/steps/marketo-associate-web-cookie-to-lead.ts create mode 100644 test/steps/marketo-associate-web-cookie-to-lead.ts diff --git a/package-lock.json b/package-lock.json index 6cc5e98..f105d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1466,6 +1466,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.3.0.tgz", "integrity": "sha512-54XaTd2VB7A6iBnXMUG2LnBOI7aRbnrVxC5Tz+rVUwYl9MX/cIJc/Ll32YUoFIE/e9UKWMZoQenQu9dFrQyZCg==" }, + "backoff": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.4.1.tgz", + "integrity": "sha512-gd7froKGnmDsq2IczAXNLMQO6GXuqU6UUGlbo/R6MlaTmqFUozc7Ny3f5vRbcRwAK//lc0/hpaOKO7AP8zAv/Q==", + "requires": { + "precond": "0.2" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1643,6 +1651,14 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "bunyan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.0.1.tgz", + "integrity": "sha512-wNZzFCTLwSJCeMezeqbTuhFDqw5RH43T3HWcCG8HrtNVU1hf+XNM0Wdbg3DRxk/etV4nr/6jDvf4SqU3iRxjdA==", + "requires": { + "mv": "~2" + } + }, "bytebuffer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", @@ -4821,7 +4837,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" }, @@ -4829,8 +4844,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, @@ -5078,6 +5092,41 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, "nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", @@ -5162,6 +5211,12 @@ } } }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "optional": true + }, "needle": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", @@ -5217,6 +5272,36 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" }, + "node-marketo-rest": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/node-marketo-rest/-/node-marketo-rest-0.7.8.tgz", + "integrity": "sha512-r/NfDf55pl9Qa+PwvTQu4/4HRWPgBwSm6qS46521aslUDIvfbXms1qFcbMe5k3QJOb+CsaHoXJPYKrkddwyiow==", + "requires": { + "backoff": "2.4.x", + "bluebird": "2.3.x", + "bunyan": "1.0.x", + "lodash": "4.17.15", + "moment": "2.24.0", + "restler": "3.4.x" + }, + "dependencies": { + "bluebird": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.3.11.tgz", + "integrity": "sha512-dszL4hhVsXR4sgpEfFoMho96Tt1J10i8k3v6X6gj4e8tXYLL1hrNWWUv0HAJcsjpo9Vwsvevj3NJeEIJeZIxgA==" + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + } + } + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -5798,6 +5883,11 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==" + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -6355,6 +6445,43 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restler": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/restler/-/restler-3.4.0.tgz", + "integrity": "sha512-QqRzF7fToAKqd+8Sjqhc47oFuGWN766YcMTKXareLXDB4vVISq0BeHAkdYQpmSz+XUMZWMBxgmQhJztyk03JPA==", + "requires": { + "iconv-lite": "0.2.11", + "qs": "1.2.0", + "xml2js": "0.4.0", + "yaml": "0.2.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw==" + }, + "qs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz", + "integrity": "sha512-XUf0O7rlGjbH+n7uqyT+xn362fmoPe4ehtHL6VK1nbSgQ7CqG0ZZLr1nU2EyXlRq++YphPdQ/5scjIWNMSPnhg==" + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha512-c0YL9VcSfcdH3F1Qij9qpYJFpKFKMXNOkLWFssBL3RuF7ZS8oZhllR2rWlCRjDTJsfq3R6wbSsaRU6o0rkEdNw==" + }, + "xml2js": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.0.tgz", + "integrity": "sha512-Uo9irqHi4waSloqTJ37nHn/XzbYi8JL/TYzMb+pq8xkINNayVhPTGOh8XWhZTP2WD3dX8Ngm5bGc+7YDnIoYIw==", + "requires": { + "sax": "0.5.x", + "xmlbuilder": ">=0.4.2" + } + } + } + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -7748,6 +7875,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, + "yaml": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-0.2.3.tgz", + "integrity": "sha512-LzdhmhritYCRww8GLH95Sk5A2c18ddRQMeooOUnqWkDUnBbmVfqgg2fXH2MxAHYHCVTHDK1EEbmgItQ8kOpM0Q==" + }, "yargs": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", diff --git a/package.json b/package.json index 5982d55..3910056 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "grpc": "^1.24.11", "lighthouse": "^5.6.0", "moment": "^2.29.1", + "node-marketo-rest": "^0.7.8", "psl": "^1.8.0", "puppeteer": "^5.5.0", "puppeteer-cluster": "^0.16.0", diff --git a/src/client/client-wrapper.ts b/src/client/client-wrapper.ts index 171b493..9d50e4f 100644 --- a/src/client/client-wrapper.ts +++ b/src/client/client-wrapper.ts @@ -3,6 +3,7 @@ import { BasicInteractionAware, DomAware, ResponseAware, MarketoAware, GoogleAna import { Field } from '../core/base-step'; import { Page, Request } from 'puppeteer'; import * as Lighthouse from 'lighthouse'; +import * as Marketo from 'node-marketo-rest'; class ClientWrapper { @@ -12,12 +13,30 @@ class ClientWrapper { public lighthouse: any; public idMap: any; public blobContainerClient: any; + public marketoClient: Marketo; + public marketoConnected: boolean = false; + public delayInSeconds: number; - constructor (page: Page, auth: grpc.Metadata, idMap: any, blobContainerClient: any, lighthouse = Lighthouse) { + constructor (page: Page, auth: grpc.Metadata, idMap: any, blobContainerClient: any, lighthouse = Lighthouse, delayInSeconds = 3) { this.client = page; this.idMap = idMap; this.blobContainerClient = blobContainerClient; this.lighthouse = lighthouse; + + // Make a marketo connection if the auth metadata is passed. + if (auth.get('endpoint').length && auth.get('clientId').length && auth.get('clientSecret').length) { + this.marketoClient = new Marketo({ + endpoint: `${auth.get('endpoint')[0]}/rest`, + identity: `${auth.get('endpoint')[0]}/identity`, + clientId: auth.get('clientId')[0], + clientSecret: auth.get('clientSecret')[0], + ...(!!auth.get('partnerId')[0] && { partnerId: auth.get('partnerId')[0] }), + }); + this.marketoConnected = true; + } + + this.delayInSeconds = delayInSeconds; + // Keeps track of the number of inflight requests. @see this.waitForNetworkIdle() this.client['__networkRequestsInflight'] = this.client['__networkRequestsInflight'] || 0; diff --git a/src/client/mixins/basic-interaction.ts b/src/client/mixins/basic-interaction.ts index afee87b..17245a3 100644 --- a/src/client/mixins/basic-interaction.ts +++ b/src/client/mixins/basic-interaction.ts @@ -387,4 +387,16 @@ export class BasicInteractionAware { selector, ); } + + /** + * Returns an array of cookies that match the given cookieName. + * + * @param {String} cookieName - The name of the cookie. + * @returns {Array} - An array of cookies that match the cookieName. + */ + public async getCookie(cookieName: string) { + const cookies = await this.client.cookies(); + const cookieArray = cookies.filter((cookie) => cookie.name === cookieName); + return cookieArray; + } } diff --git a/src/client/mixins/marketo.ts b/src/client/mixins/marketo.ts index f8160fe..76a369c 100644 --- a/src/client/mixins/marketo.ts +++ b/src/client/mixins/marketo.ts @@ -1,2 +1,85 @@ +import * as Marketo from 'node-marketo-rest'; export class MarketoAware { + marketoClient: Marketo; + leadDescription: any; + delayInSeconds: number; + mustHaveFields = [ + 'email', + 'updatedAt', + 'createdAt', + 'lastName', + 'firstName', + 'id', + 'leadPartitionId', + ].filter((f) => !!f); + + public async findLeadByField(field: string, value: string, justInCaseField: string = null, partitionId: number = null) { + this.delayInSeconds > 0 ? await this.delay(this.delayInSeconds) : null; + const fields = await this.describeLeadFields(); + const fieldList: string[] = fields.result.filter((field) => field.rest).map((field: any) => field.rest.name); + let response: any = {}; + + if (fieldList.join(',').length > 7168 && fieldList.length >= 1000) { + // If the length of the get request would be over 7KB, then the request + // would fail. And if the amount of fields is over 1000, it is likely + // not worth it to cache with the if statement below. + // Instead, we will only request the needed fields. + response = await this.marketoClient.lead.find(field, [value], { fields: [justInCaseField, ...this.mustHaveFields, partitionId ? 'leadPartitionId' : null] }); + } else if (fieldList.join(',').length > 7168) { + // If the length of the get request would be over 7KB, then the request + // would fail. Instead, we will split the request every 200 fields, and + // combine the results. + response = await this.marketoRequestHelperFuntion(fieldList, field, value); + } else { + response = await this.marketoClient.lead.find(field, [value], { fields: fieldList }); + } + + // If a partition ID was provided, filter the returned leads accordingly. + if (partitionId && response && response.result && response.result.length) { + response.result = response.result.filter((lead: Record) => { + return lead.leadPartitionId && lead.leadPartitionId === partitionId; + }); + } + + return response; + } + + public async associateLeadById(leadId: string, cookie: string) { + this.delayInSeconds > 0 ? await this.delay(this.delayInSeconds) : null; + return await this.marketoClient.lead.associateLead(leadId, cookie); + } + + private async marketoRequestHelperFuntion(fieldList, field, value) { + const response: any = {}; + let allFields: { [key: string]: string; } = {}; + + for (let i = 0; i < fieldList.length && i <= 800; i += 200) { + const currFields = fieldList.slice(i, i + 200); + const currResponse = await this.marketoClient.lead.find(field, [value], { fields: currFields }); + allFields = { ...allFields, ...currResponse.result[0] }; + if (!i) { + response.requestId = currResponse.requestId; + response.success = currResponse.success; + } + } + response.result = [allFields]; + + return response; + } + + public async describeLeadFields() { + this.delayInSeconds > 0 ? await this.delay(this.delayInSeconds) : null; + // This safely reduces the number of API calls that might have to be made + // in lead field check steps, but is an imcomplete solution. + // @todo Incorporate true caching based on https://github.com/run-crank/cli/pull/40 + if (!this.leadDescription) { + this.leadDescription = await this.marketoClient.lead.describe(); + } + + return this.leadDescription; + } + + public async delay(seconds: number) { + return new Promise((resolve) => { setTimeout(resolve, seconds * 1000); }); + } } diff --git a/src/steps/marketo-associate-web-cookie-to-lead.ts b/src/steps/marketo-associate-web-cookie-to-lead.ts new file mode 100644 index 0000000..7db2313 --- /dev/null +++ b/src/steps/marketo-associate-web-cookie-to-lead.ts @@ -0,0 +1,93 @@ +/*tslint:disable:no-else-after-return*/ + +import { BaseStep, Field, StepInterface, ExpectedRecord } from '../core/base-step'; +import { Step, FieldDefinition, StepDefinition, RecordDefinition, StepRecord } from '../proto/cog_pb'; + +export class MarketoAssociateWebCookieStep extends BaseStep implements StepInterface { + + protected stepName: string = 'Marketo Associate Web Cookie to Lead'; + protected stepExpression: string = 'associate munchkin cookie to marketo lead (?.+\@.+\..+)'; + protected stepType: StepDefinition.Type = StepDefinition.Type.ACTION; + protected expectedFields: Field[] = [ + { + field: 'email', + type: FieldDefinition.Type.STRING, + description: 'Lead\'s email address or id', + }, + { + field: 'partitionId', + type: FieldDefinition.Type.NUMERIC, + optionality: FieldDefinition.Optionality.OPTIONAL, + description: 'ID of partition lead belongs to', + }, + ]; + protected expectedRecords: ExpectedRecord[] = [{ + id: 'cookie', + type: RecordDefinition.Type.KEYVALUE, + fields: [{ + field: 'value', + type: FieldDefinition.Type.STRING, + description: 'Selector of the element', + }, { + field: 'name', + type: FieldDefinition.Type.STRING, + description: 'Selector of the element', + }], + dynamicFields: true, + }]; + + async executeStep(step: Step) { + if (!this.client.marketoConnected) { + return this.error('Marketo connection is not available'); + } + const stepData: any = step.getData().toJavaScript(); + const reference = stepData.email; + const partitionId: number = stepData.partitionId ? parseFloat(stepData.partitionId) : null; + + try { + const emailRegex = /(.+)@(.+){2,}\.(.+){2,}/; + let lookupField = 'id'; + if (emailRegex.test(reference)) { + lookupField = 'email'; + } + + const munchkinCookie: any = await this.client.getCookie('_mkto_trk'); + if (!munchkinCookie || !munchkinCookie.length) { + return this.error('Unable to find _mkto_trk cookie on current webpage'); + } + + const data: any = await this.client.findLeadByField(lookupField, reference, 'id', partitionId); + + if (data.success && data.result && data.result[0] && data.result[0].hasOwnProperty('id')) { + const associate: any = await this.client.associateLeadById(data.result[0].id, munchkinCookie[0].value); + + if (associate.success && associate.requestId) { + const record = this.createRecord(munchkinCookie[0]); + const orderedRecord = this.createOrderedRecord(munchkinCookie[0], stepData['__stepOrder']); + return this.pass('Successfully associated munchkin cookie %s with Marketo lead %s', [munchkinCookie[0].value, reference], [record, orderedRecord]); + } else { + return this.error('Unable to assocated munchkin cookie %s with Marketo lead %s', [munchkinCookie[0].value, reference]); + } + } else { + return this.error("Couldn't find a lead associated with %s%s", [ + reference, + partitionId ? ` in partition ${partitionId}` : '', + ]); + } + } catch (e) { + return this.error('There was an error associating the munchkin cookie with the Marketo lead: %s', [ + e.toString(), + ]); + } + } + + public createRecord(cookieObj): StepRecord { + return this.keyValue('cookie', 'Cookie retrieved', cookieObj); + } + + public createOrderedRecord(cookieObj, stepOrder = 1): StepRecord { + return this.keyValue(`cookie.${stepOrder}`, `Cookie retrieved from Step ${stepOrder}`, cookieObj); + } +} + +export { MarketoAssociateWebCookieStep as Step }; diff --git a/test/steps/marketo-associate-web-cookie-to-lead.ts b/test/steps/marketo-associate-web-cookie-to-lead.ts new file mode 100644 index 0000000..7052f0b --- /dev/null +++ b/test/steps/marketo-associate-web-cookie-to-lead.ts @@ -0,0 +1,120 @@ +import { Struct } from 'google-protobuf/google/protobuf/struct_pb'; +import * as chai from 'chai'; +import { default as sinon } from 'ts-sinon'; +import * as sinonChai from 'sinon-chai'; +import 'mocha'; + +import { Step as ProtoStep, StepDefinition, RunStepResponse } from '../../src/proto/cog_pb'; +import { Step } from '../../src/steps/marketo-associate-web-cookie-to-lead'; + +chai.use(sinonChai); + +describe('MarketoAssociateWebCookieStep', () => { + const expect = chai.expect; + let protoStep: ProtoStep; + let stepUnderTest: Step; + let clientWrapperStub: any; + + beforeEach(() => { + protoStep = new ProtoStep(); + clientWrapperStub = sinon.stub(); + clientWrapperStub.marketoConnected = true; + clientWrapperStub.getCookie = sinon.stub(); + clientWrapperStub.findLeadByField = sinon.stub(); + clientWrapperStub.associateLeadById = sinon.stub(); + stepUnderTest = new Step(clientWrapperStub); + }); + + it('should return expected step metadata', () => { + const stepDef: StepDefinition = stepUnderTest.getDefinition(); + expect(stepDef.getStepId()).to.equal('MarketoAssociateWebCookieStep'); + expect(stepDef.getName()).to.equal('Marketo Associate Web Cookie to Lead'); + expect(stepDef.getExpression()).to.equal('associate munchkin cookie to marketo lead (?.+\@.+\..+)'); + expect(stepDef.getType()).to.equal(StepDefinition.Type.ACTION); + }); + + it('should respond with success if the marketo executes succesfully', async () => { + const expectedEmail: string = 'sampleEmail@email.com'; + protoStep.setData(Struct.fromJavaScript({ + email: expectedEmail, + })); + clientWrapperStub.getCookie.returns(Promise.resolve([{ + name: '_mkto_trk', + value: 'id:123-ABC-789&token:_mch-stackmoxie.com-1234567891011-12345' + }])); + clientWrapperStub.findLeadByField.returns(Promise.resolve({ + success: true, + result: [ + { + id: '12345', + }, + ], + })); + clientWrapperStub.associateLeadById.returns(Promise.resolve({ + success: true, + requestId: '99999', + })); + const response: RunStepResponse = await stepUnderTest.executeStep(protoStep); + expect(response.getOutcome()).to.equal(RunStepResponse.Outcome.PASSED); + }); + + it('should respond with error if Marketo is unable to associate the cookie with the lead', async () => { + const expectedEmail: string = 'sampleEmail@email.com'; + protoStep.setData(Struct.fromJavaScript({ + email: expectedEmail, + })); + clientWrapperStub.getCookie.returns(Promise.resolve([{ + name: '_mkto_trk', + value: 'id:123-ABC-789&token:_mch-stackmoxie.com-1234567891011-12345' + }])); + clientWrapperStub.findLeadByField.returns(Promise.resolve({ + success: true, + result: [ + { + id: '12345', + }, + ], + })); + clientWrapperStub.associateLeadById.returns(Promise.resolve({ + success: false, + })); + const response: RunStepResponse = await stepUnderTest.executeStep(protoStep); + expect(response.getOutcome()).to.equal(RunStepResponse.Outcome.ERROR); + }); + + it('should respond with error if Marketo could not find lead', async () => { + const expectedEmail: string = 'sampleEmail@email.com'; + protoStep.setData(Struct.fromJavaScript({ + email: expectedEmail, + })); + clientWrapperStub.getCookie.returns(Promise.resolve([{ + name: '_mkto_trk', + value: 'id:123-ABC-789&token:_mch-stackmoxie.com-1234567891011-12345' + }])); + clientWrapperStub.findLeadByField.returns(Promise.resolve({ + success: false, + })); + const response: RunStepResponse = await stepUnderTest.executeStep(protoStep); + expect(response.getOutcome()).to.equal(RunStepResponse.Outcome.ERROR); + }); + + it('should respond with error if the marketo throws an error', async () => { + const expectedEmail: string = 'sampleEmail@email.com'; + const expectedCookie: string = 'id:123-ABC-789&token:_mch-stackmoxie.com-1234567891011-12345'; + const expectedMessage: string = 'There was an error associating the munchkin cookie with the Marketo lead: %s'; + const expectedError: string = 'any error'; + protoStep.setData(Struct.fromJavaScript({ + munchkinCookie: expectedCookie, + email: expectedEmail, + })); + clientWrapperStub.getCookie.returns(Promise.resolve([{ + name: '_mkto_trk', + value: 'id:123-ABC-789&token:_mch-stackmoxie.com-1234567891011-12345' + }])); + clientWrapperStub.findLeadByField.throws(expectedError); + const response: RunStepResponse = await stepUnderTest.executeStep(protoStep); + expect(response.getOutcome()).to.equal(RunStepResponse.Outcome.ERROR); + expect(response.getMessageFormat()).to.equal(expectedMessage); + expect(response.getMessageArgsList()[0].getStringValue()).to.equal(expectedError); + }); +});