115 lines
9.7 KiB
Python
115 lines
9.7 KiB
Python
|
|
"""Eliminación de cuentas: HTTP y navegador MCP con volumen sintético desechable."""
|
||
|
|
import json
|
||
|
|
import runpy
|
||
|
|
|
||
|
|
|
||
|
|
def verify(context, fixture, admin_password):
|
||
|
|
api = context['api']
|
||
|
|
company = fixture['company']
|
||
|
|
base = '/access/' + company
|
||
|
|
api.request('/auth/login', {'email': fixture['email'], 'password': fixture['password']})
|
||
|
|
checks, screenshots = [], []
|
||
|
|
|
||
|
|
def create(values):
|
||
|
|
response = api.request(base, {'values': values}, expected=201)
|
||
|
|
return next(item for item in reversed(response['items']) if item['values'] == values)
|
||
|
|
|
||
|
|
system = create({'kind': 'system', 'name': 'Directorio sintético', 'active': True})
|
||
|
|
account_values = {'kind': 'account', 'systemId': system['id'], 'environmentId': None,
|
||
|
|
'login': '', 'usage': 'personal', 'active': True, 'expiresOn': None, 'notes': None}
|
||
|
|
accounts = [create({**account_values, 'login': f'AUDIT-ACCOUNT-{index:02} — Álvarez'}) for index in range(60)]
|
||
|
|
person = api.request('/companies/' + company)['company']['people'][0]
|
||
|
|
linked = accounts[0]
|
||
|
|
create({'kind': 'account_person', 'accountId': linked['id'], 'personId': person['id'],
|
||
|
|
'startDate': '2026-01-01', 'endDate': None, 'configuration': 'configured'})
|
||
|
|
historic = accounts[1]
|
||
|
|
group = create({'kind': 'group', 'systemId': system['id'], 'environmentId': None,
|
||
|
|
'name': 'Grupo sintético', 'identifier': 'GRP_AUDIT', 'active': True,
|
||
|
|
'description': None, 'templateId': None})
|
||
|
|
create({'kind': 'membership', 'accountId': historic['id'], 'groupId': group['id'],
|
||
|
|
'startDate': '2020-01-01', 'endDate': '2020-01-02', 'configuration': 'removed'})
|
||
|
|
for account in [linked, historic]:
|
||
|
|
api.request(base + '/' + account['id'] + '/delete', {}, expected=409)
|
||
|
|
disposable = accounts[-1]
|
||
|
|
path = base + '/' + disposable['id'] + '/delete'
|
||
|
|
api.request('/access/' + fixture['otherCompanies'][0] + '/' + disposable['id'] + '/delete', {}, expected=404)
|
||
|
|
api.request(base + '/' + system['id'] + '/delete', {}, expected=404)
|
||
|
|
api.request(path, {'unexpected': True}, expected=400)
|
||
|
|
api.request(path + '?unexpected=true', {}, expected=400)
|
||
|
|
api.request('/access/global/' + disposable['id'] + '/delete', {}, expected=400)
|
||
|
|
context['TestApi'](api.base, context['origin']).request(path, {}, expected=401)
|
||
|
|
checks.extend(['current-person-blocks', 'closed-membership-blocks', 'company-isolation',
|
||
|
|
'only-accounts', 'strict-body-query-scope', 'unauthenticated-denied'])
|
||
|
|
inactive = create({**account_values, 'login': 'AUDIT-INACTIVE', 'active': False})
|
||
|
|
changed = {**inactive['values'], 'login': 'AUDIT-INACTIVE-RENAMED'}
|
||
|
|
api.request(base + '/' + inactive['id'] + '/update', {'values': changed})
|
||
|
|
api.request(base + '/' + inactive['id'] + '/delete', {})
|
||
|
|
api.request(base + '/' + inactive['id'] + '/delete', {}, expected=404)
|
||
|
|
create(changed)
|
||
|
|
checks.extend(['inactive-delete', 'history-delete', 'repeated-delete-not-found', 'login-reusable'])
|
||
|
|
counts = {
|
||
|
|
'companies': len(api.request('/companies')['companies']),
|
||
|
|
'people': len(api.request('/companies/' + company)['company']['people']),
|
||
|
|
'assets': len(api.request('/assets/' + company)['assets']),
|
||
|
|
'resources': len(api.request('/resources/' + company)['resources']),
|
||
|
|
'profiles': len(api.request('/work-profiles/' + company)['profiles']),
|
||
|
|
'workstations': len(fixture['softwareWorkstations']),
|
||
|
|
'accountsCreated': 62,
|
||
|
|
}
|
||
|
|
api.request('/auth/logout', {})
|
||
|
|
tool, capture = context['tool'], context['capture']
|
||
|
|
|
||
|
|
def code(client, source):
|
||
|
|
result = context['result_json'](tool(client, 'browser_run_code_unsafe', {
|
||
|
|
'code': 'async (page) => { page.setDefaultTimeout(15000); try { ' + source +
|
||
|
|
' return true; } catch(error) { return {failure:String(error.message)}; } }'}))
|
||
|
|
if isinstance(result, dict) and 'failure' in result:
|
||
|
|
raise RuntimeError(result['failure'])
|
||
|
|
return result
|
||
|
|
|
||
|
|
with context['browser'](context['config']) as client:
|
||
|
|
try:
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 900})
|
||
|
|
tool(client, 'browser_navigate', {'url': context['origin'] + '/login'})
|
||
|
|
code(client, "await page.getByRole('button',{name:'Entrar',exact:true}).waitFor();")
|
||
|
|
tool(client, 'browser_fill_form', {'fields': [
|
||
|
|
{'name': 'Correo', 'type': 'textbox', 'target': 'input[type=email]', 'value': fixture['email']},
|
||
|
|
{'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': fixture['password']}]})
|
||
|
|
tool(client, 'browser_click', {'target': 'button[type=submit]'})
|
||
|
|
code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});")
|
||
|
|
tool(client, 'browser_navigate', {'url': context['origin'] + '/companies/' + company + '/access'})
|
||
|
|
code(client, "await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-ACCOUNT-59'); await page.getByRole('button',{name:'Acciones',exact:true}).waitFor();")
|
||
|
|
runpy.run_path(str(context['ROOT'] / 'scripts/account-usage-ui-checks.py'))['verify'](context, fixture, accounts, client, code)
|
||
|
|
code(client, "await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-ACCOUNT-59');")
|
||
|
|
# Apertura y cancelación por teclado: no producen escrituras.
|
||
|
|
code(client, "await page.getByRole('button',{name:'Acciones',exact:true}).focus(); await page.keyboard.press('Enter'); await page.getByRole('menuitem',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('dialog',{name:'Eliminar cuenta',exact:true}).waitFor();")
|
||
|
|
for width in [1440, 390, 320]:
|
||
|
|
tool(client, 'browser_resize', {'width': width, 'height': 844})
|
||
|
|
code(client, "const d=page.getByRole('dialog',{name:'Eliminar cuenta',exact:true}); if(await d.evaluate(e=>e.scrollWidth>e.clientWidth+1))throw Error('Desbordamiento horizontal'); await d.getByRole('button',{name:'Eliminar cuenta',exact:true}).scrollIntoViewIfNeeded();")
|
||
|
|
screenshots.append(capture(client, context['directory'], 'account-delete-' + str(width)))
|
||
|
|
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'}); if(!await page.getByRole('button',{name:'Acciones',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco no restaurado');")
|
||
|
|
checks.extend(['keyboard-open-escape', 'focus-return', 'responsive-three-widths'])
|
||
|
|
tool(client, 'browser_resize', {'width': 1440, 'height': 900})
|
||
|
|
code(client, "await page.getByRole('button',{name:'Acciones',exact:true}).click(); await page.getByRole('menuitem',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('dialog').getByRole('button',{name:'Cancelar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'});")
|
||
|
|
# Error de red: conservar el diálogo y permitir reintento.
|
||
|
|
pattern = '**/api' + path
|
||
|
|
code(client, 'await page.route(' + json.dumps(pattern) + ",route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})})); await page.getByRole('button',{name:'Acciones',exact:true}).click(); await page.getByRole('menuitem',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('dialog').getByRole('button',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('alert').filter({hasText:'No se ha podido completar'}).waitFor();")
|
||
|
|
screenshots.append(capture(client, context['directory'], 'account-delete-network-error'))
|
||
|
|
code(client, 'await page.unroute(' + json.dumps(pattern) + "); await page.getByRole('dialog').getByRole('button',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'}); await page.getByRole('row').filter({hasText:'AUDIT-ACCOUNT-59'}).waitFor({state:'hidden'});")
|
||
|
|
checks.extend(['cancel-no-delete', 'network-error-retained', 'retry-deletes-refreshes'])
|
||
|
|
# Cuenta con titular: error recuperable y sin borrado.
|
||
|
|
code(client, "await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-ACCOUNT-00'); await page.getByRole('button',{name:'Acciones',exact:true}).click(); await page.getByRole('menuitem',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('dialog').getByRole('button',{name:'Eliminar cuenta',exact:true}).click(); await page.getByRole('alert').filter({hasText:'La cuenta tiene relaciones registradas'}).waitFor();")
|
||
|
|
screenshots.append(capture(client, context['directory'], 'account-delete-in-use'))
|
||
|
|
code(client, "await page.getByRole('dialog').getByRole('button',{name:'Cancelar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'});")
|
||
|
|
verified = code(client, "const a=await(await page.request.get(" + json.dumps(context['origin'] + '/api' + base) + ")).json(); return {deleted:!a.items.some(i=>i.id===" + json.dumps(disposable['id']) + "),linkedPreserved:a.items.some(i=>i.id===" + json.dumps(linked['id']) + ")};")
|
||
|
|
if not verified['deleted'] or not verified['linkedPreserved']:
|
||
|
|
raise RuntimeError('Persistencia inesperada')
|
||
|
|
checks.append('in-use-error-preserves-account')
|
||
|
|
except Exception:
|
||
|
|
capture(client, context['directory'], 'account-delete-failure')
|
||
|
|
raise
|
||
|
|
report = {'checks': checks, 'screenshots': screenshots, 'volume': counts,
|
||
|
|
'database': 'metis_desk_test'}
|
||
|
|
(context['directory'] / 'account-deletion-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
|
||
|
|
print('Eliminación de cuentas: ' + str(len(checks)) + ' comprobaciones. Evidencias: ' + str(context['directory']), flush=True)
|