readCacheRoutes.test.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. import { mkdtempSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import request from 'supertest'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { createApplication, type ApplicationRuntime } from '../../src/server/app.js'
  7. import type { ServerConfig } from '../../src/server/config.js'
  8. const runtimes: ApplicationRuntime[] = []
  9. afterEach(() => { for (const runtime of runtimes.splice(0)) runtime.close() })
  10. describe('authenticated read cache routes', () => {
  11. it('hits cached reads, honors fresh reads and never bypasses logout authentication', async () => {
  12. const home = mkdtempSync(join(tmpdir(), 'yaoyao-read-cache-routes-'))
  13. const config: ServerConfig = { host: '127.0.0.1', port: 15300, upstream: new URL('http://127.0.0.1:19119'),
  14. allowedHosts: new Set(), home, mediaRoot: home, attachmentsRoot: home, imagesRoot: home, mediaOwner: 'test',
  15. allowInsecureLan: false, insecureLan: false, production: false }
  16. let sessionReads = 0, statusReads = 0, currentTitle = 'original'
  17. const runtime = createApplication({ config, fetchImpl: (async (input, init) => {
  18. const path = new URL(String(input)).pathname
  19. if (path === '/api/status') { statusReads++; return Response.json({ auth_required: false }) }
  20. if (path === '/api/profiles' || path === '/api/plugins/yaoyao/profiles') return Response.json({ profiles: [{ name: 'default', is_default: true }] })
  21. if (path === '/api/sessions' && (init?.method ?? 'GET') === 'GET') { sessionReads++; return Response.json({ sessions: [{ id: 'one', title: currentTitle }] }) }
  22. if (path === '/api/sessions/one' && init?.method === 'PATCH') { currentTitle = 'renamed'; return Response.json({ ok: true }) }
  23. return Response.json({ ok: true })
  24. }) as typeof fetch })
  25. runtimes.push(runtime)
  26. const agent = request.agent(runtime.app.callback()), origin = 'http://127.0.0.1:15300'
  27. const start = await agent.get('/api/app/bootstrap').set('Host', '127.0.0.1:15300').expect(200)
  28. await agent.post('/api/app/setup').set('Host', '127.0.0.1:15300').set('Origin', origin)
  29. .set('X-CSRF-Token', start.body.csrfToken).send({ username: 'owner', password: 'fixture-password' }).expect(200)
  30. const statusBefore = statusReads
  31. const first = await agent.get('/api/sessions').set('Host', '127.0.0.1:15300').expect(200)
  32. const statusAfterColdRead = statusReads
  33. const second = await agent.get('/api/sessions').set('Host', '127.0.0.1:15300').expect(200)
  34. expect(second.body).toEqual(first.body)
  35. expect(sessionReads).toBe(1)
  36. expect(statusAfterColdRead - statusBefore).toBeLessThanOrEqual(1)
  37. expect(statusReads).toBe(statusAfterColdRead)
  38. currentTitle = 'external-change'
  39. const fresh = await agent.get('/api/sessions').set('Host', '127.0.0.1:15300').set('X-Yaoyao-Cache', 'bypass').expect(200)
  40. expect(fresh.body.sessions[0].title).toBe('external-change')
  41. const after = await agent.get('/api/sessions').set('Host', '127.0.0.1:15300').expect(200)
  42. expect(after.body.sessions[0].title).toBe('external-change')
  43. expect(sessionReads).toBe(3)
  44. await agent.patch('/api/sessions/one').set('Host', '127.0.0.1:15300')
  45. .send({ title: 'renamed' }).expect(410, {
  46. error: '原生 Hermes 会话仅供历史查看;请在聊天中开始或继续对话',
  47. code: 'native_sessions_read_only',
  48. })
  49. const unchanged = await agent.get('/api/sessions').set('Host', '127.0.0.1:15300').expect(200)
  50. expect(unchanged.body.sessions[0].title).toBe('external-change')
  51. expect(sessionReads).toBe(3)
  52. await agent.post('/auth/logout').set('Host', '127.0.0.1:15300').send({}).expect(200)
  53. await agent.get('/api/sessions').set('Host', '127.0.0.1:15300').expect(401)
  54. expect(sessionReads).toBe(3)
  55. })
  56. })