From 9ab350efe27914e9d81ea8fec8830f49629ac0dc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 18 Mar 2022 10:47:46 +0000 Subject: [PATCH 1/6] test(integrations): Add unit tests for caputureconsole --- .../integrations/test/captureconsole.test.ts | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 packages/integrations/test/captureconsole.test.ts diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts new file mode 100644 index 000000000000..cec92076e9ae --- /dev/null +++ b/packages/integrations/test/captureconsole.test.ts @@ -0,0 +1,289 @@ +import { Event, Integration } from '@sentry/types'; + +import { CaptureConsole } from '../src/captureconsole'; + +const mockScope = { + setLevel: jest.fn(), + setExtra: jest.fn(), + addEventProcessor: jest.fn(), +}; + +const mockHub = { + withScope: jest.fn(callback => { + callback(mockScope); + }), + captureMessage: jest.fn(), + captureException: jest.fn(), +}; + +const getMockHubWithIntegration = (integration: Integration) => ({ + ...mockHub, + getIntegration: jest.fn(() => integration), +}); + +// We're using this to un-monkey patch the console after each test. +const originalConsole = Object.assign({}, global.console); + +describe('CaptureConsole setup', () => { + afterEach(() => { + jest.clearAllMocks(); + + // Un-monkey-patch the console functions + Object.assign(global.console, originalConsole); + }); + + it('should respect user-provided console levels', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'warn'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + expect(global.console.error).toBe(originalConsole.error); // not monkey patched + expect(global.console.log).not.toBe(originalConsole.log); // monkey patched + expect(global.console.warn).not.toBe(originalConsole.warn); // monkey patched + }); + + it('should fall back to default console levels if none are provided', () => { + const captureConsoleIntegration = new CaptureConsole(); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // expect a set of defined console levels to have been monkey patched + expect(global.console.debug).not.toBe(originalConsole.debug); + expect(global.console.info).not.toBe(originalConsole.info); + expect(global.console.warn).not.toBe(originalConsole.warn); + expect(global.console.error).not.toBe(originalConsole.error); + expect(global.console.log).not.toBe(originalConsole.log); + expect(global.console.assert).not.toBe(originalConsole.assert); + + // any other fields should not have been patched + expect(global.console.trace).toBe(originalConsole.trace); + expect(global.console.table).toBe(originalConsole.table); + }); + + it('setup should fail gracefully when console is not available', () => { + const consoleRef = global.console; + // remove console + delete global.console; + + expect(() => { + const captureConsoleIntegration = new CaptureConsole(); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + }).not.toThrow(); + + // reinstate initial console + global.console = consoleRef; + }); + + it('should set a level in the scope when console function is called', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // call a wrapped function + global.console.error('some logging message'); + + expect(mockScope.setLevel).toHaveBeenCalledTimes(1); + expect(mockScope.setLevel).toHaveBeenCalledWith('error'); + }); + + it('should send arguments as extra data on failed assertion', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // call a wrapped function + global.console.log('some arg 1', 'some arg 2'); + global.console.log(); + + expect(mockScope.setExtra).toHaveBeenCalledTimes(2); + expect(mockScope.setExtra).toHaveBeenCalledWith('arguments', ['some arg 1', 'some arg 2']); + expect(mockScope.setExtra).toHaveBeenCalledWith('arguments', []); + }); + + it('should add an event processor that sets the `logger` field of events', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // call a wrapped function + global.console.log('some message'); + + expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); + + const addedEventProcessor = mockScope.addEventProcessor.mock.calls[0][0]; + const someEvent: Event = {}; + addedEventProcessor(someEvent); + + expect(someEvent.logger).toBe('console'); + }); + + it('should capture message on a failed assertion', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.assert(1 + 1 === 3); + + expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', []); + expect(mockHub.captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert'); + }); + + it('should capture correct message on a failed assertion with message', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.assert(1 + 1 === 3, 'expression is false'); + + expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', ['expression is false']); + expect(mockHub.captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false'); + }); + + it('should not capture message on a successful assertion', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.assert(1 + 1 === 2); + }); + + it('should capture exception when console logs an error object with level set to "error"', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + const someError = new Error('some error'); + global.console.error(someError); + + expect(mockHub.captureException).toHaveBeenCalledWith(someError); + }); + + it('should capture message when console logs a non-error object with level set to "error"', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.error('some non-error message'); + + expect(mockHub.captureMessage).toHaveBeenCalledWith('some non-error message'); + expect(mockHub.captureException).not.toHaveBeenCalled(); + }); + + it('should capture a message for non-error log levels', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['info'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.info('some message'); + + expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); + }); + + it('should call the original console function when console members are called', () => { + // Mock console log to test if it was called + const originalConsoleLog = global.console.log; + const mockConsoleLog = jest.fn(); + global.console.log = mockConsoleLog; + + const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.log('some message 1', 'some message 2'); + + expect(mockConsoleLog).toHaveBeenCalledWith('some message 1', 'some message 2'); + + // Reset console log + global.console.log = originalConsoleLog; + }); + + it('should not wrap any levels that are not members of console', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'someNonExistingLevel', 'error'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // The provided level should not be created + expect(global.console['someNonExistingLevel']).toBeUndefined(); + + // Ohter levels should be wrapped as expected + expect(global.console.log).not.toBe(originalConsole.log); + expect(global.console.error).not.toBe(originalConsole.error); + }); + + it('should not wrap any levels that are not members of console', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'someNonExistingLevel', 'error'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // The provided level should not be created + expect(global.console['someNonExistingLevel']).toBeUndefined(); + + // Ohter levels should be wrapped as expected + expect(global.console.log).not.toBe(originalConsole.log); + expect(global.console.error).not.toBe(originalConsole.error); + }); + + it('should wrap the console when the client does not have a registered captureconsole integration, but not capture any messages', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'error'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(null) as any, // simulate not having the integration registered + ); + + // Console should be wrapped + expect(global.console.log).not.toBe(originalConsole.log); + expect(global.console.error).not.toBe(originalConsole.error); + + // Should not capture messages + global.console.log('some message'); + expect(mockHub.captureMessage).not.toHaveBeenCalledWith(); + }); + + it("should not crash when the original console methods don't exist at time of invocation", () => { + const originalConsoleLog = global.console.log; + global.console.log = undefined; // don't `delete` here, otherwise `fill` won't wrap the function + + const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + expect(() => { + global.console.log('some message'); + }).not.toThrow(); + + global.console.log = originalConsoleLog; + }); +}); From 7165018b4c197d5195fc6d020f692391c7eb01ea Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 18 Mar 2022 12:23:43 +0000 Subject: [PATCH 2/6] Remove duplicate test --- packages/integrations/test/captureconsole.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index cec92076e9ae..b3c1a5fc5f4f 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -239,21 +239,6 @@ describe('CaptureConsole setup', () => { expect(global.console.error).not.toBe(originalConsole.error); }); - it('should not wrap any levels that are not members of console', () => { - const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'someNonExistingLevel', 'error'] }); - captureConsoleIntegration.setupOnce( - () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, - ); - - // The provided level should not be created - expect(global.console['someNonExistingLevel']).toBeUndefined(); - - // Ohter levels should be wrapped as expected - expect(global.console.log).not.toBe(originalConsole.log); - expect(global.console.error).not.toBe(originalConsole.error); - }); - it('should wrap the console when the client does not have a registered captureconsole integration, but not capture any messages', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'error'] }); captureConsoleIntegration.setupOnce( From 5c24751cd2056c789feb05d9d88910baf43c2948 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 18 Mar 2022 12:30:09 +0000 Subject: [PATCH 3/6] Add tests for happy path without levels in ctor --- .../integrations/test/captureconsole.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index b3c1a5fc5f4f..136030a84ec0 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -179,6 +179,31 @@ describe('CaptureConsole setup', () => { expect(mockHub.captureException).toHaveBeenCalledWith(someError); }); + it('should capture exception on `console.error` when no levels are provided in constructor', () => { + const captureConsoleIntegration = new CaptureConsole(); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + const someError = new Error('some error'); + global.console.error(someError); + + expect(mockHub.captureException).toHaveBeenCalledWith(someError); + }); + + it('should capture message on `console.log` when no levels are provided in constructor', () => { + const captureConsoleIntegration = new CaptureConsole(); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + global.console.error('some message'); + + expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); + }); + it('should capture message when console logs a non-error object with level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); captureConsoleIntegration.setupOnce( From 35d6508c35129f67950ee813486592440401247e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 18 Mar 2022 13:40:29 +0000 Subject: [PATCH 4/6] Add test with empty levels array --- .../integrations/test/captureconsole.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 136030a84ec0..81cbd2538c91 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -64,6 +64,26 @@ describe('CaptureConsole setup', () => { expect(global.console.table).toBe(originalConsole.table); }); + it('should not wrap any functions with an empty levels option', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: [] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration) as any, + ); + + // expect the default set of console levels not to have been monkey patched + expect(global.console.debug).toBe(originalConsole.debug); + expect(global.console.info).toBe(originalConsole.info); + expect(global.console.warn).toBe(originalConsole.warn); + expect(global.console.error).toBe(originalConsole.error); + expect(global.console.log).toBe(originalConsole.log); + expect(global.console.assert).toBe(originalConsole.assert); + + // expect no message to be captured with console.log + global.console.log('some message'); + expect(mockHub.captureMessage).not.toHaveBeenCalled(); + }); + it('setup should fail gracefully when console is not available', () => { const consoleRef = global.console; // remove console From 179d8d873f0f024c5ba1034a7bca70a63b020fdd Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 18 Mar 2022 14:33:04 +0000 Subject: [PATCH 5/6] Update test name to be more clear --- packages/integrations/test/captureconsole.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 81cbd2538c91..263a39cea6e4 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -32,7 +32,7 @@ describe('CaptureConsole setup', () => { Object.assign(global.console, originalConsole); }); - it('should respect user-provided console levels', () => { + it('should patch user-configured console levels', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'warn'] }); captureConsoleIntegration.setupOnce( () => undefined, From af223e9059ce507199fad8d517b2c22f1cfb0abe Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 18 Mar 2022 14:36:59 +0000 Subject: [PATCH 6/6] Add checks for the amount of function invocations --- packages/integrations/test/captureconsole.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 263a39cea6e4..bb6cce8110d3 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -160,6 +160,7 @@ describe('CaptureConsole setup', () => { global.console.assert(1 + 1 === 3); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', []); + expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert'); }); @@ -173,6 +174,7 @@ describe('CaptureConsole setup', () => { global.console.assert(1 + 1 === 3, 'expression is false'); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', ['expression is false']); + expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false'); }); @@ -196,6 +198,7 @@ describe('CaptureConsole setup', () => { const someError = new Error('some error'); global.console.error(someError); + expect(mockHub.captureException).toHaveBeenCalledTimes(1); expect(mockHub.captureException).toHaveBeenCalledWith(someError); }); @@ -209,6 +212,7 @@ describe('CaptureConsole setup', () => { const someError = new Error('some error'); global.console.error(someError); + expect(mockHub.captureException).toHaveBeenCalledTimes(1); expect(mockHub.captureException).toHaveBeenCalledWith(someError); }); @@ -221,6 +225,7 @@ describe('CaptureConsole setup', () => { global.console.error('some message'); + expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); }); @@ -233,6 +238,7 @@ describe('CaptureConsole setup', () => { global.console.error('some non-error message'); + expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('some non-error message'); expect(mockHub.captureException).not.toHaveBeenCalled(); }); @@ -246,6 +252,7 @@ describe('CaptureConsole setup', () => { global.console.info('some message'); + expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); }); @@ -263,6 +270,7 @@ describe('CaptureConsole setup', () => { global.console.log('some message 1', 'some message 2'); + expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog).toHaveBeenCalledWith('some message 1', 'some message 2'); // Reset console log