Add user-scoped Hermes profile access
This commit is contained in:
+54
-17
@@ -12,9 +12,15 @@ vi.mock('@/router', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
import { getApiKey, setApiKey, clearApiKey, hasApiKey, request } from '../../packages/client/src/api/client'
|
||||
import { getApiKey, setApiKey, clearApiKey, hasApiKey, getStoredUserRole, isStoredSuperAdmin, request } from '../../packages/client/src/api/client'
|
||||
import router from '@/router'
|
||||
|
||||
function fakeJwt(payload: Record<string, unknown>) {
|
||||
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
const body = btoa(JSON.stringify(payload)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
return `${header}.${body}.signature`
|
||||
}
|
||||
|
||||
describe('API Client', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
@@ -42,6 +48,17 @@ describe('API Client', () => {
|
||||
expect(hasApiKey()).toBe(false)
|
||||
expect(getApiKey()).toBe('')
|
||||
})
|
||||
|
||||
it('reads the role from the stored JWT payload', () => {
|
||||
setApiKey(fakeJwt({ sub: '1', role: 'super_admin' }))
|
||||
|
||||
expect(getStoredUserRole()).toBe('super_admin')
|
||||
expect(isStoredSuperAdmin()).toBe(true)
|
||||
|
||||
setApiKey(fakeJwt({ sub: '2', role: 'admin' }))
|
||||
expect(getStoredUserRole()).toBe('admin')
|
||||
expect(isStoredSuperAdmin()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('request', () => {
|
||||
@@ -56,6 +73,16 @@ describe('API Client', () => {
|
||||
expect(options.headers.Authorization).toBe('Bearer secret-key')
|
||||
})
|
||||
|
||||
it('adds the active profile header, including default', async () => {
|
||||
localStorage.setItem('hermes_active_profile_name', 'default')
|
||||
mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => ({ data: 1 }) })
|
||||
|
||||
await request('/api/hermes/sessions')
|
||||
|
||||
const [, options] = mockFetch.mock.calls[0]
|
||||
expect(options.headers['X-Hermes-Profile']).toBe('default')
|
||||
})
|
||||
|
||||
it('does not add Authorization header when no token', async () => {
|
||||
mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => ({ data: 1 }) })
|
||||
|
||||
@@ -74,6 +101,32 @@ describe('API Client', () => {
|
||||
expect(router.replace).toHaveBeenCalledWith({ name: 'login' })
|
||||
})
|
||||
|
||||
it('emits a global auth notice on local 403 responses', async () => {
|
||||
const listener = vi.fn()
|
||||
window.addEventListener('hermes-auth-notice', listener)
|
||||
mockFetch.mockResolvedValue({ ok: false, status: 403, text: () => Promise.resolve('Forbidden') })
|
||||
|
||||
await expect(request('/api/hermes/profiles')).rejects.toThrow('API Error 403')
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener.mock.calls[0][0].detail).toEqual({ kind: 'forbidden' })
|
||||
window.removeEventListener('hermes-auth-notice', listener)
|
||||
})
|
||||
|
||||
it('clears token and redirects when the JWT user no longer exists', async () => {
|
||||
setApiKey('stale-jwt')
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
text: () => Promise.resolve('{"error":"User is disabled or does not exist"}'),
|
||||
})
|
||||
|
||||
await expect(request('/api/hermes/profiles')).rejects.toThrow('API Error 403')
|
||||
|
||||
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('') })
|
||||
@@ -82,22 +135,6 @@ describe('API Client', () => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user