mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
166 lines
5.0 KiB
Python
166 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
from urllib.parse import parse_qs, quote, urlsplit
|
|
|
|
import pytest
|
|
from websockets.datastructures import Headers
|
|
|
|
from nanobot.webui.extensions_routes import WebUIExtensionsRouter
|
|
from nanobot.webui.http_utils import http_json_response
|
|
|
|
|
|
class _Service:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, object]] = []
|
|
|
|
async def status(self):
|
|
self.calls.append(("status", None))
|
|
return {"extensions": [], "diagnostics": []}
|
|
|
|
async def search(self, query, *, ecosystem, limit):
|
|
self.calls.append(("search", (query, ecosystem, limit)))
|
|
return {"packages": []}
|
|
|
|
async def install(self, source, *, kind, ref, trusted):
|
|
self.calls.append(("install", (source, kind, ref, trusted)))
|
|
return {"record": {"id": "sample"}}
|
|
|
|
|
|
def _router(
|
|
service: _Service,
|
|
*,
|
|
authorized: bool = True,
|
|
allow_remote: bool = False,
|
|
) -> WebUIExtensionsRouter:
|
|
return WebUIExtensionsRouter(
|
|
service=service,
|
|
check_api_token=lambda _request: authorized,
|
|
parse_query=lambda path: parse_qs(urlsplit(path).query),
|
|
json_response=http_json_response,
|
|
error_response=lambda status, message: http_json_response(
|
|
{"error": message},
|
|
status=status,
|
|
),
|
|
allow_remote_package_install=allow_remote,
|
|
logger=SimpleNamespace(exception=lambda *_args: None),
|
|
)
|
|
|
|
|
|
def _request(
|
|
path: str,
|
|
*,
|
|
method: str = "GET",
|
|
values: dict[str, object] | None = None,
|
|
host: str = "127.0.0.1:8765",
|
|
encode_values: bool = False,
|
|
):
|
|
headers = Headers([("Host", host)])
|
|
if values is not None:
|
|
payload = json.dumps(values)
|
|
headers["X-Nanobot-Extension-Values"] = quote(payload) if encode_values else payload
|
|
return SimpleNamespace(path=path, method=method, headers=headers)
|
|
|
|
|
|
_LOCAL = SimpleNamespace(remote_address=("127.0.0.1", 12345))
|
|
_REMOTE = SimpleNamespace(remote_address=("192.0.2.1", 12345))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_extension_status_requires_auth_and_get() -> None:
|
|
service = _Service()
|
|
|
|
unauthorized = await _router(service, authorized=False).dispatch(
|
|
_LOCAL,
|
|
_request("/api/extensions"),
|
|
"/api/extensions",
|
|
)
|
|
wrong_method = await _router(service).dispatch(
|
|
_LOCAL,
|
|
_request("/api/extensions", method="POST"),
|
|
"/api/extensions",
|
|
)
|
|
response = await _router(service).dispatch(
|
|
_LOCAL,
|
|
_request("/api/extensions"),
|
|
"/api/extensions",
|
|
)
|
|
|
|
assert unauthorized is not None and unauthorized.status_code == 401
|
|
assert wrong_method is not None and wrong_method.status_code == 405
|
|
assert response is not None and response.status_code == 200
|
|
assert service.calls == [("status", None)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_extension_market_parses_query() -> None:
|
|
service = _Service()
|
|
response = await _router(service).dispatch(
|
|
_LOCAL,
|
|
_request("/api/extensions/market?q=web&ecosystem=pi&limit=7"),
|
|
"/api/extensions/market",
|
|
)
|
|
|
|
assert response is not None and response.status_code == 200
|
|
assert service.calls == [("search", ("web", "pi", 7))]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_extension_install_is_local_and_untrusted() -> None:
|
|
service = _Service()
|
|
response = await _router(service).dispatch(
|
|
_LOCAL,
|
|
_request(
|
|
"/api/extensions/install",
|
|
method="POST",
|
|
values={"source": "中文扩展", "kind": "npm"},
|
|
encode_values=True,
|
|
),
|
|
"/api/extensions/install",
|
|
)
|
|
|
|
assert response is not None and response.status_code == 200
|
|
assert service.calls == [
|
|
("install", ("中文扩展", "npm", "", False)),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remote_install_policy_never_exposes_server_local_paths() -> None:
|
|
service = _Service()
|
|
denied = await _router(service).dispatch(
|
|
_REMOTE,
|
|
_request(
|
|
"/api/extensions/install",
|
|
method="POST",
|
|
values={"source": "pi-example", "kind": "npm"},
|
|
),
|
|
"/api/extensions/install",
|
|
)
|
|
npm_allowed = await _router(service, allow_remote=True).dispatch(
|
|
_REMOTE,
|
|
_request(
|
|
"/api/extensions/install",
|
|
method="POST",
|
|
values={"source": "pi-example", "kind": "npm"},
|
|
),
|
|
"/api/extensions/install",
|
|
)
|
|
local_denied = await _router(service, allow_remote=True).dispatch(
|
|
_REMOTE,
|
|
_request(
|
|
"/api/extensions/install",
|
|
method="POST",
|
|
values={"source": "/tmp/example", "kind": "local"},
|
|
),
|
|
"/api/extensions/install",
|
|
)
|
|
|
|
assert denied is not None and denied.status_code == 403
|
|
assert npm_allowed is not None and npm_allowed.status_code == 200
|
|
assert local_denied is not None and local_denied.status_code == 403
|
|
assert service.calls == [
|
|
("install", ("pi-example", "npm", "", False)),
|
|
]
|