upstream.test.ts 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { describe, expect, it, vi } from 'vitest'
  2. import { CookieJar, UpstreamClient } from '../../src/server/upstream.js'
  3. import type { UpstreamRequestOptions } from '../../src/server/upstream.js'
  4. describe('upstream request boundary', () => {
  5. it('rejects a path that URL normalization would escape', async () => {
  6. const fetchImpl = vi.fn<typeof fetch>()
  7. const client = new UpstreamClient(new URL('http://127.0.0.1:9119'), fetchImpl)
  8. await expect(client.request('/api/sessions/..', new CookieJar())).rejects.toThrow(/normalization/i)
  9. expect(fetchImpl).not.toHaveBeenCalled()
  10. })
  11. it('derives proxy scheme from public 15300 TLS and forwards only the trusted peer IP', async () => {
  12. let headers = new Headers()
  13. const fetchImpl = vi.fn<typeof fetch>(async (_input, init) => {
  14. headers = new Headers(init?.headers)
  15. return new Response('{}', {
  16. status: 200,
  17. headers: { 'content-type': 'application/json', 'set-cookie': 'session=test; Path=/; HttpOnly; SameSite=Lax' },
  18. })
  19. })
  20. const jar = new CookieJar()
  21. const client = new UpstreamClient(new URL('http://127.0.0.1:9119'), fetchImpl, true)
  22. await client.request('/api/status', jar, { clientAddress: '::ffff:192.168.1.22' })
  23. expect(headers.get('x-forwarded-proto')).toBe('https')
  24. expect(headers.get('x-forwarded-for')).toBe('192.168.1.22')
  25. expect(jar.browserCookies[0]).toContain('Secure')
  26. expect(jar.browserCookies[0]).not.toMatch(/Domain=/i)
  27. })
  28. })
  29. it.each([
  30. {path:'/api/config', options:{search:new URLSearchParams({profile:'yaoer',token:'private-query'})}, stage:'Profile「yaoer」读取配置'},
  31. {path:'/api/plugins/yaoyao-bot-bridge/capabilities', options:{search:new URLSearchParams({profile:'default'})}, stage:'Profile「default」检查工具桥'},
  32. {path:'/api/plugins/yaoyao-bot-bridge/bind', options:{method:'POST',body:{profile:'yaoer',token:'private-bridge-token'}}, stage:'Profile「yaoer」绑定工具桥'},
  33. {path:'/api/config', options:{search:new URLSearchParams({profile:'invalid\nprivate-value'})}, stage:'读取配置'},
  34. ] satisfies Array<{path:string;options:UpstreamRequestOptions;stage:string}>)('identifies a timed-out $path without exposing request secrets',async({path,options,stage})=>{
  35. vi.useFakeTimers()
  36. const fetchImpl=vi.fn<typeof fetch>((_url,init)=>new Promise((_resolve,reject)=>{
  37. init?.signal?.addEventListener('abort',()=>reject(new Error('aborted')),{once:true})
  38. }))
  39. const client=new UpstreamClient(new URL('http://127.0.0.1:9119'),fetchImpl)
  40. try{
  41. const failed=expect(client.request(path,new CookieJar(),options)).rejects.toMatchObject({
  42. code:'upstream_unavailable',message:`Hermes 请求超时(30 秒):${stage}。`,
  43. })
  44. await vi.advanceTimersByTimeAsync(30001);await failed
  45. expect(fetchImpl).toHaveBeenCalledTimes(1)
  46. }finally{client.close();vi.clearAllTimers();vi.useRealTimers()}
  47. })
  48. it('identifies the Profile when Hermes returns headers but the response body stalls',async()=>{
  49. vi.useFakeTimers()
  50. const cancel=vi.fn(),stream=new ReadableStream({cancel})
  51. const client=new UpstreamClient(new URL('http://127.0.0.1:9119'),vi.fn<typeof fetch>(async()=>new Response(stream)))
  52. try{
  53. const failed=expect(client.request('/api/config',new CookieJar(),{search:new URLSearchParams({profile:'yaoer'})})).rejects.toMatchObject({
  54. code:'upstream_unavailable',message:'Hermes 请求超时(30 秒):Profile「yaoer」读取配置。',
  55. })
  56. await vi.advanceTimersByTimeAsync(30001);await failed
  57. expect(cancel).toHaveBeenCalledOnce()
  58. }finally{client.close();vi.clearAllTimers();vi.useRealTimers()}
  59. })
  60. it('allows the Hermes memory helper to finish within its 90-second budget without changing normal request deadlines',async()=>{
  61. vi.useFakeTimers()
  62. const fetchImpl=vi.fn<typeof fetch>(()=>new Promise(done=>setTimeout(()=>done(new Response('{"text":"memory"}')),40000)))
  63. const client=new UpstreamClient(new URL('http://127.0.0.1:9119'),fetchImpl)
  64. try{
  65. const normal=client.request('/api/status',new CookieJar()),rejected=expect(normal).rejects.toThrow('request timed out')
  66. await vi.advanceTimersByTimeAsync(30001);await rejected
  67. const memory=client.request('/api/plugins/yaoyao-bot-bridge/memory-extract',new CookieJar(),{method:'POST',body:{profile:'default',prompt:'facts'}})
  68. await vi.advanceTimersByTimeAsync(40001)
  69. expect(JSON.parse((await memory).body.toString())).toEqual({text:'memory'})
  70. }finally{client.close();vi.clearAllTimers();vi.useRealTimers()}
  71. })