mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-27 14:30:18 +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>
184 lines
5.0 KiB
TypeScript
184 lines
5.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest'
|
|
|
|
// Mock fs/promises
|
|
vi.mock('fs/promises', () => ({
|
|
readFile: vi.fn(),
|
|
writeFile: vi.fn(),
|
|
}))
|
|
|
|
// Mock config
|
|
vi.mock('../../packages/server/src/config', () => ({
|
|
config: { dataDir: '/tmp/hermes-test-data' },
|
|
}))
|
|
|
|
import { readFile, writeFile } from 'fs/promises'
|
|
import { getToken, authMiddleware } from '../../packages/server/src/services/auth'
|
|
|
|
const mockedReadFile = vi.mocked(readFile)
|
|
const mockedWriteFile = vi.mocked(writeFile)
|
|
|
|
describe('Auth Service', () => {
|
|
const originalEnv = process.env
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv }
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterAll(() => {
|
|
process.env = originalEnv
|
|
})
|
|
|
|
describe('getToken', () => {
|
|
it('returns null when AUTH_DISABLED=1', async () => {
|
|
process.env.AUTH_DISABLED = '1'
|
|
|
|
const token = await getToken()
|
|
|
|
expect(token).toBeNull()
|
|
expect(mockedReadFile).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns null when AUTH_DISABLED=true', async () => {
|
|
process.env.AUTH_DISABLED = 'true'
|
|
|
|
const token = await getToken()
|
|
|
|
expect(token).toBeNull()
|
|
})
|
|
|
|
it('returns AUTH_TOKEN env var if set', async () => {
|
|
process.env.AUTH_TOKEN = 'my-custom-token'
|
|
|
|
const token = await getToken()
|
|
|
|
expect(token).toBe('my-custom-token')
|
|
expect(mockedReadFile).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('reads token from file if exists', async () => {
|
|
mockedReadFile.mockResolvedValue('file-token\n')
|
|
|
|
const token = await getToken()
|
|
|
|
expect(token).toBe('file-token')
|
|
expect(mockedReadFile).toHaveBeenCalledWith('/tmp/hermes-test-data/.token', 'utf-8')
|
|
})
|
|
|
|
it('generates and saves new token if file missing', async () => {
|
|
mockedReadFile.mockRejectedValue(new Error('ENOENT'))
|
|
|
|
const token = await getToken()
|
|
|
|
expect(token).toBeTruthy()
|
|
expect(token).toHaveLength(64) // 32 bytes hex
|
|
expect(mockedWriteFile).toHaveBeenCalledWith(
|
|
'/tmp/hermes-test-data/.token',
|
|
expect.stringMatching(/^[a-f0-9]{64}\n$/),
|
|
{ mode: 0o600 },
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('authMiddleware', () => {
|
|
function createMockCtx(path: string, headers: Record<string, string> = {}, query: Record<string, string> = {}) {
|
|
return {
|
|
path,
|
|
headers,
|
|
query,
|
|
status: 200,
|
|
body: null,
|
|
set: vi.fn(),
|
|
}
|
|
}
|
|
|
|
const next = vi.fn()
|
|
|
|
it('allows all requests when auth is disabled (null token)', async () => {
|
|
const middleware = await authMiddleware(null)
|
|
const ctx = createMockCtx('/api/hermes/sessions')
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('skips /health path', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/health')
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('skips non-API paths', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/index.html')
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('requires auth for /webhook path (it is an API-like endpoint)', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/webhook', {})
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(ctx.status).toBe(401)
|
|
expect(next).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects request without auth header', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/api/hermes/sessions', {})
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(ctx.status).toBe(401)
|
|
expect(next).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects request with wrong token', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/api/hermes/sessions', { authorization: 'Bearer wrong' })
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(ctx.status).toBe(401)
|
|
expect(next).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('allows request with correct Bearer token', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/api/hermes/sessions', { authorization: 'Bearer secret' })
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('allows request with correct query token', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/api/hermes/sessions', {}, { token: 'secret' })
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('returns 401 JSON on auth failure', async () => {
|
|
const middleware = await authMiddleware('secret')
|
|
const ctx = createMockCtx('/api/hermes/sessions', { authorization: 'Bearer wrong' })
|
|
|
|
await middleware(ctx, next)
|
|
|
|
expect(ctx.status).toBe(401)
|
|
expect(ctx.set).toHaveBeenCalledWith('Content-Type', 'application/json')
|
|
expect(ctx.body).toEqual({ error: 'Unauthorized' })
|
|
})
|
|
})
|
|
})
|