-
Notifications
You must be signed in to change notification settings - Fork 255
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Handles refresh token retry for multistatus responses
- Loading branch information
1 parent
4888857
commit 2b4f95c
Showing
3 changed files
with
378 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -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 | ||
|
@@ -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: {}, | ||
|
@@ -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, | ||
}, | ||
], | ||
}, | ||
] | ||
`) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.