executionResourceDialog.test.ts 5.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import {flushPromises,mount,type VueWrapper} from '@vue/test-utils'
  2. import {afterEach,beforeEach,expect,it,vi} from 'vitest'
  3. import Dialog from '@/components/app/ExecutionResourceDialog.vue'
  4. import {apiRequest} from '@/api/client'
  5. vi.mock('@/api/client',()=>({apiRequest:vi.fn()}))
  6. const request=vi.mocked(apiRequest),root='/api/app/admin/execution'
  7. let wrapper:VueWrapper
  8. const mountDialog=(resource?:any)=>mount(Dialog,{props:{agents:[{id:'bot-1',name:'夭夭',owner:'admin'}],resource},global:{stubs:{StandaloneDialog:{template:'<div role="dialog"><slot /></div>'}}}})
  9. const button=(text:string)=>wrapper.findAll('button').find(b=>b.text()===text)!
  10. beforeEach(()=>{request.mockReset();vi.spyOn(window,'open').mockReturnValue(null);request.mockResolvedValue({})})
  11. afterEach(()=>{wrapper?.unmount();vi.restoreAllMocks();vi.useRealTimers()})
  12. it('defaults to browser login, opens on the click, polls completion and saves only the attempt reference',async()=>{
  13. vi.useFakeTimers();let resolveStart!:(value:any)=>void
  14. request.mockImplementation(async(path,options)=>{
  15. if(path===root+'/codex-auth')return new Promise(resolve=>{resolveStart=resolve})
  16. if(path.startsWith(root+'/codex-auth/')&&!options?.method)return {id:'attempt',status:'authorized',expiresAt:Date.now()+60000}
  17. return {}
  18. })
  19. wrapper=mountDialog();expect(button('浏览器授权(推荐)').attributes('aria-pressed')).toBe('true')
  20. expect(wrapper.find('input[type=file]').exists()).toBe(false);expect(button('保存授权').attributes('disabled')).toBeDefined()
  21. await button('打开浏览器授权').trigger('click')
  22. expect(window.open).toHaveBeenCalledWith('https://auth.openai.com/codex/device','_blank',expect.stringContaining('noopener'))
  23. expect(wrapper.text()).toContain('正在获取授权码')
  24. const id=(request.mock.calls[0]![1]!.body as any).requestId
  25. resolveStart({id,status:'pending',loginUrl:'https://auth.openai.com/codex/device',userCode:'ABCD-12345',expiresAt:Date.now()+60000});await flushPromises()
  26. expect(wrapper.get('a').attributes('href')).toBe('https://auth.openai.com/codex/device')
  27. expect(wrapper.text()).toContain('ABCD-12345')
  28. await vi.advanceTimersByTimeAsync(1500);await flushPromises()
  29. expect(wrapper.text()).toContain('浏览器授权成功')
  30. await wrapper.get('input[maxlength="80"]').setValue('工作账号');await wrapper.get('input[type=checkbox]').setValue(true)
  31. await wrapper.get('form').trigger('submit');await flushPromises()
  32. expect(request).toHaveBeenCalledWith(root+'/resources',{method:'POST',body:{name:'工作账号',kind:'codex',agentIds:['bot-1'],revision:0,authAttemptId:'attempt'}})
  33. expect(wrapper.emitted('saved')).toHaveLength(1)
  34. })
  35. it('cancels a late login response when switching to file import and keeps file grants explicit',async()=>{
  36. let finish!:(v:any)=>void
  37. request.mockImplementation(async path=>path===root+'/codex-auth'?new Promise(resolve=>{finish=resolve}):{})
  38. wrapper=mountDialog();await button('打开浏览器授权').trigger('click')
  39. const id=(request.mock.calls[0]![1]!.body as any).requestId
  40. await button('导入授权文件').trigger('click')
  41. finish({id,status:'authorized',expiresAt:Date.now()+60000});await flushPromises()
  42. expect(request).toHaveBeenCalledWith(root+'/codex-auth/'+id,{method:'DELETE'})
  43. expect(wrapper.text()).not.toContain('浏览器授权成功')
  44. const input=wrapper.get('input[type=file]'),content=JSON.stringify({tokens:{access_token:'fixture-secret'}})
  45. Object.defineProperty(input.element,'files',{value:[{name:'auth.json',size:content.length,text:async()=>content}]})
  46. await input.trigger('change');await flushPromises();expect(wrapper.text()).toContain('已读取 auth.json')
  47. expect((wrapper.get('input[type=checkbox]').element as HTMLInputElement).checked).toBe(false)
  48. await wrapper.get('input[maxlength="80"]').setValue('导入账号');await wrapper.get('form').trigger('submit');await flushPromises()
  49. expect(request).toHaveBeenCalledWith(root+'/resources',{method:'POST',body:{name:'导入账号',kind:'codex',agentIds:[],revision:0,authJson:content}})
  50. })
  51. it('validates pasted credentials without echoing secret content in the error',async()=>{
  52. wrapper=mountDialog();await button('导入授权文件').trigger('click')
  53. await wrapper.get('details textarea').setValue('invalid fixture-secret');await wrapper.get('form').trigger('submit');await flushPromises()
  54. expect(wrapper.get('[role=alert]').text()).toBe('请选择有效的 Codex auth.json 授权文件')
  55. expect(request).not.toHaveBeenCalled()
  56. })
  57. it('retains an existing login when only changing its name or grants',async()=>{
  58. wrapper=mountDialog({id:'resource',kind:'codex',name:'旧账号',agentIds:['bot-1'],revision:2})
  59. await wrapper.get('input[maxlength="80"]').setValue('新名称');await wrapper.get('form').trigger('submit');await flushPromises()
  60. expect(request).toHaveBeenCalledWith(root+'/resources/resource',{method:'PUT',body:{name:'新名称',kind:'codex',agentIds:['bot-1'],revision:2}})
  61. })
  62. it('shows failed and expired login states and permits a fresh attempt',async()=>{
  63. request.mockResolvedValue({id:'attempt',status:'expired',expiresAt:0})
  64. wrapper=mountDialog();await button('打开浏览器授权').trigger('click');await flushPromises()
  65. expect(wrapper.text()).toContain('授权已过期');expect(button('保存授权').attributes('disabled')).toBeDefined()
  66. request.mockResolvedValue({id:'next',status:'failed',expiresAt:0,error:'服务器未找到 Codex CLI'})
  67. await button('打开浏览器授权').trigger('click');await flushPromises();expect(wrapper.text()).toContain('未找到 Codex CLI')
  68. })