realtimeApi.test.ts 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. // @vitest-environment node
  2. import { mkdtempSync, rmSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import request from 'supertest'
  6. import { afterEach, describe, expect, it } from 'vitest'
  7. import { createApplication, type ApplicationRuntime } from '../../src/server/app.js'
  8. import type { ServerConfig } from '../../src/server/config.js'
  9. const roots: string[] = []
  10. const runtimes: ApplicationRuntime[] = []
  11. afterEach(() => {
  12. for (const runtime of runtimes.splice(0)) runtime.close()
  13. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  14. })
  15. describe('native Hermes sessions are read-only on 8800', () => {
  16. it('does not advertise realtime chat and rejects all native session writes locally', async () => {
  17. const home = mkdtempSync(join(tmpdir(), 'yaoyao-native-history-'))
  18. roots.push(home)
  19. let upstreamCalls = 0
  20. const config: ServerConfig = {
  21. host: '127.0.0.1', port: 8800, upstream: new URL('http://127.0.0.1:9119'),
  22. allowedHosts: new Set(), home, mediaRoot: home, attachmentsRoot: home,
  23. imagesRoot: home, mediaOwner: 'test', allowInsecureLan: false,
  24. insecureLan: false, production: false,
  25. }
  26. const runtime = createApplication({
  27. config,
  28. fetchImpl: (async (input, init) => {
  29. upstreamCalls += 1
  30. const path = new URL(String(input)).pathname
  31. if (path === '/api/status') return Response.json({ auth_required: true })
  32. if (path === '/api/auth/me') return Response.json({ user_id: 'service' })
  33. if (path === '/api/auth/providers') return Response.json({ providers: [{ name: 'basic', supports_password: true }] })
  34. if (path === '/auth/password-login') return new Response(JSON.stringify({ ok: true }), {
  35. headers: { 'content-type': 'application/json', 'set-cookie': 'hermes_service=session; Path=/; HttpOnly' },
  36. })
  37. if (path === '/api/profiles') return Response.json({ profiles: [{ name: 'default' }] })
  38. return Response.json({ method: init?.method ?? 'GET' })
  39. }) as typeof fetch,
  40. })
  41. runtimes.push(runtime)
  42. const agent = request.agent(runtime.app.callback())
  43. const origin = 'http://127.0.0.1:8800'
  44. const bootstrap = await agent.get('/api/app/bootstrap').set('Host', '127.0.0.1:8800').expect(200)
  45. const login = await agent.post('/api/app/login').set('Host', '127.0.0.1:8800')
  46. .set('Origin', origin).set('X-CSRF-Token', bootstrap.body.csrfToken)
  47. .send({ username: 'admin', password: 'admin' }).expect(200)
  48. const credentials = await agent.put('/api/app/account/credentials')
  49. .set('Host', '127.0.0.1:8800').set('Origin', origin)
  50. .set('X-CSRF-Token', login.body.csrfToken)
  51. .send({ username: 'owner', currentPassword: 'admin', newPassword: 'fixture-password' }).expect(200)
  52. const csrf = credentials.body.csrfToken
  53. const capabilities = await agent.get('/api/realtime/capabilities').set('Host', '127.0.0.1:8800').expect(200)
  54. expect(capabilities.body.channels).toEqual([])
  55. await agent.post('/api/realtime/channels').set('Host', '127.0.0.1:8800')
  56. .set('Origin', origin).set('X-CSRF-Token', csrf).send({ channel: 'chat' })
  57. .expect(410, { error: '原生 Hermes 会话仅供历史查看;请在聊天中开始或继续对话', code: 'native_sessions_read_only' })
  58. const beforeWrites = upstreamCalls
  59. for (const path of ['/api/app/sessions/session-1', '/api/sessions/session-1']) {
  60. const response = await agent.patch(path).set('Host', '127.0.0.1:8800')
  61. .set('Origin', origin).set('X-CSRF-Token', csrf).send({ title: '不能修改' }).expect(410)
  62. expect(response.body.code).toBe('native_sessions_read_only')
  63. }
  64. const pairing = runtime.pairings.create('hermes_device=session')
  65. const paired = runtime.pairings.claim({
  66. pairingID: pairing.id,
  67. secret: pairing.secret,
  68. deviceName: 'history-only fixture',
  69. })
  70. const pairedWrite = await request(runtime.app.callback())
  71. .patch(`/node/${paired.device.id}/api/sessions/session-1`)
  72. .set('Host', '127.0.0.1:8800')
  73. .set('Authorization', `Bearer ${paired.token}`)
  74. .send({ title: '不能修改' })
  75. .expect(410)
  76. expect(pairedWrite.body.code).toBe('native_sessions_read_only')
  77. expect(upstreamCalls).toBe(beforeWrites)
  78. })
  79. })