Skip to content

Commit

Permalink
Handles refresh token retry for multistatus responses
Browse files Browse the repository at this point in the history
  • Loading branch information
varadarajan-tw committed Jan 10, 2025
1 parent 4888857 commit 2b4f95c
Show file tree
Hide file tree
Showing 3 changed files with 378 additions and 11 deletions.
332 changes: 330 additions & 2 deletions packages/core/src/__tests__/destination-kit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
TransactionContext,
AuthenticationScheme,
RefreshAccessTokenResult,
AudienceDestinationDefinition
AudienceDestinationDefinition,
OAuth2Authentication,
OAuthManagedAuthentication
} from '../destination-kit'
import { JSONObject } from '../json-object'
import { SegmentEvent } from '../segment-event'
Expand Down Expand Up @@ -374,7 +376,7 @@ const multiStatusCompatibleDestination: DestinationDefinition<JSONObject> = {
message: 'success'
}
},
performBatch: (_request, { payload }) => {
performBatch: (_request, { payload, auth }) => {
const response = new MultiStatusResponse()
payload.forEach((event) => {
// Emulate an API error
Expand All @@ -392,6 +394,21 @@ const multiStatusCompatibleDestination: DestinationDefinition<JSONObject> = {
return
}

// Emulate Auth error
if (auth?.accessToken === 'OldToken') {
response.pushErrorResponse({
status: 401,
errortype: ErrorCodes.INVALID_AUTHENTICATION,
errormessage: 'Invalid Auth',
sent: event,
body: {
events_processed: 0,
message: 'Invalid Auth'
}
})
return
}

if (event?.email) {
response.pushSuccessResponse({
body: {},
Expand Down Expand Up @@ -1919,5 +1936,316 @@ describe('destination kit', () => {
]
`)
})
test('should refresh access token and retry events in case multistatus response contains 401 for oauth2 destinations', async () => {
const mockRefreshToken = jest.fn().mockReturnValue({
accessToken: 'new-access-token'
})
const destinationWithOAuth = {
...multiStatusCompatibleDestination,
authentication: {
scheme: 'oauth2',
fields: {},
refreshAccessToken: mockRefreshToken
} as OAuth2Authentication<any>
}
const multiStatusDestination = new Destination(destinationWithOAuth)

const receivedAt = '2024-08-03T17:40:04.055Z'

const events: SegmentEvent[] = [
{
event: 'Add to Cart',
type: 'track',
properties: {
email: '[email protected]'
},
receivedAt
},
{
// Missing required fields
event: 'Add to Cart',
type: 'track',
properties: {},
receivedAt
}
]

const settings = {
apiSecret: 'test_key',
oauth: {
access_token: 'OldToken'
},
subscription: {
subscribe: 'type = "track" and event != "Order Completed"',
partnerAction: 'trackEvent',
mapping: {
name: { '@path': '$.event' },
email: { '@path': '$.properties.email' },
phone: { '@path': '$.properties.phone' }
}
}
}

multiStatusDestination.refreshAccessToken = mockRefreshToken

const response = await multiStatusDestination.onBatch(events, settings, {})
// assert that the refresh token was called once
expect(mockRefreshToken).toHaveBeenCalledTimes(1)

expect(response).toMatchInlineSnapshot(`
Array [
Object {
"multistatus": Array [
Object {
"body": Object {},
"sent": Object {},
"status": 200,
},
Object {
"errormessage": "Email is required",
"errorreporter": "INTEGRATIONS",
"errortype": "PAYLOAD_VALIDATION_FAILED",
"status": 400,
},
],
},
]
`)
})
test('should refresh access token and retry events in case multistatus response contains 401 for oauth-managed destinations', async () => {
const mockRefreshToken = jest.fn().mockReturnValue({
accessToken: 'new-access-token'
})
const destinationWithOAuth = {
...multiStatusCompatibleDestination,
authentication: {
scheme: 'oauth-managed',
fields: {},
refreshAccessToken: mockRefreshToken
} as OAuthManagedAuthentication<any>
}
const multiStatusDestination = new Destination(destinationWithOAuth)

const receivedAt = '2024-08-03T17:40:04.055Z'

const events: SegmentEvent[] = [
{
event: 'Add to Cart',
type: 'track',
properties: {
email: '[email protected]'
},
receivedAt
},
{
// Missing required fields
event: 'Add to Cart',
type: 'track',
properties: {},
receivedAt
}
]

const settings = {
apiSecret: 'test_key',
oauth: {
access_token: 'OldToken'
},
subscription: {
subscribe: 'type = "track" and event != "Order Completed"',
partnerAction: 'trackEvent',
mapping: {
name: { '@path': '$.event' },
email: { '@path': '$.properties.email' },
phone: { '@path': '$.properties.phone' }
}
}
}

multiStatusDestination.refreshAccessToken = mockRefreshToken

const response = await multiStatusDestination.onBatch(events, settings, {})
// assert that the refresh token was called once
expect(mockRefreshToken).toHaveBeenCalledTimes(1)

expect(response).toMatchInlineSnapshot(`
Array [
Object {
"multistatus": Array [
Object {
"body": Object {},
"sent": Object {},
"status": 200,
},
Object {
"errormessage": "Email is required",
"errorreporter": "INTEGRATIONS",
"errortype": "PAYLOAD_VALIDATION_FAILED",
"status": 400,
},
],
},
]
`)
})
test('should not retry events in case multistatus response doesnot contain 401 errors for oauth destinations', async () => {
const mockRefreshToken = jest.fn().mockReturnValue({
accessToken: 'new-access-token'
})
const destinationWithOAuth = {
...multiStatusCompatibleDestination,
authentication: {
scheme: 'oauth-managed',
fields: {},
refreshAccessToken: mockRefreshToken
} as OAuthManagedAuthentication<any>
}
const multiStatusDestination = new Destination(destinationWithOAuth)

const receivedAt = '2024-08-03T17:40:04.055Z'

const events: SegmentEvent[] = [
{
event: 'Add to Cart',
type: 'track',
properties: {
email: '[email protected]'
},
receivedAt
},
{
// Missing required fields
event: 'Add to Cart',
type: 'track',
properties: {},
receivedAt
}
]

const settings = {
apiSecret: 'test_key',
oauth: {},
subscription: {
subscribe: 'type = "track" and event != "Order Completed"',
partnerAction: 'trackEvent',
mapping: {
name: { '@path': '$.event' },
email: { '@path': '$.properties.email' },
phone: { '@path': '$.properties.phone' }
}
}
}

const response = await multiStatusDestination.onBatch(events, settings, {})
expect(mockRefreshToken).not.toHaveBeenCalled()
expect(response).toMatchInlineSnapshot(`
Array [
Object {
"multistatus": Array [
Object {
"body": Object {},
"sent": Object {},
"status": 200,
},
Object {
"errormessage": "Email is required",
"errorreporter": "INTEGRATIONS",
"errortype": "PAYLOAD_VALIDATION_FAILED",
"status": 400,
},
],
},
]
`)
})
test('should not retry events more than max retry attempts for 401 errors', async () => {
const mockRefreshToken = jest.fn().mockReturnValue({
accessToken: 'OldToken'
})
const destinationWithOAuth = {
...multiStatusCompatibleDestination,
authentication: {
scheme: 'oauth-managed',
fields: {},
refreshAccessToken: mockRefreshToken
} as OAuthManagedAuthentication<any>
}
const multiStatusDestination = new Destination(destinationWithOAuth)

const receivedAt = '2024-08-03T17:40:04.055Z'

const events: SegmentEvent[] = [
{
event: 'Add to Cart',
type: 'track',
properties: {
email: '[email protected]'
},
receivedAt
},
{
// Missing required fields
event: 'Add to Cart',
type: 'track',
properties: {},
receivedAt
}
]

const settings = {
apiSecret: 'test_key',
oauth: {
access_token: 'OldToken'
},
subscription: {
subscribe: 'type = "track" and event != "Order Completed"',
partnerAction: 'trackEvent',
mapping: {
name: { '@path': '$.event' },
email: { '@path': '$.properties.email' },
phone: { '@path': '$.properties.phone' }
}
}
}

const response = await multiStatusDestination.onBatch(events, settings, {})
// Default retry attempts is 2
expect(mockRefreshToken).toHaveBeenCalledTimes(2)
expect(response).toMatchInlineSnapshot(`
Array [
Object {
"multistatus": Array [
Object {
"body": Object {
"events_processed": 0,
"message": "Invalid Auth",
},
"errormessage": "Invalid Auth",
"errorreporter": "DESTINATION",
"errortype": "INVALID_AUTHENTICATION",
"sent": Object {
"email": "[email protected]",
"name": "Add to Cart",
},
"status": 401,
},
Object {
"body": Object {
"events_processed": 0,
"message": "Invalid Auth",
},
"errormessage": "Invalid Auth",
"errorreporter": "DESTINATION",
"errortype": "INVALID_AUTHENTICATION",
"sent": Object {
"name": "Add to Cart",
},
"status": 401,
},
],
},
]
`)
})
})
})
Loading

0 comments on commit 2b4f95c

Please sign in to comment.