Skip to content

Commit

Permalink
Responsys: Audiences as PETs mapping (#2398)
Browse files Browse the repository at this point in the history
* Initial implementation.

* Using `testAuthentication` to fetch the auth token.

* General improvements in Responsys Actions.

* - Better types in Responsys;
- Implementing Send Audience as PET action.

* Fixes in Responsys while testing Send Audience as PET Action.

* - Adding `computation_key`, `traits_or_props` to mapping;
- Improving Send Audiences as PET payload to work with the new parameters.

* First implementation of mapping for one audience per PET in Responsys.

* Basic unit tests.

* Adding batch unit tests.

* Code suggestions from PR.

* Code suggestions from PR.

* Reversing suggestion that breaks the PR build.

* Updating testAuthentication unit test for Responsys.

* Reversing `refreshAccessToken` method using `auth` parameters, back to `settings`, in Responsys.
  • Loading branch information
seg-leonelsanches authored Nov 12, 2024
1 parent 5e76015 commit 987ad64
Show file tree
Hide file tree
Showing 12 changed files with 718 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import nock from 'nock'

import { createTestIntegration } from '@segment/actions-core'
import Definition from '../index'
import { Settings } from '../generated-types'
Expand All @@ -7,11 +9,17 @@ const testDestination = createTestIntegration(Definition)
describe('Responsys', () => {
describe('testAuthentication', () => {
it('should validate settings correctly', async () => {
nock('https://instance-api.responsys.ocs.oraclecloud.com').post('/rest/api/v1.3/auth/token').reply(200, {
authToken: 'anything',
issuedAt: 1728492996097,
endPoint: 'https://cj01qwy-api.responsys.ocs.oraclecloud.com'
})

const settings: Settings = {
segmentWriteKey: 'testKey',
username: 'testUser',
userPassword: 'testPassword',
baseUrl: 'https://example.com',
baseUrl: 'https://instance-api.responsys.ocs.oraclecloud.com',
profileListName: 'TESTLIST',
insertOnNoMatch: true,
matchColumnName1: 'EMAIL_ADDRESS',
Expand Down
24 changes: 20 additions & 4 deletions packages/destination-actions/src/destinations/responsys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import sendCustomTraits from './sendCustomTraits'
import sendAudience from './sendAudience'
import upsertListMember from './upsertListMember'

import sendAudienceAsPet from './sendAudienceAsPet'

interface RefreshTokenResponse {
authToken: string
}
Expand Down Expand Up @@ -170,12 +172,25 @@ const destination: DestinationDefinition<Settings> = {
type: 'string'
}
},
testAuthentication: (_, { settings }) => {
if (settings.baseUrl.startsWith('https://'.toLowerCase())) {
return Promise.resolve('Success')
} else {
testAuthentication: async (request, { settings }) => {
if (!settings.baseUrl.startsWith('https://'.toLowerCase())) {
throw new IntegrationError('Responsys endpoint URL must start with https://', 'INVALID_URL', 400)
}

const baseUrl = settings.baseUrl?.replace(/\/$/, '')
const endpoint = `${baseUrl}/rest/api/v1.3/auth/token`

const res = await request<RefreshTokenResponse>(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `user_name=${encodeURIComponent(settings.username)}&password=${encodeURIComponent(
settings.userPassword
)}&auth_type=password`
})

return Promise.resolve(res.data.authToken ? true : false)
},
refreshAccessToken: async (request, { settings }) => {
const baseUrl = settings.baseUrl?.replace(/\/$/, '')
Expand Down Expand Up @@ -203,6 +218,7 @@ const destination: DestinationDefinition<Settings> = {
},
actions: {
sendAudience,
sendAudienceAsPet,
sendCustomTraits,
upsertListMember
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { enable_batching, batch_size } from '../shared_properties'
import { enable_batching, batch_size } from '../shared-properties'
import { sendCustomTraits, getUserDataFieldNames, validateCustomTraits, validateListMemberPayload } from '../utils'
import { Data } from '../types'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'
import { SegmentEvent } from '@segment/actions-core/*'

const testDestination = createTestIntegration(Destination)
const timestamp = new Date('Thu Jun 10 2024 11:08:04 GMT-0700 (Pacific Daylight Time)').toISOString()
const responsysHost = 'https://123456-api.responsys.ocs.oraclecloud.com'
const profileListName = 'TEST_PROFILE_LIST'
const folderName = 'TEST_FOLDER'

describe('Responsys.sendAudienceAsPet', () => {
describe('Successful scenarios', () => {
describe('Single event', () => {
const audienceKey = 'looney_tunes_audience'
const event = createTestEvent({
timestamp,
type: 'identify',
context: {
personas: {
computation_key: audienceKey
}
},
traits: {
email: '[email protected]',
firstName: 'Daffy',
lastName: 'Duck'
},
userId: '12345'
})

const actionPayload = {
event,
mapping: {
folder_name: folderName,
pet_name: {
'@path': '$.context.personas.computation_key'
},
userData: {
EMAIL_ADDRESS_: {
'@path': '$.traits.email'
},
CUSTOMER_ID_: {
'@path': '$.userId'
}
}
},
useDefaultMappings: true,
settings: {
baseUrl: responsysHost,
username: 'abcd',
userPassword: 'abcd',
insertOnNoMatch: false,
matchColumnName1: 'EMAIL_ADDRESS_',
updateOnMatch: 'REPLACE_ALL',
defaultPermissionStatus: 'OPTOUT',
profileListName
}
}

it('sends an event with default mappings + default settings, PET does not exist yet', async () => {
nock(responsysHost).get(`/rest/api/v1.3/lists/${profileListName}/listExtensions`).reply(200, [])

nock(responsysHost)
.post(`/rest/api/v1.3/lists/${profileListName}/listExtensions`)
.reply(200, { results: [{}] })

nock(responsysHost)
.post(`/rest/asyncApi/v1.3/lists/${profileListName}/members`)
.reply(200, { results: [{}] })

nock(responsysHost)
.post(`/rest/api/v1.3/lists/${profileListName}/listExtensions/${audienceKey}/members`)
.reply(200, { results: [{}] })

const responses = await testDestination.testAction('sendAudienceAsPet', actionPayload)

expect(responses.length).toBe(4)
expect(responses[0].status).toBe(200)
expect(responses[1].status).toBe(200)
expect(responses[2].status).toBe(200)
expect(responses[3].status).toBe(200)
})

it('sends an event with default mappings + default settings, PET exists', async () => {
nock(responsysHost)
.get(`/rest/api/v1.3/lists/${profileListName}/listExtensions`)
.reply(200, [
{
profileExtension: { objectName: audienceKey }
}
])

nock(responsysHost)
.post(`/rest/asyncApi/v1.3/lists/${profileListName}/members`)
.reply(200, { results: [{}] })

nock(responsysHost)
.post(`/rest/api/v1.3/lists/${profileListName}/listExtensions/${audienceKey}/members`)
.reply(200, { results: [{}] })

const responses = await testDestination.testAction('sendAudienceAsPet', actionPayload)

expect(responses.length).toBe(3)
expect(responses[0].status).toBe(200)
expect(responses[1].status).toBe(200)
expect(responses[2].status).toBe(200)
})
})

describe('Batch', () => {
const events: SegmentEvent[] = [
{
timestamp,
type: 'identify',
context: {
personas: {
computation_key: 'looney_tunes_audience'
}
},
traits: {
email: '[email protected]'
},
anonymousId: 'abcdef-abcd-1234-1234-1234'
},
{
timestamp,
type: 'identify',
context: {
personas: {
computation_key: 'looney_tunes_audience'
}
},
traits: {
email: '[email protected]'
},
userId: '12345'
},
{
timestamp,
type: 'identify',
context: {
personas: {
computation_key: 'looney_tunes_audience'
}
},
traits: {
riid: '123456'
},
anonymousId: 'abcdef-abcd-2345-1234-3456'
}
]

it('sends events with different match keys', async () => {
const actionPayload = {
events,
mapping: {
folder_name: folderName,
pet_name: {
'@path': '$.context.personas.computation_key'
},
computation_key: {
'@path': '$.context.personas.computation_key'
},
traits_or_props: {
'@path': '$.traits'
},
userData: {
EMAIL_ADDRESS_: {
'@path': '$.traits.email'
},
CUSTOMER_ID_: {
'@path': '$.userId'
},
RIID_: {
'@path': '$.traits.riid'
}
}
},
settings: {
baseUrl: responsysHost,
username: 'abcd',
userPassword: 'abcd',
insertOnNoMatch: false,
matchColumnName1: 'EMAIL_ADDRESS_',
updateOnMatch: 'REPLACE_ALL',
defaultPermissionStatus: 'OPTOUT',
profileListName
}
}

nock(responsysHost).get(`/rest/api/v1.3/lists/${profileListName}/listExtensions`).reply(200, [])

nock(responsysHost)
.post(`/rest/api/v1.3/lists/${profileListName}/listExtensions`)
.reply(200, { results: [{}] })

nock(responsysHost)
.post(`/rest/asyncApi/v1.3/lists/${profileListName}/members`)
.reply(200, { results: [{}] })

nock(responsysHost)
.post(`/rest/api/v1.3/lists/${profileListName}/listExtensions/looney_tunes_audience/members`)
.times(3)
.reply(200, { results: [{}] })

/* for (const event of events) {
;(event.context as any).personas.audience_settings = {
computation_key: 'looney_tunes_audience',
external_audience_id: '12345'
}
} */

const responses = await testDestination.executeBatch('sendAudienceAsPet', actionPayload)

expect(responses.length).toBe(3)
expect(responses[0].status).toBe(200)
expect(responses[1].status).toBe(200)
expect(responses[2].status).toBe(200)
})
})
})
})
Loading

0 comments on commit 987ad64

Please sign in to comment.