Newer
Older
navi-1 / webclient / tests / unit / components / settings / mcpKeysPanel.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { useMcpKeysStore } from '@/stores/mcpKeys.js'
import McpKeysPanel from '@/components/settings/McpKeysPanel.vue'

vi.mock('@/api/index.js', () => ({
  getMcpKeys: vi.fn(),
  saveMcpKey: vi.fn(),
  resetMcpKey: vi.fn(),
}))

vi.mock('@/composables/useConfirm.js', () => ({
  useConfirm: () => () => Promise.resolve(true),
}))

// No GnToastProvider in tests — swallow toast calls.
vi.mock('gnexus-ui-kit/vue', async (importOriginal) => {
  const mod = await importOriginal()
  const toast = {
    show: () => {}, info: () => {}, success: () => {},
    warning: () => {}, danger: () => {}, error: () => {}, close: () => {},
  }
  return { ...mod, useToast: () => toast }
})

import * as api from '@/api/index.js'

const ITEMS = [
  {
    server_name: 'http-server',
    transport: 'streamable_http',
    accepts_user_key: true,
    key_type: 'header',
    key_location: 'Authorization',
    prefix: 'Bearer ',
    has_key: false,
    updated_at: null,
    profiles: ['server_admin'],
  },
  {
    server_name: 'stdio-server',
    transport: 'stdio',
    accepts_user_key: true,
    key_type: 'env',
    key_location: 'API_KEY',
    prefix: null,
    has_key: true,
    updated_at: '2026-10-07T12:00:00Z',
    profiles: ['discuss', 'server_admin'],
  },
  {
    server_name: 'navi-web',
    transport: 'stdio',
    accepts_user_key: false,
    key_type: null,
    key_location: null,
    prefix: null,
    has_key: false,
    updated_at: null,
    profiles: ['discuss', 'secretary'],
  },
]

describe('McpKeysPanel', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
    // Fresh clone per test — the store mutates items in place.
    api.getMcpKeys.mockImplementation(async () => ({ items: structuredClone(ITEMS) }))
    api.saveMcpKey.mockImplementation(async (server, key) => ({
      server_name: server, has_key: true, updated_at: '2026-10-08T00:00:00Z',
    }))
    api.resetMcpKey.mockResolvedValue(undefined)
  })

  it('renders every connected server, keyed ones with their key location', async () => {
    const wrapper = mount(McpKeysPanel)
    await new Promise(r => setTimeout(r))
    expect(wrapper.text()).toContain('MCP servers')
    expect(wrapper.text()).toContain('http-server')
    expect(wrapper.text()).toContain('stdio-server')
    expect(wrapper.text()).toContain('Authorization header (Bearer …)')
    expect(wrapper.text()).toContain('API_KEY env')
    // Keyless servers are listed too, with the profile they serve.
    expect(wrapper.text()).toContain('navi-web')
    expect(wrapper.text()).toContain('discuss, secretary')
    expect(wrapper.text()).toContain('No personal key slot')
    expect(wrapper.findAll('.mcp-keys-row')).toHaveLength(3)
  })

  it('offers a key input only for servers that declare a slot', async () => {
    const wrapper = mount(McpKeysPanel)
    await new Promise(r => setTimeout(r))
    // Three rows, two of them with a slot → exactly two password inputs.
    expect(wrapper.findAll('input[type="password"]')).toHaveLength(2)
    const keylessRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('navi-web'))
    expect(keylessRow.classes()).toContain('is-keyless')
    expect(keylessRow.find('input').exists()).toBe(false)
    // A keyed server without a saved key says the shared one is in use.
    const httpRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('http-server'))
    expect(httpRow.text()).toContain('the shared key from the config is used')
    const stdioRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('stdio-server'))
    expect(stdioRow.text()).toContain('Personal key set')
  })

  it('shows an empty-state message when no server is connected', async () => {
    api.getMcpKeys.mockResolvedValue({ items: [] })
    const wrapper = mount(McpKeysPanel)
    await new Promise(r => setTimeout(r))
    expect(wrapper.find('.mcp-keys-empty').exists()).toBe(true)
    expect(wrapper.text()).toContain('No MCP servers are connected to any profile')
    expect(wrapper.find('.mcp-keys-row').exists()).toBe(false)
  })

  it('saving a key PUTs it and marks the server as keyed', async () => {
    const wrapper = mount(McpKeysPanel)
    await new Promise(r => setTimeout(r))

    const input = wrapper.findAll('input').find(i => i.attributes('type') === 'password')
    input.element.value = 'sk-user'
    await input.trigger('input')

    const saveBtn = wrapper
      .findAll('button')
      .find(b => b.text().includes('Save'))
    await saveBtn.trigger('click')
    await new Promise(r => setTimeout(r))

    expect(api.saveMcpKey).toHaveBeenCalledWith('http-server', 'sk-user')
    const store = useMcpKeysStore()
    expect(store.servers[0].has_key).toBe(true)
    // input cleared after save
    expect(input.element.value).toBe('')
  })

  it('reset asks for confirmation and DELETEs the saved key', async () => {
    const wrapper = mount(McpKeysPanel)
    await new Promise(r => setTimeout(r))

    const resetBtn = wrapper
      .findAll('button')
      .find(b => b.text().includes('Reset to default'))
    expect(resetBtn).toBeTruthy()
    await resetBtn.trigger('click')
    await new Promise(r => setTimeout(r))

    expect(api.resetMcpKey).toHaveBeenCalledWith('stdio-server')
    const store = useMcpKeysStore()
    expect(store.servers[1].has_key).toBe(false)
  })
})