| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- import { test, expect } from '@playwright/test'
- import { mkdirSync } from 'node:fs'
- test('a default installation creates and saves encrypted passwords through the settings UI', async ({ page }) => {
- const evidence='test-results/credential-vault-default';mkdirSync(evidence,{recursive:true})
- const master='DUMMY-browser-vault-master',secret='DUMMY-browser-entry-secret'
- await page.goto('/conversations')
- await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
- await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
- await page.getByRole('button',{name:'登录',exact:true}).click()
- await page.locator('.desktop-sidebar .sidebar-account-switcher__main').click()
- await page.getByRole('menuitem',{name:'我的设置',exact:true}).click()
- await page.getByRole('button',{name:'密码管理',exact:true}).click()
- const panel=page.locator('.credential-vault')
- await expect(panel.getByText(/授权一次即可使用/)).toBeVisible()
- await panel.getByLabel('主密码',{exact:true}).fill(master)
- await panel.getByLabel('确认主密码',{exact:true}).fill(master)
- await panel.getByRole('button',{name:'创建密码库',exact:true}).click()
- await expect(panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true})).toBeVisible()
- await panel.getByLabel('主密码',{exact:true}).fill(master)
- await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click()
- await panel.getByRole('button',{name:'添加凭据',exact:true}).click()
- await panel.getByLabel('名称',{exact:true}).fill('浏览器测试网站')
- await panel.getByLabel('用户名',{exact:true}).fill('dummy-user')
- await panel.getByLabel('准确网站 origin',{exact:true}).fill('https://example.test')
- await panel.getByLabel('密码',{exact:true}).fill(secret)
- await panel.getByRole('button',{name:'加密保存',exact:true}).click()
- await expect(panel.locator('li strong').filter({hasText:'浏览器测试网站'})).toBeVisible()
- await expect(panel.getByRole('status').filter({hasText:'凭据已加密保存'})).toBeVisible()
- await expect(panel.getByRole('button',{name:'授权此 Bot 使用'})).toBeDisabled()
- expect(await panel.textContent()).not.toContain(secret)
- await panel.getByRole('button',{name:'添加凭据',exact:true}).click()
- await panel.getByRole('combobox',{name:/^用途/}).selectOption('ssh')
- await expect(panel.getByRole('combobox',{name:/^SSH 认证方式/})).toHaveValue('password')
- await panel.getByLabel('名称',{exact:true}).fill('浏览器测试 SSH 密码')
- await panel.getByLabel('用户名',{exact:true}).fill('dummy-ssh-user')
- await panel.getByLabel('SSH 主机',{exact:true}).fill('ssh.example.test')
- await expect(panel.getByLabel(/^服务器指纹/)).toBeHidden()
- await expect(panel.getByLabel(/^服务器指纹/)).not.toHaveAttribute('required')
- const sshPassword=panel.getByLabel(/^SSH 登录密码/)
- await expect(sshPassword).toHaveAttribute('type','password')
- await sshPassword.fill('DUMMY-browser-ssh-password')
- await expect(panel.getByText('授权 Bot 后,允许全部远程 SSH 命令并读取输出,不需要预先填写命令。权限由 SSH 账号决定。')).toBeVisible()
- await expect(panel.getByLabel('私钥口令(可选)',{exact:true})).toHaveCount(0)
- await page.screenshot({path:evidence+'/ssh-password-editor.png'})
- await panel.getByRole('button',{name:'加密保存',exact:true}).click()
- const sshEntry=panel.locator('li').filter({hasText:'浏览器测试 SSH 密码'})
- await expect(sshEntry).toContainText('密码登录')
- await expect(panel.getByRole('heading',{name:'授权 Bot 使用',exact:true})).toBeVisible()
- expect(await panel.textContent()).not.toContain('DUMMY-browser-ssh-password')
- await sshEntry.getByRole('button',{name:'编辑',exact:true}).click()
- await expect(sshPassword).toHaveValue('')
- await expect(panel.getByRole('combobox',{name:/^SSH 认证方式/})).toHaveValue('password')
- await panel.getByText('高级连接设置(可选)',{exact:true}).click()
- await expect(panel.getByLabel(/^服务器指纹/)).toHaveValue('')
- await expect(panel.getByLabel('端口',{exact:true})).toHaveValue('22')
- await panel.getByRole('button',{name:'加密保存',exact:true}).click()
- await expect(sshEntry).toContainText('密码登录')
- await page.screenshot({path:evidence+'/desktop-saved.png'})
- await page.setViewportSize({width:375,height:812})
- await expect(panel.getByRole('button',{name:'添加凭据',exact:true})).toBeVisible()
- expect(await panel.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true)
- await page.screenshot({path:evidence+'/mobile-saved.png'})
- await page.setViewportSize({width:1440,height:1000})
- await panel.getByRole('button',{name:'锁定管理界面',exact:true}).click()
- await expect(panel.getByText('浏览器测试网站',{exact:true})).toHaveCount(0)
- await panel.getByLabel('主密码',{exact:true}).fill('DUMMY-wrong-master-password')
- await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click()
- await expect(panel.getByRole('alert')).toContainText('主密码错误')
- await expect(page.getByRole('textbox',{name:'账号',exact:true})).toHaveCount(0)
- await expect(panel.getByLabel('主密码',{exact:true})).toHaveValue('')
- await page.screenshot({path:evidence+'/wrong-master-stays-signed-in.png'})
- })
- test('UI approval survives port edits, closing settings and a full server process restart with real dummy SSH execution',async({page})=>{
- const {mkdtempSync,rmSync}=await import('node:fs'),{tmpdir}=await import('node:os'),{join}=await import('node:path'),{spawn}=await import('node:child_process')
- const {sshFixture}=await import('../fixtures/credentialProtocols')
- const first=await sshFixture('password'),second=await sshFixture('password')
- first.state.execReplies.set('uname -s',{stdout:'Linux\n',stderr:''});second.state.execReplies.set('uname -s',{stdout:'Linux\n',stderr:''});second.state.execReplies.set('df -h / | head -n 2',{stdout:'Filesystem Size Used Avail Use% Mounted on\n/dev/test 100G 40G 60G 40% /\n',stderr:''})
- const home=mkdtempSync(join(tmpdir(),'yaoyao-vault-browser-restart-')),base='http://127.0.0.1:18837',master='DUMMY-browser-persistent-master'
- let child:ReturnType<typeof spawn>|undefined,logs=''
- const start=async()=>{
- child=spawn(process.execPath,['--import','tsx','tests/fixtures/workspace-server.ts'],{env:{...process.env,WORKSPACE_FIXTURE_HOME:home,WORKSPACE_FIXTURE_PORT:'18837',WORKSPACE_FIXTURE_UPSTREAM_PORT:'19137',WORKSPACE_FIXTURE_VAULT:'1'},stdio:['ignore','pipe','pipe']})
- child.stdout!.on('data',c=>logs+=c);child.stderr!.on('data',c=>logs+=c)
- await expect.poll(async()=>{try{return (await fetch(base+'/healthz')).status}catch{return 0}},{timeout:20000,message:logs}).toBe(200)
- }
- const stop=async()=>{if(!child||child.exitCode!==null)return;await page.goto('about:blank').catch(()=>{});const current=child,exited=new Promise<void>(r=>current.once('exit',()=>r()));current.kill('SIGTERM');const timeout=setTimeout(()=>current.kill('SIGKILL'),3000);await exited;clearTimeout(timeout);child=undefined}
- const login=async()=>{
- await page.goto(base+'/conversations')
- if(await page.getByRole('textbox',{name:'账号',exact:true}).isVisible()){
- await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture');await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass');await page.getByRole('button',{name:'登录',exact:true}).click()
- }
- await expect(page.locator('.desktop-sidebar .sidebar-account-switcher__main')).toBeVisible()
- }
- const settings=async()=>{await page.locator('.desktop-sidebar .sidebar-account-switcher__main').click();await page.getByRole('menuitem',{name:'我的设置',exact:true}).click();await page.getByRole('button',{name:'密码管理',exact:true}).click()}
- const headers=async()=>({Origin:base,'X-CSRF-Token':(await(await page.request.get(base+'/api/realtime/capabilities')).json()).csrfToken})
- try{
- await start();await login()
- const agentResponse=await page.request.post(base+'/api/app/agents',{headers:await headers(),data:{name:'竹儿测试',profile:'default'}});expect(agentResponse.ok()).toBe(true)
- const agentId=(await agentResponse.json()).agent.id
- await settings();const panel=page.locator('.credential-vault')
- await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByLabel('确认主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'创建密码库',exact:true}).click()
- await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click();await panel.getByRole('button',{name:'添加凭据',exact:true}).click()
- await panel.getByRole('combobox',{name:/^用途/}).selectOption('ssh')
- await panel.getByLabel('名称',{exact:true}).fill('TrueNAS 流程测试');await panel.getByLabel('用户名',{exact:true}).fill(first.username);await panel.getByLabel('SSH 主机',{exact:true}).fill('127.0.0.1');await panel.getByLabel(/^SSH 登录密码/).fill(first.secret)
- await panel.getByText('高级连接设置(可选)',{exact:true}).click();await panel.getByLabel('端口',{exact:true}).fill(String(first.target.port))
- await panel.getByRole('combobox',{name:/^授权给 Bot/}).selectOption(agentId)
- await panel.getByRole('button',{name:'保存并授权 Bot',exact:true}).click();await expect(panel.getByText('TrueNAS 流程测试 → 竹儿测试 · 已持续授权')).toBeVisible()
- expect(first.state.authMethods).toEqual([])
- const state=await(await page.request.get(base+'/api/app/vault')).json(),credentialRef=state.entries[0].id
- const use=async(discoverOnly=false,command='uname -s')=>{const response=await page.request.post(base+'/__test/vault-use',{headers:await headers(),data:{agentId,credentialRef,discoverOnly,...(!discoverOnly?{command}:{})}});expect(response.ok(),await response.text()).toBe(true);return response.json()}
- await panel.getByRole('button',{name:'锁定管理界面',exact:true}).click()
- expect((await use()).result).toEqual({status:'complete',operation:'ssh.exec',exitCode:0,stdout:'Linux\n',stderr:'',truncated:false});expect(first.state.commands).toEqual(['uname -s'])
- await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click()
- await panel.locator('li').filter({has:page.locator('strong', {hasText:'TrueNAS 流程测试'})}).getByRole('button',{name:'编辑',exact:true}).click()
- await expect(panel.getByText('已持续授权:竹儿测试。修改后继续可用,无需重新授权。')).toBeVisible()
- await panel.getByText('高级连接设置(可选)',{exact:true}).click();await panel.getByLabel('端口',{exact:true}).fill(String(second.target.port));await panel.getByRole('button',{name:'加密保存',exact:true}).click()
- await page.getByRole('button',{name:'关闭我的设置',exact:true}).filter({visible:true}).first().click();await expect(panel).toHaveCount(0);await expect.poll(async()=>(await(await page.request.get(base+'/api/app/vault')).json()).unlocked).toBe(false)
- const edited=await use(false,'df -h / | head -n 2');expect(edited.result.stdout).toContain('40%');expect(edited.refs.references[0]).toMatchObject({authorized:true,allowedTarget:{port:second.target.port}});expect(edited.result.exitCode).toBe(0)
- expect(first.state.commands).toHaveLength(1);expect(second.state.commands).toHaveLength(1)
- await stop();await start();await login()
- const restarted=await(await page.request.get(base+'/api/app/vault')).json();expect(restarted).toMatchObject({unlocked:false,botGrants:[{credentialRef,agentId}]})
- expect((await use()).result).toEqual({status:'complete',operation:'ssh.exec',exitCode:0,stdout:'Linux\n',stderr:'',truncated:false});expect(second.state.commands).toHaveLength(2)
- await settings();await expect(panel.getByText('TrueNAS 流程测试 → 竹儿测试 · 已持续授权')).toBeVisible()
- await page.setViewportSize({width:375,height:812});expect(await panel.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true)
- mkdirSync('test-results/credential-vault-persistent',{recursive:true});await page.screenshot({path:'test-results/credential-vault-persistent/mobile-after-restart.png'})
- await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click();await panel.getByRole('button',{name:'撤销 Bot 授权',exact:true}).click();await panel.getByRole('button',{name:'锁定管理界面',exact:true}).click()
- expect(await use(true)).toMatchObject({references:[],reason:'bot_not_authorized'});expect(second.state.commands).toHaveLength(2)
- }finally{await stop();await first.close();await second.close();rmSync(home,{recursive:true,force:true})}
- })
|