Ctrl K
ComunidadGuías…

tests/app-onboarding-apikey-runtime.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-environment happy-dom

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, nextTick } from 'vue'
import AppOnboardingFlow from '../src/components/dashboard/AppOnboardingFlow.vue'

const runtimeMocks = vi.hoisted(() => {
  const app = {
    app_id: 'com.test.runtime-onboarding',
    name: 'Runtime onboarding app',
    owner_org: 'org-runtime-onboarding',
    existing_app: false,
    icon_url: null,
    ios_store_url: null,
    android_store_url: null,
  }

  return {
    app,
    createApp: vi.fn(async () => ({
      ok: true as const,
      app,
      usedAppId: app.app_id,
      originalAppId: app.app_id,
      wasRetried: false,
    })),
    createDefaultApiKey: vi.fn(),
    findUsablePlainApiKey: vi.fn(async (): Promise<string | null> => 'runtime-api-key'),
    main: {
      auth: { id: 'user-runtime-onboarding' },
      awaitInitialLoad: vi.fn(async () => undefined),
      isAdmin: false,
      plans: [],
      user: { id: 'user-runtime-onboarding', onboarding: {} } as { id: string, onboarding: Record<string, unknown> } | null,
    },
    organizationStore: {
      getOrgByAppId: vi.fn(),
      setCurrentOrganization: vi.fn(),
      awaitInitialLoad: vi.fn(async () => undefined),
      currentOrganization: { gid: 'org-runtime-onboarding', name: 'Runtime organization' },
      organizations: [],
      updateAppOnboarding: vi.fn(),
      upsertOrganizationApp: vi.fn(),
    },
    query: {} as Record<string, string>,
    router: {
      push: vi.fn(),
      replace: vi.fn(async () => undefined),
    },
  }
})

vi.mock('vue-i18n', () => ({
  useI18n: () => ({ t: (key: string) => key }),
}))
vi.mock('vue-router', () => ({
  useRoute: () => ({ query: runtimeMocks.query }),
  useRouter: () => runtimeMocks.router,
}))
vi.mock('vue-sonner', () => ({ toast: { error: vi.fn(), info: vi.fn(), success: vi.fn() } }))
vi.mock('~/services/apikeys', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../src/services/apikeys')>()
  return {
    ...actual,
    createDefaultApiKey: runtimeMocks.createDefaultApiKey,
    findUsablePlainApiKey: runtimeMocks.findUsablePlainApiKey,
  }
})
vi.mock('~/services/onboardingTracking', () => ({ sendOnboardingEvent: vi.fn() }))
vi.mock('~/services/console', () => ({
  getLocalConfig: () => ({ apiHost: 'https://api.capgo.app' }),
  isLocal: () => false,
  useConsole: () => {
    const query = {
      eq: () => query,
      maybeSingle: async () => ({ data: null, error: null }),
      select: () => query,
      single: async () => ({ data: runtimeMocks.app, error: null }),
    }
    return {
      auth: { getClaims: async () => ({ data: { claims: { sub: runtimeMocks.main.auth.id } } }) },
      from: () => query,
      rpc: async () => ({ data: null, error: null }),
    }
  },
}))
vi.mock('~/stores/dashboardApps', () => ({ useDashboardAppsStore: () => ({ upsertApp: vi.fn() }) }))
vi.mock('~/stores/dialogv2', () => ({
  useDialogV2Store: () => ({
    lastButtonRole: null,
    onDialogDismiss: vi.fn(async () => undefined),
    openDialog: vi.fn(),
  }),
}))
vi.mock('~/stores/main', () => ({ useMainStore: () => runtimeMocks.main }))
vi.mock('~/stores/organization', () => ({ useOrganizationStore: () => runtimeMocks.organizationStore }))
vi.mock('~/utils/onboardingAppCreateHelpers', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../src/utils/onboardingAppCreateHelpers')>()
  return { ...actual, createOnboardingAppWithFallbackIds: runtimeMocks.createApp }
})
vi.mock('~/utils/onboardingProgressPersistence', () => ({
  createOnboardingProgressPersistence: () => ({
    abort: vi.fn(),
    isAborted: () => false,
    isBlocked: () => false,
    persist: async () => 'persisted',
  }),
  shouldInitializeOnboardingProgressTracking: () => true,
}))

interface MountedFlow {
  app: ReturnType<typeof createApp>
  container: HTMLDivElement
}

const mountedFlows: MountedFlow[] = []

async function mountFlow(query: Record<string, string> = {}, props: { setupAppId?: string } = {}) {
  runtimeMocks.query = query
  const container = document.createElement('div')
  document.body.appendChild(container)
  const app = createApp(AppOnboardingFlow, { onboarding: false, ...props })
  app.config.warnHandler = () => undefined
  app.mount(container)
  const mounted = { app, container }
  mountedFlows.push(mounted)
  return mounted
}

function element<T extends Element>(container: Element, selector: string) {
  const value = container.querySelector<T>(selector)
  if (!value)
    throw new Error(`Missing onboarding element: ${selector}`)
  return value
}

async function click(container: Element, selector: string) {
  element<HTMLButtonElement>(container, selector).click()
  await nextTick()
}

async function reachIconStep(container: Element) {
  await vi.waitFor(() => expect(container.querySelector('[data-test="app-onboarding-name"]')).not.toBeNull())

  const nameInput = element<HTMLInputElement>(container, '[data-test="app-onboarding-name"]')
  nameInput.value = 'Runtime onboarding app'
  nameInput.dispatchEvent(new Event('input', { bubbles: true }))
  await nextTick()

  await click(container, '[data-test="app-onboarding-continue"]')
  await vi.waitFor(() => expect(container.textContent).toContain('app-onboarding-icon-step-title'))
}

beforeEach(() => {
  runtimeMocks.query = {}
  runtimeMocks.createApp.mockClear()
  runtimeMocks.router.push.mockClear()
  runtimeMocks.router.replace.mockClear()
  runtimeMocks.createDefaultApiKey.mockReset()
  runtimeMocks.createDefaultApiKey.mockResolvedValue({ data: { key: 'runtime-created-api-key' }, error: null })
  runtimeMocks.findUsablePlainApiKey.mockReset()
  runtimeMocks.findUsablePlainApiKey.mockResolvedValue('runtime-api-key')
  runtimeMocks.main.awaitInitialLoad.mockClear()
  runtimeMocks.main.user = { id: runtimeMocks.main.auth.id, onboarding: {} }
  runtimeMocks.organizationStore.awaitInitialLoad.mockClear()
  runtimeMocks.organizationStore.updateAppOnboarding.mockClear()
  Object.defineProperty(window, 'matchMedia', {
    configurable: true,
    value: vi.fn(() => ({
      addEventListener: vi.fn(),
      addListener: vi.fn(),
      matches: false,
      removeEventListener: vi.fn(),
      removeListener: vi.fn(),
    })),
  })
})

afterEach(() => {
  for (const mounted of mountedFlows.splice(0)) {
    mounted.app.unmount()
    mounted.container.remove()
  }
})

describe('app onboarding API key runtime loading', () => {
  it('reuses a key when a fresh existing-org wizard is opened', async () => {
    const { container } = await mountFlow()

    await vi.waitFor(() => expect(container.querySelector('[data-test="app-onboarding-name"]')).not.toBeNull())
    await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1))

    expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledWith(
      expect.anything(),
      runtimeMocks.main.auth.id,
      runtimeMocks.organizationStore.currentOrganization.gid,
      undefined,
    )
    expect(runtimeMocks.createDefaultApiKey).not.toHaveBeenCalled()
  })

  it('uses the authenticated user ID while the public profile is still loading', async () => {
    runtimeMocks.main.user = null

    await mountFlow()

    await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1))
    expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledWith(
      expect.anything(),
      runtimeMocks.main.auth.id,
      runtimeMocks.organizationStore.currentOrganization.gid,
      undefined,
    )
  })

  it('retries a settled failed load when setup continues on Getting started', async () => {
    const loadError = new Error('transient API-key failure')
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
    runtimeMocks.findUsablePlainApiKey
      .mockRejectedValueOnce(loadError)
      .mockResolvedValueOnce('runtime-retried-api-key')

    try {
      const { container } = await mountFlow()
      await vi.waitFor(() => expect(consoleError).toHaveBeenCalledWith('Cannot ensure API key', loadError))
      await reachIconStep(container)
      await click(container, '[data-test="app-onboarding-continue"]')
      await vi.waitFor(() => expect(runtimeMocks.createApp).toHaveBeenCalledTimes(1))
      await vi.waitFor(() => expect(runtimeMocks.router.replace).toHaveBeenCalledWith({
        path: `/app/${runtimeMocks.app.app_id}/getting-started`,
        state: {
          capgoOnboardingSetupHandoff: expect.objectContaining({
            appId: runtimeMocks.app.app_id,
            flow: 'existing_org',
            previousStep: 'app_icon',
          }),
        },
      }))
      expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1)

      // Getting started mounts the setup flow for the created app.
      await mountFlow({}, { setupAppId: runtimeMocks.app.app_id })

      await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(2))
      expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenLastCalledWith(
        expect.anything(),
        runtimeMocks.main.auth.id,
        runtimeMocks.organizationStore.currentOrganization.gid,
        runtimeMocks.app.app_id,
      )
    }
    finally {
      consoleError.mockRestore()
    }
  })

  it('shares an in-flight key load across concurrent onboarding component instances', async () => {
    let finishLookup: ((key: string | null) => void) | undefined
    runtimeMocks.findUsablePlainApiKey.mockImplementation(() => new Promise((resolve) => {
      finishLookup = resolve
    }))

    await Promise.all([
      mountFlow(),
      mountFlow(),
    ])

    await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalled())
    expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1)

    finishLookup?.(null)
    await vi.waitFor(() => expect(runtimeMocks.createDefaultApiKey).toHaveBeenCalledTimes(1))
  })
})