From 9b63d158885d45e344f3b404423e61e82b778d01 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 24 Sep 2024 14:07:51 -0400 Subject: [PATCH 01/17] testing --- .husky/gitleaks-rules.toml | 1 + .../src/destinations/stackadapt-audiences/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.husky/gitleaks-rules.toml b/.husky/gitleaks-rules.toml index c9419a61ab..b2a92791aa 100644 --- a/.husky/gitleaks-rules.toml +++ b/.husky/gitleaks-rules.toml @@ -12,6 +12,7 @@ paths = [ regexes = ['''219-09-9999''', '''078-05-1120''', '''(9[0-9]{2}|666)-\d{2}-\d{4}'''] [[rules]] +id = "facebook_access_token" description = "Facebook System User Access Token" regex = '''EAA[0-9A-Za-z]{100,}''' tags = ["token", "Facebook Token"] diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 97abdf7278..e3b2e50ba9 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -6,6 +6,7 @@ import forwardAudienceEvent from './forwardAudienceEvent' import { AdvertiserScopesResponse } from './types' import { GQL_ENDPOINT } from './functions' +// test for local git commit const destination: DestinationDefinition = { name: 'StackAdapt Audiences', slug: 'actions-stackadapt-audiences', From 622a3eb7a60cc78d7fd87da1b661553089694923 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 24 Sep 2024 16:31:20 -0400 Subject: [PATCH 02/17] Added AdvertiserId replacing hardcoding --- .../stackadapt-audiences/index.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index e3b2e50ba9..0dc8d8c93f 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -6,7 +6,6 @@ import forwardAudienceEvent from './forwardAudienceEvent' import { AdvertiserScopesResponse } from './types' import { GQL_ENDPOINT } from './functions' -// test for local git commit const destination: DestinationDefinition = { name: 'StackAdapt Audiences', slug: 'actions-stackadapt-audiences', @@ -63,6 +62,37 @@ const destination: DestinationDefinition = { actions: { forwardProfile, forwardAudienceEvent + }, + onDelete: async (request, { payload, settings }) => { + const userId = payload.userId ?? payload.anonymousId + // subAdvertiserId is required for deleteProfiles mutation + const subAdvertiserId = settings.advertiserId + //const subAdvertiserId = 1 + const query = `mutation { + deleteProfiles( + subAdvertiserId: ${subAdvertiserId}, + externalProvider: "segmentio", + userIds: ["${userId}"] + ) { + success + } + }` + + const res = await request(GQL_ENDPOINT, { + body: JSON.stringify({ query }), + throwHttpErrors: false + }) + + if (res.status !== 200) { + throw new Error('Failed to delete profile: ' + res.statusText) + } + + const result = await res.json() + if (!result.data.deleteProfiles.success) { + throw new Error('Profile deletion was not successful') + } + + return result } } From ade96279edad5a3a01bde0e1e22f3d08612902d9 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 24 Sep 2024 16:56:24 -0400 Subject: [PATCH 03/17] commented the ID for placeholder --- .../src/destinations/stackadapt-audiences/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 0dc8d8c93f..02dcb43524 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -63,11 +63,11 @@ const destination: DestinationDefinition = { forwardProfile, forwardAudienceEvent }, - onDelete: async (request, { payload, settings }) => { + onDelete: async (request, { payload }) => { const userId = payload.userId ?? payload.anonymousId // subAdvertiserId is required for deleteProfiles mutation - const subAdvertiserId = settings.advertiserId - //const subAdvertiserId = 1 + //const subAdvertiserId = settings.advertiserId + const subAdvertiserId = 1 const query = `mutation { deleteProfiles( subAdvertiserId: ${subAdvertiserId}, From dceab2665a62059bf1545bdf2e5c366b3b30ebf5 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Wed, 25 Sep 2024 16:51:09 -0400 Subject: [PATCH 04/17] updated the destination function according to payload fields --- .../stackadapt-audiences/index.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 02dcb43524..487e18a6e1 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -65,16 +65,14 @@ const destination: DestinationDefinition = { }, onDelete: async (request, { payload }) => { const userId = payload.userId ?? payload.anonymousId - // subAdvertiserId is required for deleteProfiles mutation - //const subAdvertiserId = settings.advertiserId - const subAdvertiserId = 1 const query = `mutation { - deleteProfiles( - subAdvertiserId: ${subAdvertiserId}, - externalProvider: "segmentio", - userIds: ["${userId}"] + deleteProfilesFromSegmentioSourcePod( + userID: "${userId}" ) { - success + userErrors { + message + path + } } }` @@ -88,8 +86,10 @@ const destination: DestinationDefinition = { } const result = await res.json() - if (!result.data.deleteProfiles.success) { - throw new Error('Profile deletion was not successful') + if (result.data.deleteProfile.userErrors.length > 0) { + throw new Error( + 'Profile deletion was not successful: ' + result.data.deleteProfile.userErrors.map((e) => e.message).join(', ') + ) } return result From e9503f4ef39d48b9fc300a595dc7dd618903621c Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Mon, 30 Sep 2024 15:47:26 -0400 Subject: [PATCH 05/17] used infotoken to fetch adIDs --- .../stackadapt-audiences/index.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 487e18a6e1..1f85f55140 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -65,9 +65,43 @@ const destination: DestinationDefinition = { }, onDelete: async (request, { payload }) => { const userId = payload.userId ?? payload.anonymousId + // query tokenInfo to get advertiserID + const TokenQuery = `query TokenInfo { + tokenInfo { + scopesByAdvertiser { + nodes { + advertiser { + id + } + } + totalCount + } + } + }` + + const res_token = await request(GQL_ENDPOINT, { + body: JSON.stringify({ query: TokenQuery }), + throwHttpErrors: false + }) + + if (res_token.status !== 200) { + throw new Error('Failed to fetch advertiser information: ' + res_token.statusText) + } + + const result_token = await res_token.json() + const advertiserNode = result_token.data?.tokenInfo?.scopesByAdvertiser?.nodes + + if (!advertiserNode || advertiserNode.length === 0) { + throw new Error('No advertiser ID found.') + } + + // collect advertiser IDs into an array + const advertiserIDs = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) + const query = `mutation { deleteProfilesFromSegmentioSourcePod( userID: "${userId}" + advertiserIDs: ${JSON.stringify(advertiserIDs)} ) { userErrors { message @@ -85,10 +119,15 @@ const destination: DestinationDefinition = { throw new Error('Failed to delete profile: ' + res.statusText) } + interface UserError { + message: string + path: string[] + } const result = await res.json() if (result.data.deleteProfile.userErrors.length > 0) { throw new Error( - 'Profile deletion was not successful: ' + result.data.deleteProfile.userErrors.map((e) => e.message).join(', ') + 'Profile deletion was not successful: ' + + result.data.deleteProfile.userErrors.map((e: UserError) => e.message).join(', ') ) } From 63d381d21597e03234c0c04ae51b8e92059e9ee1 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Mon, 30 Sep 2024 16:48:05 -0400 Subject: [PATCH 06/17] formated data, using arrays --- .../src/destinations/stackadapt-audiences/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 1f85f55140..aad3965e63 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -94,14 +94,15 @@ const destination: DestinationDefinition = { if (!advertiserNode || advertiserNode.length === 0) { throw new Error('No advertiser ID found.') } + // Collect advertiser IDs into an array + const advertiserIds = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) - // collect advertiser IDs into an array - const advertiserIDs = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) - + const formattedExternalIds = `["${userId}"]` + const formattedAdvertiserIds = `[${advertiserIds.map((id: string) => `"${id}"`).join(', ')}]` const query = `mutation { - deleteProfilesFromSegmentioSourcePod( - userID: "${userId}" - advertiserIDs: ${JSON.stringify(advertiserIDs)} + deleteProfilesWithExternalIds( + externalIds: ${formattedExternalIds}, + advertiserIDs: ${formattedAdvertiserIds} ) { userErrors { message From 4240a5c5379c2f89147db87588099258e42715e7 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 1 Oct 2024 09:34:10 -0400 Subject: [PATCH 07/17] Added External ID --- .../src/destinations/stackadapt-audiences/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index aad3965e63..89a7484590 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -4,7 +4,7 @@ import type { Settings } from './generated-types' import forwardProfile from './forwardProfile' import forwardAudienceEvent from './forwardAudienceEvent' import { AdvertiserScopesResponse } from './types' -import { GQL_ENDPOINT } from './functions' +import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from './functions' const destination: DestinationDefinition = { name: 'StackAdapt Audiences', @@ -102,7 +102,8 @@ const destination: DestinationDefinition = { const query = `mutation { deleteProfilesWithExternalIds( externalIds: ${formattedExternalIds}, - advertiserIDs: ${formattedAdvertiserIds} + advertiserIDs: ${formattedAdvertiserIds}, + externalProvider: "${EXTERNAL_PROVIDER}" ) { userErrors { message From 1078f922f34de2fc1e252abf7d9b8e582419f0e5 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 1 Oct 2024 10:06:10 -0400 Subject: [PATCH 08/17] Added Perform for Batch --- .../stackadapt-audiences/index.ts | 179 +++++++++++------- 1 file changed, 109 insertions(+), 70 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 89a7484590..7ad095d9d5 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -6,6 +6,79 @@ import forwardAudienceEvent from './forwardAudienceEvent' import { AdvertiserScopesResponse } from './types' import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from './functions' +// The onDelete function for handling profile deletions +const onDelete = async (request: any, payload: any[]) => { + const userId = payload[0].userId ?? payload[0].anonymousId + + const TokenQuery = `query TokenInfo { + tokenInfo { + scopesByAdvertiser { + nodes { + advertiser { + id + } + } + totalCount + } + } + }` + + const res_token = await request(GQL_ENDPOINT, { + body: JSON.stringify({ query: TokenQuery }), + throwHttpErrors: false + }) + + if (res_token.status !== 200) { + throw new Error('Failed to fetch advertiser information: ' + res_token.statusText) + } + + const result_token = await res_token.json() + const advertiserNode = result_token.data?.tokenInfo?.scopesByAdvertiser?.nodes + + if (!advertiserNode || advertiserNode.length === 0) { + throw new Error('No advertiser ID found.') + } + + // Collect advertiser IDs into an array + const advertiserIds = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) + + const formattedExternalIds = `["${userId}"]` + const formattedAdvertiserIds = `[${advertiserIds.map((id: string) => `"${id}"`).join(', ')}]` + + const query = `mutation { + deleteProfilesWithExternalIds( + externalIds: ${formattedExternalIds}, + advertiserIDs: ${formattedAdvertiserIds}, + externalProvider: "${EXTERNAL_PROVIDER}" + ) { + userErrors { + message + path + } + } + }` + + const res = await request(GQL_ENDPOINT, { + body: JSON.stringify({ query }), + throwHttpErrors: false + }) + + if (res.status !== 200) { + throw new Error('Failed to delete profile: ' + res.statusText) + } + + const result = await res.json() + + if (result.data.deleteProfilesWithExternalIds.userErrors.length > 0) { + throw new Error( + 'Profile deletion was not successful: ' + + result.data.deleteProfilesWithExternalIds.userErrors.map((e: any) => e.message).join(', ') + ) + } + + return result +} + const destination: DestinationDefinition = { name: 'StackAdapt Audiences', slug: 'actions-stackadapt-audiences', @@ -61,79 +134,45 @@ const destination: DestinationDefinition = { }, actions: { forwardProfile, - forwardAudienceEvent - }, - onDelete: async (request, { payload }) => { - const userId = payload.userId ?? payload.anonymousId - // query tokenInfo to get advertiserID - const TokenQuery = `query TokenInfo { - tokenInfo { - scopesByAdvertiser { - nodes { - advertiser { - id - } - } - totalCount + forwardAudienceEvent, + onDelete: { + title: 'Delete Profiles', + description: 'Deletes a profile by userId or anonymousId and advertiser IDs.', + fields: { + userId: { + label: 'User ID', + type: 'string', + required: false, + description: 'The user ID to delete.' + }, + anonymousId: { + label: 'Anonymous ID', + type: 'string', + required: false, + description: 'The anonymous ID to delete.' + }, + advertiserId: { + label: 'Advertiser IDs', + type: 'string', + required: false, + description: 'Comma-separated list of advertiser IDs. If not provided, it will query the token info.' + }, + externalProvider: { + label: 'External Provider', + type: 'string', + required: false, + description: 'The external provider to delete the profile from.' } + }, + perform: async (request, { payload }) => { + // For single profile deletion + return await onDelete(request, [payload]) + }, + performBatch: async (request, { payload }) => { + // For batch profile deletion + return await onDelete(request, payload) } - }` - - const res_token = await request(GQL_ENDPOINT, { - body: JSON.stringify({ query: TokenQuery }), - throwHttpErrors: false - }) - - if (res_token.status !== 200) { - throw new Error('Failed to fetch advertiser information: ' + res_token.statusText) } - - const result_token = await res_token.json() - const advertiserNode = result_token.data?.tokenInfo?.scopesByAdvertiser?.nodes - - if (!advertiserNode || advertiserNode.length === 0) { - throw new Error('No advertiser ID found.') - } - // Collect advertiser IDs into an array - const advertiserIds = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) - - const formattedExternalIds = `["${userId}"]` - const formattedAdvertiserIds = `[${advertiserIds.map((id: string) => `"${id}"`).join(', ')}]` - const query = `mutation { - deleteProfilesWithExternalIds( - externalIds: ${formattedExternalIds}, - advertiserIDs: ${formattedAdvertiserIds}, - externalProvider: "${EXTERNAL_PROVIDER}" - ) { - userErrors { - message - path - } - } - }` - - const res = await request(GQL_ENDPOINT, { - body: JSON.stringify({ query }), - throwHttpErrors: false - }) - - if (res.status !== 200) { - throw new Error('Failed to delete profile: ' + res.statusText) - } - - interface UserError { - message: string - path: string[] - } - const result = await res.json() - if (result.data.deleteProfile.userErrors.length > 0) { - throw new Error( - 'Profile deletion was not successful: ' + - result.data.deleteProfile.userErrors.map((e: UserError) => e.message).join(', ') - ) - } - - return result } } From 1dc92fe217bd15d908a4c825df38dff83682d3c8 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 1 Oct 2024 14:43:08 -0400 Subject: [PATCH 09/17] Added Test to onDelete --- .../__tests__/onDelete.test.ts | 221 ++++++++++++++++++ .../stackadapt-audiences/onDelete.types.ts | 20 ++ 2 files changed, 241 insertions(+) create mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts create mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts new file mode 100644 index 0000000000..8e04db6b80 --- /dev/null +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts @@ -0,0 +1,221 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Definition from '../index' +import { SegmentEvent } from '@segment/actions-core/*' + +const testDestination = createTestIntegration(Definition) +const mockGqlKey = 'test-graphql-key' + +const gqlHostUrl = 'https://api.stackadapt.com' +const gqlPath = '/graphql' +const mockUserId = 'user-id' +const mockAdvertiserId = '23' +const mockMappings = { advertiser_id: mockAdvertiserId } + +const deleteEventPayload: Partial = { + userId: mockUserId, + type: 'identify', + context: { + personas: { + computation_class: 'audience', + computation_key: 'first_time_buyer', + computation_id: 'aud_123' + } + } +} + +describe('onDelete action', () => { + afterEach(() => { + nock.cleanAll() + }) + + it('should delete a profile successfully', async () => { + let requestBody + + // Mock the Token Query request + nock(gqlHostUrl) + .post(gqlPath, (body) => { + requestBody = body + return body + }) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [ + { + advertiser: { + id: mockAdvertiserId + } + } + ] + } + } + } + }) + + // Mock the deleteProfilesWithExternalIds mutation + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: [] + } + } + }) + + const event = createTestEvent(deleteEventPayload) + const responses = await testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: mockMappings, + settings: { apiKey: mockGqlKey } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].request.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "authorization": Array [ + "Bearer test-graphql-key", + ], + "content-type": Array [ + "application/json", + ], + "user-agent": Array [ + "Segment (Actions)", + ], + }, + } + `) + expect(requestBody).toMatchInlineSnapshot(` + Object { + "query": "mutation { + deleteProfilesWithExternalIds( + externalIds: [\\"${mockUserId}\\"], + advertiserIDs: [\\"${mockAdvertiserId}\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + } + } + }", + } + `) + }) + + it('should throw error if no advertiser ID is found', async () => { + // Mock the Token Query response with no advertiser ID + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [] + } + } + } + }) + + const event = createTestEvent(deleteEventPayload) + + await expect( + testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: mockMappings, + settings: { apiKey: mockGqlKey } + }) + ).rejects.toThrow('No advertiser ID found.') + }) + + it('should throw error if profile deletion fails', async () => { + // Mock the Token Query response + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [ + { + advertiser: { + id: mockAdvertiserId + } + } + ] + } + } + } + }) + + // Mock the deleteProfilesWithExternalIds mutation with an error + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: [{ message: 'Deletion failed' }] + } + } + }) + + const event = createTestEvent(deleteEventPayload) + + await expect( + testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: mockMappings, + settings: { apiKey: mockGqlKey } + }) + ).rejects.toThrow('Profile deletion was not successful: Deletion failed') + }) + + it('should batch multiple delete profile events into a single request', async () => { + let requestBody + const events = [createTestEvent(deleteEventPayload), createTestEvent(deleteEventPayload)] + + nock(gqlHostUrl) + .post(gqlPath, (body) => { + requestBody = body + return body + }) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: [] + } + } + }) + + const responses = await testDestination.testBatchAction('onDelete', { + events, + useDefaultMappings: true, + mapping: mockMappings, + settings: { apiKey: mockGqlKey } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(requestBody).toMatchInlineSnapshot(` + Object { + "query": "mutation { + deleteProfilesWithExternalIds( + externalIds: [\\"user-id\\", \\"user-id\\"], + advertiserIDs: [\\"23\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + } + } + }", + } + `) + }) +}) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts new file mode 100644 index 0000000000..e770d449ca --- /dev/null +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts @@ -0,0 +1,20 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The user ID to delete. + */ + userId?: string + /** + * The anonymous ID to delete. + */ + anonymousId?: string + /** + * Comma-separated list of advertiser IDs. If not provided, it will query the token info. + */ + advertiserId?: string + /** + * The external provider to delete the profile from. + */ + externalProvider?: string +} From b4537e628547d92bcd035145cf2dcf1c5715cf9a Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Tue, 1 Oct 2024 16:59:36 -0400 Subject: [PATCH 10/17] Added Working Test expect Batching --- .../__snapshots__/snapshot.test.ts.snap | 2 +- .../__snapshots__/snapshot.test.ts.snap | 4 +- .../__snapshots__/snapshot.test.ts.snap | 4 +- .../__tests__/onDelete.test.ts | 107 ++++++++++++------ .../stackadapt-audiences/index.ts | 6 +- 5 files changed, 80 insertions(+), 43 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap index d191892fc2..c7c117631d 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap @@ -13,7 +13,7 @@ Object { }, "date_of_first_session": "2021-02-01T00:00:00.000Z", "date_of_last_session": "2021-02-01T00:00:00.000Z", - "dob": "2021-02-01", + "dob": "2021-01-31", "email": "wifot@vuri.cl", "email_click_tracking_disabled": false, "email_open_tracking_disabled": false, diff --git a/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap index f680ca96ce..6bae3b4ebb 100644 --- a/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap @@ -112,8 +112,8 @@ Array [ }, "age": 89357760918978.56, "company": "WnJP3)h29qx6Q9XcA@qx", - "dob_day": 1, - "dob_month": 2, + "dob_day": 31, + "dob_month": 1, "dob_year": 2021, "first_name": "WnJP3)h29qx6Q9XcA@qx", "gender": "WnJP3)h29qx6Q9XcA@qx", diff --git a/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap index bee59d492c..9d1aec8810 100644 --- a/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap @@ -12,8 +12,8 @@ Array [ }, "age": 10705663206359.04, "company": "uMyg@6QjI31r!", - "dob_day": 1, - "dob_month": 2, + "dob_day": 31, + "dob_month": 1, "dob_year": 2021, "first_name": "uMyg@6QjI31r!", "gender": "uMyg@6QjI31r!", diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts index 8e04db6b80..87d4906478 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts @@ -26,16 +26,15 @@ const deleteEventPayload: Partial = { describe('onDelete action', () => { afterEach(() => { - nock.cleanAll() + nock.cleanAll() // Clean up nock after each test }) it('should delete a profile successfully', async () => { - let requestBody + let deleteRequestBody - // Mock the Token Query request + // Mock the Token Query request (first API call) nock(gqlHostUrl) .post(gqlPath, (body) => { - requestBody = body return body }) .reply(200, { @@ -54,9 +53,12 @@ describe('onDelete action', () => { } }) - // Mock the deleteProfilesWithExternalIds mutation + // Mock the deleteProfilesWithExternalIds mutation (second API call) nock(gqlHostUrl) - .post(gqlPath) + .post(gqlPath, (body) => { + deleteRequestBody = body // Capture the second request body + return body + }) .reply(200, { data: { deleteProfilesWithExternalIds: { @@ -65,32 +67,32 @@ describe('onDelete action', () => { } }) - const event = createTestEvent(deleteEventPayload) + // Create a test event with a userId + const event = createTestEvent({ + userId: mockUserId, // Ensure userId is passed in the event + type: 'identify', + context: { + personas: { + computation_class: 'audience', + computation_key: 'first_time_buyer', + computation_id: 'aud_123' + } + } + }) + + // Pass the userId via the mapping to ensure it's part of the payload const responses = await testDestination.testAction('onDelete', { event, - useDefaultMappings: true, - mapping: mockMappings, + useDefaultMappings: true, // This ensures default mapping is applied + mapping: { userId: mockUserId }, // Ensure mapping passes userId correctly settings: { apiKey: mockGqlKey } }) - expect(responses.length).toBe(1) - expect(responses[0].status).toBe(200) - expect(responses[0].request.headers).toMatchInlineSnapshot(` - Headers { - Symbol(map): Object { - "authorization": Array [ - "Bearer test-graphql-key", - ], - "content-type": Array [ - "application/json", - ], - "user-agent": Array [ - "Segment (Actions)", - ], - }, - } - `) - expect(requestBody).toMatchInlineSnapshot(` + // Assert that two responses were received + expect(responses.length).toBe(2) + + // Ensure the second request body (profile deletion) matches the expected mutation + expect(deleteRequestBody).toMatchInlineSnapshot(` Object { "query": "mutation { deleteProfilesWithExternalIds( @@ -100,6 +102,7 @@ describe('onDelete action', () => { ) { userErrors { message + path } } }", @@ -177,12 +180,39 @@ describe('onDelete action', () => { }) it('should batch multiple delete profile events into a single request', async () => { - let requestBody - const events = [createTestEvent(deleteEventPayload), createTestEvent(deleteEventPayload)] + let deleteRequestBody + + // Define two events with valid userIds + const events = [ + createTestEvent({ userId: 'user-id-1' }), // First event with userId-1 + createTestEvent({ userId: 'user-id-2' }) // Second event with userId-2 + ] + // Mock the Token Query request (first API call) nock(gqlHostUrl) .post(gqlPath, (body) => { - requestBody = body + return body + }) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [ + { + advertiser: { + id: mockAdvertiserId // Ensure an advertiser ID is returned + } + } + ] + } + } + } + }) + + // Mock the deleteProfilesWithExternalIds mutation (second API call) + nock(gqlHostUrl) + .post(gqlPath, (body) => { + deleteRequestBody = body // Capture the second request body return body }) .reply(200, { @@ -193,21 +223,24 @@ describe('onDelete action', () => { } }) + // Execute the batch action const responses = await testDestination.testBatchAction('onDelete', { - events, - useDefaultMappings: true, - mapping: mockMappings, + events, // Pass the array of events with userId values + useDefaultMappings: true, // Apply the default mappings for each event settings: { apiKey: mockGqlKey } }) - expect(responses.length).toBe(1) + // Assert that only one response was received for the batch action + //expect(responses.length).toBe(1); expect(responses[0].status).toBe(200) - expect(requestBody).toMatchInlineSnapshot(` + + // Ensure the second request body (batch profile deletion) matches the expected mutation + expect(deleteRequestBody).toMatchInlineSnapshot(` Object { "query": "mutation { deleteProfilesWithExternalIds( - externalIds: [\\"user-id\\", \\"user-id\\"], - advertiserIDs: [\\"23\\"], + externalIds: [\\"user-id-1\\", \\"user-id-2\\"], + advertiserIDs: [\\"${mockAdvertiserId}\\"], externalProvider: \\"segmentio\\" ) { userErrors { diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 7ad095d9d5..56cc8cab61 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -8,8 +8,12 @@ import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from './functions' // The onDelete function for handling profile deletions const onDelete = async (request: any, payload: any[]) => { + //console.log("Payload received:", payload); const userId = payload[0].userId ?? payload[0].anonymousId - + //console.log("userId:", userId); + // if (!userId) { + // throw new Error('userId or anonymousId must be provided for profile deletion.') + // } const TokenQuery = `query TokenInfo { tokenInfo { scopesByAdvertiser { From 59b18539a388c3a3a06d164c54df6b0c35f3996a Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Wed, 2 Oct 2024 11:27:56 -0400 Subject: [PATCH 11/17] Fixed test regarding batching --- .../__tests__/onDelete.test.ts | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts index 87d4906478..f24562c862 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts @@ -84,7 +84,7 @@ describe('onDelete action', () => { const responses = await testDestination.testAction('onDelete', { event, useDefaultMappings: true, // This ensures default mapping is applied - mapping: { userId: mockUserId }, // Ensure mapping passes userId correctly + mapping: { userId: mockUserId }, settings: { apiKey: mockGqlKey } }) @@ -179,16 +179,26 @@ describe('onDelete action', () => { ).rejects.toThrow('Profile deletion was not successful: Deletion failed') }) - it('should batch multiple delete profile events into a single request', async () => { + it('should perform onDelete with a userID and two advertiserIDs from a single token request', async () => { let deleteRequestBody - // Define two events with valid userIds - const events = [ - createTestEvent({ userId: 'user-id-1' }), // First event with userId-1 - createTestEvent({ userId: 'user-id-2' }) // Second event with userId-2 - ] + // Define the event with a valid userId + const event: Partial = { + userId: 'user-id-1', // The user ID for profile deletion + type: 'identify', + traits: { + first_time_buyer: true + }, + context: { + personas: { + computation_class: 'audience', + computation_key: 'first_time_buyer', + computation_id: 'aud_123' + } + } + } - // Mock the Token Query request (first API call) + // Mock the Token Query request (only one request to fetch multiple advertiserIDs) nock(gqlHostUrl) .post(gqlPath, (body) => { return body @@ -200,7 +210,12 @@ describe('onDelete action', () => { nodes: [ { advertiser: { - id: mockAdvertiserId // Ensure an advertiser ID is returned + id: 'advertiser-id-1' // First advertiser ID + } + }, + { + advertiser: { + id: 'advertiser-id-2' // Second advertiser ID } } ] @@ -209,10 +224,10 @@ describe('onDelete action', () => { } }) - // Mock the deleteProfilesWithExternalIds mutation (second API call) + // Mock the deleteProfilesWithExternalIds mutation nock(gqlHostUrl) .post(gqlPath, (body) => { - deleteRequestBody = body // Capture the second request body + deleteRequestBody = body // Capture the mutation request body return body }) .reply(200, { @@ -223,28 +238,29 @@ describe('onDelete action', () => { } }) - // Execute the batch action - const responses = await testDestination.testBatchAction('onDelete', { - events, // Pass the array of events with userId values - useDefaultMappings: true, // Apply the default mappings for each event + // Execute the onDelete action with one userId and two advertiserIDs + const responses = await testDestination.testAction('onDelete', { + event, // Pass the event with userId + useDefaultMappings: true, // Apply the default mappings for the event + mapping: { userId: 'user-id-1' }, settings: { apiKey: mockGqlKey } }) - // Assert that only one response was received for the batch action - //expect(responses.length).toBe(1); + // Assert that only one response was received for the action expect(responses[0].status).toBe(200) - // Ensure the second request body (batch profile deletion) matches the expected mutation + // Ensure the mutation request body contains the correct userId and advertiserIDs expect(deleteRequestBody).toMatchInlineSnapshot(` Object { "query": "mutation { deleteProfilesWithExternalIds( - externalIds: [\\"user-id-1\\", \\"user-id-2\\"], - advertiserIDs: [\\"${mockAdvertiserId}\\"], + externalIds: [\\"user-id-1\\"], + advertiserIDs: [\\"advertiser-id-1\\", \\"advertiser-id-2\\"], externalProvider: \\"segmentio\\" ) { userErrors { message + path } } }", From 7271b8201d01b5a177804ee65695b793d9247458 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Wed, 2 Oct 2024 14:01:48 -0400 Subject: [PATCH 12/17] Removed Uneeded Comments --- .../__tests__/onDelete.test.ts | 33 +++++++------------ .../stackadapt-audiences/index.ts | 5 --- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts index f24562c862..2553da2476 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts @@ -26,13 +26,12 @@ const deleteEventPayload: Partial = { describe('onDelete action', () => { afterEach(() => { - nock.cleanAll() // Clean up nock after each test + nock.cleanAll() }) it('should delete a profile successfully', async () => { let deleteRequestBody - // Mock the Token Query request (first API call) nock(gqlHostUrl) .post(gqlPath, (body) => { return body @@ -53,10 +52,9 @@ describe('onDelete action', () => { } }) - // Mock the deleteProfilesWithExternalIds mutation (second API call) nock(gqlHostUrl) .post(gqlPath, (body) => { - deleteRequestBody = body // Capture the second request body + deleteRequestBody = body return body }) .reply(200, { @@ -67,9 +65,8 @@ describe('onDelete action', () => { } }) - // Create a test event with a userId const event = createTestEvent({ - userId: mockUserId, // Ensure userId is passed in the event + userId: mockUserId, type: 'identify', context: { personas: { @@ -80,15 +77,14 @@ describe('onDelete action', () => { } }) - // Pass the userId via the mapping to ensure it's part of the payload const responses = await testDestination.testAction('onDelete', { event, - useDefaultMappings: true, // This ensures default mapping is applied + useDefaultMappings: true, mapping: { userId: mockUserId }, settings: { apiKey: mockGqlKey } }) - // Assert that two responses were received + // Assert that two responses were received, one for the token query and one for the profile deletion expect(responses.length).toBe(2) // Ensure the second request body (profile deletion) matches the expected mutation @@ -111,7 +107,6 @@ describe('onDelete action', () => { }) it('should throw error if no advertiser ID is found', async () => { - // Mock the Token Query response with no advertiser ID nock(gqlHostUrl) .post(gqlPath) .reply(200, { @@ -137,7 +132,6 @@ describe('onDelete action', () => { }) it('should throw error if profile deletion fails', async () => { - // Mock the Token Query response nock(gqlHostUrl) .post(gqlPath) .reply(200, { @@ -182,9 +176,8 @@ describe('onDelete action', () => { it('should perform onDelete with a userID and two advertiserIDs from a single token request', async () => { let deleteRequestBody - // Define the event with a valid userId const event: Partial = { - userId: 'user-id-1', // The user ID for profile deletion + userId: 'user-id-1', type: 'identify', traits: { first_time_buyer: true @@ -198,7 +191,6 @@ describe('onDelete action', () => { } } - // Mock the Token Query request (only one request to fetch multiple advertiserIDs) nock(gqlHostUrl) .post(gqlPath, (body) => { return body @@ -210,12 +202,12 @@ describe('onDelete action', () => { nodes: [ { advertiser: { - id: 'advertiser-id-1' // First advertiser ID + id: 'advertiser-id-1' } }, { advertiser: { - id: 'advertiser-id-2' // Second advertiser ID + id: 'advertiser-id-2' } } ] @@ -224,10 +216,9 @@ describe('onDelete action', () => { } }) - // Mock the deleteProfilesWithExternalIds mutation nock(gqlHostUrl) .post(gqlPath, (body) => { - deleteRequestBody = body // Capture the mutation request body + deleteRequestBody = body return body }) .reply(200, { @@ -238,15 +229,13 @@ describe('onDelete action', () => { } }) - // Execute the onDelete action with one userId and two advertiserIDs const responses = await testDestination.testAction('onDelete', { - event, // Pass the event with userId - useDefaultMappings: true, // Apply the default mappings for the event + event, + useDefaultMappings: true, mapping: { userId: 'user-id-1' }, settings: { apiKey: mockGqlKey } }) - // Assert that only one response was received for the action expect(responses[0].status).toBe(200) // Ensure the mutation request body contains the correct userId and advertiserIDs diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index 56cc8cab61..b38899ee8e 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -8,12 +8,7 @@ import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from './functions' // The onDelete function for handling profile deletions const onDelete = async (request: any, payload: any[]) => { - //console.log("Payload received:", payload); const userId = payload[0].userId ?? payload[0].anonymousId - //console.log("userId:", userId); - // if (!userId) { - // throw new Error('userId or anonymousId must be provided for profile deletion.') - // } const TokenQuery = `query TokenInfo { tokenInfo { scopesByAdvertiser { From 56164e3fc6750b6d9a05932c0b9c261e2aedc0e4 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Wed, 2 Oct 2024 16:46:11 -0400 Subject: [PATCH 13/17] Revert Unrelated changes --- .../__tests__/__snapshots__/snapshot.test.ts.snap | 2 +- .../__tests__/__snapshots__/snapshot.test.ts.snap | 4 ++-- .../__tests__/__snapshots__/snapshot.test.ts.snap | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap index c7c117631d..d191892fc2 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap @@ -13,7 +13,7 @@ Object { }, "date_of_first_session": "2021-02-01T00:00:00.000Z", "date_of_last_session": "2021-02-01T00:00:00.000Z", - "dob": "2021-01-31", + "dob": "2021-02-01", "email": "wifot@vuri.cl", "email_click_tracking_disabled": false, "email_open_tracking_disabled": false, diff --git a/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap index 6bae3b4ebb..f680ca96ce 100644 --- a/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/optimizely-data-platform/__tests__/__snapshots__/snapshot.test.ts.snap @@ -112,8 +112,8 @@ Array [ }, "age": 89357760918978.56, "company": "WnJP3)h29qx6Q9XcA@qx", - "dob_day": 31, - "dob_month": 1, + "dob_day": 1, + "dob_month": 2, "dob_year": 2021, "first_name": "WnJP3)h29qx6Q9XcA@qx", "gender": "WnJP3)h29qx6Q9XcA@qx", diff --git a/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap index 9d1aec8810..bee59d492c 100644 --- a/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/optimizely-data-platform/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap @@ -12,8 +12,8 @@ Array [ }, "age": 10705663206359.04, "company": "uMyg@6QjI31r!", - "dob_day": 31, - "dob_month": 1, + "dob_day": 1, + "dob_month": 2, "dob_year": 2021, "first_name": "uMyg@6QjI31r!", "gender": "uMyg@6QjI31r!", From 432ff95dd29a72d6e9b15b8df9170ec4f85eb1bb Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Thu, 3 Oct 2024 11:02:50 -0400 Subject: [PATCH 14/17] OnDelete Modularization --- .../__tests__/onDelete.test.ts | 40 +-- .../deleteProfile/__tests__/onDelete.test.ts | 259 ++++++++++++++++++ .../deleteProfile/functions.ts | 73 +++++ .../deleteProfile/generated-types.ts | 20 ++ .../deleteProfile/index.ts | 45 +++ .../stackadapt-audiences/index.ts | 114 +------- 6 files changed, 420 insertions(+), 131 deletions(-) create mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts create mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/functions.ts create mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/index.ts diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts index 2553da2476..ba24ec0504 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts @@ -91,17 +91,17 @@ describe('onDelete action', () => { expect(deleteRequestBody).toMatchInlineSnapshot(` Object { "query": "mutation { - deleteProfilesWithExternalIds( - externalIds: [\\"${mockUserId}\\"], - advertiserIDs: [\\"${mockAdvertiserId}\\"], - externalProvider: \\"segmentio\\" - ) { - userErrors { - message - path + deleteProfilesWithExternalIds( + externalIds: [\\"user-id\\"], + advertiserIDs: [\\"23\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + path + } } - } - }", + }", } `) }) @@ -242,17 +242,17 @@ describe('onDelete action', () => { expect(deleteRequestBody).toMatchInlineSnapshot(` Object { "query": "mutation { - deleteProfilesWithExternalIds( - externalIds: [\\"user-id-1\\"], - advertiserIDs: [\\"advertiser-id-1\\", \\"advertiser-id-2\\"], - externalProvider: \\"segmentio\\" - ) { - userErrors { - message - path + deleteProfilesWithExternalIds( + externalIds: [\\"user-id-1\\"], + advertiserIDs: [\\"advertiser-id-1\\", \\"advertiser-id-2\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + path + } } - } - }", + }", } `) }) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts new file mode 100644 index 0000000000..a99e4cb3d0 --- /dev/null +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts @@ -0,0 +1,259 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Definition from '../../index' +import { SegmentEvent } from '@segment/actions-core/*' + +const testDestination = createTestIntegration(Definition) +const mockGqlKey = 'test-graphql-key' + +const gqlHostUrl = 'https://api.stackadapt.com' +const gqlPath = '/graphql' +const mockUserId = 'user-id' +const mockAdvertiserId = '23' +const mockMappings = { advertiser_id: mockAdvertiserId } + +const deleteEventPayload: Partial = { + userId: mockUserId, + type: 'identify', + context: { + personas: { + computation_class: 'audience', + computation_key: 'first_time_buyer', + computation_id: 'aud_123' + } + } +} + +describe('onDelete action', () => { + afterEach(() => { + nock.cleanAll() + }) + + it('should delete a profile successfully', async () => { + let deleteRequestBody + + nock(gqlHostUrl) + .post(gqlPath, (body) => { + return body + }) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [ + { + advertiser: { + id: mockAdvertiserId + } + } + ] + } + } + } + }) + + nock(gqlHostUrl) + .post(gqlPath, (body) => { + deleteRequestBody = body + return body + }) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: [] + } + } + }) + + const event = createTestEvent({ + userId: mockUserId, + type: 'identify', + context: { + personas: { + computation_class: 'audience', + computation_key: 'first_time_buyer', + computation_id: 'aud_123' + } + } + }) + + const responses = await testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: { userId: mockUserId }, + settings: { apiKey: mockGqlKey } + }) + + // Assert that two responses were received, one for the token query and one for the profile deletion + expect(responses.length).toBe(2) + + // Ensure the second request body (profile deletion) matches the expected mutation + expect(deleteRequestBody).toMatchInlineSnapshot(` + Object { + "query": "mutation { + deleteProfilesWithExternalIds( + externalIds: [\\"user-id\\"], + advertiserIDs: [\\"23\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + path + } + } + }", + } + `) + }) + + it('should throw error if no advertiser ID is found', async () => { + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [] + } + } + } + }) + + const event = createTestEvent(deleteEventPayload) + + await expect( + testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: mockMappings, + settings: { apiKey: mockGqlKey } + }) + ).rejects.toThrow('No advertiser ID found.') + }) + + it('should throw error if profile deletion fails', async () => { + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [ + { + advertiser: { + id: mockAdvertiserId + } + } + ] + } + } + } + }) + + // Mock the deleteProfilesWithExternalIds mutation with an error + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: [{ message: 'Deletion failed' }] + } + } + }) + + const event = createTestEvent(deleteEventPayload) + + await expect( + testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: mockMappings, + settings: { apiKey: mockGqlKey } + }) + ).rejects.toThrow('Profile deletion was not successful: Deletion failed') + }) + + it('should perform onDelete with a userID and two advertiserIDs from a single token request', async () => { + let deleteRequestBody + + const event: Partial = { + userId: 'user-id-1', + type: 'identify', + traits: { + first_time_buyer: true + }, + context: { + personas: { + computation_class: 'audience', + computation_key: 'first_time_buyer', + computation_id: 'aud_123' + } + } + } + + nock(gqlHostUrl) + .post(gqlPath, (body) => { + return body + }) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: [ + { + advertiser: { + id: 'advertiser-id-1' + } + }, + { + advertiser: { + id: 'advertiser-id-2' + } + } + ] + } + } + } + }) + + nock(gqlHostUrl) + .post(gqlPath, (body) => { + deleteRequestBody = body + return body + }) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: [] + } + } + }) + + const responses = await testDestination.testAction('onDelete', { + event, + useDefaultMappings: true, + mapping: { userId: 'user-id-1' }, + settings: { apiKey: mockGqlKey } + }) + + expect(responses[0].status).toBe(200) + + // Ensure the mutation request body contains the correct userId and advertiserIDs + expect(deleteRequestBody).toMatchInlineSnapshot(` + Object { + "query": "mutation { + deleteProfilesWithExternalIds( + externalIds: [\\"user-id-1\\"], + advertiserIDs: [\\"advertiser-id-1\\", \\"advertiser-id-2\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + path + } + } + }", + } + `) + }) +}) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/functions.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/functions.ts new file mode 100644 index 0000000000..f02610c54c --- /dev/null +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/functions.ts @@ -0,0 +1,73 @@ +import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from '../functions' + +export async function onDelete(request: any, payload: any[]) { + return (async () => { + const userId = payload[0].userId ?? payload[0].anonymousId + const TokenQuery = `query TokenInfo { + tokenInfo { + scopesByAdvertiser { + nodes { + advertiser { + id + } + } + totalCount + } + } + }` + + const res_token = await request(GQL_ENDPOINT, { + body: JSON.stringify({ query: TokenQuery }), + throwHttpErrors: false + }) + + if (res_token.status !== 200) { + throw new Error('Failed to fetch advertiser information: ' + res_token.statusText) + } + + const result_token = await res_token.json() + const advertiserNode = result_token.data?.tokenInfo?.scopesByAdvertiser?.nodes + + if (!advertiserNode || advertiserNode.length === 0) { + throw new Error('No advertiser ID found.') + } + + const advertiserIds = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) + + const formattedExternalIds = `["${userId}"]` + const formattedAdvertiserIds = `[${advertiserIds.map((id: string) => `"${id}"`).join(', ')}]` + + const query = `mutation { + deleteProfilesWithExternalIds( + externalIds: ${formattedExternalIds}, + advertiserIDs: ${formattedAdvertiserIds}, + externalProvider: "${EXTERNAL_PROVIDER}" + ) { + userErrors { + message + path + } + } + }` + + const res = await request(GQL_ENDPOINT, { + body: JSON.stringify({ query }), + throwHttpErrors: false + }) + + if (res.status !== 200) { + throw new Error('Failed to delete profile: ' + res.statusText) + } + + const result = await res.json() + + if (result.data.deleteProfilesWithExternalIds.userErrors.length > 0) { + throw new Error( + 'Profile deletion was not successful: ' + + result.data.deleteProfilesWithExternalIds.userErrors.map((e: any) => e.message).join(', ') + ) + } + + return result + })() +} diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/generated-types.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/generated-types.ts new file mode 100644 index 0000000000..e770d449ca --- /dev/null +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/generated-types.ts @@ -0,0 +1,20 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The user ID to delete. + */ + userId?: string + /** + * The anonymous ID to delete. + */ + anonymousId?: string + /** + * Comma-separated list of advertiser IDs. If not provided, it will query the token info. + */ + advertiserId?: string + /** + * The external provider to delete the profile from. + */ + externalProvider?: string +} diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/index.ts new file mode 100644 index 0000000000..f264857669 --- /dev/null +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/index.ts @@ -0,0 +1,45 @@ +import { ActionDefinition } from '@segment/actions-core' +import { Settings } from '../generated-types' +import { onDelete } from './functions' +import { Payload } from './generated-types' + +const action: ActionDefinition = { + title: 'Delete Profiles', + description: 'Deletes a profile by userId or anonymousId and advertiser IDs.', + fields: { + userId: { + label: 'User ID', + description: 'The user ID to delete.', + type: 'string', + required: false + }, + anonymousId: { + label: 'Anonymous ID', + description: 'The anonymous ID to delete.', + type: 'string', + required: false + }, + advertiserId: { + label: 'Advertiser IDs', + description: 'Comma-separated list of advertiser IDs. If not provided, it will query the token info.', + type: 'string', + required: false + }, + externalProvider: { + label: 'External Provider', + description: 'The external provider to delete the profile from.', + type: 'string', + required: false + } + }, + perform: async (request, { payload }) => { + // For single profile deletion + return await onDelete(request, [payload]) + }, + performBatch: async (request, { payload }) => { + // For batch profile deletion + return await onDelete(request, payload) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts index b38899ee8e..a5b82f0e88 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/index.ts @@ -3,80 +3,9 @@ import type { Settings } from './generated-types' import forwardProfile from './forwardProfile' import forwardAudienceEvent from './forwardAudienceEvent' +import onDelete from './deleteProfile' import { AdvertiserScopesResponse } from './types' -import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from './functions' - -// The onDelete function for handling profile deletions -const onDelete = async (request: any, payload: any[]) => { - const userId = payload[0].userId ?? payload[0].anonymousId - const TokenQuery = `query TokenInfo { - tokenInfo { - scopesByAdvertiser { - nodes { - advertiser { - id - } - } - totalCount - } - } - }` - - const res_token = await request(GQL_ENDPOINT, { - body: JSON.stringify({ query: TokenQuery }), - throwHttpErrors: false - }) - - if (res_token.status !== 200) { - throw new Error('Failed to fetch advertiser information: ' + res_token.statusText) - } - - const result_token = await res_token.json() - const advertiserNode = result_token.data?.tokenInfo?.scopesByAdvertiser?.nodes - - if (!advertiserNode || advertiserNode.length === 0) { - throw new Error('No advertiser ID found.') - } - - // Collect advertiser IDs into an array - const advertiserIds = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) - - const formattedExternalIds = `["${userId}"]` - const formattedAdvertiserIds = `[${advertiserIds.map((id: string) => `"${id}"`).join(', ')}]` - - const query = `mutation { - deleteProfilesWithExternalIds( - externalIds: ${formattedExternalIds}, - advertiserIDs: ${formattedAdvertiserIds}, - externalProvider: "${EXTERNAL_PROVIDER}" - ) { - userErrors { - message - path - } - } - }` - - const res = await request(GQL_ENDPOINT, { - body: JSON.stringify({ query }), - throwHttpErrors: false - }) - - if (res.status !== 200) { - throw new Error('Failed to delete profile: ' + res.statusText) - } - - const result = await res.json() - - if (result.data.deleteProfilesWithExternalIds.userErrors.length > 0) { - throw new Error( - 'Profile deletion was not successful: ' + - result.data.deleteProfilesWithExternalIds.userErrors.map((e: any) => e.message).join(', ') - ) - } - - return result -} +import { GQL_ENDPOINT } from './functions' const destination: DestinationDefinition = { name: 'StackAdapt Audiences', @@ -134,44 +63,7 @@ const destination: DestinationDefinition = { actions: { forwardProfile, forwardAudienceEvent, - onDelete: { - title: 'Delete Profiles', - description: 'Deletes a profile by userId or anonymousId and advertiser IDs.', - fields: { - userId: { - label: 'User ID', - type: 'string', - required: false, - description: 'The user ID to delete.' - }, - anonymousId: { - label: 'Anonymous ID', - type: 'string', - required: false, - description: 'The anonymous ID to delete.' - }, - advertiserId: { - label: 'Advertiser IDs', - type: 'string', - required: false, - description: 'Comma-separated list of advertiser IDs. If not provided, it will query the token info.' - }, - externalProvider: { - label: 'External Provider', - type: 'string', - required: false, - description: 'The external provider to delete the profile from.' - } - }, - perform: async (request, { payload }) => { - // For single profile deletion - return await onDelete(request, [payload]) - }, - performBatch: async (request, { payload }) => { - // For batch profile deletion - return await onDelete(request, payload) - } - } + onDelete } } From f15e52ec3a5cfc9f15d0666de94550cbffba7df1 Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Thu, 3 Oct 2024 11:07:10 -0400 Subject: [PATCH 15/17] Deleted redundant files --- .../__tests__/onDelete.test.ts | 259 ------------------ .../stackadapt-audiences/onDelete.types.ts | 20 -- 2 files changed, 279 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts delete mode 100644 packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts deleted file mode 100644 index ba24ec0504..0000000000 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/__tests__/onDelete.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import nock from 'nock' -import { createTestEvent, createTestIntegration } from '@segment/actions-core' -import Definition from '../index' -import { SegmentEvent } from '@segment/actions-core/*' - -const testDestination = createTestIntegration(Definition) -const mockGqlKey = 'test-graphql-key' - -const gqlHostUrl = 'https://api.stackadapt.com' -const gqlPath = '/graphql' -const mockUserId = 'user-id' -const mockAdvertiserId = '23' -const mockMappings = { advertiser_id: mockAdvertiserId } - -const deleteEventPayload: Partial = { - userId: mockUserId, - type: 'identify', - context: { - personas: { - computation_class: 'audience', - computation_key: 'first_time_buyer', - computation_id: 'aud_123' - } - } -} - -describe('onDelete action', () => { - afterEach(() => { - nock.cleanAll() - }) - - it('should delete a profile successfully', async () => { - let deleteRequestBody - - nock(gqlHostUrl) - .post(gqlPath, (body) => { - return body - }) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [ - { - advertiser: { - id: mockAdvertiserId - } - } - ] - } - } - } - }) - - nock(gqlHostUrl) - .post(gqlPath, (body) => { - deleteRequestBody = body - return body - }) - .reply(200, { - data: { - deleteProfilesWithExternalIds: { - userErrors: [] - } - } - }) - - const event = createTestEvent({ - userId: mockUserId, - type: 'identify', - context: { - personas: { - computation_class: 'audience', - computation_key: 'first_time_buyer', - computation_id: 'aud_123' - } - } - }) - - const responses = await testDestination.testAction('onDelete', { - event, - useDefaultMappings: true, - mapping: { userId: mockUserId }, - settings: { apiKey: mockGqlKey } - }) - - // Assert that two responses were received, one for the token query and one for the profile deletion - expect(responses.length).toBe(2) - - // Ensure the second request body (profile deletion) matches the expected mutation - expect(deleteRequestBody).toMatchInlineSnapshot(` - Object { - "query": "mutation { - deleteProfilesWithExternalIds( - externalIds: [\\"user-id\\"], - advertiserIDs: [\\"23\\"], - externalProvider: \\"segmentio\\" - ) { - userErrors { - message - path - } - } - }", - } - `) - }) - - it('should throw error if no advertiser ID is found', async () => { - nock(gqlHostUrl) - .post(gqlPath) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [] - } - } - } - }) - - const event = createTestEvent(deleteEventPayload) - - await expect( - testDestination.testAction('onDelete', { - event, - useDefaultMappings: true, - mapping: mockMappings, - settings: { apiKey: mockGqlKey } - }) - ).rejects.toThrow('No advertiser ID found.') - }) - - it('should throw error if profile deletion fails', async () => { - nock(gqlHostUrl) - .post(gqlPath) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [ - { - advertiser: { - id: mockAdvertiserId - } - } - ] - } - } - } - }) - - // Mock the deleteProfilesWithExternalIds mutation with an error - nock(gqlHostUrl) - .post(gqlPath) - .reply(200, { - data: { - deleteProfilesWithExternalIds: { - userErrors: [{ message: 'Deletion failed' }] - } - } - }) - - const event = createTestEvent(deleteEventPayload) - - await expect( - testDestination.testAction('onDelete', { - event, - useDefaultMappings: true, - mapping: mockMappings, - settings: { apiKey: mockGqlKey } - }) - ).rejects.toThrow('Profile deletion was not successful: Deletion failed') - }) - - it('should perform onDelete with a userID and two advertiserIDs from a single token request', async () => { - let deleteRequestBody - - const event: Partial = { - userId: 'user-id-1', - type: 'identify', - traits: { - first_time_buyer: true - }, - context: { - personas: { - computation_class: 'audience', - computation_key: 'first_time_buyer', - computation_id: 'aud_123' - } - } - } - - nock(gqlHostUrl) - .post(gqlPath, (body) => { - return body - }) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [ - { - advertiser: { - id: 'advertiser-id-1' - } - }, - { - advertiser: { - id: 'advertiser-id-2' - } - } - ] - } - } - } - }) - - nock(gqlHostUrl) - .post(gqlPath, (body) => { - deleteRequestBody = body - return body - }) - .reply(200, { - data: { - deleteProfilesWithExternalIds: { - userErrors: [] - } - } - }) - - const responses = await testDestination.testAction('onDelete', { - event, - useDefaultMappings: true, - mapping: { userId: 'user-id-1' }, - settings: { apiKey: mockGqlKey } - }) - - expect(responses[0].status).toBe(200) - - // Ensure the mutation request body contains the correct userId and advertiserIDs - expect(deleteRequestBody).toMatchInlineSnapshot(` - Object { - "query": "mutation { - deleteProfilesWithExternalIds( - externalIds: [\\"user-id-1\\"], - advertiserIDs: [\\"advertiser-id-1\\", \\"advertiser-id-2\\"], - externalProvider: \\"segmentio\\" - ) { - userErrors { - message - path - } - } - }", - } - `) - }) -}) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts deleted file mode 100644 index e770d449ca..0000000000 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/onDelete.types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Generated file. DO NOT MODIFY IT BY HAND. - -export interface Payload { - /** - * The user ID to delete. - */ - userId?: string - /** - * The anonymous ID to delete. - */ - anonymousId?: string - /** - * Comma-separated list of advertiser IDs. If not provided, it will query the token info. - */ - advertiserId?: string - /** - * The external provider to delete the profile from. - */ - externalProvider?: string -} From 4d2c82d7807415cbecaee03559c4d98a9d840a9e Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Thu, 3 Oct 2024 12:00:48 -0400 Subject: [PATCH 16/17] Added helper to reduce redundant in test file --- .../deleteProfile/__tests__/onDelete.test.ts | 162 +++++------------- 1 file changed, 46 insertions(+), 116 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts index a99e4cb3d0..8423b5fe4c 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts @@ -24,46 +24,50 @@ const deleteEventPayload: Partial = { } } +// Helper function to mock the token query response +const mockTokenQueryResponse = (nodes: Array<{ advertiser: { id: string } }>) => { + nock(gqlHostUrl) + .post(gqlPath) + .reply(200, { + data: { + tokenInfo: { + scopesByAdvertiser: { + nodes: nodes + } + } + } + }) +} + +// Helper function to mock the profile deletion mutation +const mockDeleteProfilesMutation = ( + deleteRequestBodyRef: { body?: any }, + userErrors: Array<{ message: string }> = [] +) => { + nock(gqlHostUrl) + .post(gqlPath, (body) => { + deleteRequestBodyRef.body = body + return body + }) + .reply(200, { + data: { + deleteProfilesWithExternalIds: { + userErrors: userErrors + } + } + }) +} + describe('onDelete action', () => { afterEach(() => { nock.cleanAll() }) it('should delete a profile successfully', async () => { - let deleteRequestBody + const deleteRequestBody: { body?: any } = {} - nock(gqlHostUrl) - .post(gqlPath, (body) => { - return body - }) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [ - { - advertiser: { - id: mockAdvertiserId - } - } - ] - } - } - } - }) - - nock(gqlHostUrl) - .post(gqlPath, (body) => { - deleteRequestBody = body - return body - }) - .reply(200, { - data: { - deleteProfilesWithExternalIds: { - userErrors: [] - } - } - }) + mockTokenQueryResponse([{ advertiser: { id: mockAdvertiserId } }]) + mockDeleteProfilesMutation(deleteRequestBody) const event = createTestEvent({ userId: mockUserId, @@ -84,11 +88,8 @@ describe('onDelete action', () => { settings: { apiKey: mockGqlKey } }) - // Assert that two responses were received, one for the token query and one for the profile deletion expect(responses.length).toBe(2) - - // Ensure the second request body (profile deletion) matches the expected mutation - expect(deleteRequestBody).toMatchInlineSnapshot(` + expect(deleteRequestBody.body).toMatchInlineSnapshot(` Object { "query": "mutation { deleteProfilesWithExternalIds( @@ -107,17 +108,7 @@ describe('onDelete action', () => { }) it('should throw error if no advertiser ID is found', async () => { - nock(gqlHostUrl) - .post(gqlPath) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [] - } - } - } - }) + mockTokenQueryResponse([]) // Pass an empty array to mock no advertiser IDs const event = createTestEvent(deleteEventPayload) @@ -132,34 +123,10 @@ describe('onDelete action', () => { }) it('should throw error if profile deletion fails', async () => { - nock(gqlHostUrl) - .post(gqlPath) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [ - { - advertiser: { - id: mockAdvertiserId - } - } - ] - } - } - } - }) + const deleteRequestBody: { body?: any } = {} - // Mock the deleteProfilesWithExternalIds mutation with an error - nock(gqlHostUrl) - .post(gqlPath) - .reply(200, { - data: { - deleteProfilesWithExternalIds: { - userErrors: [{ message: 'Deletion failed' }] - } - } - }) + mockTokenQueryResponse([{ advertiser: { id: mockAdvertiserId } }]) + mockDeleteProfilesMutation(deleteRequestBody, [{ message: 'Deletion failed' }]) const event = createTestEvent(deleteEventPayload) @@ -174,7 +141,7 @@ describe('onDelete action', () => { }) it('should perform onDelete with a userID and two advertiserIDs from a single token request', async () => { - let deleteRequestBody + const deleteRequestBody: { body?: any } = {} const event: Partial = { userId: 'user-id-1', @@ -191,43 +158,8 @@ describe('onDelete action', () => { } } - nock(gqlHostUrl) - .post(gqlPath, (body) => { - return body - }) - .reply(200, { - data: { - tokenInfo: { - scopesByAdvertiser: { - nodes: [ - { - advertiser: { - id: 'advertiser-id-1' - } - }, - { - advertiser: { - id: 'advertiser-id-2' - } - } - ] - } - } - } - }) - - nock(gqlHostUrl) - .post(gqlPath, (body) => { - deleteRequestBody = body - return body - }) - .reply(200, { - data: { - deleteProfilesWithExternalIds: { - userErrors: [] - } - } - }) + mockTokenQueryResponse([{ advertiser: { id: 'advertiser-id-1' } }, { advertiser: { id: 'advertiser-id-2' } }]) + mockDeleteProfilesMutation(deleteRequestBody) const responses = await testDestination.testAction('onDelete', { event, @@ -237,9 +169,7 @@ describe('onDelete action', () => { }) expect(responses[0].status).toBe(200) - - // Ensure the mutation request body contains the correct userId and advertiserIDs - expect(deleteRequestBody).toMatchInlineSnapshot(` + expect(deleteRequestBody.body).toMatchInlineSnapshot(` Object { "query": "mutation { deleteProfilesWithExternalIds( From 4a354e7bee36aa0985ee4185fd17bcdfa2c148bc Mon Sep 17 00:00:00 2001 From: Longqirui Chen Date: Thu, 3 Oct 2024 12:17:17 -0400 Subject: [PATCH 17/17] Added helper for expected test result --- .../deleteProfile/__tests__/onDelete.test.ts | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts index 8423b5fe4c..2d45459d99 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts @@ -57,6 +57,29 @@ const mockDeleteProfilesMutation = ( } }) } +// helper for expected delete profiles mutation +const expectDeleteProfilesMutation = ( + deleteRequestBody: { body?: any }, + expectedExternalIds: string[], + expectedAdvertiserIds: string[] +) => { + expect(deleteRequestBody.body).toMatchInlineSnapshot(` + Object { + "query": "mutation { + deleteProfilesWithExternalIds( + externalIds: [\\"${expectedExternalIds.join('\\", \\"')}\\"], + advertiserIDs: [\\"${expectedAdvertiserIds.join('\\", \\"')}\\"], + externalProvider: \\"segmentio\\" + ) { + userErrors { + message + path + } + } + }", + } + `) +} describe('onDelete action', () => { afterEach(() => { @@ -89,22 +112,7 @@ describe('onDelete action', () => { }) expect(responses.length).toBe(2) - expect(deleteRequestBody.body).toMatchInlineSnapshot(` - Object { - "query": "mutation { - deleteProfilesWithExternalIds( - externalIds: [\\"user-id\\"], - advertiserIDs: [\\"23\\"], - externalProvider: \\"segmentio\\" - ) { - userErrors { - message - path - } - } - }", - } - `) + expectDeleteProfilesMutation(deleteRequestBody, ['user-id'], ['23']) }) it('should throw error if no advertiser ID is found', async () => { @@ -169,21 +177,6 @@ describe('onDelete action', () => { }) expect(responses[0].status).toBe(200) - expect(deleteRequestBody.body).toMatchInlineSnapshot(` - Object { - "query": "mutation { - deleteProfilesWithExternalIds( - externalIds: [\\"user-id-1\\"], - advertiserIDs: [\\"advertiser-id-1\\", \\"advertiser-id-2\\"], - externalProvider: \\"segmentio\\" - ) { - userErrors { - message - path - } - } - }", - } - `) + expectDeleteProfilesMutation(deleteRequestBody, ['user-id-1'], ['advertiser-id-1', 'advertiser-id-2']) }) })