Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for actions-mailchimp destination: addOrRemoveTags action - all fields 1`] = `
Object {
"tags": Array [
Object {
"name": "f&(J*rH9Ln#F6",
"status": "inactive",
},
Object {
"name": "f&(J*rH9Ln#F6",
"status": "active",
},
Object {
"name": "f&(J*rH9Ln#F6",
"status": "inactive",
},
],
}
`;

exports[`Testing snapshot for actions-mailchimp destination: addOrRemoveTags action - required fields 1`] = `
Object {
"tags": Array [
Object {
"name": "Test Event",
"status": "active",
},
],
}
`;

exports[`Testing snapshot for actions-mailchimp destination: addOrUpdateMember action - all fields 1`] = `
Object {
"email_address": "ek@sopperi.ru",
"language": "A!F#*Q0UC",
"merge_fields": Object {
"testType": "A!F#*Q0UC",
},
"status": "unsubscribed",
"status_if_new": "unsubscribed",
"tags": Array [
"A!F#*Q0UC",
],
"vip": true,
}
`;

exports[`Testing snapshot for actions-mailchimp destination: addOrUpdateMember action - required fields 1`] = `
Object {
"email_address": "ek@sopperi.ru",
"language": "en-US",
"status_if_new": "unsubscribed",
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import nock from 'nock'
import { createTestIntegration } from '@segment/actions-core'
import Destination from '../index'

const testDestination = createTestIntegration(Destination)

describe('actions-mailchimp', () => {
afterEach(() => {
nock.cleanAll()
})

describe('testAuthentication', () => {
it('validates the API key against the resolved datacenter root', async () => {
nock('https://us6.api.mailchimp.com/3.0').get('/').reply(200, {})

const settings = { apiKey: 'test-api-key-us6', audienceId: 'list123' }
await expect(testDestination.testAuthentication(settings)).resolves.not.toThrow()
})

it('honors an explicit datacenter override', async () => {
nock('https://us1.api.mailchimp.com/3.0').get('/').reply(200, {})

const settings = { apiKey: 'test-api-key-us6', dataCenter: 'us1', audienceId: 'list123' }
await expect(testDestination.testAuthentication(settings)).resolves.not.toThrow()
})

it('throws when authentication fails', async () => {
nock('https://us6.api.mailchimp.com/3.0').get('/').reply(401, { title: 'API Key Invalid' })

const settings = { apiKey: 'test-api-key-us6', audienceId: 'list123' }
await expect(testDestination.testAuthentication(settings)).rejects.toThrow()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import { generateTestData } from '../../../lib/test-data'
import destination from '../index'
import nock from 'nock'

const testDestination = createTestIntegration(destination)
const destinationSlug = 'actions-mailchimp'

describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
for (const actionSlug in destination.actions) {
it(`${actionSlug} action - required fields`, async () => {
const seedName = `${destinationSlug}#${actionSlug}`
const action = destination.actions[actionSlug]
const [eventData, settingsData] = generateTestData(seedName, destination, action, true)

nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)

const event = createTestEvent({ properties: eventData })
const responses = await testDestination.testAction(actionSlug, {
event,
mapping: event.properties,
settings: settingsData,
auth: undefined,
useDefaultMappings: true
})

const request = responses[0].request
const rawBody = await request.text()
try {
const json = JSON.parse(rawBody)
expect(json).toMatchSnapshot()
return
} catch (err) {
expect(rawBody).toMatchSnapshot()
}

expect(request.headers).toMatchSnapshot()
})

it(`${actionSlug} action - all fields`, async () => {
const seedName = `${destinationSlug}#${actionSlug}`
const action = destination.actions[actionSlug]
const [eventData, settingsData] = generateTestData(seedName, destination, action, false)

nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)

const event = createTestEvent({ properties: eventData })
const responses = await testDestination.testAction(actionSlug, {
event,
mapping: event.properties,
settings: settingsData,
auth: undefined,
useDefaultMappings: true
})

const request = responses[0].request
const rawBody = await request.text()
try {
const json = JSON.parse(rawBody)
expect(json).toMatchSnapshot()
return
} catch (err) {
expect(rawBody).toMatchSnapshot()
}
})
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'
import { getSubscriberHash } from '../../utils'

const testDestination = createTestIntegration(Destination)

const settings = {
apiKey: 'test-api-key-us6',
audienceId: 'list123'
}

const BASE_URL = 'https://us6.api.mailchimp.com/3.0'

describe('actions-mailchimp addOrRemoveTags', () => {
afterEach(() => {
nock.cleanAll()
})

it('posts active and inactive tags for the member', async () => {
const email = 'jane.doe@example.com'
const hash = getSubscriberHash(email)

nock(BASE_URL)
.post(`/lists/list123/members/${hash}/tags`, {
tags: [
{ name: 'Pro Plan', status: 'active' },
{ name: 'Trial Converted', status: 'active' },
{ name: 'Free Trial', status: 'inactive' }
]
})
.reply(204)

const event = createTestEvent({
type: 'track',
event: 'Trial Converted',
properties: { email }
})

const responses = await testDestination.testAction('addOrRemoveTags', {
event,
settings,
mapping: {
email: { '@path': '$.properties.email' },
tags: [{ name: 'Pro Plan', status: 'active' }],
tags_to_add: { '@path': '$.event' },
tags_to_remove: ['Free Trial']
}
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(204)
})

it('surfaces a clear error when the member does not exist (404)', async () => {
const email = 'ghost@example.com'
const hash = getSubscriberHash(email)

nock(BASE_URL).post(`/lists/list123/members/${hash}/tags`).reply(404, { title: 'Resource Not Found' })

const event = createTestEvent({ type: 'track', event: 'Some Event', properties: { email } })

await expect(
testDestination.testAction('addOrRemoveTags', {
event,
settings,
mapping: {
email: { '@path': '$.properties.email' },
tags_to_add: ['Some Event']
}
})
).rejects.toThrow('Audience member not found')
})
})

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { updateMemberTags } from '../utils'
import { TAG_STATUSES } from '../constants'

const action: ActionDefinition<Settings, Payload> = {
title: 'Add or Remove Member Tags',
description:
'Apply or remove Mailchimp audience tags on an existing member based on behavioral events. The member must already exist in the audience.',
defaultSubscription: 'type = "track"',
fields: {
email: {
label: 'Email Address',
description:
'The email address of the audience member whose tags will be updated. Used to identify the member (hashed for the API endpoint).',
type: 'string',
format: 'email',
required: true,
default: {
'@if': {
exists: { '@path': '$.properties.email' },
then: { '@path': '$.properties.email' },
else: { '@path': '$.traits.email' }
}
}
},
list_id: {
label: 'Audience ID',
description:
'The Mailchimp Audience (List) ID the member belongs to. Defaults to the Audience ID configured in settings.',
type: 'string',
required: false
},
tags: {
label: 'Tags',
description: 'Tags to apply or remove, each with a name and a status of "active" (add) or "inactive" (remove).',
type: 'object',
multiple: true,
required: false,
properties: {
name: {
label: 'Tag Name',
description: 'The name of the tag.',
type: 'string',
required: true
},
status: {
label: 'Status',
description: 'Whether to add ("active") or remove ("inactive") the tag.',
type: 'string',
required: true,
choices: TAG_STATUSES.map((value) => ({ value, label: value }))
}
}
},
tags_to_add: {
label: 'Tags to Add',
description: 'A convenience list of tag names to add (status "active"). Defaults to the event name.',
type: 'string',
multiple: true,
required: false,
default: { '@path': '$.event' }
},
tags_to_remove: {
label: 'Tags to Remove',
description: 'A convenience list of tag names to remove (status "inactive").',
type: 'string',
multiple: true,
required: false
}
},
perform: (request, { settings, payload }) => {
return updateMemberTags(request, settings, payload)
}
}

export default action
Loading
Loading