@adtrackify/at-service-common 4.0.13 → 4.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/__tests__/identity-cache/helpers.spec.js +37 -38
- package/dist/cjs/__tests__/identity-cache/helpers.spec.js.map +1 -1
- package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js +83 -344
- package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
- package/dist/cjs/__tests__/identity-cache/identity-client.spec.js +360 -0
- package/dist/cjs/__tests__/identity-cache/identity-client.spec.js.map +1 -0
- package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js +335 -1261
- package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -1
- package/dist/cjs/clients/internal-api/identity-client.d.ts +12 -2
- package/dist/cjs/clients/internal-api/identity-client.js +59 -13
- package/dist/cjs/clients/internal-api/identity-client.js.map +1 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +1 -7
- package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js +0 -305
- package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +10 -9
- package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +116 -321
- package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/index.d.ts +0 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js +0 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js.map +1 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/types.d.ts +9 -157
- package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js +1 -15
- package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js.map +1 -1
- package/dist/esm/__tests__/identity-cache/helpers.spec.js +26 -27
- package/dist/esm/__tests__/identity-cache/helpers.spec.js.map +1 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js +83 -344
- package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
- package/dist/esm/__tests__/identity-cache/identity-client.spec.js +358 -0
- package/dist/esm/__tests__/identity-cache/identity-client.spec.js.map +1 -0
- package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js +335 -1261
- package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -1
- package/dist/esm/clients/internal-api/identity-client.d.ts +12 -2
- package/dist/esm/clients/internal-api/identity-client.js +60 -14
- package/dist/esm/clients/internal-api/identity-client.js.map +1 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +1 -7
- package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js +1 -303
- package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +10 -9
- package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +119 -321
- package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/index.d.ts +0 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/index.js +0 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/index.js.map +1 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/types.d.ts +9 -157
- package/dist/esm/services/db/identity-cache-dynamodb-service/types.js +0 -14
- package/dist/esm/services/db/identity-cache-dynamodb-service/types.js.map +1 -1
- package/package.json +2 -2
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +0 -646
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +0 -1
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +0 -325
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +0 -1
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +0 -1
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +0 -1286
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +0 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.d.ts +0 -26
- package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js +0 -252
- package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js.map +0 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts +0 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +0 -644
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +0 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts +0 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +0 -323
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +0 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +0 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +0 -1284
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +0 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.d.ts +0 -26
- package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js +0 -235
- package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js.map +0 -1
- /package/dist/cjs/__tests__/identity-cache/{identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts → identity-client.spec.d.ts} +0 -0
- /package/dist/{cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts → esm/__tests__/identity-cache/identity-client.spec.d.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const mockLambdaInvokeFunction = jest.fn();
|
|
2
2
|
const mockBuildAndSendMessage = jest.fn();
|
|
3
3
|
const mockSQSClientConstructor = jest.fn();
|
|
4
4
|
jest.mock('../../clients/index.js', () => ({
|
|
@@ -12,7 +12,7 @@ jest.mock('../../clients/index.js', () => ({
|
|
|
12
12
|
batchWrite: jest.fn(),
|
|
13
13
|
},
|
|
14
14
|
LambdaInvokeClient: jest.fn().mockImplementation(() => ({
|
|
15
|
-
invokeFunction:
|
|
15
|
+
invokeFunction: mockLambdaInvokeFunction,
|
|
16
16
|
})),
|
|
17
17
|
SQSClient: jest.fn().mockImplementation((_region, _accountId, _queueName) => {
|
|
18
18
|
mockSQSClientConstructor(_region, _accountId, _queueName);
|
|
@@ -30,6 +30,12 @@ jest.mock('../../helpers/index.js', () => ({
|
|
|
30
30
|
error: jest.fn(),
|
|
31
31
|
},
|
|
32
32
|
}));
|
|
33
|
+
const mockGetIdentity = jest.fn();
|
|
34
|
+
jest.mock('../../clients/internal-api/identity-client.js', () => ({
|
|
35
|
+
IdentityClient: jest.fn().mockImplementation(() => ({
|
|
36
|
+
getIdentity: mockGetIdentity,
|
|
37
|
+
})),
|
|
38
|
+
}));
|
|
33
39
|
import { IdentityGatewayService, } from '../../services/db/identity-cache-dynamodb-service/identity-gateway-service.js';
|
|
34
40
|
import { IDENTITY_CACHE_WITH_CACHING_OUTCOME, } from '../../services/db/identity-cache-dynamodb-service/types.js';
|
|
35
41
|
import { DynamoDbClient, SQSClient } from '../../clients/index.js';
|
|
@@ -49,13 +55,14 @@ describe('IdentityGatewayService new features', () => {
|
|
|
49
55
|
mockedDynamoDbClient.safeBatchWrite.mockReset();
|
|
50
56
|
mockedDynamoDbClient.safeQueryByGSI.mockReset();
|
|
51
57
|
mockedDynamoDbClient.batchWrite.mockReset();
|
|
52
|
-
|
|
58
|
+
mockGetIdentity.mockReset();
|
|
53
59
|
mockBuildAndSendMessage.mockReset();
|
|
54
60
|
mockSQSClientConstructor.mockReset();
|
|
55
61
|
mockedSQSClient.mockClear();
|
|
56
|
-
IdentityGatewayService.
|
|
62
|
+
IdentityGatewayService.identityClient = undefined;
|
|
57
63
|
IdentityGatewayService.sqsClient = undefined;
|
|
58
64
|
IdentityGatewayService.sqsQueueUrl = undefined;
|
|
65
|
+
IdentityGatewayService.configure({ identifyPrivateLambdaArn: lambdaArn });
|
|
59
66
|
});
|
|
60
67
|
describe('configure()', () => {
|
|
61
68
|
it('should configure with direct queue URL', () => {
|
|
@@ -270,7 +277,7 @@ describe('IdentityGatewayService new features', () => {
|
|
|
270
277
|
}));
|
|
271
278
|
});
|
|
272
279
|
});
|
|
273
|
-
describe('
|
|
280
|
+
describe('resolveIdentityWithCaching() with skipCacheWrite option', () => {
|
|
274
281
|
const incomingIdentity = {
|
|
275
282
|
identityId: 'identity456',
|
|
276
283
|
traits: { emails: ['test@email.com'] },
|
|
@@ -278,53 +285,38 @@ describe('IdentityGatewayService new features', () => {
|
|
|
278
285
|
describe('default behavior (no options)', () => {
|
|
279
286
|
it('should write to cache when Neptune returns new identity', async () => {
|
|
280
287
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
identity: {
|
|
285
|
-
identityId: 'resolved-identity',
|
|
286
|
-
traits: { emails: ['test@email.com', 'resolved@email.com'] },
|
|
287
|
-
},
|
|
288
|
-
}),
|
|
288
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
289
|
+
identityId: 'resolved-identity',
|
|
290
|
+
traits: { emails: ['test@email.com', 'resolved@email.com'] },
|
|
289
291
|
});
|
|
290
|
-
const result = await IdentityGatewayService.
|
|
291
|
-
expect(result.
|
|
292
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
293
|
+
expect(result.didWriteCache).toBe(false);
|
|
292
294
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
293
295
|
});
|
|
294
296
|
});
|
|
295
297
|
describe('skipCacheWrite: false', () => {
|
|
296
298
|
it('should write to cache (same as default)', async () => {
|
|
297
299
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
identity: {
|
|
302
|
-
identityId: 'resolved-identity',
|
|
303
|
-
traits: { emails: ['test@email.com'] },
|
|
304
|
-
},
|
|
305
|
-
}),
|
|
300
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
301
|
+
identityId: 'resolved-identity',
|
|
302
|
+
traits: { emails: ['test@email.com'] },
|
|
306
303
|
});
|
|
307
|
-
const result = await IdentityGatewayService.
|
|
308
|
-
expect(result.
|
|
304
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
305
|
+
expect(result.didWriteCache).toBe(false);
|
|
309
306
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
310
307
|
});
|
|
311
308
|
});
|
|
312
309
|
describe('skipCacheWrite: true', () => {
|
|
313
310
|
it('should NOT write to cache on Neptune write-back path', async () => {
|
|
314
311
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
identity: {
|
|
319
|
-
identityId: 'resolved-identity',
|
|
320
|
-
traits: { emails: ['test@email.com', 'resolved@email.com'] },
|
|
321
|
-
},
|
|
322
|
-
}),
|
|
312
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
313
|
+
identityId: 'resolved-identity',
|
|
314
|
+
traits: { emails: ['test@email.com', 'resolved@email.com'] },
|
|
323
315
|
});
|
|
324
|
-
const result = await IdentityGatewayService.
|
|
316
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
325
317
|
expect(result.identity?.identityId).toBe('resolved-identity');
|
|
326
|
-
expect(result.
|
|
327
|
-
expect(result.
|
|
318
|
+
expect(result.didWriteCache).toBe(false);
|
|
319
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
328
320
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
329
321
|
});
|
|
330
322
|
it('should enqueue async update on address update path (never writes sync)', async () => {
|
|
@@ -355,808 +347,149 @@ describe('IdentityGatewayService new features', () => {
|
|
|
355
347
|
addresses: [{ city: 'New City', upa: 1717200000000 }],
|
|
356
348
|
},
|
|
357
349
|
};
|
|
358
|
-
const result = await IdentityGatewayService.
|
|
359
|
-
if (result.
|
|
360
|
-
expect(result.
|
|
361
|
-
expect(result.
|
|
350
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, identityWithNewAddress);
|
|
351
|
+
if (result.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC) {
|
|
352
|
+
expect(result.didWriteCache).toBe(false);
|
|
353
|
+
expect(result.didEnqueueAsync).toBe(true);
|
|
362
354
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
363
355
|
expect(mockBuildAndSendMessage).toHaveBeenCalled();
|
|
364
356
|
}
|
|
365
357
|
});
|
|
366
358
|
it('should still return resolved identity when skipCacheWrite is true', async () => {
|
|
367
359
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
identity: {
|
|
372
|
-
identityId: 'neptune-resolved-id',
|
|
373
|
-
traits: { emails: ['test@email.com', 'new@email.com'] },
|
|
374
|
-
},
|
|
375
|
-
}),
|
|
360
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
361
|
+
identityId: 'neptune-resolved-id',
|
|
362
|
+
traits: { emails: ['test@email.com', 'new@email.com'] },
|
|
376
363
|
});
|
|
377
|
-
const result = await IdentityGatewayService.
|
|
364
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
378
365
|
expect(result.identity).toBeDefined();
|
|
379
366
|
expect(result.identity?.identityId).toBe('neptune-resolved-id');
|
|
380
|
-
expect(result.identity?.traits?.emails).toContain('new@email.com');
|
|
381
|
-
});
|
|
382
|
-
it('should return fresh cache hit without writing (skipCacheWrite irrelevant for hits)', async () => {
|
|
383
|
-
const cachedResponse = {
|
|
384
|
-
pk: `identity#${pixelId}#identity456`,
|
|
385
|
-
pixelId,
|
|
386
|
-
identityId: 'identity456',
|
|
387
|
-
response: {
|
|
388
|
-
identityId: 'identity456',
|
|
389
|
-
traits: { emails: ['test@email.com'] },
|
|
390
|
-
},
|
|
391
|
-
updatedAt: new Date().toISOString(),
|
|
392
|
-
};
|
|
393
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
394
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn, { skipCacheWrite: true });
|
|
395
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
|
|
396
|
-
expect(result.summary.didWriteCache).toBe(false);
|
|
397
|
-
expect(mockInvokeFunction).not.toHaveBeenCalled();
|
|
398
|
-
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
describe('address update path - always enqueues async (never writes sync)', () => {
|
|
402
|
-
it('should enqueue async (not write cache) when address update is detected', async () => {
|
|
403
|
-
IdentityGatewayService.configure({
|
|
404
|
-
awsRegion: 'us-east-1',
|
|
405
|
-
awsAccountId: '123456789',
|
|
406
|
-
asyncUpdateQueueName: 'test-queue',
|
|
407
|
-
});
|
|
408
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'test-msg-id' });
|
|
409
|
-
const cachedResponse = {
|
|
410
|
-
pk: `identity#${pixelId}#identity456`,
|
|
411
|
-
pixelId,
|
|
412
|
-
identityId: 'identity456',
|
|
413
|
-
response: {
|
|
414
|
-
identityId: 'identity456',
|
|
415
|
-
traits: {
|
|
416
|
-
emails: ['test@email.com'],
|
|
417
|
-
addresses: [{ city: 'Old City', upa: 1704067200000 }],
|
|
418
|
-
},
|
|
419
|
-
},
|
|
420
|
-
updatedAt: new Date().toISOString(),
|
|
421
|
-
};
|
|
422
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
423
|
-
const identityWithNewAddress = {
|
|
424
|
-
identityId: 'identity456',
|
|
425
|
-
traits: {
|
|
426
|
-
emails: ['test@email.com'],
|
|
427
|
-
addresses: [{ city: 'New City', upa: 1717200000000 }],
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn, { skipCacheWrite: false });
|
|
431
|
-
if (result.summary.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC) {
|
|
432
|
-
expect(result.summary.didWriteCache).toBe(false);
|
|
433
|
-
expect(result.summary.didEnqueueAsync).toBe(true);
|
|
434
|
-
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
435
|
-
expect(mockBuildAndSendMessage).toHaveBeenCalledWith('identify.updateAsync', expect.objectContaining({
|
|
436
|
-
pixelId,
|
|
437
|
-
incomingIdentityId: 'identity456',
|
|
438
|
-
}));
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
it('should return merged identity without waiting for cache write', async () => {
|
|
442
|
-
IdentityGatewayService.configure({
|
|
443
|
-
awsRegion: 'us-east-1',
|
|
444
|
-
awsAccountId: '123456789',
|
|
445
|
-
asyncUpdateQueueName: 'test-queue',
|
|
446
|
-
});
|
|
447
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'test-msg-id' });
|
|
448
|
-
const cachedResponse = {
|
|
449
|
-
pk: `identity#${pixelId}#identity456`,
|
|
450
|
-
pixelId,
|
|
451
|
-
identityId: 'identity456',
|
|
452
|
-
response: {
|
|
453
|
-
identityId: 'identity456',
|
|
454
|
-
traits: {
|
|
455
|
-
emails: ['test@email.com'],
|
|
456
|
-
addresses: [],
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
updatedAt: new Date().toISOString(),
|
|
460
|
-
};
|
|
461
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
462
|
-
const identityWithNewAddress = {
|
|
463
|
-
identityId: 'identity456',
|
|
464
|
-
traits: {
|
|
465
|
-
emails: ['test@email.com'],
|
|
466
|
-
addresses: [{ city: 'New City', firstName: 'John' }],
|
|
467
|
-
},
|
|
468
|
-
};
|
|
469
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn);
|
|
470
|
-
expect(result.identity).toBeDefined();
|
|
471
|
-
expect(result.identity?.identityId).toBe('identity456');
|
|
472
|
-
expect(result.summary.didSkipNeptune).toBe(true);
|
|
473
|
-
expect(result.summary.didCallNeptune).toBe(false);
|
|
474
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
475
|
-
});
|
|
476
|
-
it('should set didEnqueueAsync to false when SQS enqueue fails', async () => {
|
|
477
|
-
IdentityGatewayService.configure({
|
|
478
|
-
awsRegion: 'us-east-1',
|
|
479
|
-
awsAccountId: '123456789',
|
|
480
|
-
asyncUpdateQueueName: 'test-queue',
|
|
481
|
-
});
|
|
482
|
-
mockBuildAndSendMessage.mockRejectedValueOnce(new Error('SQS error'));
|
|
483
|
-
const cachedResponse = {
|
|
484
|
-
pk: `identity#${pixelId}#identity456`,
|
|
485
|
-
pixelId,
|
|
486
|
-
identityId: 'identity456',
|
|
487
|
-
response: {
|
|
488
|
-
identityId: 'identity456',
|
|
489
|
-
traits: {
|
|
490
|
-
emails: ['test@email.com'],
|
|
491
|
-
addresses: [],
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
updatedAt: new Date().toISOString(),
|
|
495
|
-
};
|
|
496
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
497
|
-
const identityWithNewAddress = {
|
|
498
|
-
identityId: 'identity456',
|
|
499
|
-
traits: {
|
|
500
|
-
emails: ['test@email.com'],
|
|
501
|
-
addresses: [{ city: 'New City', firstName: 'John' }],
|
|
502
|
-
},
|
|
503
|
-
};
|
|
504
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn);
|
|
505
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
506
|
-
expect(result.identity).toBeDefined();
|
|
507
|
-
expect(result.summary.didEnqueueAsync).toBe(false);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
describe('edge cases', () => {
|
|
512
|
-
it('should return invalid input when pixelId is undefined', async () => {
|
|
513
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(undefined, { identityId: 'test' }, lambdaArn);
|
|
514
|
-
expect(result.identity).toBeUndefined();
|
|
515
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
|
|
516
|
-
expect(result.summary.didReadCache).toBe(false);
|
|
517
|
-
expect(result.summary.didWriteCache).toBe(false);
|
|
518
|
-
});
|
|
519
|
-
it('should return invalid input when incomingIdentity is null', async () => {
|
|
520
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, null, lambdaArn);
|
|
521
|
-
expect(result.identity).toBeUndefined();
|
|
522
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
|
|
523
|
-
});
|
|
524
|
-
it('should return invalid input when both pixelId and identity are missing', async () => {
|
|
525
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2('', undefined, lambdaArn);
|
|
526
|
-
expect(result.identity).toBeUndefined();
|
|
527
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
|
|
528
|
-
});
|
|
529
|
-
it('should gracefully fallback when cache read fails (fail-open pattern)', async () => {
|
|
530
|
-
mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('Unexpected DynamoDB error'));
|
|
531
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
532
|
-
statusCode: 200,
|
|
533
|
-
body: JSON.stringify({
|
|
534
|
-
identity: {
|
|
535
|
-
identityId: 'neptune-resolved',
|
|
536
|
-
traits: { emails: ['test@email.com'] },
|
|
537
|
-
},
|
|
538
|
-
}),
|
|
539
|
-
});
|
|
540
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test-id', traits: { emails: ['test@email.com'] } }, lambdaArn);
|
|
541
|
-
expect(result.identity).toBeDefined();
|
|
542
|
-
expect(result.identity?.identityId).toBe('neptune-resolved');
|
|
543
|
-
expect(result.summary.didReadCache).toBe(true);
|
|
544
|
-
expect(result.summary.didCallNeptune).toBe(true);
|
|
545
|
-
});
|
|
546
|
-
it('should reject identity with undefined identityId in enqueue', async () => {
|
|
547
|
-
IdentityGatewayService.configure({
|
|
548
|
-
awsRegion: 'us-east-1',
|
|
549
|
-
awsAccountId: '123456789',
|
|
550
|
-
asyncUpdateQueueName: 'test-queue',
|
|
551
|
-
});
|
|
552
|
-
const identity = { traits: { emails: ['test@email.com'] } };
|
|
553
|
-
const context = { identity };
|
|
554
|
-
const result = await IdentityGatewayService.enqueueAsyncIdentityUpdate({
|
|
555
|
-
pixelId,
|
|
556
|
-
identity,
|
|
557
|
-
context,
|
|
558
|
-
reason: 'anonymous user',
|
|
559
|
-
});
|
|
560
|
-
expect(result.success).toBe(false);
|
|
561
|
-
expect(result.error).toBe('Missing required pixelId or identityId');
|
|
562
|
-
expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
|
|
563
|
-
expect(mockedLogger.warn).toHaveBeenCalledWith('IdentityGateway - enqueueAsyncIdentityUpdate: missing required params', expect.objectContaining({
|
|
564
|
-
pixelId,
|
|
565
|
-
identityId: undefined,
|
|
566
|
-
}));
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
describe('safeWriteBackToCache behavior with skipCacheWrite', () => {
|
|
570
|
-
it('should not call safeWriteBackToCache when skipCacheWrite is true and Neptune returns new identity', async () => {
|
|
571
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
572
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
573
|
-
statusCode: 200,
|
|
574
|
-
body: JSON.stringify({
|
|
575
|
-
identity: {
|
|
576
|
-
identityId: 'new-resolved-identity',
|
|
577
|
-
traits: { emails: ['test@email.com'] },
|
|
578
|
-
},
|
|
579
|
-
}),
|
|
580
|
-
});
|
|
581
|
-
await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'original-id', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: true });
|
|
582
|
-
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
583
|
-
});
|
|
584
|
-
it('should call safeWriteBackToCache when skipCacheWrite is false and Neptune returns new identity', async () => {
|
|
585
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
586
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
587
|
-
statusCode: 200,
|
|
588
|
-
body: JSON.stringify({
|
|
589
|
-
identity: {
|
|
590
|
-
identityId: 'new-resolved-identity',
|
|
591
|
-
traits: { emails: ['test@email.com'] },
|
|
592
|
-
},
|
|
593
|
-
}),
|
|
594
|
-
});
|
|
595
|
-
await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'original-id', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: false });
|
|
596
|
-
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
describe('summary fields consistency', () => {
|
|
600
|
-
it('should include all required summary fields in getIdentityWithCachingV2 result', async () => {
|
|
601
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
602
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
603
|
-
statusCode: 200,
|
|
604
|
-
body: JSON.stringify({
|
|
605
|
-
identity: {
|
|
606
|
-
identityId: 'resolved-id',
|
|
607
|
-
traits: { emails: ['test@email.com'] },
|
|
608
|
-
},
|
|
609
|
-
}),
|
|
610
|
-
});
|
|
611
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'incoming-id', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: true });
|
|
612
|
-
expect(result.summary).toEqual(expect.objectContaining({
|
|
613
|
-
outcome: expect.any(String),
|
|
614
|
-
pixelId,
|
|
615
|
-
identityId: 'resolved-id',
|
|
616
|
-
didReadCache: true,
|
|
617
|
-
didCallNeptune: true,
|
|
618
|
-
didWriteCache: false,
|
|
619
|
-
didSkipNeptune: false,
|
|
620
|
-
didFallbackToIncoming: false,
|
|
621
|
-
didEnqueueAsync: false,
|
|
622
|
-
staleTraits: expect.any(Array),
|
|
623
|
-
}));
|
|
624
|
-
});
|
|
625
|
-
it('should include traitChanges in summary when identity changes', async () => {
|
|
626
|
-
const cachedResponse = {
|
|
627
|
-
pk: `identity#${pixelId}#identity456`,
|
|
628
|
-
pixelId,
|
|
629
|
-
identityId: 'identity456',
|
|
630
|
-
response: {
|
|
631
|
-
identityId: 'identity456',
|
|
632
|
-
traits: { emails: ['old@email.com'] },
|
|
633
|
-
},
|
|
634
|
-
updatedAt: new Date().toISOString(),
|
|
635
|
-
};
|
|
636
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
637
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
638
|
-
statusCode: 200,
|
|
639
|
-
body: JSON.stringify({
|
|
640
|
-
identity: {
|
|
641
|
-
identityId: 'identity456',
|
|
642
|
-
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
643
|
-
},
|
|
644
|
-
}),
|
|
645
|
-
});
|
|
646
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'identity456', traits: { emails: ['old@email.com', 'new@email.com'] } }, lambdaArn);
|
|
647
|
-
if (result.summary.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED) {
|
|
648
|
-
expect(result.summary.traitChanges).toBeDefined();
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
describe('ADDRESS_UPDATE async behavior (comprehensive)', () => {
|
|
653
|
-
describe('ADDRESS_UPDATE enqueues async (not sync write)', () => {
|
|
654
|
-
it('should enqueue async when staleness tier is ADDRESS_UPDATE with non-IP address', async () => {
|
|
655
|
-
IdentityGatewayService.configure({
|
|
656
|
-
awsRegion: 'us-east-1',
|
|
657
|
-
awsAccountId: '123456789',
|
|
658
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
659
|
-
});
|
|
660
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'async-msg-123' });
|
|
661
|
-
const cachedResponse = {
|
|
662
|
-
pk: `identity#${pixelId}#identity456`,
|
|
663
|
-
pixelId,
|
|
664
|
-
identityId: 'identity456',
|
|
665
|
-
response: {
|
|
666
|
-
identityId: 'identity456',
|
|
667
|
-
traits: {
|
|
668
|
-
emails: ['test@email.com'],
|
|
669
|
-
addresses: [],
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
updatedAt: new Date().toISOString(),
|
|
673
|
-
};
|
|
674
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
675
|
-
const incomingWithAddress = {
|
|
676
|
-
identityId: 'identity456',
|
|
677
|
-
traits: {
|
|
678
|
-
emails: ['test@email.com'],
|
|
679
|
-
addresses: [{ firstName: 'John', lastName: 'Doe', city: 'New York' }],
|
|
680
|
-
},
|
|
681
|
-
};
|
|
682
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
683
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
684
|
-
expect(mockBuildAndSendMessage).toHaveBeenCalledWith('identify.updateAsync', expect.objectContaining({
|
|
685
|
-
pixelId,
|
|
686
|
-
incomingIdentityId: 'identity456',
|
|
687
|
-
}));
|
|
688
|
-
expect(result.summary.didEnqueueAsync).toBe(true);
|
|
689
|
-
expect(result.summary.didWriteCache).toBe(false);
|
|
690
|
-
expect(result.summary.didCallNeptune).toBe(false);
|
|
691
|
-
expect(result.summary.didSkipNeptune).toBe(true);
|
|
692
|
-
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
693
|
-
expect(mockInvokeFunction).not.toHaveBeenCalled();
|
|
694
|
-
});
|
|
695
|
-
it('should return merged identity with updated addresses', async () => {
|
|
696
|
-
IdentityGatewayService.configure({
|
|
697
|
-
awsRegion: 'us-east-1',
|
|
698
|
-
awsAccountId: '123456789',
|
|
699
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
700
|
-
});
|
|
701
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-456' });
|
|
702
|
-
const cachedResponse = {
|
|
703
|
-
pk: `identity#${pixelId}#identity456`,
|
|
704
|
-
pixelId,
|
|
705
|
-
identityId: 'identity456',
|
|
706
|
-
response: {
|
|
707
|
-
identityId: 'identity456',
|
|
708
|
-
traits: {
|
|
709
|
-
emails: ['test@email.com'],
|
|
710
|
-
phones: ['555-1234'],
|
|
711
|
-
addresses: [],
|
|
712
|
-
},
|
|
713
|
-
},
|
|
714
|
-
updatedAt: new Date().toISOString(),
|
|
715
|
-
};
|
|
716
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
717
|
-
const incomingWithAddress = {
|
|
718
|
-
identityId: 'identity456',
|
|
719
|
-
traits: {
|
|
720
|
-
emails: ['test@email.com'],
|
|
721
|
-
addresses: [{ firstName: 'Jane', city: 'Boston', zip: '02101' }],
|
|
722
|
-
},
|
|
723
|
-
};
|
|
724
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
725
|
-
expect(result.identity).toBeDefined();
|
|
726
|
-
expect(result.identity?.identityId).toBe('identity456');
|
|
727
|
-
expect(result.identity?.traits?.emails).toContain('test@email.com');
|
|
728
|
-
expect(result.identity?.traits?.addresses).toHaveLength(1);
|
|
729
|
-
expect(result.identity?.traits?.addresses?.[0]?.firstName).toBe('Jane');
|
|
730
|
-
});
|
|
731
|
-
it('should include writeReason ADDRESS_UPDATE in summary', async () => {
|
|
732
|
-
IdentityGatewayService.configure({
|
|
733
|
-
awsRegion: 'us-east-1',
|
|
734
|
-
awsAccountId: '123456789',
|
|
735
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
736
|
-
});
|
|
737
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-789' });
|
|
738
|
-
const cachedResponse = {
|
|
739
|
-
pk: `identity#${pixelId}#identity456`,
|
|
740
|
-
pixelId,
|
|
741
|
-
identityId: 'identity456',
|
|
742
|
-
response: {
|
|
743
|
-
identityId: 'identity456',
|
|
744
|
-
traits: {
|
|
745
|
-
emails: ['test@email.com'],
|
|
746
|
-
addresses: [],
|
|
747
|
-
},
|
|
748
|
-
},
|
|
749
|
-
updatedAt: new Date().toISOString(),
|
|
750
|
-
};
|
|
751
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
752
|
-
const incomingWithAddress = {
|
|
753
|
-
identityId: 'identity456',
|
|
754
|
-
traits: {
|
|
755
|
-
emails: ['test@email.com'],
|
|
756
|
-
addresses: [{ firstName: 'Bob', city: 'Seattle' }],
|
|
757
|
-
},
|
|
758
|
-
};
|
|
759
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
760
|
-
expect(result.summary.writeReason).toBe('address_update');
|
|
761
|
-
});
|
|
762
|
-
it('should include traitChanges for address update', async () => {
|
|
763
|
-
IdentityGatewayService.configure({
|
|
764
|
-
awsRegion: 'us-east-1',
|
|
765
|
-
awsAccountId: '123456789',
|
|
766
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
767
|
-
});
|
|
768
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-trait' });
|
|
769
|
-
const cachedResponse = {
|
|
770
|
-
pk: `identity#${pixelId}#identity456`,
|
|
771
|
-
pixelId,
|
|
772
|
-
identityId: 'identity456',
|
|
773
|
-
response: {
|
|
774
|
-
identityId: 'identity456',
|
|
775
|
-
traits: {
|
|
776
|
-
emails: ['test@email.com'],
|
|
777
|
-
addresses: [],
|
|
778
|
-
},
|
|
779
|
-
},
|
|
780
|
-
updatedAt: new Date().toISOString(),
|
|
781
|
-
};
|
|
782
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
783
|
-
const incomingWithAddress = {
|
|
784
|
-
identityId: 'identity456',
|
|
785
|
-
traits: {
|
|
786
|
-
emails: ['test@email.com'],
|
|
787
|
-
addresses: [{ firstName: 'Alice', city: 'Chicago' }],
|
|
788
|
-
},
|
|
789
|
-
};
|
|
790
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
791
|
-
expect(result.summary.traitChanges).toBeDefined();
|
|
792
|
-
});
|
|
793
|
-
});
|
|
794
|
-
describe('ADDRESS_UPDATE async failure handling', () => {
|
|
795
|
-
it('should set didEnqueueAsync to false when enqueue fails', async () => {
|
|
796
|
-
IdentityGatewayService.configure({
|
|
797
|
-
awsRegion: 'us-east-1',
|
|
798
|
-
awsAccountId: '123456789',
|
|
799
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
800
|
-
});
|
|
801
|
-
mockBuildAndSendMessage.mockRejectedValueOnce(new Error('SQS unavailable'));
|
|
802
|
-
const cachedResponse = {
|
|
803
|
-
pk: `identity#${pixelId}#identity456`,
|
|
804
|
-
pixelId,
|
|
805
|
-
identityId: 'identity456',
|
|
806
|
-
response: {
|
|
807
|
-
identityId: 'identity456',
|
|
808
|
-
traits: {
|
|
809
|
-
emails: ['test@email.com'],
|
|
810
|
-
addresses: [],
|
|
811
|
-
},
|
|
812
|
-
},
|
|
813
|
-
updatedAt: new Date().toISOString(),
|
|
814
|
-
};
|
|
815
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
816
|
-
const incomingWithAddress = {
|
|
817
|
-
identityId: 'identity456',
|
|
818
|
-
traits: {
|
|
819
|
-
emails: ['test@email.com'],
|
|
820
|
-
addresses: [{ firstName: 'Test', city: 'TestCity' }],
|
|
821
|
-
},
|
|
822
|
-
};
|
|
823
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
824
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
825
|
-
expect(result.summary.didEnqueueAsync).toBe(false);
|
|
826
|
-
expect(result.identity).toBeDefined();
|
|
827
|
-
expect(result.identity?.identityId).toBe('identity456');
|
|
828
|
-
});
|
|
829
|
-
it('should log warning when enqueue fails', async () => {
|
|
830
|
-
IdentityGatewayService.configure({
|
|
831
|
-
awsRegion: 'us-east-1',
|
|
832
|
-
awsAccountId: '123456789',
|
|
833
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
834
|
-
});
|
|
835
|
-
mockBuildAndSendMessage.mockRejectedValueOnce(new Error('Network timeout'));
|
|
836
|
-
const cachedResponse = {
|
|
837
|
-
pk: `identity#${pixelId}#identity456`,
|
|
838
|
-
pixelId,
|
|
839
|
-
identityId: 'identity456',
|
|
840
|
-
response: {
|
|
841
|
-
identityId: 'identity456',
|
|
842
|
-
traits: {
|
|
843
|
-
emails: ['test@email.com'],
|
|
844
|
-
addresses: [],
|
|
845
|
-
},
|
|
846
|
-
},
|
|
847
|
-
updatedAt: new Date().toISOString(),
|
|
848
|
-
};
|
|
849
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
850
|
-
const incomingWithAddress = {
|
|
851
|
-
identityId: 'identity456',
|
|
852
|
-
traits: {
|
|
853
|
-
emails: ['test@email.com'],
|
|
854
|
-
addresses: [{ firstName: 'Test', city: 'TestCity' }],
|
|
855
|
-
},
|
|
856
|
-
};
|
|
857
|
-
await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
858
|
-
expect(mockedLogger.warn).toHaveBeenCalledWith('IdentityGateway - enqueueAsyncIdentityUpdate: failed to enqueue (failing open)', expect.objectContaining({
|
|
859
|
-
error: expect.any(Error),
|
|
860
|
-
errorMessage: 'Network timeout',
|
|
861
|
-
pixelId,
|
|
862
|
-
}));
|
|
863
|
-
});
|
|
864
|
-
it('should set didEnqueueAsync to false when SQS client is not configured', async () => {
|
|
865
|
-
const cachedResponse = {
|
|
866
|
-
pk: `identity#${pixelId}#identity456`,
|
|
867
|
-
pixelId,
|
|
868
|
-
identityId: 'identity456',
|
|
869
|
-
response: {
|
|
870
|
-
identityId: 'identity456',
|
|
871
|
-
traits: {
|
|
872
|
-
emails: ['test@email.com'],
|
|
873
|
-
addresses: [],
|
|
874
|
-
},
|
|
875
|
-
},
|
|
876
|
-
updatedAt: new Date().toISOString(),
|
|
877
|
-
};
|
|
878
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
879
|
-
const incomingWithAddress = {
|
|
880
|
-
identityId: 'identity456',
|
|
881
|
-
traits: {
|
|
882
|
-
emails: ['test@email.com'],
|
|
883
|
-
addresses: [{ firstName: 'Test', city: 'TestCity' }],
|
|
884
|
-
},
|
|
885
|
-
};
|
|
886
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
887
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
888
|
-
expect(result.summary.didEnqueueAsync).toBe(false);
|
|
889
|
-
expect(result.identity).toBeDefined();
|
|
890
|
-
});
|
|
891
|
-
});
|
|
892
|
-
describe('IDENTITY_STALE path (comparison with ADDRESS_UPDATE)', () => {
|
|
893
|
-
it('should call Neptune when staleness tier is IDENTITY_STALE (not ADDRESS_UPDATE)', async () => {
|
|
894
|
-
IdentityGatewayService.configure({
|
|
895
|
-
awsRegion: 'us-east-1',
|
|
896
|
-
awsAccountId: '123456789',
|
|
897
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
898
|
-
});
|
|
899
|
-
const cachedResponse = {
|
|
900
|
-
pk: `identity#${pixelId}#identity456`,
|
|
901
|
-
pixelId,
|
|
902
|
-
identityId: 'identity456',
|
|
903
|
-
response: {
|
|
904
|
-
identityId: 'identity456',
|
|
905
|
-
traits: {
|
|
906
|
-
emails: ['old@email.com'],
|
|
907
|
-
},
|
|
908
|
-
},
|
|
909
|
-
updatedAt: new Date().toISOString(),
|
|
910
|
-
};
|
|
911
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
912
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
913
|
-
statusCode: 200,
|
|
914
|
-
body: JSON.stringify({
|
|
915
|
-
identity: {
|
|
916
|
-
identityId: 'identity456',
|
|
917
|
-
traits: {
|
|
918
|
-
emails: ['old@email.com', 'new@email.com'],
|
|
919
|
-
},
|
|
920
|
-
},
|
|
921
|
-
}),
|
|
922
|
-
});
|
|
923
|
-
const incomingWithNewEmail = {
|
|
924
|
-
identityId: 'identity456',
|
|
925
|
-
traits: {
|
|
926
|
-
emails: ['old@email.com', 'new@email.com'],
|
|
927
|
-
},
|
|
928
|
-
};
|
|
929
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithNewEmail, lambdaArn);
|
|
930
|
-
expect(mockInvokeFunction).toHaveBeenCalled();
|
|
931
|
-
expect(result.summary.didCallNeptune).toBe(true);
|
|
932
|
-
expect(result.summary.didSkipNeptune).toBe(false);
|
|
933
|
-
expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
|
|
934
|
-
expect(result.summary.didEnqueueAsync).toBe(false);
|
|
935
|
-
expect(result.summary.outcome).not.toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
936
|
-
});
|
|
937
|
-
it('should NOT call Neptune when staleness tier is ADDRESS_UPDATE', async () => {
|
|
938
|
-
IdentityGatewayService.configure({
|
|
939
|
-
awsRegion: 'us-east-1',
|
|
940
|
-
awsAccountId: '123456789',
|
|
941
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
942
|
-
});
|
|
943
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-no-neptune' });
|
|
944
|
-
const cachedResponse = {
|
|
945
|
-
pk: `identity#${pixelId}#identity456`,
|
|
946
|
-
pixelId,
|
|
947
|
-
identityId: 'identity456',
|
|
948
|
-
response: {
|
|
949
|
-
identityId: 'identity456',
|
|
950
|
-
traits: {
|
|
951
|
-
emails: ['test@email.com'],
|
|
952
|
-
addresses: [],
|
|
953
|
-
},
|
|
954
|
-
},
|
|
955
|
-
updatedAt: new Date().toISOString(),
|
|
956
|
-
};
|
|
957
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
958
|
-
const incomingWithAddress = {
|
|
959
|
-
identityId: 'identity456',
|
|
960
|
-
traits: {
|
|
961
|
-
emails: ['test@email.com'],
|
|
962
|
-
addresses: [{ firstName: 'Test', city: 'TestCity' }],
|
|
963
|
-
},
|
|
964
|
-
};
|
|
965
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
966
|
-
expect(mockInvokeFunction).not.toHaveBeenCalled();
|
|
967
|
-
expect(result.summary.didCallNeptune).toBe(false);
|
|
968
|
-
expect(result.summary.didSkipNeptune).toBe(true);
|
|
969
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
970
|
-
});
|
|
971
|
-
it('should write cache from Neptune in IDENTITY_STALE path (identity-service handles it)', async () => {
|
|
972
|
-
const cachedResponse = {
|
|
973
|
-
pk: `identity#${pixelId}#identity456`,
|
|
974
|
-
pixelId,
|
|
975
|
-
identityId: 'identity456',
|
|
976
|
-
response: {
|
|
977
|
-
identityId: 'identity456',
|
|
978
|
-
traits: {
|
|
979
|
-
emails: ['old@email.com'],
|
|
980
|
-
},
|
|
981
|
-
},
|
|
982
|
-
updatedAt: new Date().toISOString(),
|
|
983
|
-
};
|
|
984
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
985
|
-
mockInvokeFunction.mockResolvedValueOnce({
|
|
986
|
-
statusCode: 200,
|
|
987
|
-
body: JSON.stringify({
|
|
988
|
-
identity: {
|
|
989
|
-
identityId: 'identity456',
|
|
990
|
-
traits: {
|
|
991
|
-
emails: ['old@email.com', 'new@email.com'],
|
|
992
|
-
},
|
|
993
|
-
},
|
|
994
|
-
}),
|
|
995
|
-
});
|
|
996
|
-
const incomingWithNewEmail = {
|
|
997
|
-
identityId: 'identity456',
|
|
998
|
-
traits: {
|
|
999
|
-
emails: ['old@email.com', 'new@email.com'],
|
|
1000
|
-
},
|
|
1001
|
-
};
|
|
1002
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithNewEmail, lambdaArn);
|
|
1003
|
-
expect(result.summary.didCallNeptune).toBe(true);
|
|
1004
|
-
expect(result.summary.didWriteCache).toBe(false);
|
|
1005
|
-
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1006
|
-
});
|
|
1007
|
-
});
|
|
1008
|
-
describe('summary correctness for ADDRESS_UPDATE path', () => {
|
|
1009
|
-
it('should have correct summary fields for successful ADDRESS_UPDATE async enqueue', async () => {
|
|
1010
|
-
IdentityGatewayService.configure({
|
|
1011
|
-
awsRegion: 'us-east-1',
|
|
1012
|
-
awsAccountId: '123456789',
|
|
1013
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1014
|
-
});
|
|
1015
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'summary-test-msg' });
|
|
1016
|
-
const cachedResponse = {
|
|
1017
|
-
pk: `identity#${pixelId}#identity456`,
|
|
1018
|
-
pixelId,
|
|
1019
|
-
identityId: 'identity456',
|
|
1020
|
-
response: {
|
|
1021
|
-
identityId: 'identity456',
|
|
1022
|
-
traits: {
|
|
1023
|
-
emails: ['test@email.com'],
|
|
1024
|
-
addresses: [],
|
|
1025
|
-
},
|
|
1026
|
-
},
|
|
1027
|
-
updatedAt: new Date().toISOString(),
|
|
1028
|
-
};
|
|
1029
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1030
|
-
const incomingWithAddress = {
|
|
1031
|
-
identityId: 'identity456',
|
|
1032
|
-
traits: {
|
|
1033
|
-
emails: ['test@email.com'],
|
|
1034
|
-
addresses: [{ firstName: 'Summary', city: 'SummaryCity' }],
|
|
1035
|
-
},
|
|
1036
|
-
};
|
|
1037
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
1038
|
-
expect(result.summary).toEqual(expect.objectContaining({
|
|
1039
|
-
outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC,
|
|
1040
|
-
pixelId,
|
|
1041
|
-
identityId: 'identity456',
|
|
1042
|
-
didReadCache: true,
|
|
1043
|
-
didCallNeptune: false,
|
|
1044
|
-
didWriteCache: false,
|
|
1045
|
-
didSkipNeptune: true,
|
|
1046
|
-
didFallbackToIncoming: false,
|
|
1047
|
-
didEnqueueAsync: true,
|
|
1048
|
-
writeReason: 'address_update',
|
|
1049
|
-
}));
|
|
1050
|
-
});
|
|
1051
|
-
it('should have correct summary fields for failed ADDRESS_UPDATE async enqueue', async () => {
|
|
1052
|
-
IdentityGatewayService.configure({
|
|
1053
|
-
awsRegion: 'us-east-1',
|
|
1054
|
-
awsAccountId: '123456789',
|
|
1055
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1056
|
-
});
|
|
1057
|
-
mockBuildAndSendMessage.mockRejectedValueOnce(new Error('Enqueue failed'));
|
|
1058
|
-
const cachedResponse = {
|
|
1059
|
-
pk: `identity#${pixelId}#identity456`,
|
|
1060
|
-
pixelId,
|
|
1061
|
-
identityId: 'identity456',
|
|
1062
|
-
response: {
|
|
1063
|
-
identityId: 'identity456',
|
|
1064
|
-
traits: {
|
|
1065
|
-
emails: ['test@email.com'],
|
|
1066
|
-
addresses: [],
|
|
1067
|
-
},
|
|
1068
|
-
},
|
|
1069
|
-
updatedAt: new Date().toISOString(),
|
|
1070
|
-
};
|
|
1071
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1072
|
-
const incomingWithAddress = {
|
|
1073
|
-
identityId: 'identity456',
|
|
1074
|
-
traits: {
|
|
1075
|
-
emails: ['test@email.com'],
|
|
1076
|
-
addresses: [{ firstName: 'Fail', city: 'FailCity' }],
|
|
1077
|
-
},
|
|
1078
|
-
};
|
|
1079
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
|
|
1080
|
-
expect(result.summary).toEqual(expect.objectContaining({
|
|
1081
|
-
outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC,
|
|
1082
|
-
pixelId,
|
|
1083
|
-
identityId: 'identity456',
|
|
1084
|
-
didReadCache: true,
|
|
1085
|
-
didCallNeptune: false,
|
|
1086
|
-
didWriteCache: false,
|
|
1087
|
-
didSkipNeptune: true,
|
|
1088
|
-
didFallbackToIncoming: false,
|
|
1089
|
-
didEnqueueAsync: false,
|
|
1090
|
-
writeReason: 'address_update',
|
|
1091
|
-
}));
|
|
1092
|
-
});
|
|
1093
|
-
});
|
|
1094
|
-
describe('IP-sourced vs non-IP address behavior', () => {
|
|
1095
|
-
it('should NOT trigger ADDRESS_UPDATE for IP-sourced addresses (VOLATILE_ONLY tier)', async () => {
|
|
1096
|
-
IdentityGatewayService.configure({
|
|
1097
|
-
awsRegion: 'us-east-1',
|
|
1098
|
-
awsAccountId: '123456789',
|
|
1099
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1100
|
-
});
|
|
1101
|
-
const cachedResponse = {
|
|
1102
|
-
pk: `identity#${pixelId}#identity456`,
|
|
1103
|
-
pixelId,
|
|
1104
|
-
identityId: 'identity456',
|
|
1105
|
-
response: {
|
|
1106
|
-
identityId: 'identity456',
|
|
1107
|
-
traits: {
|
|
1108
|
-
emails: ['test@email.com'],
|
|
1109
|
-
addresses: [],
|
|
1110
|
-
},
|
|
1111
|
-
},
|
|
1112
|
-
updatedAt: new Date().toISOString(),
|
|
1113
|
-
};
|
|
1114
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1115
|
-
const incomingWithIpAddress = {
|
|
1116
|
-
identityId: 'identity456',
|
|
1117
|
-
traits: {
|
|
1118
|
-
emails: ['test@email.com'],
|
|
1119
|
-
addresses: [{ city: 'IPCity', country: 'US' }],
|
|
1120
|
-
},
|
|
1121
|
-
};
|
|
1122
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithIpAddress, lambdaArn);
|
|
1123
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
|
|
1124
|
-
expect(result.summary.didEnqueueAsync).toBe(false);
|
|
1125
|
-
expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
|
|
1126
|
-
});
|
|
1127
|
-
it('should trigger ADDRESS_UPDATE for non-IP address (has PII fields)', async () => {
|
|
1128
|
-
IdentityGatewayService.configure({
|
|
1129
|
-
awsRegion: 'us-east-1',
|
|
1130
|
-
awsAccountId: '123456789',
|
|
1131
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1132
|
-
});
|
|
1133
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'non-ip-msg' });
|
|
367
|
+
expect(result.identity?.traits?.emails).toContain('new@email.com');
|
|
368
|
+
});
|
|
369
|
+
it('should return fresh cache hit without writing (skipCacheWrite irrelevant for hits)', async () => {
|
|
1134
370
|
const cachedResponse = {
|
|
1135
371
|
pk: `identity#${pixelId}#identity456`,
|
|
1136
372
|
pixelId,
|
|
1137
373
|
identityId: 'identity456',
|
|
1138
374
|
response: {
|
|
1139
375
|
identityId: 'identity456',
|
|
1140
|
-
traits: {
|
|
1141
|
-
emails: ['test@email.com'],
|
|
1142
|
-
addresses: [],
|
|
1143
|
-
},
|
|
376
|
+
traits: { emails: ['test@email.com'] },
|
|
1144
377
|
},
|
|
1145
378
|
updatedAt: new Date().toISOString(),
|
|
1146
379
|
};
|
|
1147
380
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1148
|
-
const
|
|
381
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
382
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
|
|
383
|
+
expect(result.didWriteCache).toBe(false);
|
|
384
|
+
expect(mockGetIdentity).not.toHaveBeenCalled();
|
|
385
|
+
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
describe('edge cases', () => {
|
|
390
|
+
it('should return invalid input when pixelId is undefined', async () => {
|
|
391
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(undefined, { identityId: 'test' });
|
|
392
|
+
expect(result.identity).toBeUndefined();
|
|
393
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
|
|
394
|
+
expect(result.didReadCache).toBe(false);
|
|
395
|
+
expect(result.didWriteCache).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
it('should return invalid input when incomingIdentity is null', async () => {
|
|
398
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, null);
|
|
399
|
+
expect(result.identity).toBeUndefined();
|
|
400
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
|
|
401
|
+
});
|
|
402
|
+
it('should return invalid input when both pixelId and identity are missing', async () => {
|
|
403
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching('', undefined);
|
|
404
|
+
expect(result.identity).toBeUndefined();
|
|
405
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
|
|
406
|
+
});
|
|
407
|
+
it('should gracefully fallback when cache read fails (fail-open pattern)', async () => {
|
|
408
|
+
mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('Unexpected DynamoDB error'));
|
|
409
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
410
|
+
identityId: 'neptune-resolved',
|
|
411
|
+
traits: { emails: ['test@email.com'] },
|
|
412
|
+
});
|
|
413
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test-id', traits: { emails: ['test@email.com'] } });
|
|
414
|
+
expect(result.identity).toBeDefined();
|
|
415
|
+
expect(result.identity?.identityId).toBe('neptune-resolved');
|
|
416
|
+
expect(result.didReadCache).toBe(true);
|
|
417
|
+
expect(result.didCallNeptune).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
it('should reject identity with undefined identityId in enqueue', async () => {
|
|
420
|
+
IdentityGatewayService.configure({
|
|
421
|
+
awsRegion: 'us-east-1',
|
|
422
|
+
awsAccountId: '123456789',
|
|
423
|
+
asyncUpdateQueueName: 'test-queue',
|
|
424
|
+
});
|
|
425
|
+
const identity = { traits: { emails: ['test@email.com'] } };
|
|
426
|
+
const context = { identity };
|
|
427
|
+
const result = await IdentityGatewayService.enqueueAsyncIdentityUpdate({
|
|
428
|
+
pixelId,
|
|
429
|
+
identity,
|
|
430
|
+
context,
|
|
431
|
+
reason: 'anonymous user',
|
|
432
|
+
});
|
|
433
|
+
expect(result.success).toBe(false);
|
|
434
|
+
expect(result.error).toBe('Missing required pixelId or identityId');
|
|
435
|
+
expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
|
|
436
|
+
expect(mockedLogger.warn).toHaveBeenCalledWith('IdentityGateway - enqueueAsyncIdentityUpdate: missing required params', expect.objectContaining({
|
|
437
|
+
pixelId,
|
|
438
|
+
identityId: undefined,
|
|
439
|
+
}));
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
describe('safeWriteBackToCache behavior with skipCacheWrite', () => {
|
|
443
|
+
it('should not call safeWriteBackToCache when skipCacheWrite is true and Neptune returns new identity', async () => {
|
|
444
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
445
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
446
|
+
identityId: 'new-resolved-identity',
|
|
447
|
+
traits: { emails: ['test@email.com'] },
|
|
448
|
+
});
|
|
449
|
+
await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'original-id', traits: { emails: ['test@email.com'] } });
|
|
450
|
+
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
describe('result fields consistency', () => {
|
|
454
|
+
it('should include all required fields in resolveIdentityWithCaching result', async () => {
|
|
455
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
456
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
457
|
+
identityId: 'resolved-id',
|
|
458
|
+
traits: { emails: ['test@email.com'] },
|
|
459
|
+
});
|
|
460
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming-id', traits: { emails: ['test@email.com'] } });
|
|
461
|
+
expect(result).toEqual(expect.objectContaining({
|
|
462
|
+
outcome: expect.any(String),
|
|
463
|
+
pixelId,
|
|
464
|
+
identityId: 'resolved-id',
|
|
465
|
+
didReadCache: true,
|
|
466
|
+
didCallNeptune: true,
|
|
467
|
+
didWriteCache: false,
|
|
468
|
+
didSkipNeptune: false,
|
|
469
|
+
didFallbackToIncoming: false,
|
|
470
|
+
didEnqueueAsync: false,
|
|
471
|
+
}));
|
|
472
|
+
});
|
|
473
|
+
it('should include traitChanges in summary when identity changes', async () => {
|
|
474
|
+
const cachedResponse = {
|
|
475
|
+
pk: `identity#${pixelId}#identity456`,
|
|
476
|
+
pixelId,
|
|
477
|
+
identityId: 'identity456',
|
|
478
|
+
response: {
|
|
1149
479
|
identityId: 'identity456',
|
|
1150
|
-
traits: {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
expect(mockBuildAndSendMessage).toHaveBeenCalled();
|
|
480
|
+
traits: { emails: ['old@email.com'] },
|
|
481
|
+
},
|
|
482
|
+
updatedAt: new Date().toISOString(),
|
|
483
|
+
};
|
|
484
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
485
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
486
|
+
identityId: 'identity456',
|
|
487
|
+
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1159
488
|
});
|
|
489
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'identity456', traits: { emails: ['old@email.com', 'new@email.com'] } });
|
|
490
|
+
if (result.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED) {
|
|
491
|
+
expect(result.traitChanges).toBeDefined();
|
|
492
|
+
}
|
|
1160
493
|
});
|
|
1161
494
|
});
|
|
1162
495
|
describe('refactored flow: merge and staleness computation', () => {
|
|
@@ -1182,7 +515,7 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1182
515
|
emails: ['cached@email.com'],
|
|
1183
516
|
},
|
|
1184
517
|
};
|
|
1185
|
-
const result = await IdentityGatewayService.
|
|
518
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
1186
519
|
expect(result.identity?.traits?.emails).toContain('cached@email.com');
|
|
1187
520
|
expect(result.identity?.traits?.phones).toContain('555-cached');
|
|
1188
521
|
});
|
|
@@ -1200,14 +533,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1200
533
|
updatedAt: new Date().toISOString(),
|
|
1201
534
|
};
|
|
1202
535
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
identity: {
|
|
1207
|
-
identityId: 'staleness-compute',
|
|
1208
|
-
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1209
|
-
},
|
|
1210
|
-
}),
|
|
536
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
537
|
+
identityId: 'staleness-compute',
|
|
538
|
+
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1211
539
|
});
|
|
1212
540
|
const incomingIdentity = {
|
|
1213
541
|
identityId: 'staleness-compute',
|
|
@@ -1215,9 +543,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1215
543
|
emails: ['old@email.com', 'new@email.com'],
|
|
1216
544
|
},
|
|
1217
545
|
};
|
|
1218
|
-
const result = await IdentityGatewayService.
|
|
1219
|
-
expect(result.
|
|
1220
|
-
expect(result.
|
|
546
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
547
|
+
expect(result.didCallNeptune).toBe(true);
|
|
548
|
+
expect(result.stalenessResult?.staleTraits).toContain('emails');
|
|
1221
549
|
});
|
|
1222
550
|
});
|
|
1223
551
|
describe('deep equality check', () => {
|
|
@@ -1243,10 +571,10 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1243
571
|
phones: ['555-1234'],
|
|
1244
572
|
},
|
|
1245
573
|
};
|
|
1246
|
-
const result = await IdentityGatewayService.
|
|
1247
|
-
expect(result.
|
|
1248
|
-
expect(result.
|
|
1249
|
-
expect(
|
|
574
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
575
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
|
|
576
|
+
expect(result.didCallNeptune).toBe(false);
|
|
577
|
+
expect(mockGetIdentity).not.toHaveBeenCalled();
|
|
1250
578
|
});
|
|
1251
579
|
it('should strip address timestamps before deep equality comparison', async () => {
|
|
1252
580
|
const now = Date.now();
|
|
@@ -1271,9 +599,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1271
599
|
addresses: [{ city: 'NYC', cra: now, upa: now }],
|
|
1272
600
|
},
|
|
1273
601
|
};
|
|
1274
|
-
const result = await IdentityGatewayService.
|
|
1275
|
-
expect(result.
|
|
1276
|
-
expect(result.
|
|
602
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
603
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
|
|
604
|
+
expect(result.didCallNeptune).toBe(false);
|
|
1277
605
|
});
|
|
1278
606
|
});
|
|
1279
607
|
describe('volatile-only override', () => {
|
|
@@ -1299,10 +627,10 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1299
627
|
userAgent: 'Mozilla/5.0 (New)',
|
|
1300
628
|
},
|
|
1301
629
|
};
|
|
1302
|
-
const result = await IdentityGatewayService.
|
|
1303
|
-
expect(result.
|
|
1304
|
-
expect(result.
|
|
1305
|
-
expect(result.
|
|
630
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
631
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
|
|
632
|
+
expect(result.didCallNeptune).toBe(false);
|
|
633
|
+
expect(result.didWriteCache).toBe(false);
|
|
1306
634
|
});
|
|
1307
635
|
it('should return merged identity for VOLATILE_ONLY tier', async () => {
|
|
1308
636
|
const cachedResponse = {
|
|
@@ -1326,47 +654,14 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1326
654
|
ipAddress: '10.0.0.1',
|
|
1327
655
|
},
|
|
1328
656
|
};
|
|
1329
|
-
const result = await IdentityGatewayService.
|
|
1330
|
-
expect(result.
|
|
657
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
658
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
|
|
1331
659
|
expect(result.identity?.traits?.emails).toContain('test@email.com');
|
|
1332
660
|
expect(result.identity?.traits?.phones).toContain('555-cached');
|
|
1333
661
|
expect(result.identity?.traits?.ipAddress).toBe('10.0.0.1');
|
|
1334
662
|
});
|
|
1335
663
|
});
|
|
1336
664
|
describe('skipNeptune derivation from staleTier', () => {
|
|
1337
|
-
it('should set skipNeptune=true for ADDRESS_UPDATE tier', async () => {
|
|
1338
|
-
IdentityGatewayService.configure({
|
|
1339
|
-
awsRegion: 'us-east-1',
|
|
1340
|
-
awsAccountId: '123456789',
|
|
1341
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1342
|
-
});
|
|
1343
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'skip-neptune-test' });
|
|
1344
|
-
const cachedResponse = {
|
|
1345
|
-
pk: `identity#${pixelId}#skip-neptune-addr`,
|
|
1346
|
-
pixelId,
|
|
1347
|
-
identityId: 'skip-neptune-addr',
|
|
1348
|
-
response: {
|
|
1349
|
-
identityId: 'skip-neptune-addr',
|
|
1350
|
-
traits: {
|
|
1351
|
-
emails: ['test@email.com'],
|
|
1352
|
-
addresses: [],
|
|
1353
|
-
},
|
|
1354
|
-
},
|
|
1355
|
-
updatedAt: new Date().toISOString(),
|
|
1356
|
-
};
|
|
1357
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1358
|
-
const incomingIdentity = {
|
|
1359
|
-
identityId: 'skip-neptune-addr',
|
|
1360
|
-
traits: {
|
|
1361
|
-
emails: ['test@email.com'],
|
|
1362
|
-
addresses: [{ firstName: 'John', city: 'NYC' }],
|
|
1363
|
-
},
|
|
1364
|
-
};
|
|
1365
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
|
|
1366
|
-
expect(result.summary.didSkipNeptune).toBe(true);
|
|
1367
|
-
expect(result.summary.didCallNeptune).toBe(false);
|
|
1368
|
-
expect(mockInvokeFunction).not.toHaveBeenCalled();
|
|
1369
|
-
});
|
|
1370
665
|
it('should set skipNeptune=false for IDENTITY_STALE tier', async () => {
|
|
1371
666
|
const cachedResponse = {
|
|
1372
667
|
pk: `identity#${pixelId}#call-neptune`,
|
|
@@ -1381,14 +676,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1381
676
|
updatedAt: new Date().toISOString(),
|
|
1382
677
|
};
|
|
1383
678
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
identity: {
|
|
1388
|
-
identityId: 'call-neptune',
|
|
1389
|
-
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1390
|
-
},
|
|
1391
|
-
}),
|
|
679
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
680
|
+
identityId: 'call-neptune',
|
|
681
|
+
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1392
682
|
});
|
|
1393
683
|
const incomingIdentity = {
|
|
1394
684
|
identityId: 'call-neptune',
|
|
@@ -1396,44 +686,13 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1396
686
|
emails: ['old@email.com', 'new@email.com'],
|
|
1397
687
|
},
|
|
1398
688
|
};
|
|
1399
|
-
const result = await IdentityGatewayService.
|
|
1400
|
-
expect(result.
|
|
1401
|
-
expect(result.
|
|
1402
|
-
expect(
|
|
689
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
690
|
+
expect(result.didSkipNeptune).toBe(false);
|
|
691
|
+
expect(result.didCallNeptune).toBe(true);
|
|
692
|
+
expect(mockGetIdentity).toHaveBeenCalled();
|
|
1403
693
|
});
|
|
1404
694
|
});
|
|
1405
695
|
describe('writeReason derivation from staleTier', () => {
|
|
1406
|
-
it('should set writeReason=address_update for ADDRESS_UPDATE tier', async () => {
|
|
1407
|
-
IdentityGatewayService.configure({
|
|
1408
|
-
awsRegion: 'us-east-1',
|
|
1409
|
-
awsAccountId: '123456789',
|
|
1410
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1411
|
-
});
|
|
1412
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'addr-reason-test' });
|
|
1413
|
-
const cachedResponse = {
|
|
1414
|
-
pk: `identity#${pixelId}#addr-reason`,
|
|
1415
|
-
pixelId,
|
|
1416
|
-
identityId: 'addr-reason',
|
|
1417
|
-
response: {
|
|
1418
|
-
identityId: 'addr-reason',
|
|
1419
|
-
traits: {
|
|
1420
|
-
emails: ['test@email.com'],
|
|
1421
|
-
addresses: [],
|
|
1422
|
-
},
|
|
1423
|
-
},
|
|
1424
|
-
updatedAt: new Date().toISOString(),
|
|
1425
|
-
};
|
|
1426
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1427
|
-
const incomingIdentity = {
|
|
1428
|
-
identityId: 'addr-reason',
|
|
1429
|
-
traits: {
|
|
1430
|
-
emails: ['test@email.com'],
|
|
1431
|
-
addresses: [{ firstName: 'Test', city: 'TestCity' }],
|
|
1432
|
-
},
|
|
1433
|
-
};
|
|
1434
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
|
|
1435
|
-
expect(result.summary.writeReason).toBe('address_update');
|
|
1436
|
-
});
|
|
1437
696
|
it('should set writeReason=stale_traits for IDENTITY_STALE tier', async () => {
|
|
1438
697
|
const cachedResponse = {
|
|
1439
698
|
pk: `identity#${pixelId}#stale-reason`,
|
|
@@ -1448,14 +707,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1448
707
|
updatedAt: new Date().toISOString(),
|
|
1449
708
|
};
|
|
1450
709
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
identity: {
|
|
1455
|
-
identityId: 'stale-reason',
|
|
1456
|
-
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1457
|
-
},
|
|
1458
|
-
}),
|
|
710
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
711
|
+
identityId: 'stale-reason',
|
|
712
|
+
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1459
713
|
});
|
|
1460
714
|
const incomingIdentity = {
|
|
1461
715
|
identityId: 'stale-reason',
|
|
@@ -1463,84 +717,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1463
717
|
emails: ['old@email.com', 'new@email.com'],
|
|
1464
718
|
},
|
|
1465
719
|
};
|
|
1466
|
-
const result = await IdentityGatewayService.
|
|
1467
|
-
expect(result.
|
|
1468
|
-
expect(result.
|
|
1469
|
-
});
|
|
1470
|
-
});
|
|
1471
|
-
describe('ADDRESS_UPDATE → enqueueAsyncIdentityUpdate path', () => {
|
|
1472
|
-
it('should enqueue async update instead of calling Neptune', async () => {
|
|
1473
|
-
IdentityGatewayService.configure({
|
|
1474
|
-
awsRegion: 'us-east-1',
|
|
1475
|
-
awsAccountId: '123456789',
|
|
1476
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1477
|
-
});
|
|
1478
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'async-path-test' });
|
|
1479
|
-
const cachedResponse = {
|
|
1480
|
-
pk: `identity#${pixelId}#async-enqueue`,
|
|
1481
|
-
pixelId,
|
|
1482
|
-
identityId: 'async-enqueue',
|
|
1483
|
-
response: {
|
|
1484
|
-
identityId: 'async-enqueue',
|
|
1485
|
-
traits: {
|
|
1486
|
-
emails: ['test@email.com'],
|
|
1487
|
-
addresses: [],
|
|
1488
|
-
},
|
|
1489
|
-
},
|
|
1490
|
-
updatedAt: new Date().toISOString(),
|
|
1491
|
-
};
|
|
1492
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1493
|
-
const incomingIdentity = {
|
|
1494
|
-
identityId: 'async-enqueue',
|
|
1495
|
-
traits: {
|
|
1496
|
-
emails: ['test@email.com'],
|
|
1497
|
-
addresses: [{ firstName: 'Async', city: 'AsyncCity' }],
|
|
1498
|
-
},
|
|
1499
|
-
};
|
|
1500
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
|
|
1501
|
-
expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
|
|
1502
|
-
expect(result.summary.didEnqueueAsync).toBe(true);
|
|
1503
|
-
expect(result.summary.didCallNeptune).toBe(false);
|
|
1504
|
-
expect(mockBuildAndSendMessage).toHaveBeenCalledWith('identify.updateAsync', expect.objectContaining({
|
|
1505
|
-
pixelId,
|
|
1506
|
-
incomingIdentityId: 'async-enqueue',
|
|
1507
|
-
}));
|
|
1508
|
-
expect(mockInvokeFunction).not.toHaveBeenCalled();
|
|
1509
|
-
});
|
|
1510
|
-
it('should return merged identity immediately (no Neptune wait)', async () => {
|
|
1511
|
-
IdentityGatewayService.configure({
|
|
1512
|
-
awsRegion: 'us-east-1',
|
|
1513
|
-
awsAccountId: '123456789',
|
|
1514
|
-
asyncUpdateQueueName: 'identity-async-queue',
|
|
1515
|
-
});
|
|
1516
|
-
mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'immediate-return' });
|
|
1517
|
-
const cachedResponse = {
|
|
1518
|
-
pk: `identity#${pixelId}#immediate`,
|
|
1519
|
-
pixelId,
|
|
1520
|
-
identityId: 'immediate',
|
|
1521
|
-
response: {
|
|
1522
|
-
identityId: 'immediate',
|
|
1523
|
-
traits: {
|
|
1524
|
-
emails: ['test@email.com'],
|
|
1525
|
-
phones: ['555-cached'],
|
|
1526
|
-
addresses: [],
|
|
1527
|
-
},
|
|
1528
|
-
},
|
|
1529
|
-
updatedAt: new Date().toISOString(),
|
|
1530
|
-
};
|
|
1531
|
-
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1532
|
-
const incomingIdentity = {
|
|
1533
|
-
identityId: 'immediate',
|
|
1534
|
-
traits: {
|
|
1535
|
-
emails: ['test@email.com'],
|
|
1536
|
-
addresses: [{ firstName: 'Immediate', city: 'ImmediateCity' }],
|
|
1537
|
-
},
|
|
1538
|
-
};
|
|
1539
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
|
|
1540
|
-
expect(result.identity).toBeDefined();
|
|
1541
|
-
expect(result.identity?.identityId).toBe('immediate');
|
|
1542
|
-
expect(result.identity?.traits?.phones).toContain('555-cached');
|
|
1543
|
-
expect(result.identity?.traits?.addresses).toHaveLength(1);
|
|
720
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
721
|
+
expect(result.writeReason).toBeUndefined();
|
|
722
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1544
723
|
});
|
|
1545
724
|
});
|
|
1546
725
|
describe('IDENTITY_STALE → Neptune path', () => {
|
|
@@ -1558,14 +737,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1558
737
|
updatedAt: new Date().toISOString(),
|
|
1559
738
|
};
|
|
1560
739
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
identity: {
|
|
1565
|
-
identityId: 'neptune-path',
|
|
1566
|
-
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1567
|
-
},
|
|
1568
|
-
}),
|
|
740
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
741
|
+
identityId: 'neptune-path',
|
|
742
|
+
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
1569
743
|
});
|
|
1570
744
|
const incomingIdentity = {
|
|
1571
745
|
identityId: 'neptune-path',
|
|
@@ -1573,10 +747,12 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1573
747
|
emails: ['old@email.com', 'new@email.com'],
|
|
1574
748
|
},
|
|
1575
749
|
};
|
|
1576
|
-
const result = await IdentityGatewayService.
|
|
1577
|
-
expect(result.
|
|
1578
|
-
expect(
|
|
1579
|
-
|
|
750
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
751
|
+
expect(result.didCallNeptune).toBe(true);
|
|
752
|
+
expect(mockGetIdentity).toHaveBeenCalledWith(pixelId, expect.objectContaining({
|
|
753
|
+
identity: expect.objectContaining({
|
|
754
|
+
identityId: expect.any(String),
|
|
755
|
+
}),
|
|
1580
756
|
}));
|
|
1581
757
|
});
|
|
1582
758
|
it('should write Neptune result to cache when different from cached', async () => {
|
|
@@ -1593,17 +769,12 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1593
769
|
updatedAt: new Date().toISOString(),
|
|
1594
770
|
};
|
|
1595
771
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
emails: ['old@email.com', 'new@email.com'],
|
|
1603
|
-
phones: ['555-neptune'],
|
|
1604
|
-
},
|
|
1605
|
-
},
|
|
1606
|
-
}),
|
|
772
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
773
|
+
identityId: 'neptune-write',
|
|
774
|
+
traits: {
|
|
775
|
+
emails: ['old@email.com', 'new@email.com'],
|
|
776
|
+
phones: ['555-neptune'],
|
|
777
|
+
},
|
|
1607
778
|
});
|
|
1608
779
|
const incomingIdentity = {
|
|
1609
780
|
identityId: 'neptune-write',
|
|
@@ -1611,9 +782,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1611
782
|
emails: ['old@email.com', 'new@email.com'],
|
|
1612
783
|
},
|
|
1613
784
|
};
|
|
1614
|
-
const result = await IdentityGatewayService.
|
|
1615
|
-
expect(result.
|
|
1616
|
-
expect(result.
|
|
785
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
786
|
+
expect(result.didWriteCache).toBe(false);
|
|
787
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1617
788
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1618
789
|
});
|
|
1619
790
|
it('should skip cache write when Neptune result matches cached', async () => {
|
|
@@ -1630,16 +801,11 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1630
801
|
updatedAt: new Date().toISOString(),
|
|
1631
802
|
};
|
|
1632
803
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
traits: {
|
|
1639
|
-
emails: ['existing@email.com'],
|
|
1640
|
-
},
|
|
1641
|
-
},
|
|
1642
|
-
}),
|
|
804
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
805
|
+
identityId: 'neptune-skip-write',
|
|
806
|
+
traits: {
|
|
807
|
+
emails: ['existing@email.com'],
|
|
808
|
+
},
|
|
1643
809
|
});
|
|
1644
810
|
const incomingIdentity = {
|
|
1645
811
|
identityId: 'neptune-skip-write',
|
|
@@ -1647,10 +813,10 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1647
813
|
emails: ['existing@email.com', 'thought-new@email.com'],
|
|
1648
814
|
},
|
|
1649
815
|
};
|
|
1650
|
-
const result = await IdentityGatewayService.
|
|
1651
|
-
expect(result.
|
|
1652
|
-
expect(result.
|
|
1653
|
-
expect(result.
|
|
816
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
817
|
+
expect(result.didCallNeptune).toBe(true);
|
|
818
|
+
expect(result.didWriteCache).toBe(false);
|
|
819
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1654
820
|
});
|
|
1655
821
|
it('should return NEPTUNE_RESOLUTION_FAILED when Neptune throws', async () => {
|
|
1656
822
|
const cachedResponse = {
|
|
@@ -1666,17 +832,17 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1666
832
|
updatedAt: new Date().toISOString(),
|
|
1667
833
|
};
|
|
1668
834
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1669
|
-
|
|
835
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Neptune invocation failed'));
|
|
1670
836
|
const incomingIdentity = {
|
|
1671
837
|
identityId: 'neptune-throws',
|
|
1672
838
|
traits: {
|
|
1673
839
|
emails: ['old@email.com', 'new@email.com'],
|
|
1674
840
|
},
|
|
1675
841
|
};
|
|
1676
|
-
const result = await IdentityGatewayService.
|
|
1677
|
-
expect(result.
|
|
1678
|
-
expect(result.
|
|
1679
|
-
expect(result.
|
|
842
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
843
|
+
expect(result.didCallNeptune).toBe(true);
|
|
844
|
+
expect(result.didWriteCache).toBe(false);
|
|
845
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
|
|
1680
846
|
expect(result.identity).toBeDefined();
|
|
1681
847
|
expect(result.identity?.identityId).toBe('neptune-throws');
|
|
1682
848
|
expect(result.identity?.traits?.emails).toEqual(['old@email.com', 'new@email.com']);
|
|
@@ -1687,43 +853,29 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1687
853
|
describe('cache miss without discovered identity ID', () => {
|
|
1688
854
|
it('should call Neptune with incoming identity when no discovered ID', async () => {
|
|
1689
855
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
identity: {
|
|
1694
|
-
identityId: 'neptune-resolved-id',
|
|
1695
|
-
traits: { emails: ['test@email.com'] },
|
|
1696
|
-
},
|
|
1697
|
-
}),
|
|
856
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
857
|
+
identityId: 'neptune-resolved-id',
|
|
858
|
+
traits: { emails: ['test@email.com'] },
|
|
1698
859
|
});
|
|
1699
860
|
const incomingIdentity = {
|
|
1700
861
|
identityId: 'incoming-id',
|
|
1701
862
|
traits: { emails: ['test@email.com'] },
|
|
1702
863
|
};
|
|
1703
|
-
const result = await IdentityGatewayService.
|
|
1704
|
-
expect(
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
identity: expect.objectContaining({
|
|
1708
|
-
identityId: 'incoming-id',
|
|
1709
|
-
}),
|
|
864
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
865
|
+
expect(mockGetIdentity).toHaveBeenCalledWith(pixelId, expect.objectContaining({
|
|
866
|
+
identity: expect.objectContaining({
|
|
867
|
+
identityId: 'incoming-id',
|
|
1710
868
|
}),
|
|
1711
869
|
}));
|
|
1712
|
-
expect(result.
|
|
1713
|
-
expect(result.
|
|
1714
|
-
expect(result.summary.writeReason).toBeUndefined();
|
|
870
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
871
|
+
expect(result.writeReason).toBeUndefined();
|
|
1715
872
|
});
|
|
1716
873
|
it('should set state.didReadCache=true and state.didCallNeptune=true', async () => {
|
|
1717
874
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
}),
|
|
1723
|
-
});
|
|
1724
|
-
const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test', traits: {} }, lambdaArn);
|
|
1725
|
-
expect(result.summary.didReadCache).toBe(true);
|
|
1726
|
-
expect(result.summary.didCallNeptune).toBe(true);
|
|
875
|
+
mockGetIdentity.mockResolvedValueOnce({ identityId: 'resolved', traits: {} });
|
|
876
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test', traits: {} });
|
|
877
|
+
expect(result.didReadCache).toBe(true);
|
|
878
|
+
expect(result.didCallNeptune).toBe(true);
|
|
1727
879
|
});
|
|
1728
880
|
});
|
|
1729
881
|
describe('email pointer discovery with discovered identity ID', () => {
|
|
@@ -1735,16 +887,11 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1735
887
|
pk: `email#${pixelId}#discovered@email.com`,
|
|
1736
888
|
identityId: 'discovered-identity-id',
|
|
1737
889
|
});
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
identity: {
|
|
1742
|
-
identityId: 'discovered-identity-id',
|
|
1743
|
-
traits: { emails: ['discovered@email.com', 'other@email.com'] },
|
|
1744
|
-
},
|
|
1745
|
-
}),
|
|
890
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
891
|
+
identityId: 'discovered-identity-id',
|
|
892
|
+
traits: { emails: ['discovered@email.com', 'other@email.com'] },
|
|
1746
893
|
});
|
|
1747
|
-
const result = await IdentityGatewayService.
|
|
894
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingNoId);
|
|
1748
895
|
expect(result.identity?.identityId).toBe('discovered-identity-id');
|
|
1749
896
|
});
|
|
1750
897
|
it('should use incoming identity in Neptune call (not discovered ID)', async () => {
|
|
@@ -1752,22 +899,14 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1752
899
|
pk: `email#${pixelId}#test@email.com`,
|
|
1753
900
|
identityId: 'discovered-from-email',
|
|
1754
901
|
});
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
identity: {
|
|
1759
|
-
identityId: 'neptune-resolved',
|
|
1760
|
-
traits: { emails: ['test@email.com'] },
|
|
1761
|
-
},
|
|
1762
|
-
}),
|
|
902
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
903
|
+
identityId: 'neptune-resolved',
|
|
904
|
+
traits: { emails: ['test@email.com'] },
|
|
1763
905
|
});
|
|
1764
|
-
await IdentityGatewayService.
|
|
1765
|
-
expect(
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
identity: expect.objectContaining({
|
|
1769
|
-
traits: { emails: ['test@email.com'] },
|
|
1770
|
-
}),
|
|
906
|
+
await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } });
|
|
907
|
+
expect(mockGetIdentity).toHaveBeenCalledWith(pixelId, expect.objectContaining({
|
|
908
|
+
identity: expect.objectContaining({
|
|
909
|
+
traits: expect.objectContaining({ emails: ['test@email.com'] }),
|
|
1771
910
|
}),
|
|
1772
911
|
}));
|
|
1773
912
|
});
|
|
@@ -1775,48 +914,33 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1775
914
|
describe('Neptune resolution success → write-back', () => {
|
|
1776
915
|
it('should write to cache when Neptune returns identity', async () => {
|
|
1777
916
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
identity: {
|
|
1782
|
-
identityId: 'neptune-success',
|
|
1783
|
-
traits: { emails: ['test@email.com'], phones: ['555-1234'] },
|
|
1784
|
-
},
|
|
1785
|
-
}),
|
|
917
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
918
|
+
identityId: 'neptune-success',
|
|
919
|
+
traits: { emails: ['test@email.com'], phones: ['555-1234'] },
|
|
1786
920
|
});
|
|
1787
|
-
const result = await IdentityGatewayService.
|
|
1788
|
-
expect(result.
|
|
1789
|
-
expect(result.
|
|
921
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming', traits: { emails: ['test@email.com'] } });
|
|
922
|
+
expect(result.didWriteCache).toBe(false);
|
|
923
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1790
924
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1791
925
|
});
|
|
1792
926
|
it('should set resolvedIdentity to Neptune response', async () => {
|
|
1793
927
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
identity: {
|
|
1798
|
-
identityId: 'from-neptune',
|
|
1799
|
-
traits: { emails: ['neptune@email.com'] },
|
|
1800
|
-
},
|
|
1801
|
-
}),
|
|
928
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
929
|
+
identityId: 'from-neptune',
|
|
930
|
+
traits: { emails: ['neptune@email.com'] },
|
|
1802
931
|
});
|
|
1803
|
-
const result = await IdentityGatewayService.
|
|
932
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'original', traits: { emails: ['original@email.com'] } });
|
|
1804
933
|
expect(result.identity?.identityId).toBe('from-neptune');
|
|
1805
934
|
expect(result.identity?.traits?.emails).toContain('neptune@email.com');
|
|
1806
935
|
});
|
|
1807
936
|
it('should include traitChanges with "no cached identity" reason', async () => {
|
|
1808
937
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
identity: {
|
|
1813
|
-
identityId: 'new-identity',
|
|
1814
|
-
traits: { emails: ['new@email.com'] },
|
|
1815
|
-
},
|
|
1816
|
-
}),
|
|
938
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
939
|
+
identityId: 'new-identity',
|
|
940
|
+
traits: { emails: ['new@email.com'] },
|
|
1817
941
|
});
|
|
1818
|
-
const result = await IdentityGatewayService.
|
|
1819
|
-
expect(result.
|
|
942
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming', traits: { emails: ['incoming@email.com'] } });
|
|
943
|
+
expect(result.traitChanges).toEqual({
|
|
1820
944
|
reason: { resolved: 'no cached identity' },
|
|
1821
945
|
});
|
|
1822
946
|
});
|
|
@@ -1824,21 +948,21 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1824
948
|
describe('Neptune resolution failure → fallback to incoming', () => {
|
|
1825
949
|
it('should fallback to incoming identity when Neptune fails', async () => {
|
|
1826
950
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1827
|
-
|
|
951
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Neptune unavailable'));
|
|
1828
952
|
const incomingIdentity = {
|
|
1829
953
|
identityId: 'fallback-incoming',
|
|
1830
954
|
traits: { emails: ['fallback@email.com'] },
|
|
1831
955
|
};
|
|
1832
|
-
const result = await IdentityGatewayService.
|
|
1833
|
-
expect(result.
|
|
1834
|
-
expect(result.
|
|
956
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
|
|
957
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
|
|
958
|
+
expect(result.didFallbackToIncoming).toBe(true);
|
|
1835
959
|
expect(result.identity?.identityId).toBe('fallback-incoming');
|
|
1836
960
|
});
|
|
1837
961
|
it('should set didWriteCache=false when Neptune fails', async () => {
|
|
1838
962
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1839
|
-
|
|
1840
|
-
const result = await IdentityGatewayService.
|
|
1841
|
-
expect(result.
|
|
963
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Lambda timeout'));
|
|
964
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test', traits: {} });
|
|
965
|
+
expect(result.didWriteCache).toBe(false);
|
|
1842
966
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1843
967
|
});
|
|
1844
968
|
it('should use identityForNeptune (with discovered ID) as fallback when available', async () => {
|
|
@@ -1846,9 +970,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1846
970
|
pk: `email#${pixelId}#test@email.com`,
|
|
1847
971
|
identityId: 'discovered-id',
|
|
1848
972
|
});
|
|
1849
|
-
|
|
1850
|
-
const result = await IdentityGatewayService.
|
|
1851
|
-
expect(result.
|
|
973
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Neptune error'));
|
|
974
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } });
|
|
975
|
+
expect(result.didFallbackToIncoming).toBe(true);
|
|
1852
976
|
expect(result.identity?.identityId).toBe('discovered-id');
|
|
1853
977
|
expect(result.identity?.traits?.emails).toContain('test@email.com');
|
|
1854
978
|
});
|
|
@@ -1856,31 +980,21 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1856
980
|
describe('skipCacheWrite=true behavior', () => {
|
|
1857
981
|
it('should NOT write to cache when skipCacheWrite=true', async () => {
|
|
1858
982
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
identity: {
|
|
1863
|
-
identityId: 'skip-write-identity',
|
|
1864
|
-
traits: { emails: ['test@email.com'] },
|
|
1865
|
-
},
|
|
1866
|
-
}),
|
|
983
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
984
|
+
identityId: 'skip-write-identity',
|
|
985
|
+
traits: { emails: ['test@email.com'] },
|
|
1867
986
|
});
|
|
1868
|
-
const result = await IdentityGatewayService.
|
|
1869
|
-
expect(result.
|
|
987
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming', traits: { emails: ['test@email.com'] } });
|
|
988
|
+
expect(result.didWriteCache).toBe(false);
|
|
1870
989
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1871
990
|
expect(result.identity?.identityId).toBe('skip-write-identity');
|
|
1872
|
-
expect(result.
|
|
991
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1873
992
|
});
|
|
1874
993
|
it('should still call Neptune when skipCacheWrite=true', async () => {
|
|
1875
994
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
identity: { identityId: 'resolved', traits: {} },
|
|
1880
|
-
}),
|
|
1881
|
-
});
|
|
1882
|
-
await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test', traits: {} }, lambdaArn, { skipCacheWrite: true });
|
|
1883
|
-
expect(mockInvokeFunction).toHaveBeenCalled();
|
|
995
|
+
mockGetIdentity.mockResolvedValueOnce({ identityId: 'resolved', traits: {} });
|
|
996
|
+
await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test', traits: {} });
|
|
997
|
+
expect(mockGetIdentity).toHaveBeenCalled();
|
|
1884
998
|
});
|
|
1885
999
|
});
|
|
1886
1000
|
});
|
|
@@ -1898,18 +1012,13 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1898
1012
|
updatedAt: new Date().toISOString(),
|
|
1899
1013
|
};
|
|
1900
1014
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
identity: {
|
|
1905
|
-
identityId: 'stale-changes',
|
|
1906
|
-
traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
|
|
1907
|
-
},
|
|
1908
|
-
}),
|
|
1015
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1016
|
+
identityId: 'stale-changes',
|
|
1017
|
+
traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
|
|
1909
1018
|
});
|
|
1910
|
-
const result = await IdentityGatewayService.
|
|
1911
|
-
expect(result.
|
|
1912
|
-
expect(result.
|
|
1019
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'stale-changes', traits: { emails: ['old@email.com', 'new@email.com'] } });
|
|
1020
|
+
expect(result.didWriteCache).toBe(false);
|
|
1021
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1913
1022
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1914
1023
|
});
|
|
1915
1024
|
it('should compute traitChanges between cached and Neptune identity', async () => {
|
|
@@ -1927,21 +1036,16 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1927
1036
|
updatedAt: new Date().toISOString(),
|
|
1928
1037
|
};
|
|
1929
1038
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
emails: ['old@email.com', 'new@email.com'],
|
|
1937
|
-
phones: ['555-old', '555-new'],
|
|
1938
|
-
},
|
|
1939
|
-
},
|
|
1940
|
-
}),
|
|
1039
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1040
|
+
identityId: 'trait-changes',
|
|
1041
|
+
traits: {
|
|
1042
|
+
emails: ['old@email.com', 'new@email.com'],
|
|
1043
|
+
phones: ['555-old', '555-new'],
|
|
1044
|
+
},
|
|
1941
1045
|
});
|
|
1942
|
-
const result = await IdentityGatewayService.
|
|
1943
|
-
expect(result.
|
|
1944
|
-
expect(result.
|
|
1046
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'trait-changes', traits: { emails: ['old@email.com', 'new@email.com'] } });
|
|
1047
|
+
expect(result.traitChanges).toBeDefined();
|
|
1048
|
+
expect(result.traitChanges?.emails).toBeDefined();
|
|
1945
1049
|
});
|
|
1946
1050
|
});
|
|
1947
1051
|
describe('Neptune resolution success without changes → skip write', () => {
|
|
@@ -1957,19 +1061,14 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1957
1061
|
updatedAt: new Date().toISOString(),
|
|
1958
1062
|
};
|
|
1959
1063
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
identity: {
|
|
1964
|
-
identityId: 'no-changes',
|
|
1965
|
-
traits: { emails: ['same@email.com'] },
|
|
1966
|
-
},
|
|
1967
|
-
}),
|
|
1064
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1065
|
+
identityId: 'no-changes',
|
|
1066
|
+
traits: { emails: ['same@email.com'] },
|
|
1968
1067
|
});
|
|
1969
|
-
const result = await IdentityGatewayService.
|
|
1970
|
-
expect(result.
|
|
1971
|
-
expect(result.
|
|
1972
|
-
expect(result.
|
|
1068
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'no-changes', traits: { emails: ['same@email.com', 'thought-new@email.com'] } });
|
|
1069
|
+
expect(result.didCallNeptune).toBe(true);
|
|
1070
|
+
expect(result.didWriteCache).toBe(false);
|
|
1071
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
1973
1072
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
1974
1073
|
});
|
|
1975
1074
|
it('should still return Neptune identity even when skipping write', async () => {
|
|
@@ -1984,16 +1083,11 @@ describe('IdentityGatewayService new features', () => {
|
|
|
1984
1083
|
updatedAt: new Date().toISOString(),
|
|
1985
1084
|
};
|
|
1986
1085
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
identity: {
|
|
1991
|
-
identityId: 'return-identity',
|
|
1992
|
-
traits: { emails: ['cached@email.com'] },
|
|
1993
|
-
},
|
|
1994
|
-
}),
|
|
1086
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1087
|
+
identityId: 'return-identity',
|
|
1088
|
+
traits: { emails: ['cached@email.com'] },
|
|
1995
1089
|
});
|
|
1996
|
-
const result = await IdentityGatewayService.
|
|
1090
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'return-identity', traits: { emails: ['cached@email.com', 'extra@email.com'] } });
|
|
1997
1091
|
expect(result.identity?.identityId).toBe('return-identity');
|
|
1998
1092
|
});
|
|
1999
1093
|
it('should strip address timestamps for comparison', async () => {
|
|
@@ -2012,21 +1106,16 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2012
1106
|
updatedAt: new Date().toISOString(),
|
|
2013
1107
|
};
|
|
2014
1108
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
emails: ['test@email.com'],
|
|
2022
|
-
addresses: [{ city: 'NYC', cra: now, upa: now }],
|
|
2023
|
-
},
|
|
2024
|
-
},
|
|
2025
|
-
}),
|
|
1109
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1110
|
+
identityId: 'addr-timestamp',
|
|
1111
|
+
traits: {
|
|
1112
|
+
emails: ['test@email.com'],
|
|
1113
|
+
addresses: [{ city: 'NYC', cra: now, upa: now }],
|
|
1114
|
+
},
|
|
2026
1115
|
});
|
|
2027
|
-
const result = await IdentityGatewayService.
|
|
2028
|
-
expect(result.
|
|
2029
|
-
expect(result.
|
|
1116
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'addr-timestamp', traits: { emails: ['test@email.com', 'new@email.com'] } });
|
|
1117
|
+
expect(result.didWriteCache).toBe(false);
|
|
1118
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
2030
1119
|
});
|
|
2031
1120
|
});
|
|
2032
1121
|
describe('Neptune resolution failure', () => {
|
|
@@ -2042,10 +1131,10 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2042
1131
|
updatedAt: new Date().toISOString(),
|
|
2043
1132
|
};
|
|
2044
1133
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2045
|
-
|
|
2046
|
-
const result = await IdentityGatewayService.
|
|
2047
|
-
expect(result.
|
|
2048
|
-
expect(result.
|
|
1134
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Lambda invocation failed'));
|
|
1135
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'neptune-fail', traits: { emails: ['cached@email.com', 'new@email.com'] } });
|
|
1136
|
+
expect(result.didCallNeptune).toBe(true);
|
|
1137
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
|
|
2049
1138
|
});
|
|
2050
1139
|
it('should not write to cache when Neptune fails', async () => {
|
|
2051
1140
|
const cachedResponse = {
|
|
@@ -2059,9 +1148,9 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2059
1148
|
updatedAt: new Date().toISOString(),
|
|
2060
1149
|
};
|
|
2061
1150
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2062
|
-
|
|
2063
|
-
const result = await IdentityGatewayService.
|
|
2064
|
-
expect(result.
|
|
1151
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Network error'));
|
|
1152
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'no-write-fail', traits: { emails: ['test@email.com', 'extra@email.com'] } });
|
|
1153
|
+
expect(result.didWriteCache).toBe(false);
|
|
2065
1154
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
2066
1155
|
});
|
|
2067
1156
|
it('should still return merged identity when Neptune fails', async () => {
|
|
@@ -2079,8 +1168,8 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2079
1168
|
updatedAt: new Date().toISOString(),
|
|
2080
1169
|
};
|
|
2081
1170
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2082
|
-
|
|
2083
|
-
const result = await IdentityGatewayService.
|
|
1171
|
+
mockGetIdentity.mockRejectedValueOnce(new Error('Timeout'));
|
|
1172
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'merged-on-fail', traits: { emails: ['cached@email.com', 'new@email.com'] } });
|
|
2084
1173
|
expect(result.identity).toBeDefined();
|
|
2085
1174
|
});
|
|
2086
1175
|
});
|
|
@@ -2097,19 +1186,14 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2097
1186
|
updatedAt: new Date().toISOString(),
|
|
2098
1187
|
};
|
|
2099
1188
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
identity: {
|
|
2104
|
-
identityId: 'skip-stale',
|
|
2105
|
-
traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
|
|
2106
|
-
},
|
|
2107
|
-
}),
|
|
1189
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1190
|
+
identityId: 'skip-stale',
|
|
1191
|
+
traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
|
|
2108
1192
|
});
|
|
2109
|
-
const result = await IdentityGatewayService.
|
|
2110
|
-
expect(result.
|
|
1193
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'skip-stale', traits: { emails: ['old@email.com', 'new@email.com'] } });
|
|
1194
|
+
expect(result.didWriteCache).toBe(false);
|
|
2111
1195
|
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
2112
|
-
expect(result.
|
|
1196
|
+
expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
|
|
2113
1197
|
});
|
|
2114
1198
|
it('should still call Neptune when skipCacheWrite=true', async () => {
|
|
2115
1199
|
const cachedResponse = {
|
|
@@ -2123,17 +1207,12 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2123
1207
|
updatedAt: new Date().toISOString(),
|
|
2124
1208
|
};
|
|
2125
1209
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
identity: {
|
|
2130
|
-
identityId: 'call-neptune-skip',
|
|
2131
|
-
traits: { emails: ['test@email.com', 'added@email.com'] },
|
|
2132
|
-
},
|
|
2133
|
-
}),
|
|
1210
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1211
|
+
identityId: 'call-neptune-skip',
|
|
1212
|
+
traits: { emails: ['test@email.com', 'added@email.com'] },
|
|
2134
1213
|
});
|
|
2135
|
-
await IdentityGatewayService.
|
|
2136
|
-
expect(
|
|
1214
|
+
await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'call-neptune-skip', traits: { emails: ['test@email.com', 'added@email.com'] } });
|
|
1215
|
+
expect(mockGetIdentity).toHaveBeenCalled();
|
|
2137
1216
|
});
|
|
2138
1217
|
it('should return Neptune identity when skipCacheWrite=true', async () => {
|
|
2139
1218
|
const cachedResponse = {
|
|
@@ -2147,19 +1226,14 @@ describe('IdentityGatewayService new features', () => {
|
|
|
2147
1226
|
updatedAt: new Date().toISOString(),
|
|
2148
1227
|
};
|
|
2149
1228
|
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
emails: ['cached@email.com', 'neptune@email.com'],
|
|
2157
|
-
phones: ['555-neptune'],
|
|
2158
|
-
},
|
|
2159
|
-
},
|
|
2160
|
-
}),
|
|
1229
|
+
mockGetIdentity.mockResolvedValueOnce({
|
|
1230
|
+
identityId: 'return-skip',
|
|
1231
|
+
traits: {
|
|
1232
|
+
emails: ['cached@email.com', 'neptune@email.com'],
|
|
1233
|
+
phones: ['555-neptune'],
|
|
1234
|
+
},
|
|
2161
1235
|
});
|
|
2162
|
-
const result = await IdentityGatewayService.
|
|
1236
|
+
const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'return-skip', traits: { emails: ['cached@email.com', 'neptune@email.com'] } });
|
|
2163
1237
|
expect(result.identity?.traits?.phones).toContain('555-neptune');
|
|
2164
1238
|
});
|
|
2165
1239
|
});
|