227 lines
19 KiB
Python
227 lines
19 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Pruebas MCP del SO actual sobre el entorno privado desechable de visual-audit.py."""
|
||
|
|
import base64
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
import runpy
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
BASE = runpy.run_path(str(ROOT / 'scripts/workstation-dashboard-ui.test.py'))
|
||
|
|
|
||
|
|
|
||
|
|
class CurrentOperatingSystemsTests(BASE['WorkstationDashboardTests']):
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls):
|
||
|
|
super().setUpClass()
|
||
|
|
cls.output = cls.output.parent.parent / 'current-operating-systems'
|
||
|
|
cls.output.mkdir(exist_ok=True)
|
||
|
|
evidence = cls.output / "measurements.json"
|
||
|
|
cls.evidence = json.loads(evidence.read_text()) if evidence.exists() else []
|
||
|
|
existing = cls.output / 'fixtures.json'
|
||
|
|
if existing.exists():
|
||
|
|
cls.os = json.loads(existing.read_text())
|
||
|
|
cls.code('''const origin=__ORIGIN__;async function get(path){const r=await page.request.get(origin+'/api'+path);if(!r.ok())throw Error('GET '+path);return r.json()}const {csrfToken}=await get('/auth/session');async function post(path,data){const r=await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken,'X-Metis-Resources-Contract':'2'}});if(!r.ok())throw Error('POST '+path+': '+r.status());return r.json()}page.__osApi={get,post};return true;'''.replace('__ORIGIN__',json.dumps(cls.origin)))
|
||
|
|
return
|
||
|
|
cls.os = cls.code('''
|
||
|
|
const origin=__ORIGIN__,company=__COMPANY__;
|
||
|
|
async function get(path){const r=await page.request.get(origin+'/api'+path);if(!r.ok())throw Error('GET '+path+': '+r.status());return r.json()}
|
||
|
|
const {csrfToken}=await get('/auth/session');
|
||
|
|
async function post(path,data){const r=await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken,'X-Metis-Resources-Contract':'2'}});if(!r.ok())throw Error('POST '+path+': '+r.status());return r.json()}
|
||
|
|
const catalog=(await get('/asset-catalog/'+company)).items;
|
||
|
|
const rc=(await get('/resource-catalog/'+company)).items;
|
||
|
|
const laptop=catalog.find(x=>x.builtinKey==='template:laptop');
|
||
|
|
const desktop=catalog.find(x=>x.builtinKey==='template:desktop');
|
||
|
|
const server=catalog.find(x=>x.builtinKey==='template:server');
|
||
|
|
const factory=catalog.find(x=>x.builtinKey==='field:factory_os').id;
|
||
|
|
const custom=(await post('/asset-catalog/'+company,{values:{kind:'template',name:'OSUI — plantilla propia',description:'Solo datos sintéticos',fields:laptop.values.fields.map(x=>({fieldId:x.fieldId,required:false}))}})).items.find(x=>x.values.name==='OSUI — plantilla propia');
|
||
|
|
async function asset(name,template,original){return (await post('/assets/'+company,{values:{name,templateId:template.id,siteId:null,status:'operational',notes:'Prueba sintética de SO actual',characteristics:original?{[factory]:original}:{}}})).asset}
|
||
|
|
async function resource(name,kind){return (await post('/resources/'+company,{values:{name,templateId:rc.find(x=>x.builtinKey==='template:'+kind).id,active:true,notes:'Prueba sintética',characteristics:{}}})).resource}
|
||
|
|
const win=await resource('Windows 11 Pro','os'),ubuntu=await resource('Ubuntu 24.04 LTS — edición de ingeniería y documentación con un nombre extenso de prueba','os');
|
||
|
|
const application=await resource('OSUI Editor de documentos','application');
|
||
|
|
const main=await asset('OSUI — portátil con FreeDOS de fábrica',laptop,'FreeDOS');
|
||
|
|
const same=await asset('OSUI — sobremesa original Windows',desktop,'Windows 11 Pro');
|
||
|
|
const physicalServer=await asset('OSUI — servidor sin puesto',server);
|
||
|
|
const own=await asset('OSUI — activo de plantilla propia',custom);
|
||
|
|
const empty=await asset('OSUI — Windows de fábrica sin instalación',laptop,'Windows 11 Pro');
|
||
|
|
const pending=await asset('OSUI — pendiente',laptop);
|
||
|
|
const removed=await asset('OSUI — retirado',laptop);
|
||
|
|
const other=await asset('OSUI — segundo ordenador',laptop);
|
||
|
|
async function install(a,r,state='configured',version='24H2'){return (await post('/software-installations/'+company,{values:{assetId:a.id,resourceId:r.id,licenseResourceId:null,version,state,notes:null}})).configuration}
|
||
|
|
const current=await install(main,win);
|
||
|
|
await install(main,application);
|
||
|
|
for(const a of [same,physicalServer,own])await install(a,win);
|
||
|
|
await install(pending,win,'pending');
|
||
|
|
let closed=await install(removed,win);
|
||
|
|
for(const state of ['removal_pending','removed'])closed=(await post('/software-installations/'+company+'/'+closed.id+'/update',{values:{...closed.values,state}})).configuration;
|
||
|
|
await install(other,ubuntu,'configured','24.04 LTS');
|
||
|
|
const station=(await post('/assets/'+company+'/workstations',{values:{name:'OSUI — puesto de comprobación',active:true,siteId:null,personId:null,computerAssetId:main.id,assetIds:[main.id]}})).workstation;
|
||
|
|
page.__osApi={get,post};
|
||
|
|
return {main:main.id,same:same.id,server:physicalServer.id,custom:own.id,empty:empty.id,pending:pending.id,removed:removed.id,other:other.id,station,current,win,ubuntu,application};
|
||
|
|
'''.replace('__ORIGIN__',json.dumps(cls.origin)).replace('__COMPANY__',json.dumps(cls.fixture['companyId'])))
|
||
|
|
(cls.output/'fixtures.json').write_text(json.dumps(cls.os,ensure_ascii=False,indent=2)+'\n')
|
||
|
|
|
||
|
|
def capture(self,name):
|
||
|
|
self.settle()
|
||
|
|
encoded=self.code("return (await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').screenshot()).toString('base64');")
|
||
|
|
data=base64.b64decode(encoded,validate=True)
|
||
|
|
path=self.output/(name+'.png')
|
||
|
|
path.write_bytes(data)
|
||
|
|
self.record(name,{'path':str(path.relative_to(ROOT)),'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data),'scope':'current operating system card'})
|
||
|
|
|
||
|
|
def open_asset(self,key):
|
||
|
|
self.tool('browser_navigate',{'url':self.origin+self.base+'/assets/'+self.os[key]})
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').waitFor();return true;")
|
||
|
|
|
||
|
|
def ready(self,name='Windows 11 Pro'):
|
||
|
|
self.code('await page.locator("[data-current-operating-systems]").getByRole("link",{name:'+json.dumps(name)+',exact:true}).waitFor({timeout:20000});return true;')
|
||
|
|
|
||
|
|
def summary(self):
|
||
|
|
return self.code("return await page.locator('[data-current-operating-systems]').innerText();")
|
||
|
|
|
||
|
|
def test_os_01_source_and_states(self):
|
||
|
|
for key in ['main','same','server','custom']:
|
||
|
|
self.open_asset(key)
|
||
|
|
self.ready()
|
||
|
|
text=self.summary()
|
||
|
|
self.assertIn('Windows 11 Pro',text)
|
||
|
|
self.assertNotIn('FreeDOS',text)
|
||
|
|
self.assertNotIn(self.os['application']['values']['name'],text)
|
||
|
|
for key in ['empty','pending','removed']:
|
||
|
|
self.open_asset(key)
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').getByText('No registrado',{exact:true}).waitFor();return true;")
|
||
|
|
self.assertNotIn('Windows 11 Pro',self.summary())
|
||
|
|
self.record('OSUI-01-05-07-12-14-17-18',{'source':'SoftwareInstallation','factoryIndependent':True,'serverWithoutWorkstation':True,'customTemplate':True})
|
||
|
|
|
||
|
|
def test_os_02_change_computer(self):
|
||
|
|
self.code("await page.__osApi.post('/assets/'+"+json.dumps(self.fixture['companyId'])+"+'/workstations/'+"+json.dumps(self.os['station']['id'])+"+'/update',{values:"+json.dumps(self.os['station']['values'])+"});return true;")
|
||
|
|
self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.os['station']['id']})
|
||
|
|
self.ready()
|
||
|
|
result=self.code('''
|
||
|
|
const {get,post}=page.__osApi;
|
||
|
|
const before=await get('/software-installations/'+__COMPANY__);
|
||
|
|
const station=__STATION__,other=__OTHER__;
|
||
|
|
await post('/assets/'+__COMPANY__+'/workstations/'+station.id+'/update',{values:{...station.values,computerAssetId:other,assetIds:[other]}});
|
||
|
|
return {before,after:await get('/software-installations/'+__COMPANY__)};
|
||
|
|
'''.replace('__COMPANY__',json.dumps(self.fixture['companyId'])).replace('__STATION__',json.dumps(self.os['station'])).replace('__OTHER__',json.dumps(self.os['other'])))
|
||
|
|
self.assertEqual(result['before'],result['after'])
|
||
|
|
self.code("await page.reload();return true;")
|
||
|
|
self.ready(self.os['ubuntu']['values']['name'])
|
||
|
|
self.assertNotIn('Windows 11 Pro',self.summary())
|
||
|
|
self.record('OSUI-08',{'newPrincipal':True,'installationsAndHistoryUnchanged':True})
|
||
|
|
|
||
|
|
def test_os_03_multiple_responsive_and_themes(self):
|
||
|
|
self.open_asset('main')
|
||
|
|
self.ready()
|
||
|
|
self.code('''
|
||
|
|
const existing=await page.__osApi.get('/software-installations/'+__COMPANY__);if(!existing.configurations.some(x=>x.values.assetId===__ASSET__&&x.values.resourceId===__RESOURCE__))await page.__osApi.post('/software-installations/'+__COMPANY__,{values:{assetId:__ASSET__,resourceId:__RESOURCE__,licenseResourceId:null,version:'24.04 LTS',state:'configured',notes:null}});
|
||
|
|
await page.reload();return true;
|
||
|
|
'''.replace('__COMPANY__',json.dumps(self.fixture['companyId'])).replace('__ASSET__',json.dumps(self.os['main'])).replace('__RESOURCE__',json.dumps(self.os['ubuntu']['id'])))
|
||
|
|
for theme in ['light','dark']:
|
||
|
|
for width,height in [(320,800),(390,844),(768,1024),(1024,768),(1280,900),(1600,1000)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height})
|
||
|
|
self.open_asset('main')
|
||
|
|
self.code("await page.evaluate(theme=>document.documentElement.classList.toggle('dark',theme==='dark'),"+json.dumps(theme)+");return true;")
|
||
|
|
self.ready(self.os['ubuntu']['values']['name'])
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').scrollIntoViewIfNeeded();return true;")
|
||
|
|
self.ready(self.os['ubuntu']['values']['name'])
|
||
|
|
measurements=self.code("return await page.locator('[data-current-operating-systems]').evaluate(e=>({viewport:innerWidth,overflow:e.scrollWidth>e.clientWidth,links:e.querySelectorAll('a').length,text:e.innerText,colors:[...e.querySelectorAll('a,p')].map(x=>({text:x.textContent,color:getComputedStyle(x).color,background:getComputedStyle(x.closest('[data-slot=card]')).backgroundColor}))}));")
|
||
|
|
self.assertFalse(measurements['overflow'])
|
||
|
|
self.assertEqual(measurements['links'],2)
|
||
|
|
self.assertNotIn('Versión 24.04 LTS',measurements['text'])
|
||
|
|
self.record(f'OSUI-06-{theme}-{width}',measurements)
|
||
|
|
self.capture(f'asset-{theme}-{width}')
|
||
|
|
keyboard=self.code("const link=page.locator('[data-current-operating-systems] a').first();await link.focus();await page.keyboard.press('Tab');return await page.locator('[data-current-operating-systems] a').last().evaluate(e=>({focused:e===document.activeElement,outline:getComputedStyle(e).outlineStyle}));")
|
||
|
|
self.assertTrue(keyboard['focused'])
|
||
|
|
self.assertEqual(keyboard['outline'],'solid')
|
||
|
|
self.record('keyboard',keyboard)
|
||
|
|
self.code("await page.evaluate(()=>{document.documentElement.classList.remove('dark');document.documentElement.style.zoom='2'});return true;")
|
||
|
|
self.capture('asset-zoom-200')
|
||
|
|
self.assertFalse(self.code("return await page.locator('[data-current-operating-systems]').evaluate(e=>e.scrollWidth>e.clientWidth);"))
|
||
|
|
self.code("await page.evaluate(()=>document.documentElement.style.removeProperty('zoom'));return true;")
|
||
|
|
|
||
|
|
def test_os_04_read_failures(self):
|
||
|
|
self.open_asset('main')
|
||
|
|
self.ready()
|
||
|
|
path='**/api/software-installations/'+self.fixture['companyId']
|
||
|
|
self.addCleanup(self.code, 'await page.unroute('+json.dumps(path)+');return true;')
|
||
|
|
self.code("await page.route("+json.dumps(path)+",route=>route.fulfill({status:500,json:{code:'INTERNAL_ERROR'}}));await page.reload();return true;")
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').getByRole('button',{name:'Reintentar',exact:true}).waitFor({timeout:20000});return true;")
|
||
|
|
self.assertNotIn('No registrado',self.summary())
|
||
|
|
self.capture('asset-error')
|
||
|
|
self.code("await page.unroute("+json.dumps(path)+");await page.locator('[data-current-operating-systems]').getByRole('button',{name:'Reintentar',exact:true}).click();return true;")
|
||
|
|
self.ready()
|
||
|
|
refs='**/api/resource-application-references/**/resolve'
|
||
|
|
self.addCleanup(self.code, 'await page.unroute('+json.dumps(refs)+');return true;')
|
||
|
|
self.code("await page.route("+json.dumps(refs)+",async route=>{const r=await route.fetch();const data=await r.json();await route.fulfill({response:r,json:{...data,items:data.items.map(x=>({id:x.id,status:'unavailable'}))}})});await page.reload();return true;")
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').getByText(/Referencia de software no disponible/).waitFor();return true;")
|
||
|
|
self.assertNotIn('No registrado',self.summary())
|
||
|
|
self.capture('asset-unavailable-reference')
|
||
|
|
self.code("await page.unroute("+json.dumps(refs)+");return true;")
|
||
|
|
self.record('OSUI-10-11',{'errorIsNotEmpty':True,'retryRecovers':True,'unavailableReferenceIsNotEmpty':True})
|
||
|
|
|
||
|
|
def test_os_05_save_version_and_invalidation(self):
|
||
|
|
self.open_asset('same')
|
||
|
|
self.ready()
|
||
|
|
self.code("await page.getByRole('region',{name:'Software del ordenador',exact:true}).getByRole('button',{name:/Editar/}).first().click();return true;")
|
||
|
|
self.code("const dialog=page.getByRole('dialog');await dialog.getByLabel('Versión', {exact:true}).fill('25H2');await dialog.getByRole('button',{name:'Guardar',exact:true}).click();await dialog.waitFor({state:'hidden'});return true;")
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').getByText('Versión 25H2',{exact:true}).waitFor();return true;")
|
||
|
|
self.capture('saved-version')
|
||
|
|
self.record('OSUI-09-15',{'savedThroughExistingForm':True,'summaryUpdatedWithoutReload':True})
|
||
|
|
|
||
|
|
|
||
|
|
def test_os_06_workstation_loading_language_and_contrast(self):
|
||
|
|
self.code("await page.__osApi.post('/assets/'+"+json.dumps(self.fixture['companyId'])+"+'/workstations/'+"+json.dumps(self.os['station']['id'])+"+'/update',{values:"+json.dumps(self.os['station']['values'])+"});return true;")
|
||
|
|
for theme in ['light','dark']:
|
||
|
|
for width,height in [(320,800),(390,844),(768,1024),(1024,768),(1280,900),(1600,1000)]:
|
||
|
|
self.tool('browser_resize',{'width':width,'height':height})
|
||
|
|
self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.os['station']['id']})
|
||
|
|
self.ready(self.os['ubuntu']['values']['name'])
|
||
|
|
self.code("await page.evaluate(theme=>document.documentElement.classList.toggle('dark',theme==='dark'),"+json.dumps(theme)+");return true;")
|
||
|
|
self.code("await page.locator('[data-current-operating-systems]').scrollIntoViewIfNeeded();return true;")
|
||
|
|
self.ready(self.os['ubuntu']['values']['name'])
|
||
|
|
measurements=self.code("return await page.locator('[data-current-operating-systems]').evaluate(e=>({viewport:innerWidth,overflow:e.scrollWidth>e.clientWidth,links:e.querySelectorAll('a').length,text:e.innerText}));")
|
||
|
|
self.record(f'workstation-{theme}-{width}',measurements)
|
||
|
|
self.assertFalse(measurements['overflow'])
|
||
|
|
self.assertEqual(measurements['links'],2,measurements['text'])
|
||
|
|
self.capture(f'workstation-{theme}-{width}')
|
||
|
|
ratios=self.code("""return await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').evaluate(root=>{
|
||
|
|
const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d',{willReadFrequently:true});
|
||
|
|
const rgba=color=>{ctx.clearRect(0,0,1,1);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].map((x,i)=>i===3?x/255:x)};
|
||
|
|
const over=(a,b)=>{const alpha=a[3]+b[3]*(1-a[3]);return [...a.slice(0,3).map((v,i)=>(v*a[3]+b[i]*b[3]*(1-a[3]))/alpha),alpha]};
|
||
|
|
const bg=e=>e?over(rgba(getComputedStyle(e).backgroundColor),bg(e.parentElement)):[255,255,255,1];
|
||
|
|
const luminance=c=>c.slice(0,3).map(x=>x/255).map(x=>x<=.04045?x/12.92:((x+.055)/1.055)**2.4).reduce((a,x,i)=>a+x*[.2126,.7152,.0722][i],0);
|
||
|
|
const ratio=(a,b)=>{const l=[luminance(a),luminance(b)].sort((a,b)=>b-a);return (l[0]+.05)/(l[1]+.05)};
|
||
|
|
return [...root.querySelectorAll('h2,a,p')].map(e=>({text:e.textContent,ratio:ratio(over(rgba(getComputedStyle(e).color),bg(e)),bg(e))}));
|
||
|
|
});""")
|
||
|
|
self.record('contrast-'+theme,ratios)
|
||
|
|
for item in ratios:
|
||
|
|
self.assertGreaterEqual(item['ratio'],4.5,item['text'])
|
||
|
|
|
||
|
|
def test_os_07_loading_and_language(self):
|
||
|
|
self.tool('browser_resize',{'width':1600,'height':1000})
|
||
|
|
self.tool('browser_navigate',{'url':self.origin+self.base+'/workstations/'+self.os['station']['id']})
|
||
|
|
self.ready(self.os['ubuntu']['values']['name'])
|
||
|
|
self.code("await page.evaluate(()=>document.documentElement.classList.remove('dark'));return true;")
|
||
|
|
path='**/api/software-installations/'+self.fixture['companyId']
|
||
|
|
result=self.code("await page.route("+json.dumps(path)+",route=>route.fulfill({status:503,json:{code:'INTERNAL_ERROR'}}));try{await page.reload();await page.locator('[data-current-operating-systems]').getByRole('status',{name:'Cargando sistema operativo actual',exact:true}).waitFor();return (await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').screenshot()).toString('base64')}finally{await page.unroute("+json.dumps(path)+")}")
|
||
|
|
self.assertIsInstance(result,str,result)
|
||
|
|
data=base64.b64decode(result,validate=True)
|
||
|
|
screenshot=self.output/'workstation-loading.png'
|
||
|
|
screenshot.write_bytes(data)
|
||
|
|
self.record('workstation-loading',{'path':str(screenshot.relative_to(ROOT)),'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data)})
|
||
|
|
self.ready()
|
||
|
|
self.code("await page.getByRole('button',{name:'Configuración',exact:true}).click();await page.getByRole('dialog').getByRole('combobox',{name:'Idioma',exact:true}).click();await page.getByRole('option',{name:'English',exact:true}).click();await page.getByRole('heading',{name:'Settings',exact:true}).waitFor();await page.keyboard.press('Escape');return true;")
|
||
|
|
self.code("await page.getByRole('heading',{name:'Current operating systems',exact:true}).waitFor();return true;")
|
||
|
|
self.capture('workstation-english')
|
||
|
|
self.record('OSUI-16-browser',{'language':'en','loadingDistinctFromEmpty':True})
|
||
|
|
|
||
|
|
|
||
|
|
if __name__=='__main__':
|
||
|
|
names=sys.argv[1:] or [name for name in dir(CurrentOperatingSystemsTests) if name.startswith('test_os_')]
|
||
|
|
suite=unittest.TestSuite(CurrentOperatingSystemsTests(name) for name in names)
|
||
|
|
result=unittest.TextTestRunner(verbosity=2).run(suite)
|
||
|
|
raise SystemExit(not result.wasSuccessful())
|