desktopHosts.test.ts 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. // @vitest-environment node
  2. import {afterEach,beforeEach,expect,it} from 'vitest'
  3. import {mkdtempSync,rmSync} from 'node:fs'
  4. import {tmpdir} from 'node:os'
  5. import {join} from 'node:path'
  6. import Koa from 'koa'
  7. import {bodyParser} from '@koa/bodyparser'
  8. import request from 'supertest'
  9. import {WorkspaceStore} from '../../src/server/workspaceStore'
  10. import {DesktopHostHub} from '../../src/server/desktopHosts'
  11. import type {DesktopHostExchange} from '../../src/shared/desktopHost'
  12. let home:string,store:WorkspaceStore,hub:DesktopHostHub,token:string,hostId:string,exchanged:DesktopHostExchange[]
  13. const auth={requireAdmin:()=>({id:'admin',role:'admin'}),require:()=>({id:'owner'})} as any
  14. const body=():DesktopHostExchange=>({host:{id:'11111111-1111-4111-8111-111111111111',name:'书房 iMac',platform:'darwin',screen:true,accessibility:true,approved:[]},results:[]})
  15. beforeEach(()=>{home=mkdtempSync(join(tmpdir(),'desktop-hosts-'));store=new WorkspaceStore(home);hub=new DesktopHostHub(store,auth);exchanged=[];hub.exchange=(id,value)=>{exchanged.push(value);hostId=id;return {commands:[]}}})
  16. afterEach(()=>{hub.close();store.close();rmSync(home,{recursive:true,force:true})})
  17. const app=()=>{const a=new Koa();a.use(async(ctx,next)=>{try{await next()}catch(e:any){ctx.status=e.status??500;ctx.body={error:e.message,code:e.code}}});a.use((ctx,next)=>ctx.path.startsWith('/api/app/')?bodyParser()(ctx,next):next());a.use(hub.middleware());a.use(hub.adminRouter().routes());return a.callback()}
  18. async function enroll(name='书房 iMac'){const response=await request(app()).post('/api/app/admin/desktop-hosts').send({name}).expect(201);token=response.body.token;hostId=response.body.host.id;return response.body}
  19. const post=(url:string)=>request(app()).post(url).set('Authorization','Bearer '+token).set('x-desktop-host-protocol','1')
  20. it('issues the token once and keeps it out of listings',async()=>{
  21. const enrolled=await enroll()
  22. expect(token).toMatch(/^[\w-]{40,}$/)
  23. expect(enrolled.host).not.toHaveProperty('tokenHash')
  24. const list=(await request(app()).get('/api/app/admin/desktop-hosts').expect(200)).body
  25. expect(list.protocol).toBe(1)
  26. expect(JSON.stringify(list)).not.toContain(token)
  27. expect(list.hosts).toHaveLength(1)
  28. expect(list.hosts[0]).toMatchObject({name:'书房 iMac',enabled:true,online:false})
  29. })
  30. it('rejects unauthorized transports before parsing the body',async()=>{
  31. await enroll()
  32. await request(app()).post(`/api/desktop-host/v1/${hostId}/exchange`).send(body()).expect(403)
  33. await post(`/api/desktop-host/v1/${hostId}/exchange`).set('Origin','http://evil.test').send(body()).expect(403)
  34. await request(app()).post(`/api/desktop-host/v1/${hostId}/exchange`).set('Authorization','Bearer wrong-token-wrong-token-wrong').set('x-desktop-host-protocol','1').send(body()).expect(403)
  35. await request(app()).post(`/api/desktop-host/v1/11111111-2222-4333-8444-555555555555/exchange`).set('Authorization','Bearer '+token).set('x-desktop-host-protocol','1').send(body()).expect(403)
  36. })
  37. it('requires the machine protocol header and POST method',async()=>{
  38. await enroll()
  39. await request(app()).post(`/api/desktop-host/v1/${hostId}/exchange`).set('Authorization','Bearer '+token).send(body()).expect(409)
  40. await request(app()).get(`/api/desktop-host/v1/${hostId}/exchange`).set('Authorization','Bearer '+token).set('x-desktop-host-protocol','1').send(body()).expect(405)
  41. })
  42. it('forwards a valid exchange to the desktop environment and reports it online',async()=>{
  43. await enroll()
  44. const response=await post(`/api/desktop-host/v1/${hostId}/exchange`).send(body()).expect(200)
  45. expect(response.body).toEqual({commands:[]})
  46. expect(exchanged).toHaveLength(1)
  47. expect(exchanged[0].host.name).toBe('书房 iMac')
  48. const list=(await request(app()).get('/api/app/admin/desktop-hosts').expect(200)).body
  49. expect(list.hosts[0]).toMatchObject({online:true,hostName:'书房 iMac',platform:'darwin'})
  50. })
  51. it('revocation fences the transport and notifies the live session',async()=>{
  52. const dropped:string[]=[]
  53. hub.drop=id=>dropped.push(id)
  54. await enroll()
  55. await post(`/api/desktop-host/v1/${hostId}/exchange`).send(body()).expect(200)
  56. await request(app()).delete(`/api/app/admin/desktop-hosts/${hostId}`).expect(200)
  57. expect(dropped).toEqual([hostId])
  58. await post(`/api/desktop-host/v1/${hostId}/exchange`).send(body()).expect(403)
  59. })
  60. it('validates the exchange payload shape',async()=>{
  61. await enroll()
  62. await post(`/api/desktop-host/v1/${hostId}/exchange`).send({host:{id:'not-a-uuid',name:'x',platform:'darwin',screen:true,accessibility:true,approved:[]},results:[]}).expect(400)
  63. await post(`/api/desktop-host/v1/${hostId}/exchange`).send('string').expect(400)
  64. })