onboarding.test.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { DesktopOnboarding } from './onboarding.mjs'
  4. function fixture(overrides = {}) {
  5. const calls = []
  6. const flow = new DesktopOnboarding({
  7. remoteServer: () => 'http://remote.test:15300',
  8. prepareLocal: async () => { calls.push('prepare'); return 'http://127.0.0.1:15300' },
  9. inspect: async () => ({ authenticated: false, setupRequired: true }),
  10. authenticate: async value => { calls.push(value); return { username: value.username } },
  11. activate: async value => { calls.push({ mode: value.mode, remember: value.remember }) },
  12. navigate: async url => { calls.push(url) }, ...overrides,
  13. })
  14. return { flow, calls }
  15. }
  16. test('selection, local preparation and account creation run in order; snapshots contain no credentials', async () => {
  17. const { flow, calls } = fixture()
  18. await assert.rejects(() => flow.submit({ username: 'admin', password: 'password' }), /检测/)
  19. flow.select('local')
  20. await flow.prepare()
  21. assert.equal(flow.snapshot().setupRequired, true)
  22. await assert.rejects(() => flow.submit({ username: 'admin', password: 'password', confirmation: 'different' }), /不一致/)
  23. await flow.submit({ username: 'admin', password: 'password', confirmation: 'password', remember: true })
  24. assert.deepEqual(calls, ['prepare', { mode: 'local', serverURL: 'http://127.0.0.1:15300', setup: true, username: 'admin', password: 'password' }, { mode: 'local', remember: true }, 'http://127.0.0.1:15300'])
  25. assert.equal(flow.snapshot().active, false)
  26. assert.equal(JSON.stringify(flow.snapshot()).includes('password'), false)
  27. assert.doesNotThrow(() => flow.open({ mode: 'remote' }), 'completed flows can reopen in the same window')
  28. })
  29. test('an in-flight server check cannot be switched underneath credentials', async () => {
  30. let release
  31. const { flow, calls } = fixture({ inspect: () => new Promise(resolve => { release = resolve }) })
  32. flow.select('remote')
  33. const checking = flow.prepare()
  34. assert.throws(() => flow.select('local'), /尚未完成/)
  35. await assert.rejects(() => flow.submit({ username: 'admin', password: 'password' }), /检测/)
  36. release({ authenticated: false }); await checking
  37. assert.equal(flow.snapshot().serverURL, 'http://remote.test:15300')
  38. assert.deepEqual(calls, [])
  39. })
  40. test('remote setup is blocked; failed checks and wrong passwords stay inline and can be retried', async () => {
  41. let checks = 0, logins = 0
  42. const { flow } = fixture({
  43. inspect: async () => ++checks === 1 ? { setupRequired: true, authenticated: false } : { authenticated: false },
  44. authenticate: async () => { if (++logins === 1) throw new Error('密码错误'); return { username: 'user' } },
  45. })
  46. flow.select('remote'); await flow.prepare()
  47. assert.match(flow.snapshot().error, /本机创建管理员/)
  48. assert.equal(flow.snapshot().phase, 'error')
  49. await flow.prepare()
  50. await flow.submit({ username: 'user', password: 'bad' })
  51. assert.equal(flow.snapshot().phase, 'ready'); assert.equal(flow.snapshot().error, '密码错误')
  52. await flow.submit({ username: 'user', password: 'good' })
  53. assert.equal(flow.snapshot().active, false)
  54. })
  55. test('valid remembered sessions skip credentials, but reauthorization requests a fresh login', async () => {
  56. const { flow, calls } = fixture({ inspect: async () => ({ authenticated: true, user: { username: 'user' } }) })
  57. flow.open({ mode: 'remote', serverURL: 'http://remote.test', remember: true })
  58. await flow.prepare({ autoEnter: true })
  59. assert.equal(flow.snapshot().active, false)
  60. flow.open({ mode: 'remote', serverURL: 'http://remote.test', forceLogin: true })
  61. await flow.prepare({ autoEnter: true })
  62. assert.equal(flow.snapshot().active, true); assert.equal(flow.snapshot().authenticated, false)
  63. assert.equal(calls.length, 2)
  64. })
  65. test('restoring sessions stays hidden through inspection and navigation for either server mode', async () => {
  66. for (const mode of ['local', 'remote']) {
  67. let inspected, navigated
  68. const { flow } = fixture({
  69. inspect: () => new Promise(resolve => { inspected = resolve }),
  70. navigate: () => new Promise(resolve => { navigated = resolve }),
  71. })
  72. flow.open({ mode, serverURL: 'http://remote.test', restoring: true })
  73. const checking = flow.prepare({ autoEnter: true })
  74. await new Promise(resolve => setImmediate(resolve))
  75. assert.equal(flow.snapshot().restoring, true)
  76. inspected({ authenticated: true })
  77. await new Promise(resolve => setImmediate(resolve))
  78. assert.equal(flow.snapshot().restoring, true, 'the guide must not appear while the workspace loads')
  79. navigated(); await checking
  80. assert.equal(flow.snapshot().restoring, true)
  81. assert.equal(flow.snapshot().active, false)
  82. }
  83. })
  84. test('expired sessions and failed restoration reveal the guide with actionable state', async () => {
  85. for (const overrides of [
  86. { inspect: async () => ({ authenticated: false }) },
  87. { inspect: async () => { throw new Error('服务器离线') } },
  88. { inspect: async () => ({ authenticated: true }), navigate: async () => { throw new Error('工作区加载失败') } },
  89. ]) {
  90. const { flow } = fixture(overrides)
  91. flow.open({ mode: 'remote', serverURL: 'http://remote.test', restoring: true })
  92. await flow.prepare({ autoEnter: true })
  93. assert.equal(flow.snapshot().restoring, false)
  94. assert.equal(flow.snapshot().active, true)
  95. }
  96. const { flow } = fixture()
  97. flow.open({ mode: 'local', restoring: true })
  98. flow.serviceChanged({ phase: 'error', message: '服务恢复失败' })
  99. assert.equal(flow.snapshot().restoring, false, 'background recovery errors must expose retry controls')
  100. })
  101. test('failed computer enrollment preserves login and offers inline continuation', async () => {
  102. const { flow, calls } = fixture({ inspect: async () => ({ authenticated: false }), authenticate: async () => ({ username: 'admin', warning: '电脑授权未恢复' }) })
  103. flow.select('remote'); await flow.prepare(); await flow.submit({ username: 'admin', password: 'password' })
  104. assert.equal(flow.snapshot().phase, 'ready'); assert.equal(flow.snapshot().authenticated, true)
  105. assert.match(flow.snapshot().error, /授权未恢复/); assert.deepEqual(calls, [])
  106. await flow.submit(); assert.equal(flow.snapshot().active, false)
  107. })
  108. test('registration stays anonymous and does not enter or enroll a computer', async () => {
  109. const calls = []
  110. const flow = new DesktopOnboarding({ remoteServer: () => 'http://127.0.0.1:15300', inspect: async () => ({ registrationAvailable: true }), register: async input => { calls.push(input) }, authenticate: async () => { throw new Error('must not log in') }, activate: async () => { throw new Error('must not activate') } })
  111. flow.select('remote'); await flow.prepare()
  112. const next = await flow.submit({ register: true, username: 'child', password: 'password', confirmation: 'password' })
  113. assert.equal(next.authenticated, false)
  114. assert.match(next.registrationNotice, /等待管理员开通/)
  115. assert.equal(calls.length, 1)
  116. assert.equal(JSON.stringify(next).includes('password'), false)
  117. await flow.prepare({ serverURL: 'http://another-server.test' })
  118. assert.equal(flow.snapshot().registrationNotice, '', 'registration belongs to the checked server')
  119. })