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 { useAuthStore } from '@/stores/auth.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()
// Ordinary user by default — the copy differs per role, and "user" is the
// case the panel has to get right.
useAuthStore().user = { role: 'user', permissions: [] }
// 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')
// The key destination is shown for the server that has a saved key; the
// other one's location only appears in the admin wording (see below).
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 is NOT served the owner's credential —
// for an ordinary user the server is simply absent. Saying "the shared key is
// in use" here would be a lie.
const httpRow = wrapper.findAll('.mcp-keys-row').find(r => r.text().includes('http-server'))
expect(httpRow.text()).toContain('this server is not connected for you')
expect(httpRow.text()).not.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('tells an admin that the shared key stands in for a missing personal one', async () => {
useAuthStore().user = { role: 'admin', permissions: [] }
const wrapper = mount(McpKeysPanel)
await new Promise(r => setTimeout(r))
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')
expect(httpRow.text()).not.toContain('this server is not connected for you')
// The panel is a settings surface the user must be able to read: the
// description has to explain the rule that actually applies to them.
expect(wrapper.text()).toContain('leave yours unset and it keeps using the shared key')
})
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)
})
})