Add tests
This commit is contained in:
28
README.md
28
README.md
@@ -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
103
tests/test_api.py
Normal 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()
|
||||||
|
|
||||||
29
tests/test_config_utils.py
Normal file
29
tests/test_config_utils.py
Normal 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
24
tests/test_utils.py
Normal 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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user