mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 21:40:13 +00:00
076a7c2a38
- Set up Vitest with jsdom for client tests, node for server tests - Add tests for auth service, proxy handler, API client, and profiles store - Strip Authorization header in proxy to prevent web-ui token leaking to gateway - Distinguish local BFF vs proxied gateway 401s to avoid false logouts - Remove unused hero.png asset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const mockFetch = vi.fn()
|
|
vi.stubGlobal('fetch', mockFetch)
|
|
|
|
// vi.mock is hoisted, so mockReplace must be inside the factory
|
|
vi.mock('@/router', () => ({
|
|
default: {
|
|
currentRoute: { value: { name: 'hermes.chat' } },
|
|
replace: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
import { getApiKey, setApiKey, clearApiKey, hasApiKey, request } from '../../packages/client/src/api/client'
|
|
import router from '@/router'
|
|
|
|
describe('API Client', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('token management', () => {
|
|
it('hasApiKey returns false when no token', () => {
|
|
expect(hasApiKey()).toBe(false)
|
|
})
|
|
|
|
it('hasApiKey returns true after setApiKey', () => {
|
|
setApiKey('test-token')
|
|
expect(hasApiKey()).toBe(true)
|
|
})
|
|
|
|
it('getApiKey returns the stored token', () => {
|
|
setApiKey('my-token')
|
|
expect(getApiKey()).toBe('my-token')
|
|
})
|
|
|
|
it('clearApiKey removes the token', () => {
|
|
setApiKey('my-token')
|
|
clearApiKey()
|
|
expect(hasApiKey()).toBe(false)
|
|
expect(getApiKey()).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('request', () => {
|
|
it('adds Authorization header when token exists', async () => {
|
|
setApiKey('secret-key')
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => ({ data: 1 }) })
|
|
|
|
await request('/api/hermes/sessions')
|
|
|
|
expect(mockFetch).toHaveBeenCalledOnce()
|
|
const [, options] = mockFetch.mock.calls[0]
|
|
expect(options.headers.Authorization).toBe('Bearer secret-key')
|
|
})
|
|
|
|
it('does not add Authorization header when no token', async () => {
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => ({ data: 1 }) })
|
|
|
|
await request('/api/hermes/sessions')
|
|
|
|
const [, options] = mockFetch.mock.calls[0]
|
|
expect(options.headers.Authorization).toBeUndefined()
|
|
})
|
|
|
|
it('clears token and redirects on 401 for local BFF endpoints', async () => {
|
|
setApiKey('secret-key')
|
|
mockFetch.mockResolvedValue({ ok: false, status: 401 })
|
|
|
|
await expect(request('/api/hermes/sessions')).rejects.toThrow('Unauthorized')
|
|
expect(hasApiKey()).toBe(false)
|
|
expect(router.replace).toHaveBeenCalledWith({ name: 'login' })
|
|
})
|
|
|
|
it('does NOT clear token on 401 for proxied v1 endpoints', async () => {
|
|
setApiKey('secret-key')
|
|
mockFetch.mockResolvedValue({ ok: false, status: 401, text: () => Promise.resolve('') })
|
|
|
|
await expect(request('/api/hermes/v1/runs')).rejects.toThrow('API Error 401')
|
|
expect(hasApiKey()).toBe(true)
|
|
})
|
|
|
|
it('does NOT clear token on 401 for proxied jobs endpoints', async () => {
|
|
setApiKey('secret-key')
|
|
mockFetch.mockResolvedValue({ ok: false, status: 401, text: () => Promise.resolve('') })
|
|
|
|
await expect(request('/api/hermes/jobs')).rejects.toThrow('API Error 401')
|
|
expect(hasApiKey()).toBe(true)
|
|
})
|
|
|
|
it('does NOT clear token on 401 for proxied skills endpoints', async () => {
|
|
setApiKey('secret-key')
|
|
mockFetch.mockResolvedValue({ ok: false, status: 401, text: () => Promise.resolve('') })
|
|
|
|
await expect(request('/api/hermes/skills')).rejects.toThrow('API Error 401')
|
|
expect(hasApiKey()).toBe(true)
|
|
})
|
|
|
|
it('throws error on non-401 failure', async () => {
|
|
mockFetch.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
text: () => Promise.resolve('Internal Server Error'),
|
|
})
|
|
|
|
await expect(request('/api/hermes/sessions')).rejects.toThrow('API Error 500: Internal Server Error')
|
|
})
|
|
|
|
it('returns parsed JSON on success', async () => {
|
|
const data = { sessions: [{ id: '1' }] }
|
|
mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve(data) })
|
|
|
|
const result = await request('/api/hermes/sessions')
|
|
expect(result).toEqual(data)
|
|
})
|
|
})
|
|
})
|