Skip to content

Commit e6f8590

Browse files
[MS Bing Ads Audiences] Fix multistatus bug (#3295)
* Fix multistatus * Add unit tests to improve coverage
1 parent e0f3f7e commit e6f8590

File tree

5 files changed

+434
-40
lines changed

5 files changed

+434
-40
lines changed

packages/destination-actions/src/destinations/ms-bing-ads-audiences/__tests__/utils.test.ts

Lines changed: 309 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
preparePayload,
55
sendDataToMicrosoftBingAds,
66
handleHttpError,
7-
handleMultistatusResponse
7+
handleMultistatusResponse,
8+
categorizePayloadByAction
89
} from '../utils'
910
import { BASE_URL } from '../constants'
1011
import { MultiStatusResponse, HTTPError, RequestClient, IntegrationError } from '@segment/actions-core'
@@ -47,6 +48,56 @@ describe('preparePayload', () => {
4748
})
4849
})
4950

51+
describe('categorizePayloadByAction', () => {
52+
it('handles duplicate identifiers correctly', () => {
53+
const payload: Payload[] = [
54+
{
55+
audience_id: 'a1',
56+
identifier_type: 'Email',
57+
58+
enable_batching: true,
59+
batch_size: 1000,
60+
traits_or_props: { a1: true },
61+
audience_key: 'a1',
62+
computation_class: 'Default'
63+
},
64+
{
65+
audience_id: 'a1',
66+
identifier_type: 'Email',
67+
email: '[email protected]', // Same email as first payload
68+
enable_batching: true,
69+
batch_size: 1000,
70+
traits_or_props: { a1: true },
71+
audience_key: 'a1',
72+
computation_class: 'Default'
73+
},
74+
{
75+
audience_id: 'a1',
76+
identifier_type: 'Email',
77+
78+
enable_batching: true,
79+
batch_size: 1000,
80+
traits_or_props: {},
81+
audience_key: 'a1',
82+
computation_class: 'Default'
83+
}
84+
]
85+
86+
const addMap: Map<string, number[]> = new Map()
87+
const removeMap: Map<string, number[]> = new Map()
88+
89+
categorizePayloadByAction(payload, addMap, removeMap)
90+
91+
const hashedEmail = hashEmail('[email protected]')
92+
const hashedOtherEmail = hashEmail('[email protected]')
93+
94+
expect(addMap.get(hashedEmail)).toEqual([0, 1])
95+
96+
// Check that the remove map contains the index for the other email
97+
expect(removeMap.get(hashedOtherEmail)).toEqual([2])
98+
})
99+
})
100+
50101
describe('sendDataToMicrosoftBingAds', () => {
51102
it('sends data to Microsoft Bing Ads', async () => {
52103
nock(BASE_URL).post('/CustomerListUserData/Apply', { foo: 'bar' }).reply(200, { ok: true })
@@ -64,7 +115,7 @@ describe('sendDataToMicrosoftBingAds', () => {
64115
describe('handleHttpError', () => {
65116
it('sets error responses for all items', async () => {
66117
const msResponse = createMockMsResponse()
67-
const listItemsMap = new Map<string, number>([['abc', 0]])
118+
const listItemsMap = new Map<string, number[]>([['abc', [0]]])
68119
const payload: Payload[] = [
69120
{
70121
audience_id: 'a1',
@@ -97,10 +148,56 @@ describe('handleHttpError', () => {
97148
})
98149

99150
describe('handleMultistatusResponse', () => {
151+
it('uses default Code (400) when error.Code is missing', () => {
152+
const msResponse = createMockMsResponse()
153+
const items = ['item1']
154+
const listItemsMap = new Map<string, number[]>([['item1', [0]]])
155+
const payload: Payload[] = [
156+
{
157+
audience_id: 'a1',
158+
identifier_type: 'Email',
159+
160+
enable_batching: true,
161+
batch_size: 1000,
162+
traits_or_props: {},
163+
audience_key: 'a1',
164+
computation_class: 'Default'
165+
}
166+
]
167+
168+
const response = {
169+
data: {
170+
PartialErrors: [
171+
{
172+
FieldPath: null,
173+
ErrorCode: 'InvalidInput',
174+
Message: 'The input is invalid',
175+
Code: undefined, // Missing Code
176+
Details: null,
177+
Index: 0,
178+
Type: 'Error',
179+
ForwardCompatibilityMap: null
180+
}
181+
]
182+
}
183+
}
184+
185+
handleMultistatusResponse(msResponse, response as unknown as ModifiedResponse, items, listItemsMap, payload, true)
186+
187+
// Should use default 400 status code
188+
expect(msResponse.setErrorResponseAtIndex).toHaveBeenCalledWith(
189+
0,
190+
expect.objectContaining({
191+
status: 400, // Default value
192+
errormessage: 'InvalidInput: The input is invalid'
193+
})
194+
)
195+
})
196+
100197
it('throws IntegrationError when not in batch mode and there are partial errors', () => {
101198
const msResponse = createMockMsResponse()
102199
const items = ['item1']
103-
const listItemsMap = new Map<string, number>([['item1', 0]])
200+
const listItemsMap = new Map<string, number[]>([['item1', [0]]])
104201
const payload: Payload[] = [
105202
{
106203
audience_id: 'a1',
@@ -146,10 +243,10 @@ describe('handleMultistatusResponse', () => {
146243
it('processes partial errors in batch mode', () => {
147244
const msResponse = createMockMsResponse()
148245
const items = ['item1', 'item2', 'item3']
149-
const listItemsMap = new Map<string, number>([
150-
['item1', 0],
151-
['item2', 1],
152-
['item3', 2]
246+
const listItemsMap = new Map<string, number[]>([
247+
['item1', [0]],
248+
['item2', [1]],
249+
['item3', [2]]
153250
])
154251
const payload: Payload[] = [
155252
{
@@ -230,9 +327,9 @@ describe('handleMultistatusResponse', () => {
230327
it('marks all items as successful when there are no partial errors', () => {
231328
const msResponse = createMockMsResponse()
232329
const items = ['item1', 'item2']
233-
const listItemsMap = new Map<string, number>([
234-
['item1', 0],
235-
['item2', 1]
330+
const listItemsMap = new Map<string, number[]>([
331+
['item1', [0]],
332+
['item2', [1]]
236333
])
237334
const payload: Payload[] = [
238335
{
@@ -279,7 +376,7 @@ describe('handleMultistatusResponse', () => {
279376
it('handles partial errors with invalid indices', () => {
280377
const msResponse = createMockMsResponse()
281378
const items = ['item1']
282-
const listItemsMap = new Map<string, number>([['item1', 0]])
379+
const listItemsMap = new Map<string, number[]>([['item1', [0]]])
283380
const payload: Payload[] = [
284381
{
285382
audience_id: 'a1',
@@ -320,4 +417,205 @@ describe('handleMultistatusResponse', () => {
320417
})
321418
)
322419
})
420+
421+
it('uses default Message ("No error message provided") when Message is missing in partial errors', () => {
422+
const msResponse = createMockMsResponse()
423+
const items = ['item1']
424+
const listItemsMap = new Map<string, number[]>([['item1', [0]]])
425+
const payload: Payload[] = [
426+
{
427+
audience_id: 'a1',
428+
identifier_type: 'Email',
429+
430+
enable_batching: true,
431+
batch_size: 1000,
432+
traits_or_props: {},
433+
audience_key: 'a1',
434+
computation_class: 'Default'
435+
}
436+
]
437+
438+
const response = {
439+
data: {
440+
PartialErrors: [
441+
{
442+
FieldPath: null,
443+
ErrorCode: 'InvalidInput',
444+
// Message is missing
445+
Code: 403,
446+
Details: null,
447+
Index: 0,
448+
Type: 'Error',
449+
ForwardCompatibilityMap: null
450+
}
451+
]
452+
}
453+
}
454+
455+
handleMultistatusResponse(msResponse, response as unknown as ModifiedResponse, items, listItemsMap, payload, true)
456+
457+
// Check that the default Message "No error message provided" is used
458+
expect(msResponse.setErrorResponseAtIndex).toHaveBeenCalledWith(
459+
0,
460+
expect.objectContaining({
461+
status: 403,
462+
errormessage: 'InvalidInput: No error message provided'
463+
})
464+
)
465+
})
466+
467+
it('uses default ErrorCode ("UnknownError") when ErrorCode is missing in partial errors', () => {
468+
const msResponse = createMockMsResponse()
469+
const items = ['item1']
470+
const listItemsMap = new Map<string, number[]>([['item1', [0]]])
471+
const payload: Payload[] = [
472+
{
473+
audience_id: 'a1',
474+
identifier_type: 'Email',
475+
476+
enable_batching: true,
477+
batch_size: 1000,
478+
traits_or_props: {},
479+
audience_key: 'a1',
480+
computation_class: 'Default'
481+
}
482+
]
483+
484+
const response = {
485+
data: {
486+
PartialErrors: [
487+
{
488+
FieldPath: null,
489+
// ErrorCode is missing
490+
Message: 'The input is invalid',
491+
Code: 403,
492+
Details: null,
493+
Index: 0,
494+
Type: 'Error',
495+
ForwardCompatibilityMap: null
496+
}
497+
]
498+
}
499+
}
500+
501+
handleMultistatusResponse(msResponse, response as unknown as ModifiedResponse, items, listItemsMap, payload, true)
502+
503+
// Check that the default ErrorCode "UnknownError" is used
504+
expect(msResponse.setErrorResponseAtIndex).toHaveBeenCalledWith(
505+
0,
506+
expect.objectContaining({
507+
status: 403,
508+
errormessage: 'UnknownError: The input is invalid'
509+
})
510+
)
511+
})
512+
513+
it('uses default status code (400) when Code is missing in partial errors', () => {
514+
const msResponse = createMockMsResponse()
515+
const items = ['item1']
516+
const listItemsMap = new Map<string, number[]>([['item1', [0]]])
517+
const payload: Payload[] = [
518+
{
519+
audience_id: 'a1',
520+
identifier_type: 'Email',
521+
522+
enable_batching: true,
523+
batch_size: 1000,
524+
traits_or_props: {},
525+
audience_key: 'a1',
526+
computation_class: 'Default'
527+
}
528+
]
529+
530+
const response = {
531+
data: {
532+
PartialErrors: [
533+
{
534+
FieldPath: null,
535+
ErrorCode: 'InvalidInput',
536+
Message: 'The input is invalid',
537+
// Code is missing
538+
Details: null,
539+
Index: 0,
540+
Type: 'Error',
541+
ForwardCompatibilityMap: null
542+
}
543+
]
544+
}
545+
}
546+
547+
handleMultistatusResponse(msResponse, response as unknown as ModifiedResponse, items, listItemsMap, payload, true)
548+
549+
// Check that the default status code 400 is used
550+
expect(msResponse.setErrorResponseAtIndex).toHaveBeenCalledWith(
551+
0,
552+
expect.objectContaining({
553+
status: 400, // Default value
554+
errormessage: 'InvalidInput: The input is invalid'
555+
})
556+
)
557+
})
558+
559+
it('handles duplicate identifiers correctly in partial errors', () => {
560+
const msResponse = createMockMsResponse()
561+
const items = ['dupItem']
562+
const listItemsMap = new Map<string, number[]>([['dupItem', [0, 1]]]) // dupItem maps to two payload indices
563+
const payload: Payload[] = [
564+
{
565+
audience_id: 'a1',
566+
identifier_type: 'Email',
567+
568+
enable_batching: true,
569+
batch_size: 1000,
570+
traits_or_props: {},
571+
audience_key: 'a1',
572+
computation_class: 'Default'
573+
},
574+
{
575+
audience_id: 'a1',
576+
identifier_type: 'Email',
577+
578+
enable_batching: true,
579+
batch_size: 1000,
580+
traits_or_props: {},
581+
audience_key: 'a1',
582+
computation_class: 'Default'
583+
}
584+
]
585+
586+
const response = {
587+
data: {
588+
PartialErrors: [
589+
{
590+
FieldPath: null,
591+
ErrorCode: 'InvalidInput',
592+
Message: 'The input is invalid',
593+
Code: 400,
594+
Details: null,
595+
Index: 0,
596+
Type: 'Error',
597+
ForwardCompatibilityMap: null
598+
}
599+
]
600+
}
601+
}
602+
603+
handleMultistatusResponse(msResponse, response as any as ModifiedResponse, items, listItemsMap, payload, true)
604+
605+
// Check that the error item was handled correctly
606+
expect(msResponse.setErrorResponseAtIndex).toHaveBeenCalledWith(
607+
0,
608+
expect.objectContaining({
609+
status: 400,
610+
errormessage: 'InvalidInput: The input is invalid'
611+
})
612+
)
613+
expect(msResponse.setErrorResponseAtIndex).toHaveBeenCalledWith(
614+
1,
615+
expect.objectContaining({
616+
status: 400,
617+
errormessage: 'InvalidInput: The input is invalid'
618+
})
619+
)
620+
})
323621
})

0 commit comments

Comments
 (0)