mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 17:01:38 +01:00
134 lines
6.9 KiB
Python
134 lines
6.9 KiB
Python
"""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')
|