104 lines
4.9 KiB
Python
104 lines
4.9 KiB
Python
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import socket
|
|
import sys
|
|
import threading
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import uvicorn
|
|
|
|
from viano_bug_analysis.api import APIConfig,Client,Project,IntakeLimits
|
|
from viano_bug_analysis.api.app import create_app
|
|
|
|
ROOT=Path(__file__).resolve().parents[3]
|
|
spec=importlib.util.spec_from_file_location('buganalysis_handoff',ROOT/'deploy/capture-worker/buganalysis_handoff.py')
|
|
bridge=importlib.util.module_from_spec(spec);spec.loader.exec_module(bridge)
|
|
sys.modules['buganalysis_handoff']=bridge
|
|
|
|
|
|
def config(tmp_path,url='http://127.0.0.1:8080'):
|
|
return bridge.settings({'enabled':True,'url':url,'project_id':'core','producer_id':'test-capture','outbox':'outbox.sqlite3'},tmp_path)
|
|
|
|
|
|
def test_settings_reject_credential_leak_paths(tmp_path):
|
|
assert bridge.settings({},tmp_path) is None
|
|
for url in ('http://external.example','https://user:password@example.org','https://example.org/path','https://example.org?secret=x'):
|
|
with pytest.raises(bridge.HandoffError):config(tmp_path,url)
|
|
|
|
|
|
def test_outbox_retries_integrity_and_parallel_lease(tmp_path,source_pair):
|
|
now=[10.0];cfg=config(tmp_path);outbox=bridge.Outbox(cfg,clock=lambda:now[0])
|
|
diag,src,*_=source_pair
|
|
outbox.stage(1,20,diag,src,'revision');outbox.stage(1,20,diag,src,'revision')
|
|
assert len(outbox.status())==1
|
|
seen=[]
|
|
def failed(cfg,payload,key):seen.append(key);raise OSError('secret must not persist')
|
|
outbox.flush(send=failed)
|
|
assert outbox.status()[0]['status']=='pending'
|
|
assert 'secret' not in json.dumps(outbox.status())
|
|
other=bridge.Outbox(cfg,clock=lambda:now[0])
|
|
def successful(cfg,payload,key):
|
|
seen.append(key)
|
|
other.flush(force=True,send=lambda *_:pytest.fail('Concurrent lease ignored'))
|
|
return 'OK',{'submission_id':'id'}
|
|
now[0]+=61;outbox.flush(send=successful)
|
|
assert len(set(seen))==1 and outbox.status()[0]['status']=='sent'
|
|
outbox.stage(2,20,diag,src,'revision')
|
|
diag.write_bytes(b'changed')
|
|
outbox.flush(send=lambda *_:pytest.fail('Changed archives uploaded'))
|
|
assert outbox.status()[1]['code']=='HANDOFF_ARCHIVE_CHANGED'
|
|
|
|
|
|
def test_real_streaming_intake_and_lost_response_replay(tmp_path,source_pair,monkeypatch):
|
|
token='test_token_'+'x'*40;monkeypatch.setenv('VBA_INTAKE_TOKEN',token)
|
|
api=APIConfig(tmp_path/'service',(Project('core','wow','vcore'),),
|
|
(Client('capture','VBA_INTAKE_TOKEN',('core',)),),limits=IntakeLimits(min_free_bytes=1))
|
|
app=create_app(api)
|
|
sock=socket.socket();sock.bind(('127.0.0.1',0));sock.listen()
|
|
port=sock.getsockname()[1]
|
|
server=uvicorn.Server(uvicorn.Config(app,log_level='critical',access_log=False))
|
|
thread=threading.Thread(target=lambda:server.run(sockets=[sock]),daemon=True);thread.start()
|
|
try:
|
|
for _ in range(100):
|
|
if server.started:break
|
|
time.sleep(.02)
|
|
assert server.started
|
|
cfg=config(tmp_path,f'http://127.0.0.1:{port}')
|
|
outbox=bridge.Outbox(cfg)
|
|
outbox.stage(1,20,source_pair[0],source_pair[1],'verified-source-revision')
|
|
def lose_response(*args):
|
|
assert bridge.upload(*args)[0]=='OK'
|
|
raise OSError('Response lost after acceptance')
|
|
outbox.flush(send=lose_response)
|
|
assert outbox.status()[0]['status']=='pending'
|
|
bridge.Outbox(cfg).flush(force=True)
|
|
row=outbox.status()[0]
|
|
assert row['status']=='sent' and json.loads(row['receipt'])['case_id']=='BUG-000020'
|
|
assert len(list((tmp_path/'service/runtime/intake/requests').rglob('*.json')))==1
|
|
assert token.encode() not in outbox.path.read_bytes()
|
|
finally:
|
|
server.should_exit=True;thread.join(10);sock.close()
|
|
assert not thread.is_alive()
|
|
|
|
|
|
def test_worker_stages_before_capture_completion_and_can_disable(tmp_path,source_pair,monkeypatch):
|
|
worker_spec=importlib.util.spec_from_file_location('capture_worker_v04',ROOT/'deploy/capture-worker/wowdiag_worker.py')
|
|
worker=importlib.util.module_from_spec(worker_spec);sys.modules[worker_spec.name]=worker;worker_spec.loader.exec_module(worker)
|
|
closed=[];completed=[]
|
|
monkeypatch.setattr(worker,'connect',lambda cfg:SimpleNamespace(close=lambda:closed.append(True)))
|
|
monkeypatch.setattr(worker,'claim_one',lambda *_:worker.Job(1,20,'claim'))
|
|
monkeypatch.setattr(worker,'execute_pipeline',lambda *_:(source_pair[0],source_pair[1],'version','revision'))
|
|
cfg=SimpleNamespace(buganalysis=config(tmp_path))
|
|
def complete(*args):
|
|
if cfg.buganalysis:assert bridge.Outbox(cfg.buganalysis).status()[0]['status']=='pending'
|
|
completed.append(True)
|
|
monkeypatch.setattr(worker,'mark_completed',complete)
|
|
monkeypatch.setattr(worker,'mark_failed',lambda *_:pytest.fail('Successful capture marked failed'))
|
|
assert worker.process_one(cfg,None)
|
|
cfg.buganalysis=None
|
|
assert worker.process_one(cfg,None)
|
|
assert len(completed)==len(closed)==2
|