Ctrl K
ComunidadGuías…

tests/apikey-hidden-scope-notice.unit.test.ts

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

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

import type { App } from 'vue'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import en from '../messages/en.json'
import ApiKeyHiddenScopeNotice from '../src/components/ApiKeyHiddenScopeNotice.vue'
import DataTable from '../src/components/DataTable.vue'

vi.mock('@vuepic/vue-datepicker', async () => {
  const { defineComponent, h } = await import('vue')
  return {
    VueDatePicker: defineComponent({
      setup() {
        return () => h('div')
      },
    }),
  }
})

vi.mock('~/components/permissions/ChannelPermissionOverridesPanel.vue', async () => {
  const { defineComponent, h } = await import('vue')
  return {
    default: defineComponent({
      setup() {
        return () => h('div')
      },
    }),
  }
})

vi.mock('~/services/capgoApi', () => ({
  invokeCapgoApi: async () => ({
    data: [
      {
        id: 1,
        name: 'Current organization key',
        key: 'current-org-key',
        key_hash: null,
        rbac_id: 'rbac-org-a',
        created_at: '2026-08-11T12:00:00.000Z',
        expires_at: null,
      },
      {
        id: 2,
        name: 'Hidden organization key',
        key: 'hidden-org-key',
        key_hash: null,
        rbac_id: 'rbac-org-b',
        created_at: '2026-08-11T11:00:00.000Z',
        expires_at: null,
      },
    ],
    error: null,
  }),
}))

vi.mock('~/services/nativeCompliance', () => ({
  isNativeAppStoreContext: () => false,
}))

vi.mock('~/services/permissions', () => ({
  checkPermissions: async () => true,
}))

vi.mock('~/services/console', () => {
  function createQuery(table: string) {
    let selectedId: string | null = null
    const query: Record<string, any> = {
      select: () => query,
      eq: (column: string, value: string) => {
        if (column === 'id')
          selectedId = value
        return query
      },
      in: () => query,
      order: () => query,
      single: async () => ({
        data: selectedId
          ? { id: selectedId, name: selectedId === 'org-a' ? 'Organization A' : 'Organization B' }
          : null,
        error: null,
      }),
      then: (resolve: (value: unknown) => void, reject: (reason?: unknown) => void) => {
        const data = table === 'role_bindings'
          ? [
              {
                id: 'binding-a',
                principal_id: 'rbac-org-a',
                scope_type: 'org',
                org_id: 'org-a',
                app_id: null,
                roles: { name: 'org_member' },
              },
              {
                id: 'binding-b',
                principal_id: 'rbac-org-b',
                scope_type: 'org',
                org_id: 'org-b',
                app_id: null,
                roles: { name: 'org_member' },
              },
            ]
          : table === 'roles'
            ? [
                {
                  id: 'role-org-member',
                  name: 'org_member',
                  scope_type: 'org',
                  description: null,
                  priority_rank: 10,
                },
              ]
            : []

        return Promise.resolve({ data, error: null }).then(resolve, reject)
      },
    }
    return query
  }

  return {
    useConsole: () => ({
      auth: {
        getClaims: async () => ({ data: { claims: { sub: 'user-id' } } }),
      },
      from: (table: string) => createQuery(table),
    }),
  }
})

vi.mock('~/stores/dialogv2', () => ({
  useDialogV2Store: () => ({
    openDialog: vi.fn(),
    onDialogDismiss: async () => undefined,
  }),
}))

vi.mock('~/stores/display', () => ({
  useDisplayStore: () => ({ NavTitle: '', defaultBack: '' }),
}))

vi.mock('~/stores/main', () => ({
  useMainStore: () => ({ user: { id: 'user-id' } }),
}))

vi.mock('~/stores/organization', async () => {
  const { reactive } = await import('vue')
  const store = reactive({
    currentOrganization: { gid: 'org-a', name: 'Organization A' },
    organizations: [
      { gid: 'org-a', name: 'Organization A', role: 'org_admin' },
      { gid: 'org-b', name: 'Organization B', role: 'org_admin' },
    ],
  })
  return {
    getRbacRoleI18nKey: () => null,
    useOrganizationStore: () => store,
  }
})

const mountedApps: App[] = []

function testI18n() {
  return createI18n({
    legacy: false,
    locale: 'en',
    messages: { en },
  })
}

function mountApp(app: App) {
  const container = document.createElement('div')
  app.use(testI18n())
  app.config.warnHandler = () => undefined
  app.mount(container)
  mountedApps.push(app)
  return container
}

function mountNotice(hiddenCount: number, isLoading = false) {
  const removeFilter = vi.fn()
  const app = createApp(ApiKeyHiddenScopeNotice, {
    hiddenCount,
    isLoading,
    onRemoveFilter: removeFilter,
  })
  const container = mountApp(app)

  return { container, removeFilter }
}

async function flushPromises() {
  await Promise.resolve()
  await Promise.resolve()
  await Promise.resolve()
}

afterEach(() => {
  mountedApps.splice(0).forEach(app => app.unmount())
})

describe('api key hidden-scope notice', () => {
  it('stays hidden when no matching keys are excluded or while keys are loading', () => {
    expect(mountNotice(0).container.querySelector('[role="status"]')).toBeNull()
    expect(mountNotice(3, true).container.querySelector('[role="status"]')).toBeNull()
  })

  it('renders the plural translated status and emits the remove-filter action', () => {
    const { container, removeFilter } = mountNotice(3)
    const status = container.querySelector('[role="status"]')
    const button = container.querySelector('button')

    expect(status?.querySelector('span')?.textContent?.trim()).toBe(
      '3 API keys are hidden by the current scope filter.',
    )
    expect(status?.querySelector('[data-test="scope-notice-icon"]')).not.toBeNull()
    expect(status?.classList.contains('border-slate-200')).toBe(true)
    expect(status?.classList.contains('bg-slate-50')).toBe(true)
    expect(status?.className).not.toContain('cyan')
    expect(button?.textContent?.trim()).toBe('Remove the filter')
    expect(button?.classList.contains('d-btn')).toBe(true)
    expect(button?.classList.contains('d-btn-link')).toBe(true)
    expect(button?.classList.contains('text-blue-600')).toBe(true)

    button?.click()

    expect(removeFilter).toHaveBeenCalledOnce()
  })

  it('renders the singular translated status', () => {
    const { container } = mountNotice(1)

    expect(container.querySelector('[role="status"] span')?.textContent?.trim()).toBe(
      '1 API key is hidden by the current scope filter.',
    )
  })

  it('renders the DataTable notice slot after its toolbar and before its table', () => {
    const root = defineComponent({
      setup() {
        return () => h(DataTable, {
          autoReload: false,
          columns: [],
          currentPage: 1,
          elementList: [],
          total: 0,
        }, {
          'table-notice': () => h('div', { 'data-test': 'table-notice' }, 'Scope notice'),
        })
      },
    })
    const container = mountApp(createApp(root))
    const toolbarButton = container.querySelector('button')
    const notice = container.querySelector('[data-test="table-notice"]')
    const table = container.querySelector('table')

    expect(toolbarButton).not.toBeNull()
    expect(notice).not.toBeNull()
    expect(table).not.toBeNull()
    expect(toolbarButton!.compareDocumentPosition(notice!) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0)
    expect(notice!.compareDocumentPosition(table!) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0)
  })

  it('clears the active ApiKeys page scope, resets pagination, and preserves search', async () => {
    const DataTableStub = defineComponent({
      props: {
        currentPage: {
          type: Number,
          default: 1,
        },
        filters: {
          type: Object,
          default: () => ({}),
        },
        search: {
          type: String,
          default: '',
        },
      },
      emits: ['update:currentPage', 'update:search'],
      setup(props, { emit, slots }) {
        return () => h('section', {
          'data-test': 'api-key-data-table',
          'data-active-scope-count': Object.values(props.filters).filter(Boolean).length,
          'data-current-page': props.currentPage,
          'data-search': props.search,
        }, [
          h('button', {
            'data-test': 'set-search',
            'onClick': () => emit('update:search', 'organization'),
          }),
          h('button', {
            'data-test': 'set-page',
            'onClick': () => emit('update:currentPage', 3),
          }),
          slots['table-notice']?.(),
          h('table'),
        ])
      },
    })
    const ApiKeysPage = (await import('../src/pages/ApiKeys.vue')).default
    const app = createApp(ApiKeysPage)
    app.component('DataTable', DataTableStub)
    const container = mountApp(app)

    await flushPromises()
    await vi.waitFor(() => {
      expect(container.querySelector('[data-test="api-key-data-table"]')?.getAttribute('data-active-scope-count')).toBe('1')
      expect(container.querySelector('[role="status"] button')).not.toBeNull()
    })

    const table = container.querySelector('[data-test="api-key-data-table"]')
    const setSearch = container.querySelector('[data-test="set-search"]') as HTMLButtonElement
    const setPage = container.querySelector('[data-test="set-page"]') as HTMLButtonElement

    setSearch.click()
    setPage.click()
    await nextTick()

    expect(table?.getAttribute('data-current-page')).toBe('3')
    expect(table?.getAttribute('data-search')).toBe('organization')

    const removeFilter = container.querySelector('[role="status"] button') as HTMLButtonElement

    removeFilter.click()
    await nextTick()

    expect(table?.getAttribute('data-active-scope-count')).toBe('0')
    expect(table?.getAttribute('data-current-page')).toBe('1')
    expect(table?.getAttribute('data-search')).toBe('organization')
    expect(container.querySelector('[role="status"]')).toBeNull()
  })
})