113 lines
5.0 KiB
Python
113 lines
5.0 KiB
Python
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from app.ota.diagnostics import DiagnosticLog, MAX_FAILURE_LOG_BYTES
|
|
from scripts import ota_components as component
|
|
|
|
|
|
def test_directory_loss_restores_recent_diagnostics(tmp_path):
|
|
path = tmp_path / 'run/log'
|
|
log = DiagnosticLog(path)
|
|
log.reset();log.write('before stop')
|
|
shutil.rmtree(path.parent)
|
|
log.write('after stop')
|
|
assert 'before stop' in path.read_text(encoding='utf-8')
|
|
assert 'after stop' in path.read_text(encoding='utf-8')
|
|
|
|
|
|
def test_unwritable_log_does_not_mask_command_failure_or_prevent_persistence(tmp_path, monkeypatch, capsys):
|
|
path = tmp_path / 'run/log'
|
|
log = DiagnosticLog(path)
|
|
log.reset()
|
|
original = Path.open
|
|
def broken(self, *args, **kwargs):
|
|
if self == path:
|
|
raise PermissionError('do not print confidential OS details')
|
|
return original(self, *args, **kwargs)
|
|
monkeypatch.setattr(Path, 'open', broken)
|
|
monkeypatch.setattr(subprocess, 'run', lambda *a, **k: subprocess.CompletedProcess(a, 9, b'original failure'))
|
|
with pytest.raises(RuntimeError, match='original failure'):
|
|
log.run(['example'])
|
|
log.write('rollback executed')
|
|
target = log.persist(tmp_path/'data')
|
|
assert 'rollback executed' in target.read_text(encoding='utf-8')
|
|
assert 'confidential' not in capsys.readouterr().err
|
|
|
|
|
|
def test_fallback_buffer_is_bounded(tmp_path):
|
|
log = DiagnosticLog(tmp_path/'log')
|
|
log.write('x' * (MAX_FAILURE_LOG_BYTES * 2))
|
|
assert len(log._recent) <= MAX_FAILURE_LOG_BYTES
|
|
|
|
|
|
def test_runtime_guard_verified_and_does_not_overwrite_user_configuration(tmp_path, monkeypatch):
|
|
path = tmp_path/'dropin/guard.conf'
|
|
monkeypatch.setattr(component, 'RUNTIME_GUARD', path)
|
|
monkeypatch.setattr(component, 'sync_directory', lambda p: None)
|
|
commands=[]
|
|
def run(args, **kwargs):
|
|
commands.append(args)
|
|
return subprocess.CompletedProcess(args, 0, b'yes\n')
|
|
monkeypatch.setattr(component, 'run', run)
|
|
component.protect_runtime()
|
|
assert path.read_bytes() == component.RUNTIME_GUARD_BODY
|
|
assert commands[-1][1] == 'show'
|
|
component.release_runtime_guard()
|
|
assert not path.exists()
|
|
path.parent.mkdir();path.write_bytes(b'user setting')
|
|
with pytest.raises(RuntimeError, match='overwrite'):
|
|
component.protect_runtime()
|
|
with pytest.raises(RuntimeError, match='modified'):
|
|
component.release_runtime_guard()
|
|
assert path.read_bytes() == b'user setting'
|
|
|
|
|
|
def test_ineffective_guard_refuses_to_proceed(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(component, 'RUNTIME_GUARD', tmp_path/'guard')
|
|
monkeypatch.setattr(component, 'sync_directory', lambda p: None)
|
|
monkeypatch.setattr(component, 'run', lambda a, **k: subprocess.CompletedProcess(a,0,b'restart\n'))
|
|
with pytest.raises(RuntimeError, match='refusing to stop'):
|
|
component.protect_runtime()
|
|
|
|
|
|
def test_component_validation_accepts_current_transaction_runtime_guard(tmp_path, monkeypatch):
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
from types import SimpleNamespace
|
|
source = tmp_path/'source'
|
|
(source/'systemd').mkdir(parents=True)
|
|
(source/'systemd/matrix-screen-frpc.service').write_bytes(b'unit')
|
|
(source/'UPGRADE_POLICY.json').write_text(json.dumps({'checkpoints':[
|
|
{'version':'1.1.0','components':{'frpc':{'version':'0.71.0',
|
|
'sha256':hashlib.sha256(b'binary').hexdigest()}}}]}),encoding='utf-8')
|
|
files={name:tmp_path/name for name in ('frpc','frpc-unit','frpc-dropin')}
|
|
for name,body in [('frpc',b'binary'),('frpc-unit',b'unit'),
|
|
('frpc-dropin',b'[Service]\nUser=maintainer\nGroup=maintainer\n')]:
|
|
files[name].write_bytes(body)
|
|
data=tmp_path/'data';(data/'frp/profiles').mkdir(parents=True)
|
|
guard=tmp_path/'guard';guard.write_bytes(component.RUNTIME_GUARD_BODY)
|
|
monkeypatch.setattr(component,'FILES',files)
|
|
monkeypatch.setattr(component,'DATA',data)
|
|
monkeypatch.setattr(component,'RUNTIME_GUARD',guard)
|
|
monkeypatch.setattr(component,'account',lambda:'maintainer')
|
|
monkeypatch.setitem(sys.modules,'pwd',SimpleNamespace(getpwnam=lambda u:SimpleNamespace(pw_gid=1000)))
|
|
monkeypatch.setitem(sys.modules,'grp',SimpleNamespace(getgrgid=lambda g:SimpleNamespace(gr_name='maintainer')))
|
|
monkeypatch.setattr(component,'run',lambda a,**k:subprocess.CompletedProcess(a,0,b'0.71.0\n'))
|
|
monkeypatch.setattr(component,'metadata',lambda p:{'mode':0o755 if p==files['frpc'] else 0o2750 if p.is_relative_to(data) else 0o644,
|
|
'uid':0,'gid':1000 if p.is_relative_to(data) else 0})
|
|
original=Path.stat
|
|
def normalized_stat(self,*a,**k):
|
|
value=original(self,*a,**k)
|
|
fields=list(value)
|
|
if self==data:fields[0]=(fields[0]&~0o7777)|0o711
|
|
if self==files['frpc']:fields[4]=fields[5]=0
|
|
return os.stat_result(fields)
|
|
monkeypatch.setattr(Path,'stat',normalized_stat)
|
|
component.check_installed(source,'1.1.0')
|
|
assert guard.read_bytes()==component.RUNTIME_GUARD_BODY
|