85 lines
4.4 KiB
Python
85 lines
4.4 KiB
Python
"""Protocol robustness tests against a synthetic command, never a real remote host."""
|
|
from dataclasses import replace
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import threading
|
|
import time
|
|
import pytest
|
|
from aim_webgui.core.client import CoreClient
|
|
from aim_webgui.config import Settings
|
|
from aim_webgui.core.protocol import validate_secrets, response_result, COUNTS
|
|
from aim_webgui.errors import WebError
|
|
|
|
@pytest.fixture
|
|
def transport(tmp_path):
|
|
script=tmp_path/'fake.py'
|
|
settings=Settings(core_transport='stdio',core_command=(sys.executable,str(script)),core_config=tmp_path/'core.yml')
|
|
return script,settings
|
|
|
|
|
|
def script_write(script,body):
|
|
script.write_text('import sys,json,os,time,stat\n'+body)
|
|
|
|
|
|
def test_credentials_go_only_to_nonstandard_pipe_fd(transport):
|
|
script,settings=transport
|
|
script_write(script,"""
|
|
line=sys.stdin.buffer.readline(); request=json.loads(line)
|
|
assert 'credentials' not in request
|
|
fd=int(sys.argv[sys.argv.index('--credentials-fd')+1]);assert fd>=3 and stat.S_ISFIFO(os.fstat(fd).st_mode)
|
|
with os.fdopen(fd,'rb')as stream:secret=json.load(stream)
|
|
assert secret['vault_password']=='fixture+%!{{data}}'
|
|
assert secret['vault_password'].encode() not in line
|
|
assert all(secret['vault_password']not in x for x in os.environ.values())
|
|
assert all(secret['vault_password']not in x for x in sys.argv)
|
|
print(json.dumps({'type':'response','api_version':'1.0','ok':False,'result':{
|
|
'status':'failed','stage':'credentials','exit_code':None,'remote_work_may_have_started':False,
|
|
'error':{'code':'vault_unlock_failed'},'counts':{},
|
|
'target_summary':{'schema':'target_outcome_summary_v1','requested':1,'successful':0,'failed':0,'unreachable':0,'not_started':1,'indeterminate':0,'complete':True,'accounted':1},
|
|
'targets':[{'host':'h','outcome':'not_started','counts':{'ok':0,'changed':0,'failures':0,'unreachable':0,'skipped':0,'rescued':0,'ignored':0}}]}}),flush=True)
|
|
""")
|
|
request={'customer':'a','playbook':'p','hosts':['h']}
|
|
result=CoreClient(settings).request('execute',request=request,expected_revision='a'*64,credentials={'vault_password':'fixture+%!{{data}}'})
|
|
assert result['status']=='failed' and result['error']['code']=='vault_unlock_failed'
|
|
|
|
@pytest.mark.parametrize('body,code',[
|
|
("print('not-json',flush=True)",'core_protocol'),
|
|
("print(json.dumps({'type':'event','event':{'event_version':'1.0','kind':'stage','sequence':1,'stage':'execution'}}),flush=True)",'core_outcome_unknown'),
|
|
("print(json.dumps({'type':'response','api_version':'9','ok':True,'result':{}}),flush=True)",'core_protocol'),
|
|
("print('X'*1048577,flush=True)",'core_protocol'),
|
|
])
|
|
def test_invalid_wire_is_not_success(transport,body,code):
|
|
script,settings=transport;script_write(script,body)
|
|
with pytest.raises(WebError)as e:CoreClient(settings).request('capabilities')
|
|
assert e.value.code==code
|
|
|
|
|
|
def test_cancellation_stops_child(transport):
|
|
script,settings=transport;script_write(script,'time.sleep(60)')
|
|
cancel=threading.Event();timer=threading.Timer(.25,cancel.set);timer.start()
|
|
try:
|
|
with pytest.raises(WebError)as e:CoreClient(settings).request('capabilities',cancel=cancel)
|
|
assert e.value.code=='core_cancelled'
|
|
finally:timer.cancel()
|
|
|
|
|
|
def test_start_deadline_cancels_preparation(transport):
|
|
script,settings=transport;script_write(script,'time.sleep(60)')
|
|
with pytest.raises(WebError)as e:
|
|
CoreClient(settings).request('execute',request={'customer':'a','playbook':'p','hosts':['h']},
|
|
expected_revision='a'*64,deadline=time.monotonic()+.3)
|
|
assert e.value.code=='credential_expired'
|
|
|
|
@pytest.mark.parametrize('secret',[{'vault_password':'x\n'},{'username':'admin'},{'vault_password':'\ud800'},{'become_password':'x'}])
|
|
def test_invalid_secrets_rejected(secret):
|
|
with pytest.raises(WebError):validate_secrets(secret)
|
|
|
|
|
|
def test_success_needs_valid_complete_counters():
|
|
result={'type':'response','api_version':'1.0','ok':True,'result':{'status':'succeeded','exit_code':0,'remote_work_may_have_started':True,'counts':dict.fromkeys(COUNTS,0),'target_summary':{'schema':'target_outcome_summary_v1','requested':0,'successful':0,'failed':0,'unreachable':0,'not_started':0,'indeterminate':0,'complete':True,'accounted':0},'targets':[]}}
|
|
assert response_result(result,'execute')['status']=='succeeded'
|
|
result['result']['counts']['ok']=-1
|
|
with pytest.raises(WebError):response_result(result,'execute')
|