chore: rename project coven → hack-house ⛧

Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -0,0 +1,4 @@
from sanic_testing.manager import TestManager
__version__ = "24.6.0"
__all__ = ("TestManager",)
@@ -0,0 +1,12 @@
from sanic import Sanic # type: ignore
from sanic_testing.testing import SanicASGITestClient, SanicTestClient
class TestManager:
__test__ = False
def __init__(self, app: Sanic) -> None:
self.test_client = SanicTestClient(app)
self.asgi_client = SanicASGITestClient(app)
app._test_manager = self # type: ignore
@@ -0,0 +1,237 @@
import asyncio
import typing
from functools import partial
from random import randint
from typing import Any, Dict, List, Optional, Tuple
import httpx
from sanic import Sanic
from sanic.application.state import ApplicationServerInfo
from sanic.log import logger
from sanic.request import Request
from sanic_testing.websocket import websocket_proxy
from .testing import HOST, PORT, TestingResponse
class ReusableClient:
def __init__(
self,
app: Sanic,
host=HOST,
port=PORT,
loop=None,
server_kwargs=None,
client_kwargs=None,
):
if not loop:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
server_kwargs = server_kwargs or {}
client_kwargs = client_kwargs or {}
Sanic.test_mode = True
self.app = app
self.host = host
self.port = port or randint(5000, 65000)
self._loop = loop
self.debug = False
self._server = None
self.app.state.server_info.append(
ApplicationServerInfo(
settings={
"version": "1.1",
"ssl": None,
"unix": None,
"sock": None,
"loop": None,
"host": self.host,
"port": self.port,
}
)
)
self._session = httpx.AsyncClient(verify=False, **client_kwargs)
self._server_co = self.app.create_server(
host=self.host,
debug=self.debug,
port=self.port,
return_asyncio_server=True,
**server_kwargs,
)
def __enter__(self):
self.run()
return self
def __exit__(self, *_):
self.stop()
def run(self):
self._loop._stopping = False
self.app.router.reset()
self.app.signal_router.reset()
self._run(self.app._startup())
self._run(self.app._server_event("init", "before", loop=self._loop))
self._server = self._run(self._server_co)
self._run(self.app._server_event("init", "after", loop=self._loop))
def stop(self):
self._run(
self.app._server_event("shutdown", "before", loop=self._loop)
)
if self._session:
self._run(self._session.aclose())
self._session = None
if self._server:
self._server.close()
self._run(self._server.wait_closed())
self._server = None
self._run(self.app._server_event("shutdown", "after", loop=self._loop))
def _sanic_endpoint_test(
self,
method: str = "get",
uri: str = "/",
gather_request: bool = True,
debug: bool = False,
server_kwargs: Optional[Dict[str, Any]] = None,
host: Optional[str] = None,
port: Optional[int] = None,
allow_none: bool = False,
*request_args,
**request_kwargs,
) -> Tuple[Optional[Request], Optional[TestingResponse]]:
request_data: Dict[str, Request] = {}
exceptions: List[Exception] = []
host = host or self.host
port = port or self.port
if gather_request:
_collect_request = partial(self._collect_request, request_data)
self.app.request_middleware.appendleft(_collect_request) # type: ignore # noqa
for route in self.app.router.routes:
if _collect_request not in route.extra.request_middleware:
route.extra.request_middleware.appendleft(_collect_request)
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
uri = uri if uri.startswith("/") else f"/{uri}"
scheme = "ws" if method == "websocket" else "http"
url = f"{scheme}://{host}:{port}{uri}"
if exceptions:
raise ValueError(f"Exception during request: {exceptions}")
response = self._run(
self._local_request(method, url, *request_args, **request_kwargs)
)
try:
self.app.request_middleware.remove(_collect_request) # type: ignore # noqa
except BaseException: # noqa
pass
try:
request = request_data.get("request") if gather_request else None
if response is None:
if not allow_none:
raise ValueError(
"No response returned to Sanic Test Client."
)
return request, response
except BaseException: # noqa
if not allow_none:
raise ValueError(
"Request and response object expected, "
f"got ({request}, {response})"
)
return None, None
async def _local_request(self, method, url, *args, **kwargs):
raw_cookies = kwargs.pop("raw_cookies", None)
if method == "websocket":
return await websocket_proxy(url, *args, **kwargs)
else:
session = self._session
try:
if method == "request":
args = tuple([url] + list(args))
url = kwargs.pop("http_method", "GET").upper()
response = await getattr(session, method.lower())(
url, *args, **kwargs
)
except httpx.HTTPError as e:
if hasattr(e, "response"):
response = getattr(e, "response")
else:
logger.error(
f"{method.upper()} {url} received no response!",
exc_info=True,
)
return None
response.__class__ = TestingResponse
if raw_cookies:
response.raw_cookies = {}
for cookie in response.cookies.jar:
response.raw_cookies[cookie.name] = cookie
return response
def _run(self, coro):
if not self._loop:
raise RuntimeError("Test client has no loop")
return self._loop.run_until_complete(coro)
@staticmethod
def _collect_request(data, request):
data["request"] = request
def request(self, *args, **kwargs):
return self._sanic_endpoint_test("request", *args, **kwargs)
def get(self, *args, **kwargs):
return self._sanic_endpoint_test("get", *args, **kwargs)
def post(self, *args, **kwargs):
return self._sanic_endpoint_test("post", *args, **kwargs)
def put(self, *args, **kwargs):
return self._sanic_endpoint_test("put", *args, **kwargs)
def delete(self, *args, **kwargs):
return self._sanic_endpoint_test("delete", *args, **kwargs)
def patch(self, *args, **kwargs):
return self._sanic_endpoint_test("patch", *args, **kwargs)
def options(self, *args, **kwargs):
return self._sanic_endpoint_test("options", *args, **kwargs)
def head(self, *args, **kwargs):
return self._sanic_endpoint_test("head", *args, **kwargs)
def websocket(
self,
*args,
mimic: typing.Optional[
typing.Callable[..., typing.Coroutine[None, None, typing.Any]]
] = None,
**kwargs,
):
kwargs["mimic"] = mimic
return self._sanic_endpoint_test("websocket", *args, **kwargs)
@@ -0,0 +1,476 @@
import typing
from functools import partial
from ipaddress import IPv6Address, ip_address
from json import JSONDecodeError
from socket import AF_INET6, SOCK_STREAM, socket
from string import ascii_lowercase
import httpx
from sanic import Sanic # type: ignore
from sanic.asgi import ASGIApp # type: ignore
from sanic.exceptions import MethodNotSupported, ServerError # type: ignore
from sanic.log import logger # type: ignore
from sanic.request import Request # type: ignore
from sanic.response import text # type: ignore
from sanic_testing.websocket import websocket_proxy
ASGI_HOST = "mockserver"
ASGI_PORT = 1234
ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}"
HOST = "127.0.0.1"
PORT = None
httpx_version = tuple(
map(int, httpx.__version__.strip(ascii_lowercase).split("."))
)
class TestingResponse(httpx.Response):
@property
def status(self):
return self.status_code
@property
def body(self):
return self.content
@property
def content_type(self):
return self.headers.get("content-type")
@property
def json(self):
if getattr(self, "_json", None):
return self._json
try:
self._json = super().json()
except (JSONDecodeError, UnicodeDecodeError):
self._json = None
return self._json
def _blank(*_, **__):
...
class SanicTestClient:
def __init__(
self, app: Sanic, port: typing.Optional[int] = PORT, host: str = HOST
) -> None:
"""Use port=None to bind to a random port"""
Sanic.test_mode = True
self.app = app
self.port = port
self.host = host
self._do_request = _blank
app.after_server_start(self._run_request)
def _run_request(self, *args, **kwargs):
return self._do_request(*args, **kwargs)
@classmethod
def _start_test_mode(cls, sanic, *args, **kwargs):
Sanic.test_mode = True
@classmethod
def _end_test_mode(cls, sanic, *args, **kwargs):
Sanic.test_mode = False
def get_new_session(self, **kwargs) -> httpx.AsyncClient:
return httpx.AsyncClient(verify=False, **kwargs)
async def _local_request(self, method: str, url: str, *args, **kwargs):
logger.info(url)
raw_cookies = kwargs.pop("raw_cookies", None)
session_kwargs = kwargs.pop("session_kwargs", {})
if httpx_version >= (0, 20) and method != "websocket":
kwargs["follow_redirects"] = True
allow_redirects = kwargs.pop("allow_redirects", None)
if allow_redirects is not None:
kwargs["follow_redirects"] = allow_redirects
if method == "websocket":
return await websocket_proxy(url, *args, **kwargs)
else:
async with self.get_new_session(**session_kwargs) as session:
try:
if method == "request":
args = tuple([url] + list(args))
url = kwargs.pop("http_method", "GET").upper()
response = await getattr(session, method.lower())(
url, *args, **kwargs
)
except httpx.HTTPError as e:
if hasattr(e, "response"):
response = getattr(e, "response")
else:
logger.error(
f"{method.upper()} {url} received no response!",
exc_info=True,
)
return None
response.__class__ = TestingResponse
if raw_cookies:
response.raw_cookies = {}
for cookie in response.cookies.jar:
response.raw_cookies[cookie.name] = cookie
return response
@classmethod
def _collect_request(cls, results, request):
if results[0] is None:
results[0] = request
async def _collect_response(
self,
method,
url,
exceptions,
results,
sanic,
loop,
**request_kwargs,
):
try:
response = await self._local_request(method, url, **request_kwargs)
results[-1] = response
if method == "websocket":
await response.ws.close()
except Exception as e:
logger.exception("Exception")
exceptions.append(e)
finally:
self.app.stop()
async def _error_handler(self, request, exception):
if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]:
return text("", exception.status_code, headers=exception.headers)
else:
return self.app.error_handler.default(request, exception)
def _sanic_endpoint_test(
self,
method: str = "get",
uri: str = "/",
gather_request: bool = True,
debug: bool = False,
server_kwargs: typing.Optional[typing.Dict[str, typing.Any]] = None,
host: typing.Optional[str] = None,
allow_none: bool = False,
*request_args,
**request_kwargs,
) -> typing.Tuple[
typing.Optional[Request], typing.Optional[TestingResponse]
]:
results = [None, None]
exceptions: typing.List[Exception] = []
server_kwargs = server_kwargs or {"auto_reload": False}
_collect_request = partial(self._collect_request, results)
self.app.router.reset()
self.app.signal_router.reset()
if gather_request:
self.app.request_middleware.appendleft(_collect_request) # type: ignore # noqa
try:
self.app.exception(MethodNotSupported)(self._error_handler)
except ServerError:
...
if self.port:
server_kwargs = dict(
host=host or self.host,
port=self.port,
**server_kwargs,
)
host, port = host or self.host, self.port
else:
bind = host or self.host
ip = ip_address(bind)
if isinstance(ip, IPv6Address):
sock = socket(AF_INET6, SOCK_STREAM)
port = ASGI_PORT
else:
sock = socket()
port = 0
sock.bind((bind, port))
server_kwargs = dict(sock=sock, **server_kwargs)
if isinstance(ip, IPv6Address):
host, port, _, _ = sock.getsockname()
host = f"[{host}]"
else:
host, port = sock.getsockname()
self.port = port
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
uri = uri if uri.startswith("/") else f"/{uri}"
scheme = "ws" if method == "websocket" else "http"
url = f"{scheme}://{host}:{port}{uri}"
# Tests construct URLs using PORT = None, which means random port not
# known until this function is called, so fix that here
url = url.replace(":None/", f":{port}/")
self._do_request = partial(
self._collect_response,
method,
url,
exceptions,
results,
**request_kwargs,
)
self.app.run( # type: ignore
debug=debug,
single_process=True,
**server_kwargs,
)
if exceptions:
raise ValueError(f"Exception during request: {exceptions}")
if gather_request:
try:
self.app.request_middleware.remove(_collect_request) # type: ignore # noqa
except BaseException: # noqa
pass
try:
request, response = results
if response is None:
if not allow_none:
raise ValueError(
"No response returned to Sanic Test Client."
)
return request, response
except BaseException: # noqa
if not allow_none:
raise ValueError(
"Request and response object expected, "
f"got ({results})"
)
return None, None
else:
try:
if results[-1] is None:
if not allow_none:
raise ValueError(
"No response returned to Sanic Test Client."
)
return None, results[-1]
except BaseException: # noqa
if not allow_none:
raise ValueError(
f"Request object expected, got ({results})"
)
return None, None
def request(self, *args, **kwargs):
return self._sanic_endpoint_test("request", *args, **kwargs)
def get(self, *args, **kwargs):
return self._sanic_endpoint_test("get", *args, **kwargs)
def post(self, *args, **kwargs):
return self._sanic_endpoint_test("post", *args, **kwargs)
def put(self, *args, **kwargs):
return self._sanic_endpoint_test("put", *args, **kwargs)
def delete(self, *args, **kwargs):
return self._sanic_endpoint_test("delete", *args, **kwargs)
def patch(self, *args, **kwargs):
return self._sanic_endpoint_test("patch", *args, **kwargs)
def options(self, *args, **kwargs):
return self._sanic_endpoint_test("options", *args, **kwargs)
def head(self, *args, **kwargs):
return self._sanic_endpoint_test("head", *args, **kwargs)
def websocket(
self,
*args,
mimic: typing.Optional[
typing.Callable[..., typing.Coroutine[None, None, typing.Any]]
] = None,
**kwargs,
):
kwargs["mimic"] = mimic
return self._sanic_endpoint_test("websocket", *args, **kwargs)
class TestASGIApp(ASGIApp):
async def __call__(self):
await super().__call__()
return self.request
async def app_call_with_return(self, scope, receive, send):
asgi_app = await TestASGIApp.create(self, scope, receive, send)
return await asgi_app()
class SanicASGITestClient(httpx.AsyncClient):
def __init__(
self,
app: Sanic,
base_url: str = ASGI_BASE_URL,
suppress_exceptions: bool = False,
) -> None:
Sanic.test_mode = True
app.__class__.__call__ = app_call_with_return # type: ignore
app.asgi = True
self.sanic_app = app
transport = httpx.ASGITransport(app=app, client=(ASGI_HOST, ASGI_PORT))
super().__init__(transport=transport, base_url=base_url)
self.gather_request = True
self.last_request = None
def _collect_request(self, request):
if self.gather_request:
self.last_request = request
else:
self.last_request = None
@classmethod
def _start_test_mode(cls, sanic, *args, **kwargs):
Sanic.test_mode = True
@classmethod
def _end_test_mode(cls, sanic, *args, **kwargs):
Sanic.test_mode = False
async def request( # type: ignore
self, method, url, gather_request=True, *args, **kwargs
) -> typing.Tuple[
typing.Optional[Request], typing.Optional[TestingResponse]
]:
self.sanic_app.router.reset()
self.sanic_app.signal_router.reset()
await self.sanic_app._startup() # type: ignore
await self.sanic_app._server_event("init", "before")
await self.sanic_app._server_event("init", "after")
for route in self.sanic_app.router.routes:
if self._collect_request not in route.extra.request_middleware:
route.extra.request_middleware.appendleft(
self._collect_request
)
if not url.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = url if url.startswith("/") else f"/{url}"
scheme = "ws" if method == "websocket" else "http"
url = f"{scheme}://{ASGI_HOST}:{ASGI_PORT}{url}"
if self._collect_request not in self.sanic_app.request_middleware:
self.sanic_app.request_middleware.appendleft(
self._collect_request # type: ignore
)
self.gather_request = gather_request
response = await super().request(method, url, *args, **kwargs)
await self.sanic_app._server_event("shutdown", "before")
await self.sanic_app._server_event("shutdown", "after")
response.__class__ = TestingResponse
if gather_request:
return self.last_request, response # type: ignore
return None, response # type: ignore
@classmethod
async def _ws_receive(cls):
return {}
@classmethod
async def _ws_send(cls, message):
pass
async def websocket(
self,
uri,
subprotocols=None,
*args,
mimic: typing.Optional[
typing.Callable[..., typing.Coroutine[None, None, typing.Any]]
] = None,
**kwargs,
):
if mimic:
raise RuntimeError(
"SanicASGITestClient does not currently support the mimic "
"keyword argument. Please use SanicTestClient instead."
)
scheme = "ws"
path = uri
root_path = f"{scheme}://{ASGI_HOST}:{ASGI_PORT}"
headers = kwargs.get("headers", {})
headers.setdefault("connection", "upgrade")
headers.setdefault("sec-websocket-key", "testserver==")
headers.setdefault("sec-websocket-version", "13")
if subprotocols is not None:
headers.setdefault(
"sec-websocket-protocol", ", ".join(subprotocols)
)
scope = {
"type": "websocket",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"headers": [map(lambda y: y.encode(), x) for x in headers.items()],
"scheme": scheme,
"root_path": root_path,
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"subprotocols": subprotocols,
}
self.sanic_app.router.reset()
self.sanic_app.signal_router.reset()
await self.sanic_app._startup()
await self.sanic_app(scope, self._ws_receive, self._ws_send)
return None, {"opened": True}
def __getstate__(self):
# Cookies cannot be pickled, because they contain a ThreadLock
try:
del self._cookies
except AttributeError:
pass
return self.__dict__
def __setstate__(self, d):
try:
del d["_cookies"]
except LookupError:
pass
self.__dict__.update(d)
# Need to create a new CookieJar when unpickling,
# because it was killed on Pickle
self._cookies = httpx.Cookies()
@@ -0,0 +1,50 @@
import typing
from websockets.exceptions import ConnectionClosedOK
from websockets.legacy.client import connect
class WebsocketProxy:
def __init__(self, ws):
self.ws = ws
self.opened = True
self.client_received: typing.List[str] = []
self.client_sent: typing.List[str] = []
@property
def server_received(self):
return self.client_sent
@property
def server_sent(self):
return self.client_received
async def websocket_proxy(url, *args, **kwargs) -> WebsocketProxy:
mimic = kwargs.pop("mimic", None)
async with connect(url, *args, **kwargs) as websocket:
ws_proxy = WebsocketProxy(websocket)
if mimic:
do_send = websocket.send
do_recv = websocket.recv
async def send(data):
ws_proxy.client_sent.append(data)
await do_send(data)
async def recv():
message = await do_recv()
ws_proxy.client_received.append(message)
return message
websocket.send = send # type: ignore
websocket.recv = recv # type: ignore
try:
await mimic(websocket)
except ConnectionClosedOK:
pass
else:
await websocket.send("")
return ws_proxy