aim-web2.1.0rc9
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""New rc4 ASGI-backed browser fixtures with synthetic retained reports.
|
||||
|
||||
Bootstrap5.3.6 is a declared fixture substitute; HTMX is not loaded. This does not
|
||||
qualify production HTTPS/proxy traffic or native Ansible/Windows Update behavior.
|
||||
No substituted browser asset is copied to the release static/vendor directory.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'src'))
|
||||
from playwright.sync_api import sync_playwright
|
||||
from fastapi.testclient import TestClient
|
||||
from aim_webgui.app import create_app
|
||||
from aim_webgui.auth.service import Auth
|
||||
from aim_webgui.reports import persist
|
||||
from conftest import core_root as core_fixture, settings as settings_fixture
|
||||
from evidence_fixtures import declared, plan, result, job
|
||||
from patch_fixtures import patch_data, update_failure
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--out',type=Path,required=True)
|
||||
parser.add_argument('--bootstrap',type=Path,default=Path('/opt/imagemagick/share/doc/ImageMagick-7/www/assets/bootstrap.min.css'))
|
||||
args=parser.parse_args();args.out.mkdir(parents=True,exist_ok=True)
|
||||
records=[]
|
||||
with tempfile.TemporaryDirectory(prefix='aim-rc4-patch-browser-') as tmp:
|
||||
root=Path(tmp);core=core_fixture.__wrapped__(root);base=settings_fixture.__wrapped__(root,core)
|
||||
url=base.public_url
|
||||
settings=replace(base,public_url=url,execution_enabled=False,execution_transport_verified=False,credentials_enabled=False)
|
||||
a=Auth(settings);a.bootstrap()
|
||||
with a.store.transaction() as db:
|
||||
db.execute('UPDATE users SET must_change_password=0');uid=db.execute('SELECT id FROM users').fetchone()[0]
|
||||
d=declared('patch_summary_v1');paths={}
|
||||
scenarios={
|
||||
'next-wave':patch_data(),
|
||||
'pending':patch_data(remaining_updates_known=True,reboot_performed=False,pending=[
|
||||
{'name':'Synthetic security update','identifier':'fixture-pending','kb':['KB0000002']}]),
|
||||
'failed-update':patch_data(complete=False,blocked_reason='install_not_allowed',failed_updates=[update_failure()]),
|
||||
'reboot-deferred':patch_data(reboot_deferred=True,reboot_performed=False,reboot_required=True,reboot_required_after=True),
|
||||
}
|
||||
for name,content in scenarios.items():
|
||||
p=plan(d,['win01.example']);p['playbook']='maintenance_patch_os';p['core_request']['playbook']=p['playbook']
|
||||
failed=name=='failed-update'
|
||||
ident=job(a,uid,p,status='failed' if failed else 'successful')
|
||||
r=result(d,hosts=p['targets'],report_data=content,status='failed' if failed else 'succeeded')
|
||||
if failed:
|
||||
r.update(stage='execution',exit_code=2,error={'code':'playbook_failed','message':'Synthetic task failed','stage':'execution'})
|
||||
r['targets'][0]['outcome']='failed';r['targets'][0]['counts']['failures']=1;r['counts']['failures']=1
|
||||
r['target_summary'].update(successful=0,failed=1)
|
||||
with a.store.transaction() as db:
|
||||
small=persist(db,settings,ident,p,r)
|
||||
db.execute('UPDATE jobs SET core_result=?,finished_at=? WHERE id=?',(json.dumps(small),time.time(),ident))
|
||||
paths[name]=f'/jobs/{ident}/reports'
|
||||
client=TestClient(create_app(settings),base_url=url)
|
||||
token,_=a.new_session(uid);client.cookies.set(settings.cookie_name,token)
|
||||
pages={}
|
||||
def bridge_read(source,path):
|
||||
response=client.get(path)
|
||||
return {'status':response.status_code,'text':response.text}
|
||||
def mount(page,path):
|
||||
if path not in pages:
|
||||
response=client.get(path);assert response.status_code==200,(path,response.text[:500])
|
||||
content=response.text
|
||||
def css(match):
|
||||
name=match.group(1).split('?')[0]
|
||||
source=args.bootstrap if name.endswith('bootstrap.min.css') else ROOT/'src/aim_webgui'/name.lstrip('/')
|
||||
return '<style>'+source.read_text()+'</style>'
|
||||
def js(match):
|
||||
name=match.group(1).split('?')[0]
|
||||
if 'htmx.min.js' in name:return ''
|
||||
return '<script>'+(ROOT/'src/aim_webgui'/name.lstrip('/')).read_text()+'</script>'
|
||||
content=re.sub(r'<link[^>]+href="(/static/[^"]+\.css(?:\?[^"]*)?)"[^>]*>',css,content)
|
||||
content=re.sub(r'<script[^>]+src="(/static/[^"]+\.js(?:\?[^"]*)?)"[^>]*></script>',js,content)
|
||||
fixture="""<script>window.fetch=async function(url,opts){const r=await window.fixtureRead(String(url));return {ok:r.status>=200&&r.status<300,status:r.status,json:async()=>JSON.parse(r.text),text:async()=>r.text};};</script>"""
|
||||
pages[path]=content.replace('<head>','<head>'+fixture)
|
||||
page.set_content(pages[path],wait_until='domcontentloaded')
|
||||
try:
|
||||
with sync_playwright() as pw:
|
||||
browser=pw.chromium.launch(executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
|
||||
try:
|
||||
for width,height in ((320,750),(390,844),(760,900),(1440,1000),(740,390)):
|
||||
for theme in ('light','dark'):
|
||||
context=browser.new_context(viewport={'width':width,'height':height},locale='en-GB',timezone_id='Europe/Berlin',reduced_motion='reduce')
|
||||
page=context.new_page();page.expose_binding('fixtureRead',bridge_read);errors=[];page.on('pageerror',lambda exc:errors.append(str(exc)))
|
||||
for name,path in paths.items():
|
||||
mount(page,path)
|
||||
page.locator('[data-theme-option='+theme+']:visible').click()
|
||||
assert page.evaluate('document.documentElement.scrollWidth<=innerWidth+1'),(name,width,height)
|
||||
assert page.locator('.patch-wave-panel').is_visible()
|
||||
if name=='next-wave':
|
||||
assert 'Another patch run needs review' in page.locator('.patch-wave-panel').inner_text()
|
||||
assert 'Final pending list not established' in page.locator('#report-pending').inner_text()
|
||||
assert '0 entries' not in page.locator('#report-pending').inner_text()
|
||||
elif name=='pending':
|
||||
assert 'final read-only discovery' in page.locator('#report-pending').inner_text()
|
||||
assert 'Synthetic security update' in page.locator('#report-pending').inner_text()
|
||||
elif name=='failed-update':
|
||||
assert '0x80240016' in page.locator('#report-failed_updates').inner_text()
|
||||
assert 'does not by itself prove a pending reboot' in page.locator('.patch-wave-panel').inner_text()
|
||||
page.locator('[data-report-json] summary').click()
|
||||
page.wait_for_function("document.querySelector('[data-json-status]').textContent.includes('Retained')")
|
||||
stored=json.loads(page.locator('[data-json-output]').inner_text())
|
||||
assert stored==scenarios[name] or stored.get('data')==scenarios[name]
|
||||
if name=='next-wave' and width in(390,1440) and theme=='dark':
|
||||
page.locator('[data-report-json] summary').click();page.evaluate('window.scrollTo(0,0)')
|
||||
page.screenshot(path=str(args.out/f'patch-wave-{width}-dark.png'),full_page=True)
|
||||
records.append({'page':name,'width':width,'height':height,'theme':theme,'passed':True})
|
||||
mount(page,'/plan?customer=example&playbook=maintenance_patch_os')
|
||||
page.locator('[data-theme-option='+theme+']:visible').click()
|
||||
option=page.locator('[name="option.os_patching_rescan_after_reboot"]')
|
||||
assert option.input_value()=='' and 'catalog hint: false' in option.inner_text()
|
||||
option.select_option('false')
|
||||
page.locator('[name="option.os_patching_reboot_delay_minutes"]').fill('10')
|
||||
page.locator('[name="option.os_patching_reboot_message"]').fill('Synthetic approved maintenance')
|
||||
assert page.evaluate('document.documentElement.scrollWidth<=innerWidth+1')
|
||||
records.append({'page':'catalog patch options','width':width,'height':height,'theme':theme,'passed':True})
|
||||
assert not errors,errors
|
||||
context.close()
|
||||
finally:browser.close()
|
||||
finally:
|
||||
client.close()
|
||||
evidence={'cases':records,'count':len(records),'real_local_http_and_fetch':False,'asgi_bridge':True,'production_https_tested':False,
|
||||
'bootstrap_substitute':'5.3.6 for pinned5.3.8','htmx_loaded':False,'native_updates_executed':False,
|
||||
'source':'synthetic retained data; real public Core catalog/discovery; about:blank fetch bridge to ASGI TestClient'}
|
||||
(args.out/'results.json').write_text(json.dumps(evidence,indent=2)+'\n')
|
||||
print(len(records),'patch UI cases passed')
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in New Issue
Block a user