Add isolated read-only spectator monitor for public sharing
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
import importlib.util
|
||||
import json
|
||||
import threading
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
spec = importlib.util.spec_from_file_location('spectator', Path(__file__).with_name('spectator-server.py'))
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
|
||||
|
||||
class SpectatorTests(unittest.TestCase):
|
||||
def test_public_projection_excludes_secrets_and_whispers(self):
|
||||
bots = [{'name': n, 'status': 'online', 'race': r, 'role': 'Death Knight', 'level': 55,
|
||||
'autonomy': {'managed': True, 'accountId': 123}, 'memories': ['PRIVATE_MEMORY'],
|
||||
'world': {'team': team, 'groupId': 'internal-guid', 'groupNames': ['A', 'B', 'HumanPlayer']}}
|
||||
for n, r, team in [('A', 'Human', 0), ('B', 'Orc', 1)]]
|
||||
feed = [{'id': i, 'kind': 'conversation', 'status': 'confirmed', 'names': names, 'text': t}
|
||||
for i, names, t in [('whisper-1', ['HumanPlayer', 'A'], 'PRIVATE_WHISPER'),
|
||||
('social-1', ['A', 'B'], 'SYNTHETIC_PROMPT'),
|
||||
('social-1:reply', ['A', 'B'], 'Hello friend'),
|
||||
('social-2:reply', ['HumanPlayer', 'A'], 'NOT_BOT_CHAT')]]
|
||||
projected = m.project({'bots': bots, 'settings': {'token': 'SECRET'}, 'community': {'feed': feed}})
|
||||
serialized = json.dumps(projected)
|
||||
for private in ['PRIVATE_MEMORY', 'PRIVATE_WHISPER', 'SYNTHETIC_PROMPT', 'NOT_BOT_CHAT', 'HumanPlayer', 'SECRET', 'accountId', 'internal-guid']:
|
||||
self.assertNotIn(private, serialized)
|
||||
self.assertEqual([b['faction'] for b in projected['bots']], ['Alliance', 'Horde'])
|
||||
self.assertEqual([e['text'] for e in projected['feed']], ['Hello friend'])
|
||||
|
||||
def test_http_allowlist_and_mutations(self):
|
||||
server = m.Server(('127.0.0.1', 0), m.Handler)
|
||||
worker = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
worker.start()
|
||||
base = 'http://127.0.0.1:' + str(server.server_port)
|
||||
try:
|
||||
for route in ['/', '/app.js', '/style.css', '/health']:
|
||||
with urllib.request.urlopen(base + route) as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertIn("default-src 'none'", response.headers['Content-Security-Policy'])
|
||||
for path in ['/api/state', '/api/actions', '/api/ale/operator', '/api/operations', '/.dev.vars', '/../.dev.vars', '/%2e%2e/.dev.vars', '//api/state']:
|
||||
with self.assertRaises(urllib.error.HTTPError) as error:
|
||||
urllib.request.urlopen(base + path)
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
for method in ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE']:
|
||||
with self.assertRaises(urllib.error.HTTPError) as error:
|
||||
urllib.request.urlopen(urllib.request.Request(base + '/api/actions', method=method, data=b'{}'))
|
||||
self.assertEqual(error.exception.code, 405)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
worker.join()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user