"""Read-only projections and mobile shell; no Ansible, keys or remote hosts.""" from dataclasses import replace from pathlib import Path import json import time import uuid import pytest from fastapi.testclient import TestClient from aim_webgui.activity import Activity, HistoryFilter, host_url from aim_webgui.explorer import Explorer from aim_webgui.auth.service import Auth from aim_webgui.config import Settings from aim_webgui.app import create_app from aim_webgui.errors import WebError from aim_webgui.workflows import Workflows COUNTS=('ok','changed','failures','unreachable','skipped','rescued','ignored') @pytest.fixture def local_auth(tmp_path): s=Settings(state_dir=tmp_path/'state',public_url='https://aim.example.test').validate() a=Auth(s);a.bootstrap() a.create_user('operator','Fixture-only-long-password-2026','viewer') a.create_user('other','Fixture-only-other-password-2026','viewer') with a.store.transaction() as db: db.execute('UPDATE users SET must_change_password=0') return a def uid(auth,name): with auth.store.read() as db:return db.execute('SELECT id FROM users WHERE username=?',(name,)).fetchone()[0] def add_job(auth,owner='operator',customer='example',book='debug_test_connection',hosts=None,status='successful',mode='apply',when=None,legacy=False): hosts=hosts or {'test01.example':'successful'} when=int(time.time())-60 if when is None else when target_list=[];summ=dict.fromkeys(('successful','failed','unreachable','not_started','indeterminate'),0) for host,outcome in hosts.items(): counts=dict.fromkeys(COUNTS,0);counts['ok']=4 if outcome=='unreachable':counts['unreachable']=1 if outcome=='failed':counts['failures']=1 if outcome=='successful':counts['changed']=1 summ[outcome]+=1 target_list.append({'host':host,'outcome':outcome,'counts':counts}) ident=uuid.uuid4().hex result={'status':'failed' if status=='failed' else 'succeeded','targets':target_list, 'target_summary':{'schema':'target_outcome_summary_v1','requested':len(hosts),'accounted':len(hosts),'complete':True,**summ}} plan={'customer':customer,'playbook':book,'targets':list(hosts),'overrides':{'ignored_fixture_field':'MUST-NOT-LEAVE-PROJECTION'}} with auth.store.transaction() as db: db.execute('''INSERT INTO jobs(id,owner_id,plan,mode,status,created_at,scheduled_at,finished_at,core_result) VALUES(?,?,?,?,?,?,?,?,?)''',(ident,uid(auth,owner),json.dumps(plan),mode,status,when,when, when if status not in ('queued','running','pending') else None,None if legacy else json.dumps(result))) return ident def test_mixed_target_semantics_and_no_task_count_inference(local_auth): a=local_auth;ident=add_job(a,hosts={'test01.example':'successful','test02.example':'unreachable'},status='failed') data=Activity(a.settings).report(uid(a,'operator'),HistoryFilter(customer='example',days='all')) assert data['jobs']==1 and data['samples']==2 assert data['counts']['successful']==1 and data['counts']['unreachable']==1 assert data['success_percent']==50 and data['eligible']==2 assert data['records'][0]['job_display_status']=='partially_succeeded' assert 'MUST-NOT-LEAVE' not in repr(data) def test_all_history_not_last_100_and_pagination(local_auth): a=local_auth for i in range(135):add_job(a,when=int(time.time())-1000-i) data=Activity(a.settings).report(uid(a,'operator'),HistoryFilter(host='test01.example',days='all',page=5)) assert data['jobs']==135 and data['samples']==135 and data['pages']==6 assert len(data['records'])==25 def test_visibility_for_totals_filters_and_matrix(local_auth): a=local_auth add_job(a);add_job(a,owner='other',customer='secret-customer',book='secret-book',hosts={'secret-host.example':'failed'},status='failed') data=Activity(a.settings).report(uid(a,'operator'),HistoryFilter(days='all')) assert data['samples']==1 and 'secret-' not in repr(data) admin=Activity(a.settings).report(uid(a,'admin'),HistoryFilter(days='all')) assert admin['samples']==2 def test_mode_range_and_unknown_outcomes(local_auth): a=local_auth add_job(a,mode='check');add_job(a,legacy=True);add_job(a,status='queued') add_job(a,hosts={'test01.example':'not_started'});add_job(a,hosts={'test01.example':'indeterminate'}) add_job(a,when=int(time.time())-86400*100) data=Activity(a.settings).report(uid(a,'operator'),HistoryFilter(days='30')) assert data['samples']==4 and data['eligible']==0 and data['success_percent'] is None assert data['counts']['unavailable']==1 and data['counts']['outstanding']==1 check=Activity(a.settings).report(uid(a,'operator'),HistoryFilter(mode='check',days='all')) assert check['samples']==1 and check['counts']['successful']==1 def test_customer_identity_deletion_and_bad_facts(local_auth): a=local_auth ident=add_job(a);add_job(a,customer='second') s=Activity(a.settings) assert s.report(uid(a,'operator'),HistoryFilter(customer='example',days='all'))['samples']==1 Workflows(a.settings).delete_jobs(uid(a,'operator'), [ident]) assert s.report(uid(a,'operator'),HistoryFilter(customer='example',days='all'))['samples']==0 ident=add_job(a) with a.store.transaction() as db:db.execute('UPDATE jobs SET core_result=? WHERE id=?',(json.dumps({'targets':[],'target_summary':{}}),ident)) assert s.report(uid(a,'operator'),HistoryFilter(customer='example',days='all'))['counts']['unavailable']==1 def test_plan_visibility_is_owner_only(local_auth): a=local_auth with a.store.transaction() as db: for name in ('admin','operator','other'): db.execute('INSERT INTO plans(id,owner_id,name,customer,playbook,payload,created_at) VALUES(?,?,?,?,?,?,?)', (uuid.uuid4().hex,uid(a,name),name+' plan','example','debug_test_connection',json.dumps({'targets':['test01.example']}),int(time.time()))) data=Activity(a.settings).report(uid(a,'admin'),HistoryFilter(customer='example',host='test01.example',days='all')) assert [p['name'] for p in data['plans']]==['admin plan'] class FakeCore: operations=[] def __init__(self,*args,**kwargs):self.client=self def request(self,operation,**kwargs): self.operations.append(operation) assert operation=='list_hosts' return [{'name':h,'address':'192.0.2.'+str(i+1),'platforms':['linux']} for i,h in enumerate(('direct.example','shared.example','leaf.example'))] def inventory_hierarchy(self,customer): self.operations.append('inventory_hierarchy') def node(name,path,hosts,children=None):return dict(name=name,path=path,hosts=hosts,children=children or []) return {'schema':'inventory_hierarchy_v1','customer':customer,'hosts':['direct.example'],'groups':[ node('linux',['linux'],['shared.example'],[node('servers',['linux','servers'],['shared.example','leaf.example'])]), node('lab',['lab'],[],[node('servers',['lab','servers'],['shared.example'])]),node('empty',['empty'],[])]} def test_explorer_distinct_counts_paths_root_members_and_search(local_auth,monkeypatch): monkeypatch.setattr('aim_webgui.explorer.CoreAdapter',FakeCore) e=Explorer(local_auth.settings) p=e.page('example') assert p['snap']['host_count']==3 and len(p['direct_hosts'])==1 assert len(p['snap']['groups'][('linux',)]['members'])==2 assert len(p['snap']['hosts']['shared.example']['memberships'])==3 p=e.page('example',branch='["lab","servers"]') assert p['selected']['path']==['lab','servers'] and len(p['direct_hosts'])==1 assert 'activity?host=' in p['direct_hosts'][0]['url'] assert e.page('example',q='shared')['search_count']==1 with pytest.raises(WebError):e.page('example',branch='["missing"]') def test_explorer_map_is_bounded(local_auth,monkeypatch): class Large(FakeCore): def request(self,op,**kwargs):return [] def inventory_hierarchy(self,customer):return {'schema':'inventory_hierarchy_v1','customer':customer,'hosts':[], 'groups':[{'name':str(n),'path':[str(n)],'hosts':[],'children':[]}for n in range(50)]} monkeypatch.setattr('aim_webgui.explorer.CoreAdapter',Large) page=Explorer(local_auth.settings).page('example') assert len(page['groups'])==12 and page['group_pages']==5 and len(page['graph_nodes'])<=49 assert page['groups_next'] def test_new_get_views_are_read_only_and_survive_core_outage(local_auth,monkeypatch): a=local_auth;add_job(a);FakeCore.operations=[] monkeypatch.setattr('aim_webgui.explorer.CoreAdapter',FakeCore) token,_=a.new_session(uid(a,'operator')) with TestClient(create_app(a.settings),base_url=a.settings.public_url) as c: c.cookies.set(a.settings.cookie_name,token) for path in ('/inventory/example/explore','/inventory/example/explore?view=map', '/inventory/example/explore?q=shared',host_url('example','test01.example'), '/insights','/api/v2/insights','/api/v2/activity?customer=example&host=test01.example', '/api/v2/inventory/example/explore'): r=c.get(path); assert r.status_code==200,(path,r.text[:2000]) assert r.headers['cache-control']=='no-store' assert set(FakeCore.operations)=={'inventory_hierarchy','list_hosts'} before=Activity(a.settings).report(uid(a,'operator'),HistoryFilter(days='all'))['jobs'] assert before==1 def down(*a,**k):raise WebError('core_offline','Do not echo sensitive path',503) monkeypatch.setattr(FakeCore,'inventory_hierarchy',down) r=c.get(host_url('example','test01.example')) assert r.status_code==200 and 'Current inventory is unavailable' in r.text assert 'Do not echo sensitive path' not in r.text assert c.get('/inventory/example/explore').status_code==503 def test_filters_bad_input_and_anonymous_access(local_auth): a=local_auth for f in (HistoryFilter(mode='invalid'),HistoryFilter(page=0),HistoryFilter(days='-1'),HistoryFilter(outcome='fake'),HistoryFilter(q='x'*256)): with pytest.raises(WebError):Activity(a.settings).report(uid(a,'operator'),f) with TestClient(create_app(a.settings),base_url=a.settings.public_url,follow_redirects=False) as c: assert c.get('/api/v2/insights').status_code==401 assert c.get('/insights').status_code==303 def test_menu_markup_and_escaped_history(local_auth,monkeypatch): a=local_auth;add_job(a,book='') monkeypatch.setattr('aim_webgui.explorer.CoreAdapter',FakeCore) token,_=a.new_session(uid(a,'operator')) with TestClient(create_app(a.settings),base_url=a.settings.public_url) as c: c.cookies.set(a.settings.cookie_name,token) r=c.get('/insights?days=all') assert '' not in r.text assert '<script>' in r.text assert 'data-mobile-menu' in r.text and 'aria-controls="mobile-navigation"' in r.text assert 'data-nav-forward' not in r.text and 'Swipe or use arrows' not in r.text assert 'No terminal history collection' in r.text def test_public_core_explorer_with_execution_disabled(settings): # Real public Core discovery only; no Ansible or remote execution. s=replace(settings,execution_enabled=False,credentials_enabled=False) page=Explorer(s).page('example') assert page['snap']['host_count']==2 assert ('linux','lab') in page['snap']['groups'] assert page['snap']['hosts']['test01.example']['memberships'][0]['label']=='linux / lab' assert 'activity?host=test01.example' in page['snap']['hosts']['test01.example']['url'] def test_real_core_read_pages_with_empty_history(settings): s=replace(settings,execution_enabled=False,credentials_enabled=False) a=Auth(s);a.bootstrap() with a.store.transaction() as db:db.execute('UPDATE users SET must_change_password=0') token,_=a.new_session(uid(a,'admin')) with TestClient(create_app(s),base_url=s.public_url) as c: c.cookies.set(s.cookie_name,token) for path in ('/inventory/example/explore',host_url('example','test01.example'),'/insights'): r=c.get(path) assert r.status_code==200,(path,r.text[:200]) r=c.get(host_url('example','old-host.example')) assert 'Not in current inventory.' in r.text assert 'No retained runs match' in r.text with a.store.read() as db: assert db.execute('SELECT COUNT(*) FROM jobs').fetchone()[0]==0 assert db.execute('SELECT COUNT(*) FROM run_reviews').fetchone()[0]==0