Compare commits

1 Commits
v0.3.0 ... main

Author SHA1 Message Date
Yunxiao Xu
47a1f3c9a9 Add tests 2025-09-19 05:29:27 -07:00
4 changed files with 183 additions and 1 deletions

View File

@@ -75,7 +75,33 @@ Install the built wheel elsewhere:
uv pip install dist/*.whl uv pip install dist/*.whl
``` ```
## Testing
Run the included unit tests with the standard library `unittest` runner.
```bash
# Using uv
uv run python -m unittest discover -s tests -v
# Or with an activated venv
python -m unittest discover -s tests -v
```
Run a single test file:
```bash
uv run python -m unittest tests/test_api.py -v
```
## Development
Run directly from source without installing (useful while iterating):
```bash
PYTHONPATH=src uv run python -m hysteria_panel
```
## Troubleshooting ## Troubleshooting
- No module named `hysteria-panel`: ensure the project is installed (`uv sync` or `pip install -e .`). - No module named `hysteria_panel`: ensure the project is installed (`uv sync` or `pip install -e .`).
- Cannot connect: verify host/port/secret and whether HTTPS is required. - Cannot connect: verify host/port/secret and whether HTTPS is required.

103
tests/test_api.py Normal file
View File

@@ -0,0 +1,103 @@
import unittest
from unittest.mock import patch
from typing import Callable, Dict, Tuple, Any
import requests
from hysteria_panel.api import HysteriaAPI
class FauxResponse:
def __init__(self, status: int = 200, headers: Dict[str, str] | None = None, json_data: Any = None, text: str = "OK"):
self.status_code = status
self.headers = headers or {}
self._json_data = json_data
self.text = text
def raise_for_status(self):
if 400 <= self.status_code:
raise requests.HTTPError(f"HTTP {self.status_code}")
def json(self):
return self._json_data
class FakeSession:
def __init__(self, responder: Callable[[str, str], FauxResponse]):
self.headers: Dict[str, str] = {}
self._responder = responder
def request(self, method: str, url: str, headers=None, json=None, params=None, timeout=None):
# Extract only the path for routing in tests
# URL format: scheme://host:port/path
path = url.split("//", 1)[-1]
path = path[path.find(":") + 1:] # remove host:port
path = path[path.find("/"):] # keep leading /path
return self._responder(method.lower(), path)
class TestHysteriaAPI(unittest.TestCase):
def _make_responder(self, mapping: Dict[Tuple[str, str], FauxResponse]):
def responder(method: str, path: str) -> FauxResponse:
# Default to 200 OK text response
return mapping.get((method, path), FauxResponse())
return responder
def test_init_sets_base_url_and_headers_http(self):
mapping = {('get', '/'): FauxResponse(status=200, text='ok')}
with patch('requests.Session', lambda: FakeSession(self._make_responder(mapping))):
api = HysteriaAPI('example.com', 9999, 'secret', use_https=False)
self.assertEqual(api.base_url, 'http://example.com:9999')
# Authorization header is set
self.assertIn('Authorization', api._session.headers)
self.assertEqual(api._session.headers['Authorization'], 'secret')
# User-Agent prefix
ua = api._session.headers.get('User-Agent')
# Support both str and bytes user-agent values
if isinstance(ua, (bytes, bytearray)):
self.assertTrue(ua.startswith(b'HysteriaPanel/'))
else:
self.assertTrue(str(ua).startswith('HysteriaPanel/'))
def test_init_sets_base_url_https(self):
mapping = {('get', '/'): FauxResponse(status=200, text='ok')}
with patch('requests.Session', lambda: FakeSession(self._make_responder(mapping))):
api = HysteriaAPI('example.com', 443, 'secret', use_https=True)
self.assertEqual(api.base_url, 'https://example.com:443')
def test_request_json_and_kick(self):
mapping = {
('get', '/'): FauxResponse(status=200, text='ok'),
('get', '/traffic'): FauxResponse(status=200, headers={'Content-Type': 'application/json'}, json_data={'alice': {'tx': 1, 'rx': 2}}),
('post', '/kick'): FauxResponse(status=204, headers={}),
}
with patch('requests.Session', lambda: FakeSession(self._make_responder(mapping))):
api = HysteriaAPI('h', 1, 's')
ok, data = api.get_traffic()
self.assertTrue(ok)
self.assertIsInstance(data, dict)
self.assertIn('alice', data)
ok, data = api.kick_users(['alice'])
self.assertTrue(ok)
self.assertIsNone(data)
def test_request_error(self):
def responder(method: str, path: str) -> FauxResponse:
if path == '/':
return FauxResponse(status=200, text='ok')
raise requests.RequestException("boom")
with patch('requests.Session', lambda: FakeSession(responder)):
api = HysteriaAPI('h', 1, 's')
ok, data = api._request('get', '/error')
self.assertFalse(ok)
self.assertIn('boom', data)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,29 @@
import unittest
import tempfile
from pathlib import Path
from unittest.mock import patch
from hysteria_panel import utils
class TestConfigUtils(unittest.TestCase):
def test_save_load_clear_config(self):
with tempfile.TemporaryDirectory() as td:
temp_dir = Path(td)
temp_file = temp_dir / "config.json"
with patch.object(utils, 'CONFIG_DIR', temp_dir), patch.object(utils, 'CONFIG_FILE', temp_file):
data = {"host": "127.0.0.1", "port": 9999, "secret": "abc", "https": True}
utils.save_config(data)
self.assertTrue(temp_file.exists())
loaded = utils.load_config()
self.assertEqual(loaded, data)
utils.clear_config()
self.assertFalse(temp_file.exists())
if __name__ == '__main__':
unittest.main()

24
tests/test_utils.py Normal file
View File

@@ -0,0 +1,24 @@
import unittest
from hysteria_panel import utils
class TestUtils(unittest.TestCase):
def test_format_bytes(self):
self.assertEqual(utils.format_bytes(0), "0 B")
self.assertEqual(utils.format_bytes(1), "1.00 B")
self.assertEqual(utils.format_bytes(1024), "1.00 KB")
self.assertEqual(utils.format_bytes(1024**2), "1.00 MB")
self.assertEqual(utils.format_bytes(1536), "1.50 KB")
def test_format_speed(self):
self.assertEqual(utils.format_speed(0), "0 B/s")
self.assertEqual(utils.format_speed(1), "1.00 B/s")
self.assertEqual(utils.format_speed(1024), "1.00 KB/s")
self.assertEqual(utils.format_speed(1024**2), "1.00 MB/s")
self.assertEqual(utils.format_speed(1536), "1.50 KB/s")
if __name__ == "__main__":
unittest.main()