kanbanProtocol.test.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import { afterEach, describe, expect, it, vi } from 'vitest'
  2. import {
  3. addKanbanComment,
  4. createKanbanBoard,
  5. createKanbanTask,
  6. deleteKanbanTask,
  7. dispatchKanban,
  8. getKanbanBoard,
  9. getKanbanStatus,
  10. getKanbanTask,
  11. listKanbanBoards,
  12. listKanbanProfiles,
  13. updateKanbanTask,
  14. } from '@/api/kanban'
  15. import { setApiCsrfToken } from '@/api/client'
  16. afterEach(() => {
  17. vi.unstubAllGlobals()
  18. setApiCsrfToken('')
  19. })
  20. describe('Kanban Web client protocol', () => {
  21. it('keeps every browser request under the CSRF-protected app alias and scopes board reads', async () => {
  22. const calls: Array<{ path: string; init?: RequestInit }> = []
  23. vi.stubGlobal('fetch', vi.fn(async (path: string, init?: RequestInit) => {
  24. calls.push({ path, init })
  25. if (path.endsWith('/status')) return Response.json({ available: true, version: '1.0.0' })
  26. if (path.endsWith('/boards')) return Response.json({ boards: [], current: 'default', board: { slug: 'mobile-release' } })
  27. if (path.includes('/board?')) return Response.json({ columns: [], tenants: [], assignees: [], latest_event_id: 0, now: 1 })
  28. if (path.endsWith('/profiles')) return Response.json({ profiles: [] })
  29. if (path.includes('/tasks/task%20%2F%20one?')) return Response.json({ task: { id: 'task / one', title: 'Task', status: 'todo' }, comments: [], events: [], links: { parents: [], children: [] }, runs: [] })
  30. return Response.json({ ok: true, task: { id: 'task / one', title: 'Task', status: 'ready' }, spawned: [] })
  31. }))
  32. setApiCsrfToken('csrf-kanban')
  33. await getKanbanStatus()
  34. await listKanbanBoards()
  35. await getKanbanBoard('mobile / 中文', true)
  36. await listKanbanProfiles()
  37. await getKanbanTask('mobile / 中文', 'task / one')
  38. await createKanbanBoard({ slug: 'mobile-release', name: '移动发布' })
  39. await createKanbanTask('mobile / 中文', { title: '实现看板', assignee: 'yaoyao' })
  40. await updateKanbanTask('mobile / 中文', 'task / one', { status: 'ready' })
  41. await addKanbanComment('mobile / 中文', 'task / one', { body: '请继续' })
  42. await dispatchKanban('mobile / 中文')
  43. await deleteKanbanTask('mobile / 中文', 'task / one')
  44. expect(calls.map(call => call.path)).toEqual([
  45. '/api/app/kanban/status',
  46. '/api/app/kanban/boards',
  47. '/api/app/kanban/board?include_archived=true&board=mobile+%2F+%E4%B8%AD%E6%96%87',
  48. '/api/app/kanban/profiles',
  49. '/api/app/kanban/tasks/task%20%2F%20one?board=mobile+%2F+%E4%B8%AD%E6%96%87',
  50. '/api/app/kanban/boards',
  51. '/api/app/kanban/tasks?board=mobile+%2F+%E4%B8%AD%E6%96%87',
  52. '/api/app/kanban/tasks/task%20%2F%20one?board=mobile+%2F+%E4%B8%AD%E6%96%87',
  53. '/api/app/kanban/tasks/task%20%2F%20one/comments?board=mobile+%2F+%E4%B8%AD%E6%96%87',
  54. '/api/app/kanban/dispatch?board=mobile+%2F+%E4%B8%AD%E6%96%87',
  55. '/api/app/kanban/tasks/task%20%2F%20one?board=mobile+%2F+%E4%B8%AD%E6%96%87',
  56. ])
  57. const mutations = calls.filter(call => !['GET', 'HEAD'].includes((call.init?.method ?? 'GET').toUpperCase()))
  58. expect(mutations.every(call => new Headers(call.init?.headers).get('X-CSRF-Token') === 'csrf-kanban')).toBe(true)
  59. expect(JSON.parse(String(calls[5]!.init?.body))).toMatchObject({ slug: 'mobile-release', switch: false })
  60. expect(calls.some(call => call.path.includes('/switch'))).toBe(false)
  61. })
  62. })