172 lines
12 KiB
Python
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()
|