From a8d542f17cf312dd95b2a7873d9ed4e8bc02fae5 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Fri, 11 Sep 2026 04:55:30 -0700 Subject: [PATCH 1/3] fix: handle rejected credential prefetch --- .../core/__tests__/dispatch/Dispatch.test.ts | 96 ++++++++++++++++++- packages/core/src/dispatch/Dispatch.ts | 6 +- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/packages/core/__tests__/dispatch/Dispatch.test.ts b/packages/core/__tests__/dispatch/Dispatch.test.ts index 08ff1286..126afcfe 100644 --- a/packages/core/__tests__/dispatch/Dispatch.test.ts +++ b/packages/core/__tests__/dispatch/Dispatch.test.ts @@ -8,6 +8,7 @@ import { } from '@aws-rum/web-core/test-utils/test-utils'; import { EventCache } from '@aws-rum/web-core/event-cache/EventCache'; import { CRED_KEY, IDENTITY_KEY } from '@aws-rum/web-core/utils/constants'; +import { InternalLogger } from '@aws-rum/web-core/utils/InternalLogger'; global.fetch = mockFetch; const sendFetch = jest.fn(() => Promise.resolve()); @@ -18,8 +19,12 @@ jest.mock('@aws-rum/web-core/dispatch/DataPlaneClient', () => ({ .mockImplementation(() => ({ sendFetch, sendBeacon })) })); -const mockBasicAuthProvider = jest.fn(); -const mockEnhancedAuthProvider = jest.fn(); +const mockBasicAuthProvider = jest.fn(() => + Promise.resolve(Utils.createAwsCredentials()) +); +const mockEnhancedAuthProvider = jest.fn(() => + Promise.resolve(Utils.createAwsCredentials()) +); /** * Helper to create a mock CognitoCredentialProviderFactory. @@ -85,7 +90,9 @@ describe('Dispatch tests', () => { test('when CredentialProvider is used then credentials are immediately fetched', async () => { // Init - const credentialProvider: AwsCredentialIdentityProvider = jest.fn(); + const credentialProvider: AwsCredentialIdentityProvider = jest.fn(() => + Promise.resolve(Utils.createAwsCredentials()) + ); dispatch = new Dispatch( Utils.APPLICATION_ID, Utils.AWS_RUM_REGION, @@ -104,6 +111,89 @@ describe('Dispatch tests', () => { expect(credentialProvider).toHaveBeenCalledTimes(1); }); + describe('credential prefetch', () => { + beforeEach(() => { + dispatch = new Dispatch( + Utils.APPLICATION_ID, + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + Utils.createDefaultEventCacheWithEvents(), + { + ...DEFAULT_CONFIG, + dispatchInterval: Utils.AUTO_DISPATCH_OFF + } + ); + }); + + afterEach(() => { + InternalLogger.configure(false); + jest.restoreAllMocks(); + }); + + test.each([false, true])( + 'handles rejection and preserves the provider with debug=%s', + async (debug) => { + InternalLogger.configure(debug); + const warn = jest.spyOn(console, 'warn').mockImplementation(); + const error = new Error('Synthetic credential failure'); + const prefetch = Promise.reject(error); + // Keep a failing regression test from leaking a rejection. + void prefetch.catch(() => undefined); + const catchSpy = jest.spyOn(prefetch, 'catch'); + const provider = jest.fn(() => prefetch); + + dispatch.setAwsCredentials(provider); + + expect(provider).toHaveBeenCalledTimes(1); + expect(catchSpy).toHaveBeenCalledTimes(1); + await expect( + catchSpy.mock.results[0].value + ).resolves.toBeUndefined(); + if (debug) { + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith( + expect.any(String), + 'Could not get AWS credentials. RUM may be unable to send monitoring data.' + ); + } else { + expect(warn).not.toHaveBeenCalled(); + } + expect(DataPlaneClient).toHaveBeenLastCalledWith( + expect.objectContaining({ credentials: provider }), + undefined + ); + await expect(provider()).rejects.toBe(error); + } + ); + + test.each(['plain credentials', 'then-only object'])( + 'preserves a JavaScript provider returning %s', + async (kind) => { + InternalLogger.configure(true); + const warn = jest.spyOn(console, 'warn').mockImplementation(); + const credentials = Utils.createAwsCredentials(); + const then = jest.fn((resolve) => resolve(credentials)); + const provider = jest.fn(() => + kind === 'plain credentials' ? credentials : { then } + ); + + // JavaScript callers can return values outside the declared type. + expect(() => + dispatch.setAwsCredentials( + provider as unknown as AwsCredentialIdentityProvider + ) + ).not.toThrow(); + expect(provider).toHaveBeenCalledTimes(1); + await Promise.resolve(); + await Promise.resolve(); + if (kind === 'then-only object') { + expect(then).toHaveBeenCalledTimes(1); + } + expect(warn).not.toHaveBeenCalled(); + } + ); + }); + test('dispatch() throws exception when send fails', async () => { // Init sendFetch.mockImplementationOnce(() => diff --git a/packages/core/src/dispatch/Dispatch.ts b/packages/core/src/dispatch/Dispatch.ts index 82d08015..220f3b8d 100644 --- a/packages/core/src/dispatch/Dispatch.ts +++ b/packages/core/src/dispatch/Dispatch.ts @@ -158,7 +158,11 @@ export class Dispatch { if (typeof credentialProvider === 'function') { // In case a beacon in the first dispatch, we must pre-fetch credentials into a cookie so there is no delay // to fetch credentials while the page is closing. - (credentialProvider as () => Promise)(); + void Promise.resolve(credentialProvider()).catch(() => { + InternalLogger.warn( + 'Could not get AWS credentials. RUM may be unable to send monitoring data.' + ); + }); } } From 64ba4bfaca65f396a44b6bc88410b0c65fa60c64 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Sun, 13 Sep 2026 23:03:27 -0700 Subject: [PATCH 2/3] test: simplify credential prefetch regression coverage --- .../core/__tests__/dispatch/Dispatch.test.ts | 112 +++++++----------- 1 file changed, 42 insertions(+), 70 deletions(-) diff --git a/packages/core/__tests__/dispatch/Dispatch.test.ts b/packages/core/__tests__/dispatch/Dispatch.test.ts index 126afcfe..9241679a 100644 --- a/packages/core/__tests__/dispatch/Dispatch.test.ts +++ b/packages/core/__tests__/dispatch/Dispatch.test.ts @@ -1,7 +1,10 @@ import { Dispatch } from '@aws-rum/web-core/dispatch/Dispatch'; import * as Utils from '@aws-rum/web-core/test-utils/test-utils'; import { DataPlaneClient } from '@aws-rum/web-core/dispatch/DataPlaneClient'; -import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { + AwsCredentialIdentity, + AwsCredentialIdentityProvider +} from '@aws-sdk/types'; import { DEFAULT_CONFIG, mockFetch @@ -19,12 +22,8 @@ jest.mock('@aws-rum/web-core/dispatch/DataPlaneClient', () => ({ .mockImplementation(() => ({ sendFetch, sendBeacon })) })); -const mockBasicAuthProvider = jest.fn(() => - Promise.resolve(Utils.createAwsCredentials()) -); -const mockEnhancedAuthProvider = jest.fn(() => - Promise.resolve(Utils.createAwsCredentials()) -); +const mockBasicAuthProvider = jest.fn(); +const mockEnhancedAuthProvider = jest.fn(); /** * Helper to create a mock CognitoCredentialProviderFactory. @@ -90,9 +89,7 @@ describe('Dispatch tests', () => { test('when CredentialProvider is used then credentials are immediately fetched', async () => { // Init - const credentialProvider: AwsCredentialIdentityProvider = jest.fn(() => - Promise.resolve(Utils.createAwsCredentials()) - ); + const credentialProvider: AwsCredentialIdentityProvider = jest.fn(); dispatch = new Dispatch( Utils.APPLICATION_ID, Utils.AWS_RUM_REGION, @@ -118,80 +115,55 @@ describe('Dispatch tests', () => { Utils.AWS_RUM_REGION, Utils.AWS_RUM_ENDPOINT, Utils.createDefaultEventCacheWithEvents(), - { - ...DEFAULT_CONFIG, - dispatchInterval: Utils.AUTO_DISPATCH_OFF - } + { ...DEFAULT_CONFIG, dispatchInterval: Utils.AUTO_DISPATCH_OFF } ); }); - afterEach(() => { - InternalLogger.configure(false); - jest.restoreAllMocks(); - }); - - test.each([false, true])( - 'handles rejection and preserves the provider with debug=%s', - async (debug) => { - InternalLogger.configure(debug); - const warn = jest.spyOn(console, 'warn').mockImplementation(); - const error = new Error('Synthetic credential failure'); - const prefetch = Promise.reject(error); - // Keep a failing regression test from leaking a rejection. + test('warns on prefetch rejection without replacing the provider', async () => { + const warn = jest + .spyOn(InternalLogger, 'warn') + .mockImplementation(); + try { + let rejectPrefetch!: (reason: Error) => void; + const prefetch = new Promise( + (_, reject) => { + rejectPrefetch = reject; + } + ); + // Keep the test safe when the production rejection handler is missing. void prefetch.catch(() => undefined); - const catchSpy = jest.spyOn(prefetch, 'catch'); - const provider = jest.fn(() => prefetch); + const provider = () => prefetch; dispatch.setAwsCredentials(provider); + expect(warn).not.toHaveBeenCalled(); + rejectPrefetch(new Error('Example credential failure')); + await Promise.resolve(); - expect(provider).toHaveBeenCalledTimes(1); - expect(catchSpy).toHaveBeenCalledTimes(1); - await expect( - catchSpy.mock.results[0].value - ).resolves.toBeUndefined(); - if (debug) { - expect(warn).toHaveBeenCalledTimes(1); - expect(warn).toHaveBeenCalledWith( - expect.any(String), + expect(warn.mock.calls).toEqual([ + [ 'Could not get AWS credentials. RUM may be unable to send monitoring data.' - ); - } else { - expect(warn).not.toHaveBeenCalled(); - } + ] + ]); expect(DataPlaneClient).toHaveBeenLastCalledWith( expect.objectContaining({ credentials: provider }), undefined ); - await expect(provider()).rejects.toBe(error); + } finally { + warn.mockRestore(); } - ); - - test.each(['plain credentials', 'then-only object'])( - 'preserves a JavaScript provider returning %s', - async (kind) => { - InternalLogger.configure(true); - const warn = jest.spyOn(console, 'warn').mockImplementation(); - const credentials = Utils.createAwsCredentials(); - const then = jest.fn((resolve) => resolve(credentials)); - const provider = jest.fn(() => - kind === 'plain credentials' ? credentials : { then } - ); + }); - // JavaScript callers can return values outside the declared type. - expect(() => - dispatch.setAwsCredentials( - provider as unknown as AwsCredentialIdentityProvider - ) - ).not.toThrow(); - expect(provider).toHaveBeenCalledTimes(1); - await Promise.resolve(); - await Promise.resolve(); - if (kind === 'then-only object') { - expect(then).toHaveBeenCalledTimes(1); - } - expect(warn).not.toHaveBeenCalled(); - } - ); + test.each([ + ['plain credentials', Utils.createAwsCredentials()], + [ + 'then-only object', + { then: (resolve) => resolve(Utils.createAwsCredentials()) } + ] + ])('accepts a JavaScript provider returning %s', (_name, result) => { + // JavaScript callers can return values outside the declared type. + const provider = (() => result) as AwsCredentialIdentityProvider; + expect(() => dispatch.setAwsCredentials(provider)).not.toThrow(); + }); }); test('dispatch() throws exception when send fails', async () => { From 4cd4be9f9ec3ea4fdac5f2dde4c790690e465772 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Mon, 14 Sep 2026 04:02:46 -0700 Subject: [PATCH 3/3] test: simplify credential tests to match existing style --- .../core/__tests__/dispatch/Dispatch.test.ts | 126 ++++++++++-------- 1 file changed, 74 insertions(+), 52 deletions(-) diff --git a/packages/core/__tests__/dispatch/Dispatch.test.ts b/packages/core/__tests__/dispatch/Dispatch.test.ts index 9241679a..6b1f8c71 100644 --- a/packages/core/__tests__/dispatch/Dispatch.test.ts +++ b/packages/core/__tests__/dispatch/Dispatch.test.ts @@ -13,6 +13,8 @@ import { EventCache } from '@aws-rum/web-core/event-cache/EventCache'; import { CRED_KEY, IDENTITY_KEY } from '@aws-rum/web-core/utils/constants'; import { InternalLogger } from '@aws-rum/web-core/utils/InternalLogger'; +jest.mock('@aws-rum/web-core/utils/InternalLogger'); + global.fetch = mockFetch; const sendFetch = jest.fn(() => Promise.resolve()); const sendBeacon = jest.fn(() => Promise.resolve()); @@ -106,64 +108,84 @@ describe('Dispatch tests', () => { // Assert expect(credentialProvider).toHaveBeenCalledTimes(1); + expect(DataPlaneClient).toHaveBeenLastCalledWith( + expect.objectContaining({ credentials: credentialProvider }), + undefined + ); }); - describe('credential prefetch', () => { - beforeEach(() => { - dispatch = new Dispatch( - Utils.APPLICATION_ID, - Utils.AWS_RUM_REGION, - Utils.AWS_RUM_ENDPOINT, - Utils.createDefaultEventCacheWithEvents(), - { ...DEFAULT_CONFIG, dispatchInterval: Utils.AUTO_DISPATCH_OFF } - ); - }); + test('when credentials fail then a warning is logged', async () => { + // Init + const credentialProvider = () => + Promise.reject(new Error('Example credential failure')); - test('warns on prefetch rejection without replacing the provider', async () => { - const warn = jest - .spyOn(InternalLogger, 'warn') - .mockImplementation(); - try { - let rejectPrefetch!: (reason: Error) => void; - const prefetch = new Promise( - (_, reject) => { - rejectPrefetch = reject; - } - ); - // Keep the test safe when the production rejection handler is missing. - void prefetch.catch(() => undefined); - const provider = () => prefetch; - - dispatch.setAwsCredentials(provider); - expect(warn).not.toHaveBeenCalled(); - rejectPrefetch(new Error('Example credential failure')); - await Promise.resolve(); - - expect(warn.mock.calls).toEqual([ - [ - 'Could not get AWS credentials. RUM may be unable to send monitoring data.' - ] - ]); - expect(DataPlaneClient).toHaveBeenLastCalledWith( - expect.objectContaining({ credentials: provider }), - undefined - ); - } finally { - warn.mockRestore(); + dispatch = new Dispatch( + Utils.APPLICATION_ID, + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + Utils.createDefaultEventCacheWithEvents(), + { + ...DEFAULT_CONFIG, + dispatchInterval: Utils.AUTO_DISPATCH_OFF } - }); + ); + + // Run + dispatch.setAwsCredentials(credentialProvider); + await Promise.resolve(); // Let the rejection handler run. + + // Assert + expect(InternalLogger.warn).toHaveBeenCalledWith( + 'Could not get AWS credentials. RUM may be unable to send monitoring data.' + ); + }); - test.each([ - ['plain credentials', Utils.createAwsCredentials()], - [ - 'then-only object', - { then: (resolve) => resolve(Utils.createAwsCredentials()) } - ] - ])('accepts a JavaScript provider returning %s', (_name, result) => { - // JavaScript callers can return values outside the declared type. - const provider = (() => result) as AwsCredentialIdentityProvider; - expect(() => dispatch.setAwsCredentials(provider)).not.toThrow(); + test('when a provider returns credentials directly then it is accepted', () => { + // Init + const credentialProvider = jest + .fn() + .mockReturnValue(Utils.createAwsCredentials()); + + dispatch = new Dispatch( + Utils.APPLICATION_ID, + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + Utils.createDefaultEventCacheWithEvents(), + { + ...DEFAULT_CONFIG, + dispatchInterval: Utils.AUTO_DISPATCH_OFF + } + ); + + // Run and Assert + expect(() => + dispatch.setAwsCredentials(credentialProvider) + ).not.toThrow(); + }); + + test('when a provider returns an object with a then method then it is accepted', () => { + // Init + const credentials = Utils.createAwsCredentials(); + const credentialProvider = jest.fn().mockReturnValue({ + then: (resolve: (value: AwsCredentialIdentity) => void) => + resolve(credentials) }); + + dispatch = new Dispatch( + Utils.APPLICATION_ID, + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + Utils.createDefaultEventCacheWithEvents(), + { + ...DEFAULT_CONFIG, + dispatchInterval: Utils.AUTO_DISPATCH_OFF + } + ); + + // Run and Assert + expect(() => + dispatch.setAwsCredentials(credentialProvider) + ).not.toThrow(); }); test('dispatch() throws exception when send fails', async () => {