diff --git a/packages/server/tests/unit/auth/token.test.ts b/packages/server/tests/unit/auth/token.test.ts index 41e840a..c4cfd4a 100644 --- a/packages/server/tests/unit/auth/token.test.ts +++ b/packages/server/tests/unit/auth/token.test.ts @@ -5,6 +5,7 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Hono } from 'hono'; import { generateToken, maskToken, @@ -15,6 +16,8 @@ import { setAuthEnabled, validateToken, extractToken, + authMiddleware, + getAuthContext, } from '../../../src/auth/token.js'; import { createMockHonoContext } from '../../mocks/hono.mock.js'; @@ -220,4 +223,182 @@ describe('Auth Token', () => { expect(token).toBeNull(); }); }); + + describe('authMiddleware - 认证中间件', () => { + let app: Hono; + + beforeEach(() => { + app = new Hono(); + app.use('*', authMiddleware); + app.get('/test', (c) => { + const auth = getAuthContext(c); + return c.json({ auth }); + }); + app.get('/health', (c) => c.json({ status: 'ok' })); + }); + + it('认证禁用时允许所有请求', async () => { + initAuth({ enabled: false }); + + const res = await app.request('/test'); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.auth.authenticated).toBe(true); + }); + + it('跳过配置的路径', async () => { + initAuth({ enabled: true, skipPaths: ['/health'] }); + + const res = await app.request('/health'); + + expect(res.status).toBe(200); + }); + + it('本地请求 (x-forwarded-for: 127.0.0.1) 跳过认证', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { 'x-forwarded-for': '127.0.0.1' }, + }); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.auth.authenticated).toBe(true); + }); + + it('本地请求 (x-forwarded-for: ::1) 跳过认证', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { 'x-forwarded-for': '::1' }, + }); + + expect(res.status).toBe(200); + }); + + it('本地请求 (x-real-ip: 192.168.1.1) 跳过认证', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { 'x-real-ip': '192.168.1.1' }, + }); + + expect(res.status).toBe(200); + }); + + it('本地请求 (x-real-ip: 10.0.0.1) 跳过认证', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { 'x-real-ip': '10.0.0.1' }, + }); + + expect(res.status).toBe(200); + }); + + it('本地请求 (x-forwarded-for: 172.16.0.1) 跳过认证', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { 'x-forwarded-for': '172.16.0.1' }, + }); + + expect(res.status).toBe(200); + }); + + it('无代理头时默认为本地请求', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + // 不设置任何代理头,应该被视为本地请求 + const res = await app.request('/test'); + + expect(res.status).toBe(200); + }); + + it('远程请求无 token 时返回 401', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { 'x-forwarded-for': '8.8.8.8' }, + }); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.error).toBe('Authentication required'); + }); + + it('远程请求无效 token 时返回 401', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { + 'x-forwarded-for': '8.8.8.8', + authorization: 'Bearer invalid-token', + }, + }); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.error).toBe('Invalid token'); + }); + + it('远程请求有效 token 时通过认证', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + const res = await app.request('/test', { + headers: { + 'x-forwarded-for': '8.8.8.8', + authorization: 'Bearer valid-token', + }, + }); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.auth.authenticated).toBe(true); + expect(json.auth.tokenHint).toBe('vali...oken'); + }); + + it('query parameter token 也可以认证', async () => { + initAuth({ enabled: true, tokens: ['query-token-12345'] }); + + const res = await app.request('/test?token=query-token-12345', { + headers: { 'x-forwarded-for': '8.8.8.8' }, + }); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.auth.authenticated).toBe(true); + }); + + it('x-forwarded-for 多个 IP 时使用第一个', async () => { + initAuth({ enabled: true, tokens: ['valid-token'] }); + + // 第一个是本地 IP + const res = await app.request('/test', { + headers: { 'x-forwarded-for': '127.0.0.1, 8.8.8.8, 1.1.1.1' }, + }); + + expect(res.status).toBe(200); + }); + }); + + describe('getAuthContext - 获取认证上下文', () => { + it('未设置时返回 authenticated: false', () => { + const c = createMockHonoContext(); + (c.get as any) = vi.fn().mockReturnValue(undefined); + + const auth = getAuthContext(c as any); + expect(auth.authenticated).toBe(false); + }); + + it('已设置时返回设置的值', () => { + const c = createMockHonoContext(); + const mockAuth = { authenticated: true, tokenHint: 'test...hint' }; + (c.get as any) = vi.fn().mockReturnValue(mockAuth); + + const auth = getAuthContext(c as any); + expect(auth).toEqual(mockAuth); + }); + }); }); diff --git a/packages/server/tests/unit/sse.test.ts b/packages/server/tests/unit/sse.test.ts index ad4e33c..25b6652 100644 --- a/packages/server/tests/unit/sse.test.ts +++ b/packages/server/tests/unit/sse.test.ts @@ -140,6 +140,41 @@ describe('SSE Handler', () => { expect(typeof stats.sessions).toBe('number'); expect(typeof stats.subscribers).toBe('number'); }); + + it('初始状态统计为 0', () => { + const stats = getSSEStats(); + expect(stats.sessions).toBe(0); + expect(stats.subscribers).toBe(0); + }); + }); + + describe('事件格式化', () => { + it('emitEvent 格式化带 event 字段的事件', () => { + // 无订阅者时调用不会抛错,但内部会格式化 + emitEvent('test-session', { + event: 'custom_event', + data: { timestamp: 12345, payload: { key: 'value' } }, + }); + // 测试不抛错即为通过 + }); + + it('emitEvent 格式化不带 event 字段的事件', () => { + emitEvent('test-session', { + data: { timestamp: 12345, payload: null }, + }); + }); + + it('复杂 payload 数据正确序列化', () => { + expect(() => { + emitStatusEvent('session-1', 'processing', { + nested: { deep: { value: [1, 2, 3] } }, + array: ['a', 'b', 'c'], + number: 42, + boolean: true, + null: null, + }); + }).not.toThrow(); + }); }); describe('handleSSE - SSE 路由处理', () => { diff --git a/packages/server/tests/unit/ws.test.ts b/packages/server/tests/unit/ws.test.ts index 5d7d10c..16a3fba 100644 --- a/packages/server/tests/unit/ws.test.ts +++ b/packages/server/tests/unit/ws.test.ts @@ -202,6 +202,94 @@ describe('WebSocket Handler', () => { content: '', }); }); + + it('处理 Blob 数据', async () => { + const ws = createMockWSContext(); + + const message = { type: 'message', payload: { content: 'Blob test' } }; + const blob = new Blob([JSON.stringify(message)]); + + await handleWebSocketMessage(ws as any, 'session-1', blob); + + expect(mockAddMessage).toHaveBeenCalledWith('session-1', { + role: 'user', + content: 'Blob test', + }); + }); + + it('处理非标准数据类型(转为字符串)', async () => { + const ws = createMockWSContext(); + + // 使用一个对象作为数据,它会被 String() 转换 + const message = { type: 'cancel' }; + const objData = { toString: () => JSON.stringify(message) }; + + await handleWebSocketMessage(ws as any, 'session-1', objData); + + expect(cancelProcessing).toHaveBeenCalledWith('session-1'); + }); + + it('addMessage 返回 null 时不调用 processMessage', async () => { + const ws = createMockWSContext(); + mockAddMessage.mockReturnValue(null); + + const message = JSON.stringify({ + type: 'message', + payload: { content: 'Test' }, + }); + + await handleWebSocketMessage(ws as any, 'session-1', message); + + expect(mockAddMessage).toHaveBeenCalled(); + expect(processMessage).not.toHaveBeenCalled(); + }); + + it('处理 tool_response 类型消息(TODO 场景)', async () => { + const ws = createMockWSContext(); + const message = JSON.stringify({ + type: 'tool_response', + payload: { toolId: 'tool-123', result: 'success' }, + }); + + // 当前实现是 TODO,所以不会有任何处理 + await handleWebSocketMessage(ws as any, 'session-1', message); + + // 不应该发送错误消息 + expect(ws.send).not.toHaveBeenCalledWith( + expect.stringContaining('"type":"error"') + ); + }); + + it('permission_response 无 requestId 时不调用 handler', async () => { + const ws = createMockWSContext(); + const message = JSON.stringify({ + type: 'permission_response', + payload: { allow: true }, + }); + + await handleWebSocketMessage(ws as any, 'session-1', message); + + expect(handlePermissionResponse).not.toHaveBeenCalled(); + }); + + it('permission_response handler 返回 false 时打印警告', async () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const { handlePermissionResponse: mockHandler } = await import('../../src/permission/handler.js'); + vi.mocked(mockHandler).mockReturnValue(false); + + const ws = createMockWSContext(); + const message = JSON.stringify({ + type: 'permission_response', + payload: { requestId: 'unknown-req', allow: true }, + }); + + await handleWebSocketMessage(ws as any, 'session-1', message); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('unknown-req') + ); + consoleWarnSpy.mockRestore(); + }); }); describe('handleWebSocketClose - 关闭处理', () => {