From b9da81ded40656e3dfdade620ad603171645801d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 15:00:12 +0000 Subject: [PATCH] Fix trackInApp* payload mutation and add JWT auth test coverage (#552) Replace delete statements with destructuring to avoid mutating the caller's payload object. The interceptor already overwrites email/userId with the correct values from SDK state, so the delete was both unnecessary and harmful as a side effect. Also adds missing JWT auth test coverage for in-app event endpoints. https://claude.ai/code/session_016eGrVtXnTGForEz4J3Krrm --- src/authorization/authorization.test.ts | 94 +++++++++++++++++++++++++ src/events/events.test.ts | 28 ++++++++ src/events/inapp/events.ts | 38 +++++----- 3 files changed, 139 insertions(+), 21 deletions(-) diff --git a/src/authorization/authorization.test.ts b/src/authorization/authorization.test.ts index 4cf8bbe1..4f431392 100644 --- a/src/authorization/authorization.test.ts +++ b/src/authorization/authorization.test.ts @@ -934,6 +934,57 @@ describe('User Identification', () => { ); }); + it('adds email body to in-app event endpoints in JWT auth (trackInAppDelivery, trackInAppOpen, trackInAppClick, inAppConsume)', async () => { + const { setEmail } = initialize('123', () => + Promise.resolve(MOCK_JWT_KEY) + ); + await setEmail('hello@gmail.com'); + + mockRequest.onPost('/events/trackInAppDelivery').reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/trackInAppOpen').reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/trackInAppClick').reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/inAppConsume').reply(200, { + data: 'something' + }); + + const deliveryResponse = await trackInAppDelivery({ + messageId: '123', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + const openResponse = await trackInAppOpen({ + messageId: '123', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + const clickResponse = await trackInAppClick({ + messageId: '123', + clickedUrl: 'https://example.com', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + const consumeResponse = await trackInAppConsume({ + messageId: '123', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + + expect(JSON.parse(deliveryResponse.config.data).email).toBe( + 'hello@gmail.com' + ); + expect(JSON.parse(openResponse.config.data).email).toBe( + 'hello@gmail.com' + ); + expect(JSON.parse(clickResponse.config.data).email).toBe( + 'hello@gmail.com' + ); + expect(JSON.parse(consumeResponse.config.data).email).toBe( + 'hello@gmail.com' + ); + }); + it('adds currentEmail body to endpoint that need an currentEmail as a body', async () => { const { setEmail } = initialize('123', () => Promise.resolve(MOCK_JWT_KEY) @@ -1099,6 +1150,49 @@ describe('User Identification', () => { expect(JSON.parse(trackResponse.config.data).userId).toBe('999'); }); + it('adds userId body to in-app event endpoints in JWT auth (trackInAppDelivery, trackInAppOpen, trackInAppClick, inAppConsume)', async () => { + const { setUserID } = initialize('123', () => + Promise.resolve(MOCK_JWT_KEY) + ); + await setUserID('999'); + + mockRequest.onPost('/events/trackInAppDelivery').reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/trackInAppOpen').reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/trackInAppClick').reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/inAppConsume').reply(200, { + data: 'something' + }); + + const deliveryResponse = await trackInAppDelivery({ + messageId: '123', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + const openResponse = await trackInAppOpen({ + messageId: '123', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + const clickResponse = await trackInAppClick({ + messageId: '123', + clickedUrl: 'https://example.com', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + const consumeResponse = await trackInAppConsume({ + messageId: '123', + deviceInfo: { appPackageName: 'my-lil-website' } + }); + + expect(JSON.parse(deliveryResponse.config.data).userId).toBe('999'); + expect(JSON.parse(openResponse.config.data).userId).toBe('999'); + expect(JSON.parse(clickResponse.config.data).userId).toBe('999'); + expect(JSON.parse(consumeResponse.config.data).userId).toBe('999'); + }); + it('adds currentUserId body to endpoint that need an currentUserId as a body', async () => { const { setUserID } = initialize('123', () => Promise.resolve(MOCK_JWT_KEY) diff --git a/src/events/events.test.ts b/src/events/events.test.ts index 854bfc01..5102d673 100644 --- a/src/events/events.test.ts +++ b/src/events/events.test.ts @@ -313,6 +313,34 @@ describe('Events Requests', () => { } }); + it('should not mutate the caller payload object', async () => { + setTypeOfAuthForTestingOnly('email'); + const payload: any = { + messageId: '123', + email: 'test@example.com', + userId: 'user123', + deviceInfo: { appPackageName: 'my-lil-site' } + }; + await trackInAppOpen(payload); + expect(payload.email).toBe('test@example.com'); + expect(payload.userId).toBe('user123'); + }); + + it('should exclude email and userId from request data', async () => { + setTypeOfAuthForTestingOnly('email'); + const payload: any = { + messageId: '123', + email: 'test@example.com', + userId: 'user123', + deviceInfo: { appPackageName: 'my-lil-site' } + }; + const response = await trackInAppOpen(payload); + const data = JSON.parse(response.config.data); + expect(data.email).toBeUndefined(); + expect(data.userId).toBeUndefined(); + expect(data.messageId).toBe('123'); + }); + it('return the correct payload for embedded message received', async () => { const response = await trackEmbeddedReceived('abc123', 'packageName'); expect(JSON.parse(response.config.data).messageId).toBe('abc123'); diff --git a/src/events/inapp/events.ts b/src/events/inapp/events.ts index 16c21d34..e798ea05 100644 --- a/src/events/inapp/events.ts +++ b/src/events/inapp/events.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import { baseIterableRequest } from '../../request'; import { InAppEventRequestParams } from './types'; import { IterableResponse } from '../../types'; @@ -6,15 +5,16 @@ import { ENDPOINTS, WEB_PLATFORM } from '../../constants'; import { eventRequestSchema } from './events.schema'; export const trackInAppClose = (payload: InAppEventRequestParams) => { - /* a customer could potentially send these up if they're not using TypeScript */ - delete (payload as any).userId; - delete (payload as any).email; + /* strip email/userId without mutating the caller's object; + the interceptor adds the correct identity from SDK state */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + const { email, userId, ...rest } = payload as any; return baseIterableRequest({ method: 'POST', url: ENDPOINTS.track_app_close.route, data: { - ...payload, + ...rest, deviceInfo: { ...payload.deviceInfo, platform: WEB_PLATFORM, @@ -33,15 +33,14 @@ export const trackInAppOpen = ( 'clickedUrl' | 'inboxSessionId' | 'closeAction' > ) => { - /* a customer could potentially send these up if they're not using TypeScript */ - delete (payload as any).userId; - delete (payload as any).email; + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + const { email, userId, ...rest } = payload as any; return baseIterableRequest({ method: 'POST', url: ENDPOINTS.track_app_open.route, data: { - ...payload, + ...rest, deviceInfo: { ...payload.deviceInfo, platform: WEB_PLATFORM, @@ -62,16 +61,15 @@ export const trackInAppClick = ( payload: Omit, sendBeacon = false ) => { - /* a customer could potentially send these up if they're not using TypeScript */ - delete (payload as any).userId; - delete (payload as any).email; + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + const { email, userId, ...rest } = payload as any; return baseIterableRequest({ method: 'POST', url: ENDPOINTS.track_app_click.route, sendBeacon, data: { - ...payload, + ...rest, deviceInfo: { ...payload.deviceInfo, platform: WEB_PLATFORM, @@ -90,15 +88,14 @@ export const trackInAppDelivery = ( 'clickedUrl' | 'closeAction' | 'inboxSessionId' > ) => { - /* a customer could potentially send these up if they're not using TypeScript */ - delete (payload as any).userId; - delete (payload as any).email; + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + const { email, userId, ...rest } = payload as any; return baseIterableRequest({ method: 'POST', url: ENDPOINTS.track_app_delivery.route, data: { - ...payload, + ...rest, deviceInfo: { ...payload.deviceInfo, platform: WEB_PLATFORM, @@ -121,15 +118,14 @@ export const trackInAppConsume = ( 'clickedUrl' | 'closeAction' | 'inboxSessionId' > ) => { - /* a customer could potentially send these up if they're not using TypeScript */ - delete (payload as any).userId; - delete (payload as any).email; + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + const { email, userId, ...rest } = payload as any; return baseIterableRequest({ method: 'POST', url: ENDPOINTS.track_app_consume.route, data: { - ...payload, + ...rest, deviceInfo: { ...payload.deviceInfo, platform: WEB_PLATFORM,