chatTranscriptAuthorization.test.ts 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. // @vitest-environment node
  2. import { EventEmitter } from 'node:events'
  3. import { mkdtempSync, rmSync } from 'node:fs'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import type Koa from 'koa'
  7. import { afterEach, beforeEach, expect, it, vi } from 'vitest'
  8. import { LocalAuthStore, type UpstreamServiceSession } from '../../src/server/localAuth'
  9. import { ChatCacheStore, ChatCacheCoordinator } from '../../src/server/chatCache'
  10. import { chatTranscriptRouter } from '../../src/server/chatTranscriptApi'
  11. class Response extends EventEmitter {
  12. writableEnded = false
  13. destroyed = false
  14. writableLength = 0
  15. writes: string[] = []
  16. writeHead() {}
  17. flushHeaders() {}
  18. write(value: string) { this.writes.push(value); return true }
  19. end() { this.writableEnded = true; this.emit('close') }
  20. }
  21. function context(cookie = ''): Koa.Context {
  22. const headers: Record<string, string | string[]> = {}
  23. return { state: {}, request: {}, response: { headers }, get: (name: string) => name === 'cookie' ? cookie : '',
  24. set: (name: string, value: string | string[]) => { headers[name.toLowerCase()] = value } } as unknown as Koa.Context
  25. }
  26. const issuedCookie = (ctx: Koa.Context) => (ctx.response.headers['set-cookie'] as string[])[0]!.split(';')[0]!
  27. let home: string, store: ChatCacheStore, cache: ChatCacheCoordinator, auth: LocalAuthStore, cookie: string, owner: string, res: Response
  28. beforeEach(async () => {
  29. vi.useFakeTimers()
  30. home = mkdtempSync(join(tmpdir(), 'yaoyao-transcript-auth-'))
  31. store = new ChatCacheStore(home)
  32. cache = new ChatCacheCoordinator(store, { request: vi.fn() } as unknown as UpstreamServiceSession)
  33. auth = new LocalAuthStore(home)
  34. const setup = context(); owner = auth.setupAdmin(setup, 'fixture-user', 'fixture-password').id; cookie = issuedCookie(setup)
  35. store.recordCommand(owner, 'default', 'session', 'session.create', {})
  36. store.transcripts.seed(owner, 'default', 'session')
  37. res = new Response()
  38. const ctx = context(cookie), cursor = `${store.transcripts.epochFor(owner, 'default', 'session')}:0`
  39. ctx.state.localUser = auth.require(ctx)
  40. ctx.path = '/api/app/chat/sessions/session/events'; ctx.method = 'GET'; ctx.query = { profile: 'default', after: cursor }
  41. ctx.res = res as unknown as Koa.Context['res']
  42. await chatTranscriptRouter(cache, auth).routes()(ctx, async () => {})
  43. expect(res.writes.join('')).toContain('event: ready')
  44. })
  45. afterEach(() => { res?.end(); cache?.close(); store?.close(); rmSync(home, { recursive: true, force: true }); vi.useRealTimers() })
  46. const publish = () => store.recordCommand(owner, 'default', 'session', 'prompt.submit', { text: 'after-auth-change', _delivery_id: 'fixture-prompt' })
  47. it('closes a cached-user transcript stream before publishing any message after logout', async () => {
  48. auth.logout(context(cookie))
  49. publish(); await Promise.resolve()
  50. expect(res.writableEnded).toBe(true)
  51. expect(res.writes.join('')).not.toContain('after-auth-change')
  52. })
  53. it('closes an idle expired-session transcript stream on its next heartbeat', () => {
  54. vi.setSystemTime(Date.now() + 31 * 24 * 60 * 60 * 1000)
  55. vi.advanceTimersByTime(5000)
  56. expect(res.writableEnded).toBe(true)
  57. })
  58. it('keeps a valid transcript stream open when another login logs out', async () => {
  59. const other = context(); auth.login(other, 'fixture-user', 'fixture-password'); auth.logout(context(issuedCookie(other)))
  60. publish(); await Promise.resolve()
  61. expect(res.writableEnded).toBe(false)
  62. expect(res.writes.join('')).toContain('after-auth-change')
  63. })
  64. it('closes the transcript stream after credentials revoke its session', async () => {
  65. auth.changeCredentials(context(cookie), 'fixture-password', 'fixture-password-changed')
  66. publish(); await Promise.resolve()
  67. expect(res.writableEnded).toBe(true)
  68. expect(res.writes.join('')).not.toContain('after-auth-change')
  69. })