Files

172 lines
12 KiB
Python

import json
import threading
import unittest
import tempfile
import secrets
import time
import urllib.request
import urllib.error
from unittest.mock import patch
from server import Server, HardwareProvider
class Tests(unittest.TestCase):
def setUp(self):
self.state=tempfile.TemporaryDirectory()
self.server=Server(0,state_dir=self.state.name)
self.password=secrets.token_urlsafe(32)
self.api_token=secrets.token_urlsafe(32)
record=self.server.credentials.setup(self.password,self.api_token)
self.server.sessions['test-session']=dict(expires=time.monotonic()+300,revision=record['password']['hash'])
self.cookie='deck_session=test-session'
self.thread=threading.Thread(target=self.server.serve_forever)
self.thread.start()
self.url=f'http://127.0.0.1:{self.server.server_port}'
def tearDown(self):
self.server.shutdown()
self.server.dashboard.close()
self.server.endpoint.close()
self.server.server_close()
self.thread.join()
self.state.cleanup()
def request(self,path,method='GET',headers=None):
req=urllib.request.Request(self.url+'/api/v1/'+path,method=method,headers={"Cookie":self.cookie,**(headers or {})})
with urllib.request.urlopen(req) as r:return json.load(r)
def test_catalog_checks_runtime_once_without_video_model_scan(self):
entries=[dict(id=str(i),repo='test/model',file='model.gguf',kind='chat',size=8) for i in range(12)]
with patch.object(self.server.catalog,'status',return_value=dict(entries=entries)), \
patch.object(self.server.runtime,'status',return_value={'active':True}) as runtime, \
patch.object(self.server.video,'status',side_effect=AssertionError('No video model scan needed')):
result=self.request('catalog')
self.assertEqual(len(result['entries']),12)
self.assertEqual(runtime.call_count,1)
self.assertTrue(all('execution' in row and 'used_by_profiles' in row for row in result['entries']))
def test_container_actions_require_admin_and_delete_confirmation(self):
ident='a'*64
with patch.object(self.server.docker,'container_action',return_value={'changed':True}) as action:
for operation in ('start','stop','delete'):
data={'id':ident,**({'confirm':True} if operation=='delete' else {})}
req=urllib.request.Request(self.url+'/api/v1/docker/'+operation,data=json.dumps(data).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
with urllib.request.urlopen(req) as response:self.assertTrue(json.load(response)['changed'])
action.assert_called_with(operation,ident)
count=action.call_count
req=urllib.request.Request(self.url+'/api/v1/docker/delete',data=json.dumps({'id':ident}).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req)
self.assertEqual(error.exception.code,400);error.exception.close()
req=urllib.request.Request(self.url+'/api/v1/docker/start',data=json.dumps({'id':ident}).encode(),headers={'X-Athena-Deck':'1','Content-Type':'application/json'})
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req)
self.assertEqual(error.exception.code,401);error.exception.close()
self.assertEqual(action.call_count,count)
def test_separator_runtime_is_admin_only_and_rejects_unknown_package(self):
status=self.request('separator-runtime')
self.assertFalse(status['installed']);self.assertEqual(len(status['models']),2)
req=urllib.request.Request(self.url+'/api/v1/separator-runtime/download',data=b'{"model":"unknown"}',headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req)
self.assertEqual(error.exception.code,400);error.exception.close()
req=urllib.request.Request(self.url+'/api/v1/separator-runtime/install',data=b'{}',headers={'Authorization':'Bearer '+self.api_token,'X-Athena-Deck':'1','Content-Type':'application/json'})
with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req)
self.assertEqual(error.exception.code,401);error.exception.close()
def test_demo_removed_from_status(self):
state=self.request('status')
self.assertNotIn('demo',state)
self.assertEqual(state['endpoint']['state'],'stopped')
with self.assertRaises(urllib.error.HTTPError) as exc:self.request('demo')
self.assertEqual(exc.exception.code,404);exc.exception.close()
def test_dashboard_routes_are_authenticated_and_read_only(self):
state=self.request('dashboard')
self.assertIn('gpus',state)
self.assertIn('llama_runtime',state)
history=self.request('dashboard/history?range=24h')
self.assertEqual(history['range'],'24h')
self.assertIn('token_totals',history)
self.assertIn('backups',self.request('dashboard/backups'))
with self.assertRaises(urllib.error.HTTPError) as exc:
urllib.request.urlopen(self.url+'/api/v1/dashboard')
self.assertEqual(exc.exception.code,401);exc.exception.close()
def test_profile_and_download_routes(self):
from pathlib import Path
target=Path(self.state.name)/'models'/('a'*64);target.mkdir(parents=True)
(target/'model.gguf').write_bytes(b'GGUFtest')
(target/'entry.json').write_text(json.dumps(dict(repo='test/image',file='model.gguf',size=8,kind='image',revision='b'*40)))
payload=dict(id=None,revision=0,name='image-test',kind='image',model_id='a'*64,parameters=dict(width=512,height=512,steps=10,seed=-1,guidance=1))
req=urllib.request.Request(self.url+'/api/v1/profiles/save',data=json.dumps(payload).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
with urllib.request.urlopen(req) as r:self.assertEqual(r.status,200)
row=self.request('profiles')['profiles'][0]
self.assertEqual(row['name'],'image-test')
delete=urllib.request.Request(self.url+'/api/v1/profiles/delete',data=json.dumps(dict(id=row['id'],revision=row['revision'])).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
self.server.image_tests.job=dict(state='running',profile_id=row['id'])
with self.assertRaises(urllib.error.HTTPError) as busy:urllib.request.urlopen(delete)
self.assertEqual(busy.exception.code,400);busy.exception.close()
self.server.image_tests.job=None
with urllib.request.urlopen(delete) as r:self.assertEqual(r.status,200)
self.assertEqual(self.request('profiles')['profiles'],[])
self.assertTrue((target/'model.gguf').exists())
with self.assertRaises(urllib.error.HTTPError) as error:
urllib.request.urlopen(urllib.request.Request(self.url+'/api/v1/profiles',headers={'Authorization':'Bearer '+self.api_token}))
self.assertEqual(error.exception.code,401);error.exception.close()
def test_image_test_routes_are_admin_only(self):
self.assertIn('installed',self.request('image-runtime'))
self.assertIn('runtime_installed',self.request('image-tests'))
with self.assertRaises(urllib.error.HTTPError) as error:
urllib.request.urlopen(urllib.request.Request(self.url+'/api/v1/image-tests',headers={'Authorization':'Bearer '+self.api_token}))
self.assertEqual(error.exception.code,401);error.exception.close()
with self.assertRaises(urllib.error.HTTPError) as error:self.request('image-tests/image?id=../../auth.json')
self.assertEqual(error.exception.code,404);error.exception.close()
def test_docker_admin_auth_and_explicit_install_confirmation(self):
with patch.object(self.server.docker,'status',return_value={'installed':True,'services':[]}):
self.assertTrue(self.request('docker')['installed'])
with self.assertRaises(urllib.error.HTTPError) as error:
urllib.request.urlopen(urllib.request.Request(self.url+'/api/v1/docker',headers={'Authorization':'Bearer '+self.api_token}))
self.assertEqual(error.exception.code,401);error.exception.close()
with patch.object(self.server.docker,'install',return_value={'started':True}) as install:
for payload,expected in [({},400),({'confirm':True},200)]:
req=urllib.request.Request(self.url+'/api/v1/docker/install',data=json.dumps(payload).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
try:
with urllib.request.urlopen(req) as result:self.assertEqual(result.status,expected)
except urllib.error.HTTPError as error:self.assertEqual(error.code,expected);error.close()
install.assert_called_once()
def test_endpoint_admin_only_configuration(self):
self.assertEqual(self.request('endpoint')['state'],'stopped')
url=self.url+'/api/v1/endpoint/config'
for headers,expected in [({'Authorization':'Bearer '+self.api_token,'X-Athena-Deck':'1'},401),({'Cookie':self.cookie},403)]:
req=urllib.request.Request(url,data=b'{"port":8120}',headers={'Content-Type':'application/json',**headers})
with self.assertRaises(urllib.error.HTTPError) as exc:urllib.request.urlopen(req)
self.assertEqual(exc.exception.code,expected);exc.exception.close()
with patch.object(self.server.endpoint,'configure',return_value={'port':8120}) as configure:
req=urllib.request.Request(url,data=b'{"port":8120}',headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
with urllib.request.urlopen(req) as r:self.assertEqual(json.load(r)['port'],8120)
configure.assert_called_once_with({'port':8120})
def test_chat_test_routes_are_admin_only(self):
self.assertIsNone(self.request('chat-tests')['job'])
for path,method,body in [('chat-tests','GET',None),('chat-tests/start','POST',b'{}'),('auto-tests','GET',None),('auto-tests/start','POST',b'{}'),('auto-tests/save','POST',b'{}')]:
req=urllib.request.Request(self.url+'/api/v1/'+path,method=method,data=body,headers={'Authorization':'Bearer '+self.api_token,'X-Athena-Deck':'1','Content-Type':'application/json'})
with self.assertRaises(urllib.error.HTTPError) as exc:urllib.request.urlopen(req)
self.assertEqual(exc.exception.code,401);exc.exception.close()
with patch.object(self.server.chat_tests,'start',return_value={'state':'running'}) as start:
data={'profile_id':'test','messages':[{'role':'user','content':'synthetic'}],'max_tokens':8}
req=urllib.request.Request(self.url+'/api/v1/chat-tests/start',data=json.dumps(data).encode(),headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'})
with urllib.request.urlopen(req) as r:self.assertEqual(json.load(r)['state'],'running')
start.assert_called_once_with(data)
def test_control_guard(self):
for headers in ({},{'X-Athena-Deck':'1','Origin':'http://evil.invalid'}):
with self.assertRaises(urllib.error.HTTPError) as e:self.request('demo/start','POST',headers)
self.assertEqual(e.exception.code,403)
e.exception.close()
with self.assertRaises(urllib.error.HTTPError) as e:self.request('models/start','POST',{'X-Athena-Deck':'1'})
self.assertEqual(e.exception.code,404)
e.exception.close()
def test_unavailable_hardware(self):
with patch.dict('os.environ', {'DECK_LOCAL_HARDWARE':'0'}), patch('server.subprocess.run',side_effect=OSError()):
result=HardwareProvider().snapshot()
self.assertFalse(result['available'])
self.assertEqual(result['gpus'],[])
self.assertIsNone(result['sampled_at'])
if __name__=='__main__':unittest.main()