runner-manager.test.mjs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. import {test} from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import {EventEmitter} from 'node:events'
  4. import {mkdtemp,readFile,writeFile,stat,rm} from 'node:fs/promises'
  5. import {tmpdir} from 'node:os'
  6. import {join} from 'node:path'
  7. import {randomUUID,randomBytes,createCipheriv,createDecipheriv} from 'node:crypto'
  8. import {DesktopRunnerManager} from './runner-manager.mjs'
  9. async function fixture(run){
  10. const home=await mkdtemp(join(tmpdir(),'yaoyao-native-runner-')),key=randomBytes(32),children=[]
  11. const encrypt=value=>{const iv=randomBytes(12),cipher=createCipheriv('aes-256-gcm',key,iv);const bytes=Buffer.concat([cipher.update(value),cipher.final()]);return Buffer.concat([iv,cipher.getAuthTag(),bytes])}
  12. const decrypt=bytes=>{const cipher=createDecipheriv('aes-256-gcm',key,bytes.subarray(0,12));cipher.setAuthTag(bytes.subarray(12,28));return Buffer.concat([cipher.update(bytes.subarray(28)),cipher.final()]).toString()}
  13. const manager=new DesktopRunnerManager({home,encrypt,decrypt,validate:value=>{if(value.protocol!==1)throw new Error('invalid');return value},fork:()=>{
  14. const child=new EventEmitter();child.messages=[]
  15. child.postMessage=value=>{child.messages.push(value);if(value.type==='shutdown')queueMicrotask(()=>child.emit('exit',0))}
  16. child.kill=()=>{queueMicrotask(()=>child.emit('exit',0));return true}
  17. children.push(child);queueMicrotask(()=>child.emit('spawn'));return child
  18. }})
  19. const config={protocol:1,runnerId:randomUUID(),token:randomBytes(32).toString('base64url'),serverURL:'https://fixture.invalid',hermesURL:'http://127.0.0.1:9119',allowedProfiles:['default'],artifactRoots:[]}
  20. const file=join(home,'download.json');await writeFile(file,JSON.stringify(config))
  21. try{await run({home,manager,children,config,file})}finally{await manager.stop();await rm(home,{recursive:true,force:true})}
  22. }
  23. test('stores only encrypted credentials and restarts from private IPC configuration',()=>fixture(async({manager,children,config,file})=>{
  24. await manager.importFile(file)
  25. const encrypted=await readFile(manager.path)
  26. assert.equal(encrypted.includes(config.token),false)
  27. assert.equal((await stat(manager.path)).mode&0o077,0)
  28. assert.deepEqual(children[0].messages.find(m=>m.type==='configure').config,config)
  29. await manager.stop();await manager.start()
  30. assert.equal(children.length,2)
  31. assert.deepEqual(children[1].messages.find(m=>m.type==='configure').config,config)
  32. await manager.forget()
  33. await assert.rejects(readFile(manager.path),{code:'ENOENT'})
  34. await manager.start();assert.equal(children.length,2)
  35. }))
  36. test('a Keychain failure preserves a running node and its previous encrypted configuration',()=>fixture(async({manager,children,file})=>{
  37. await manager.importFile(file);const encrypted=await readFile(manager.path)
  38. manager.options.encrypt=()=>{throw new Error('keychain unavailable')}
  39. await assert.rejects(manager.importFile(file),/keychain/)
  40. assert.equal(children[0].finished,undefined)
  41. assert.deepEqual(await readFile(manager.path),encrypted)
  42. }))
  43. test('a queued import cannot start a node after stop was requested',()=>fixture(async({manager,children,file})=>{
  44. const imported=manager.importFile(file),stopped=manager.stop()
  45. await Promise.all([imported,stopped])
  46. assert.equal(children.length,0)
  47. }))