"""Verified Decky-store-shaped install flow through the real HTTP server and fake Frame SSH.""" import hashlib from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import io import json import os import shlex import subprocess import threading import zipfile from harness import FrameTestCase, ROOT, Server, api, finished, ok, require, ssh require() # Skip the module outside the disposable fake Frame, like its peers. def archive(version): raw = io.BytesIO() with zipfile.ZipFile(raw, 'w') as bundle: bundle.writestr('sample-panel/plugin.json', json.dumps({'name':'Sample Panel','api_version':2,'flags':[]})) bundle.writestr('sample-panel/dist/index.js', f"export default () => ({{content: SP_REACT.createElement('p', null, 'Sample {version}')}});") bundle.writestr('sample-panel/README.txt', ''.join(hashlib.sha256(f'{version}-{n}'.encode()).hexdigest() for n in range(35))) return raw.getvalue() ARTIFACTS = {hashlib.sha256(a).hexdigest(): a for a in (archive('1.0.0'), archive('1.1.0'))} HASHES = list(ARTIFACTS) class StoreFixture(BaseHTTPRequestHandler): release = 0 def log_message(self, *_): pass def do_GET(self): if self.path == '/plugins': body = json.dumps([{'id':123,'name':'Sample Panel','author':'Fixture', 'description':'A harmless panel','tags':[], 'versions':[{'name':['1.0.0','1.1.0'][self.release], 'hash':HASHES[self.release]}]}]).encode() content_type = 'application/json' elif self.path.startswith('/versions/') and self.path.endswith('.zip'): body = ARTIFACTS.get(self.path.split('/')[-1][:-4]) content_type = 'application/zip' else: body = None self.send_response(200 if body is not None else 404) self.send_header('Content-Type', content_type if body is not None else 'text/plain') self.end_headers() self.wfile.write(body or b'not found') class FrameyFlow(FrameTestCase): @classmethod def setUpClass(cls): require() StoreFixture.release = 0 cls.fixture = ThreadingHTTPServer(('127.0.0.1', 0), StoreFixture) cls.thread = threading.Thread(target=cls.fixture.serve_forever, daemon=True) cls.thread.start() port = cls.fixture.server_port cls.previous = {key:os.environ.get(key) for key in ('FRAMEY_STORE_URL','FRAMEY_CDN_URL')} os.environ['FRAMEY_STORE_URL'] = f'http://127.0.0.1:{port}/plugins' os.environ['FRAMEY_CDN_URL'] = f'http://127.0.0.1:{port}/versions/' Server.stop() super().setUpClass() @classmethod def tearDownClass(cls): Server.stop() cls.fixture.shutdown() for key, value in cls.previous.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value def backend(self): code = '''import json,pathlib h=pathlib.Path.home(); b=h/'.local/share/frame-control/decky/.frame-control'; b.mkdir(parents=True,exist_ok=True) (b/'receipt.json').write_text(json.dumps({'managedBy':'Frame Control','version':'v3.3.0'})) (b.parent/'plugins').mkdir(exist_ok=True) u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=True,exist_ok=True); u.write_text('[Service]')''' ssh('python3 -c ' + shlex.quote(code)) ssh('mkdir -p ~/.local/share/frame-control/headset/bundle/ui') source = (ROOT/'ui/frame_steam.py').read_bytes() copy = subprocess.run(['ssh','frame','cat > ~/.local/share/frame-control/headset/bundle/ui/frame_steam.py'], input=source, capture_output=True, timeout=20) self.assertEqual(copy.returncode, 0, copy.stderr) def test_store_install_update_remove_and_guards(self): self.backend() status, _, _ = api('GET', '/api/decky/store', headers={'X-Frame-UI':''}) self.assertEqual(status, 403) catalogue = ok('GET', '/api/decky/store')['plugins'] self.assertEqual((catalogue[0]['version'], catalogue[0]['sha256']), ('1.0.0', HASHES[0])) status, _, _ = api('POST', '/api/decky/plugin', {'action':'install','id':123}) self.assertEqual(status, 400) # Third-party code needs explicit acknowledgement. job = finished(ok('POST', '/api/decky/plugin', {'action':'install','id':123,'acknowledged':True})) self.assertFalse(job.get('error'), job) self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.0.0') self.assertIn('Sample 1.0.0', ssh('cat ~/.local/share/frame-control/decky/plugins/sample-panel/dist/index.js')) status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':[]}, headers={'X-Frame-UI':''}) self.assertEqual(status, 403) status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'x;alert(1)','args':[]}) self.assertEqual(status, 400) self.assertEqual(ok('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'dark') self.assertTrue(ok('POST', '/api/framey/method', {'id':123,'method':'set_setting','args':['theme','light']})['result']) self.assertEqual(ok('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'light') StoreFixture.release = 1 job = finished(ok('POST', '/api/decky/plugin', {'action':'update','id':123,'acknowledged':True})) self.assertFalse(job.get('error'), job) self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.1.0') self.assertEqual(ok('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'light') job = finished(ok('POST', '/api/decky/plugin', {'action':'remove','id':123,'acknowledged':True})) self.assertFalse(job.get('error'), job) self.assertEqual(ok('GET', '/api/decky')['plugins'], []) self.assertEqual(ssh('test -e ~/.local/share/frame-control/decky/plugins/sample-panel && echo yes || echo no').strip(), 'no') status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':['theme','dark']}) self.assertEqual(status, 400) # No method calls while the plugin is removed. job = finished(ok('POST', '/api/decky/plugin', {'action':'install','id':123,'acknowledged':True})) self.assertFalse(job.get('error'), job) self.assertEqual(ok('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'light')