59 lines
4.0 KiB
Python
59 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Log into the task's Chrome QA session without credentials in CLI arguments, code, or logs.
|
|
|
|
The protected configuration is read through File API on a separate about:blank page,
|
|
never uploaded to the application; only two login values are filled, solely in memory.
|
|
Requires the Playwright CLI skill and an already-open isolated round5v4 Chrome session.
|
|
"""
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import qa
|
|
|
|
CODE = r'''async (page) => {
|
|
if(!USE_CURRENT_PAGE){await page.context().clearCookies();await page.goto('http://127.0.0.1:18088/login');}
|
|
await page.getByRole('textbox',{name:'账号',exact:true}).waitFor();
|
|
await page.waitForLoadState('networkidle');
|
|
page.__qaEvidence={responses:[],errors:[]};
|
|
if(page.__qaErrorListener)page.removeListener('pageerror',page.__qaErrorListener);
|
|
if(page.__qaResponseListener)page.removeListener('response',page.__qaResponseListener);
|
|
page.__qaErrorListener=error=>page.__qaEvidence.errors.push(error.message);
|
|
page.__qaResponseListener=async response=>{const url=response.url();if(!url.includes('/prod-api/'))return;let code=null;try{code=(await response.json()).code;}catch{}page.__qaEvidence.responses.push({path:url.split('/prod-api')[1],status:response.status(),code});};
|
|
page.on('pageerror',page.__qaErrorListener);page.on('response',page.__qaResponseListener);
|
|
const local=await page.context().newPage();let credentials={};
|
|
try {
|
|
await local.goto('about:blank');await local.setContent('<input id="protected-config" type="file">');
|
|
await local.locator('#protected-config').setInputFiles(CONFIG_PATH);
|
|
credentials=await local.locator('#protected-config').evaluate(async input=>{
|
|
const text=await input.files[0].text();const vals={};
|
|
for(const line of text.split('\n')){const m=line.match(/^(?:export )?(QA_ADMIN_USERNAME|QA_ADMIN_PASSWORD)=(.*)$/);if(m){let v=m[2].trim();if((v.startsWith("'")&&v.endsWith("'"))||(v.startsWith('"')&&v.endsWith('"')))v=v.slice(1,-1);vals[m[1]]=v;}}
|
|
return vals;
|
|
});
|
|
}finally{await local.close();}
|
|
if(!credentials.QA_ADMIN_USERNAME||!credentials.QA_ADMIN_PASSWORD)throw new Error('Protected QA login configuration missing');
|
|
if(await page.getByRole('textbox',{name:'验证码',exact:true}).isVisible()){
|
|
if(!CAPTCHA_ANSWER)throw new Error('Captcha enabled: complete visual captcha manually; do not disable it');
|
|
await page.getByRole('textbox',{name:'验证码',exact:true}).fill(CAPTCHA_ANSWER);
|
|
}
|
|
await page.getByRole('textbox',{name:'账号',exact:true}).fill(credentials.QA_ADMIN_USERNAME);
|
|
await page.getByRole('textbox',{name:'密码',exact:true}).fill(credentials.QA_ADMIN_PASSWORD);credentials={};
|
|
await page.getByRole('button',{name:'登 录'}).click();await page.waitForURL(url=>url.origin==='http://127.0.0.1:18088'&&!['/login','/404','/401'].includes(url.pathname),{timeout:20000});await page.waitForLoadState('networkidle');
|
|
return {url:page.url(),title:await page.title(),api:page.__qaEvidence.responses,errors:page.__qaEvidence.errors};
|
|
}'''
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--config',type=Path,default=Path.home()/'.railwaysystem-qa.env')
|
|
parser.add_argument('--cli',type=Path,default=Path.home()/'.codex/skills/playwright/scripts/playwright_cli.sh')
|
|
parser.add_argument('--captcha',default='',help='Answer from the actual displayed captcha, never disable validation')
|
|
parser.add_argument('--use-current-page',action='store_true')
|
|
args=parser.parse_args();qa.load_config(args.config)
|
|
code=CODE.replace('CONFIG_PATH',json.dumps(str(args.config.expanduser().resolve()))).replace('CAPTCHA_ANSWER',json.dumps(args.captcha)).replace('USE_CURRENT_PAGE',str(args.use_current_page).lower())
|
|
result=subprocess.run([str(args.cli),'-s=round5v4','run-code',code],text=True,capture_output=True,timeout=45)
|
|
# The emitted code contains a path, never credential values. Do not snapshot a filled login form.
|
|
print(result.stdout)
|
|
return result.returncode
|
|
|
|
if __name__=='__main__':raise SystemExit(main())
|