import pytest
from typing import TYPE_CHECKING
try:
import hypertor
from hypertor import (
Client, AsyncClient, Response,
OnionApp, AppConfig, Request, AppResponse,
HypertorError,
)
HYPERTOR_AVAILABLE = True
except ImportError as e:
HYPERTOR_AVAILABLE = False
IMPORT_ERROR = str(e)
pytestmark = pytest.mark.skipif(
not HYPERTOR_AVAILABLE,
reason=f"hypertor not installed: {IMPORT_ERROR if not HYPERTOR_AVAILABLE else ''}"
)
class TestImports:
def test_version_exists(self):
assert hasattr(hypertor, '__version__')
assert isinstance(hypertor.__version__, str)
assert hypertor.__version__ == "0.3.0"
def test_client_classes_exist(self):
assert Client is not None
assert AsyncClient is not None
assert Response is not None
def test_server_classes_exist(self):
assert OnionApp is not None
assert AppConfig is not None
assert Request is not None
assert AppResponse is not None
def test_exception_classes_exist(self):
assert HypertorError is not None
assert issubclass(HypertorError, Exception)
class TestClientAPI:
def test_client_has_context_manager(self):
assert hasattr(Client, '__enter__')
assert hasattr(Client, '__exit__')
def test_async_client_has_context_manager(self):
assert hasattr(AsyncClient, '__aenter__')
assert hasattr(AsyncClient, '__aexit__')
def test_client_has_http_methods(self):
for method in ['get', 'post', 'put', 'delete']:
assert hasattr(Client, method), f"Client missing {method} method"
def test_async_client_has_http_methods(self):
for method in ['get', 'post']:
assert hasattr(AsyncClient, method), f"AsyncClient missing {method} method"
def test_client_has_pool_methods(self):
assert hasattr(Client, 'pool_size')
assert hasattr(Client, 'clear_pool')
class TestOnionAppAPI:
def test_onionapp_creation(self):
app = OnionApp()
assert app is not None
def test_onionapp_with_port(self):
app = OnionApp(port=8080)
assert app is not None
def test_onionapp_has_route_decorators(self):
app = OnionApp()
for method in ['get', 'post', 'put', 'delete', 'patch', 'route']:
assert hasattr(app, method), f"OnionApp missing {method} decorator"
def test_onionapp_get_decorator(self):
app = OnionApp()
@app.get("/")
def home():
return "Hello"
routes = app.routes_info()
assert len(routes) == 1
assert routes[0]['method'] == 'GET'
assert routes[0]['path'] == '/'
def test_onionapp_post_decorator(self):
app = OnionApp()
@app.post("/api/data")
def create_data():
return {"status": "created"}
routes = app.routes_info()
assert len(routes) == 1
assert routes[0]['method'] == 'POST'
assert routes[0]['path'] == '/api/data'
def test_onionapp_multiple_routes(self):
app = OnionApp()
@app.get("/")
def home():
return "Home"
@app.get("/about")
def about():
return "About"
@app.post("/api/users")
def create_user():
return {"id": 1}
routes = app.routes_info()
assert len(routes) == 3
def test_onionapp_route_with_methods(self):
app = OnionApp()
@app.route("/api/resource", methods=["GET", "POST"])
def resource():
return "Resource"
routes = app.routes_info()
assert len(routes) == 2
def test_onionapp_has_lifecycle_hooks(self):
app = OnionApp()
assert hasattr(app, 'on_startup')
assert hasattr(app, 'on_shutdown')
assert hasattr(app, 'middleware')
assert hasattr(app, 'error_handler')
def test_onionapp_middleware_decorator(self):
app = OnionApp()
@app.middleware
def log_middleware(request, call_next):
return call_next(request)
assert True
def test_onionapp_error_handler_decorator(self):
app = OnionApp()
@app.error_handler(404)
def not_found(request):
return {"error": "Not found"}
assert True
def test_onionapp_address_before_run(self):
app = OnionApp()
assert app.address() is None
def test_onionapp_is_running_before_run(self):
app = OnionApp()
assert app.is_running() is False
class TestAppConfig:
def test_default_config(self):
config = AppConfig()
assert config.port == 80
assert config.debug is False
assert config.log_requests is True
assert config.timeout == 30
assert config.enable_pow is False
assert config.security_level == "standard"
def test_custom_port(self):
config = AppConfig(port=8080)
assert config.port == 8080
def test_enable_pow(self):
config = AppConfig(enable_pow=True)
assert config.enable_pow is True
def test_security_levels(self):
for level in ["standard", "enhanced", "maximum", "paranoid"]:
config = AppConfig(security_level=level)
assert config.security_level == level
def test_key_file(self):
config = AppConfig(key_file="/path/to/keys")
assert config.key_file == "/path/to/keys"
def test_max_body_size(self):
config = AppConfig(max_body_size=1024*1024) assert config.max_body_size == 1024*1024
def test_onionapp_with_config(self):
config = AppConfig(
port=443,
enable_pow=True,
security_level="maximum",
)
app = OnionApp(config=config)
assert app is not None
class TestAppResponse:
def test_string_response(self):
response = AppResponse("Hello, World!")
assert response.status == 200
def test_custom_status(self):
response = AppResponse("Not Found", status=404)
assert response.status == 404
def test_json_response(self):
response = AppResponse.json({"key": "value"})
assert response.status == 200
def test_json_response_with_status(self):
response = AppResponse.json({"error": "Bad Request"}, status=400)
assert response.status == 400
def test_html_response(self):
response = AppResponse.html("<h1>Hello</h1>")
assert response.status == 200
def test_redirect_response(self):
response = AppResponse.redirect("/new-location")
assert response.status == 302
def test_redirect_with_custom_status(self):
response = AppResponse.redirect("/permanent", status=301)
assert response.status == 301
def test_set_header(self):
response = AppResponse("OK")
response.set_header("X-Custom", "value")
headers = response.get_headers()
assert "X-Custom" in headers
assert headers["X-Custom"] == "value"
class TestSecurityConfiguration:
def test_standard_security(self):
config = AppConfig(security_level="standard")
assert config.security_level == "standard"
assert config.enable_pow is False
def test_enhanced_security(self):
config = AppConfig(security_level="enhanced")
assert config.security_level == "enhanced"
def test_maximum_security(self):
config = AppConfig(security_level="maximum", enable_pow=True)
assert config.security_level == "maximum"
assert config.enable_pow is True
def test_paranoid_security(self):
config = AppConfig(security_level="paranoid")
assert config.security_level == "paranoid"
class TestIntegrationPatterns:
def test_fastapi_like_pattern(self):
app = OnionApp()
@app.get("/")
def home():
return {"message": "Hello"}
@app.get("/items/{item_id}")
def get_item(item_id: int):
return {"item_id": item_id}
@app.post("/items")
def create_item():
return {"status": "created"}
routes = app.routes_info()
assert len(routes) == 3
def test_restful_api_pattern(self):
app = OnionApp()
@app.get("/users")
def list_users():
return []
@app.post("/users")
def create_user():
return {"id": 1}
@app.get("/users/{id}")
def get_user(id: int):
return {"id": id}
@app.put("/users/{id}")
def update_user(id: int):
return {"id": id, "updated": True}
@app.delete("/users/{id}")
def delete_user(id: int):
return {"deleted": True}
routes = app.routes_info()
assert len(routes) == 5
def test_secure_service_pattern(self):
config = AppConfig(
port=80,
enable_pow=True, security_level="maximum", timeout=60, max_body_size=50*1024*1024, )
app = OnionApp(config=config)
@app.get("/")
def index():
return {"status": "secure"}
@app.post("/submit")
def submit_document():
return {"received": True}
assert len(app.routes_info()) == 2
@pytest.mark.network
class TestNetworkIntegration:
@pytest.mark.skip(reason="Requires Tor network")
def test_client_connects_to_tor(self):
with Client(timeout=120) as client:
response = client.get("https://check.torproject.org/api/ip")
assert response.status == 200
@pytest.mark.skip(reason="Requires Tor network")
def test_onion_service_starts(self):
app = OnionApp(port=8080)
@app.get("/")
def home():
return "Hello"
def pytest_configure(config):
config.addinivalue_line(
"markers", "network: marks tests as requiring Tor network"
)