|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the Apache 2.0. |
| 3 | + |
| 4 | +// Every other test mocks one side of the consent/index seam: consent.test.ts |
| 5 | +// mocks `./index` wholesale, and index.test.ts never composes with |
| 6 | +// `consent.ts`. This file composes the REAL consent.ts against the REAL |
| 7 | +// index.ts, mocking only the App Insights SDK transport (the same |
| 8 | +// vi.hoisted pattern index.test.ts uses), and asserts on the event names |
| 9 | +// that actually reach that transport across a full revoke -> grant cycle. |
| 10 | + |
| 11 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 12 | + |
| 13 | +const aiMocks = vi.hoisted(() => { |
| 14 | + const trackEvent = vi.fn(); |
| 15 | + const loadAppInsights = vi.fn(); |
| 16 | + const addTelemetryInitializer = vi.fn(); |
| 17 | + const unload = vi.fn(); |
| 18 | + const config: Record<string, unknown> = {}; |
| 19 | + const context = { location: {}, user: {} }; |
| 20 | + const ApplicationInsightsCtor = vi.fn().mockImplementation(() => ({ |
| 21 | + trackEvent, |
| 22 | + loadAppInsights, |
| 23 | + addTelemetryInitializer, |
| 24 | + unload, |
| 25 | + config, |
| 26 | + context, |
| 27 | + })); |
| 28 | + return { |
| 29 | + trackEvent, |
| 30 | + loadAppInsights, |
| 31 | + addTelemetryInitializer, |
| 32 | + unload, |
| 33 | + config, |
| 34 | + context, |
| 35 | + ApplicationInsightsCtor, |
| 36 | + }; |
| 37 | +}); |
| 38 | +const { |
| 39 | + trackEvent, |
| 40 | + loadAppInsights, |
| 41 | + addTelemetryInitializer, |
| 42 | + unload, |
| 43 | + config, |
| 44 | + context, |
| 45 | + ApplicationInsightsCtor, |
| 46 | +} = aiMocks; |
| 47 | + |
| 48 | +vi.mock('@microsoft/applicationinsights-web', () => ({ |
| 49 | + ApplicationInsights: aiMocks.ApplicationInsightsCtor, |
| 50 | +})); |
| 51 | + |
| 52 | +const registerMock = vi.hoisted(() => ({ registerHeadlampEventCallback: vi.fn() })); |
| 53 | +vi.mock('@kinvolk/headlamp-plugin/lib', async () => { |
| 54 | + const actual = await vi.importActual<any>('@kinvolk/headlamp-plugin/lib'); |
| 55 | + return { ...actual, registerHeadlampEventCallback: registerMock.registerHeadlampEventCallback }; |
| 56 | +}); |
| 57 | + |
| 58 | +// Imported AFTER mocks are registered. Both the orchestrator (consent.ts) |
| 59 | +// and the producers/gate (index.ts) are real — nothing under test here is |
| 60 | +// mocked. |
| 61 | +import { grantConsent, revokeConsent } from './consent'; |
| 62 | +import { __resetForTests, initTelemetry, setTelemetryEnabled, trackFeature } from './index'; |
| 63 | + |
| 64 | +const VALID_INSTALL_ID = '11111111-1111-4111-8111-111111111111'; |
| 65 | +const SESSION_PROPS = { |
| 66 | + appVersion: '1.0.0', |
| 67 | + locale: 'en-US', |
| 68 | + os: 'linux' as const, |
| 69 | + arch: 'x64', |
| 70 | + electronVersion: '32.1.0', |
| 71 | + headlampVersion: '0.30.0', |
| 72 | +}; |
| 73 | + |
| 74 | +function realInitialize(): void { |
| 75 | + initTelemetry({ |
| 76 | + connectionString: 'InstrumentationKey=test', |
| 77 | + installId: VALID_INSTALL_ID, |
| 78 | + sessionProps: SESSION_PROPS, |
| 79 | + }); |
| 80 | +} |
| 81 | + |
| 82 | +function eventNames(): string[] { |
| 83 | + return trackEvent.mock.calls.map(([envelope]) => envelope.name); |
| 84 | +} |
| 85 | + |
| 86 | +beforeEach(() => { |
| 87 | + trackEvent.mockClear(); |
| 88 | + loadAppInsights.mockClear(); |
| 89 | + addTelemetryInitializer.mockClear(); |
| 90 | + unload.mockClear(); |
| 91 | + for (const key of Object.keys(config)) delete config[key]; |
| 92 | + ApplicationInsightsCtor.mockReset(); |
| 93 | + ApplicationInsightsCtor.mockImplementation(() => ({ |
| 94 | + trackEvent, |
| 95 | + loadAppInsights, |
| 96 | + addTelemetryInitializer, |
| 97 | + unload, |
| 98 | + config, |
| 99 | + context, |
| 100 | + })); |
| 101 | + context.location = {}; |
| 102 | + context.user = {}; |
| 103 | + registerMock.registerHeadlampEventCallback.mockClear(); |
| 104 | + __resetForTests(); |
| 105 | + setTelemetryEnabled(true); |
| 106 | + realInitialize(); |
| 107 | + trackEvent.mockClear(); |
| 108 | +}); |
| 109 | + |
| 110 | +afterEach(() => { |
| 111 | + // Don't call vi.restoreAllMocks(): it would wipe the ApplicationInsightsCtor |
| 112 | + // mockImplementation re-applied in beforeEach. |
| 113 | +}); |
| 114 | + |
| 115 | +describe('consent <-> index integration: revoke -> grant cycle', () => { |
| 116 | + it('revoke delivers the revoked event and suppresses an ordinary event during the transition', async () => { |
| 117 | + const revokePromise = revokeConsent(); |
| 118 | + // An ordinary event fired mid-transition (gate closed by beginConsentTransition) |
| 119 | + // must not reach the transport. |
| 120 | + trackFeature({ feature: 'headlamp.logs', status: 'opened' }); |
| 121 | + await revokePromise; |
| 122 | + |
| 123 | + expect(eventNames()).toContain('headlamp.telemetry-consent'); |
| 124 | + expect(eventNames()).not.toContain('headlamp.feature'); |
| 125 | + const consentCall = trackEvent.mock.calls.find( |
| 126 | + ([e]) => e.name === 'headlamp.telemetry-consent' |
| 127 | + ); |
| 128 | + expect(consentCall?.[0].properties.consent).toBe('revoked'); |
| 129 | + }); |
| 130 | + |
| 131 | + it('grant delivers both session-start and the granted consent event', async () => { |
| 132 | + // Simulate a real disable (as revokeConsent would have left things), |
| 133 | + // then grant. `initialize` here is the real initTelemetry, exactly as |
| 134 | + // TelemetryBoot wires it up. |
| 135 | + await revokeConsent(); |
| 136 | + trackEvent.mockClear(); |
| 137 | + |
| 138 | + await grantConsent(() => { |
| 139 | + realInitialize(); |
| 140 | + }); |
| 141 | + |
| 142 | + // This is the assertion that would fail before the session-start fix: |
| 143 | + // grantConsent closes the gate via beginConsentTransition before calling |
| 144 | + // initialize, and initTelemetry's trackSessionStart used to route |
| 145 | + // through the gated `emit`, so it was dropped every time. With |
| 146 | + // trackSessionStart routed through emitInternal (bypassing the gate, |
| 147 | + // the same carve-out emitConsentEvent already takes), it reaches the |
| 148 | + // transport even while the gate is still closed during initialize(). |
| 149 | + expect(eventNames()).toContain('headlamp.session-start'); |
| 150 | + expect(eventNames()).toContain('headlamp.telemetry-consent'); |
| 151 | + const consentCall = trackEvent.mock.calls.find( |
| 152 | + ([e]) => e.name === 'headlamp.telemetry-consent' |
| 153 | + ); |
| 154 | + expect(consentCall?.[0].properties.consent).toBe('granted'); |
| 155 | + }); |
| 156 | +}); |
0 commit comments