🏡 index : quine-global/webdriverio.git

import * as FunnelTestEvent from '../../src/instrumentation/funnelInstrumentation.js'
import { sendFinish, sendStart } from '../../src/instrumentation/funnelInstrumentation.js'
import { BStackLogger } from '../../src/bstackLogger.js'
import fs from 'node:fs'
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import { FUNNEL_INSTRUMENTATION_URL } from '../../src/constants.js'
import type { BrowserstackHealing } from '@browserstack/ai-sdk-node'

vi.mock('fetch')
const mockedFetch = vi.mocked(fetch)

const config = {
    userName: 'your-username',
    accessKey: 'your-access-key',
    testObservability: { enabled: true },
    framework: 'framework',
    buildName: 'build-name',
    buildIdentifier: 'your-build-identifier',
    accessibility: true,
    percy: true,
    automate: true,
    appAutomate: false,
}

const expectedEventData = {
    userName: '[REDACTED]',
    accessKey: '[REDACTED]',
    event_type: 'SDKTestAttempted',
    detectedFramework: 'WebdriverIO-framework',
    event_properties: {
        language_framework: 'WebdriverIO_framework',
        referrer: expect.stringContaining('WebdriverIO-'),
        language: 'WebdriverIO',
        languageVersion: process.version,
        buildName: config.buildName,
        buildIdentifier: config.buildIdentifier,
        os: expect.any(String),
        hostname: expect.any(String),
        productMap: {
            'observability': true,
            'accessibility': true,
            'percy': true,
            'automate': true,
            'app_automate': false
        },
        product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']),
        framework: 'framework'
    }
}

describe('funnelInstrumentation', () => {
    let originalCwd: { (): string; (): string }

    beforeEach(() => {
        originalCwd = process.cwd
        process.cwd = () => '/path/to/project'
        vi.spyOn(BStackLogger, 'warn').mockImplementation(() => {})
    })

    afterEach(() => {
        process.cwd = originalCwd
        vi.restoreAllMocks()
        vi.resetAllMocks()
        vi.clearAllMocks()
    })

    describe('sendStart', () => {
        it('does nothing if userName or accessKey is missing in config', async () => {
            const config = { userName: '', accessKey: '' }
            await FunnelTestEvent.sendStart(config as any)

            expect(fetch).not.toHaveBeenCalled()
        })

        it('sendStart calls sends request with correct data', async () => {
            await sendStart(config as any)

            expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
                method: 'POST',
                headers: expect.any(Object),
                body: expect.any(String) // TODO: find a way to match exact
            }))
        })
    })

    describe('sendFinish', () => {
        it('sendFinish calls sends request with correct data', async () => {
            const finishConfig = {
                ...config,
                'accessibility': false,
                'percy': false,
            }

            const finishExpectedEventData = {
                ...expectedEventData,
                event_type: 'SDKTestSuccessful',
                event_properties: {
                    ...expectedEventData.event_properties,
                    productMap: {
                        'observability': true,
                        'accessibility': false,
                        'percy': false,
                        'automate': true,
                        'app_automate': false
                    },
                    product: expect.arrayContaining(['observability', 'automate']),
                    productUsage: expect.objectContaining({
                        testObservability: expect.any(Object)
                    })
                },
            }

            await sendFinish(finishConfig as any)
            expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
                method: 'POST',
                headers: expect.any(Object),
                body: expect.any(String) // TODO: find a way to match exact
            }))
        })
    })

    it('saveFunnelData writes data to file and returns file path', () => {
        BStackLogger.ensureLogsFolder = vi.fn()
        vi.spyOn(fs, 'writeFileSync').mockImplementationOnce(() => {})
        const filePath = FunnelTestEvent.saveFunnelData('SDKTestSuccessful', config as any)
        expect(fs.writeFileSync).toHaveBeenCalledWith(filePath, expect.any(String))
    })

    it('fireFunnelRequest sends request with correct data', async () => {
        const data = { key: 'value', userName: '[REDACTED]', accessKey: '[REDACTED]' }
        mockedFetch.mockReturnValueOnce(Promise.resolve(Response.json({})))
        await FunnelTestEvent.fireFunnelRequest(data)
        expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
            method: 'POST',
            headers: expect.any(Object),
            body: JSON.stringify(data)
        }))
    })

    // NOT WORKING CODE:

    describe('Healing instrumentation', () => {

        it('should not send instrumentation event in case user receives an upgrade required warning', async () => {
            const authResult = { message: 'Upgrade required' } as BrowserstackHealing.InitErrorResponse
            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)
            expect(fetch).not.toHaveBeenCalled()
        })

        it('should send server error event when isAuthenticated is false and status is 5xx', async () => {
            const authResult = { isAuthenticated: false, status: 500 } as BrowserstackHealing.InitErrorResponse

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)

            expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
                method: 'POST',
                headers: {
                    'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
                    'content-type': 'application/json',
                },

                body: expect.stringContaining('SDKTestTcgDownResponse')
            }))
            const [[, { body }]] = (fetch as jest.Mock).mock.calls
            const parsedBody = JSON.parse(body)

            expectedEventData.event_type = 'SDKTestTcgDownResponse'
            expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
        })

        it('should send initialization success event when userId is present', async () => {
            const authResult = {
                isAuthenticated: true,
                userId: 123456,
                groupId: 234567,
                sessionToken: 'session-token',
                isGroupAIEnabled: true,
                defaultLogDataEnabled: true,
                isHealingEnabled: true
            } as BrowserstackHealing.InitSuccessResponse

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)

            expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
                method: 'POST',
                headers: {
                    'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
                    'content-type': 'application/json',
                },

                body: expect.stringContaining('SDKTestTcgtInitSuccessful')
            }))
            const [[, { body }]] = (fetch as jest.Mock).mock.calls
            const parsedBody = JSON.parse(body)

            expectedEventData.event_type = 'SDKTestTcgtInitSuccessful'
            expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
        })

        it('should send initialization failed event if status is 4xx', async () => {
            const authResult = {
                isAuthenticated: true,
                status: 400,
                message: 'Request failed with status code 400'
            } as any

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)

            expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
                method: 'POST',
                headers: {
                    'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
                    'content-type': 'application/json',
                },

                body: expect.stringContaining('SDKTestInitFailedResponse')
            }))
            const [[, { body }]] = (fetch as jest.Mock).mock.calls
            const parsedBody = JSON.parse(body)

            expectedEventData.event_type = 'SDKTestInitFailedResponse'
            expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
        })

        it('should send server error event when isAuthenticated is false and status is 5xx', async () => {
            const authResult = { isAuthenticated: true } as any

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)

            expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({
                method: 'POST',
                headers: {
                    'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`,
                    'content-type': 'application/json',
                },
                body: expect.stringContaining('SDKTestInvalidTcgAuthResponseWithUserImpact')
            }))
            const [[, { body }]] = (fetch as jest.Mock).mock.calls
            const parsedBody = JSON.parse(body)

            expectedEventData.event_type = 'SDKTestInvalidTcgAuthResponseWithUserImpact'
            expect(parsedBody).toEqual(expect.objectContaining(expectedEventData))
        })

        it('should not send user impact event if selfHeal is not enabled by user', async () => {
            const authResult = {
                isAuthenticated: true
            } as any

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, false)

            expect(fetch).not.toHaveBeenCalled()
        })

        it('should handle exceptions during healing instrumentation', async () => {
            // Create an authResult that throws an error when 'userId' is accessed
            const authResult = {
                isAuthenticated: true,
                get userId() {
                    throw new Error('Property access error')
                }
            } as any

            const debugSpy = vi.spyOn(BStackLogger, 'debug')

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true)

            expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('Error in handling healing instrumentation:'))
            expect(fetch).not.toHaveBeenCalled() // Ensure no event is sent
        })

        it('should handle healing not enabled for user', async () => {
            const authResult = {
                isAuthenticated: true,
                userId: 123456,
                groupId: 234567,
                sessionToken: 'session-token',
                isGroupAIEnabled: true,
                defaultLogDataEnabled: true,
                isHealingEnabled: false
            } as BrowserstackHealing.InitSuccessResponse

            FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, false)

            expect(fetch).not.toHaveBeenCalled()
        })

        it('should handle healing disabled for the group', async () => {
            const authResult = {
                isAuthenticated: true,
                userId: 123456,
                groupId: 234567,
                sessionToken: 'session-token',
                isGroupAIEnabled: true,
                defaultLogDataEnabled: true,
                isHealingEnabled: false
            } as BrowserstackHealing.InitSuccessResponse

            const bstackConfig = {
                ...config,
                selfHeal: true
            }

            FunnelTestEvent.handleHealingInstrumentation(authResult, bstackConfig as any, true)

            expect(fetch).not.toHaveBeenCalled()
            expect(BStackLogger.warn).toHaveBeenCalledWith('Healing is not enabled for your group, please contact the admin')
        })
    })
})