210 lines
7.7 KiB
Python
210 lines
7.7 KiB
Python
import os
|
|
import tempfile
|
|
import unittest
|
|
from contextlib import ExitStack
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
from uuid import uuid4
|
|
|
|
from platform_sources.paper_server import (
|
|
PaperServerFetchTool,
|
|
PaperServerSearchTool,
|
|
_media_url,
|
|
)
|
|
from tools.run_python import RunPythonTool
|
|
|
|
|
|
class PaperServerToolTests(unittest.TestCase):
|
|
def test_search_forwards_publication_type_to_server_filter(self):
|
|
captured = {}
|
|
|
|
def fake_get_json(url, *, api_key, params=None):
|
|
captured.update(params or {})
|
|
self.assertEqual(api_key, "secret")
|
|
return {"results": [{"id": "p1", "type": "book", "title": "Book"}]}
|
|
|
|
with (
|
|
patch(
|
|
"platform_sources.paper_server._config",
|
|
return_value=(
|
|
"https://paper.test",
|
|
"https://paper.test/api/resm/paper",
|
|
"secret",
|
|
),
|
|
),
|
|
patch("platform_sources.paper_server._get_json", side_effect=fake_get_json),
|
|
):
|
|
result = PaperServerSearchTool().execute(publication_type="book", limit=20)
|
|
|
|
self.assertEqual(captured["type"], "book")
|
|
self.assertEqual(captured["page_size"], 20)
|
|
self.assertIn('"type": "book"', result)
|
|
self.assertNotIn("secret", result)
|
|
|
|
def test_search_accepts_open_raw_type_without_closed_enum(self):
|
|
with (
|
|
patch(
|
|
"platform_sources.paper_server._config",
|
|
return_value=(
|
|
"https://paper.test",
|
|
"https://paper.test/api/resm/paper",
|
|
"secret",
|
|
),
|
|
),
|
|
patch("platform_sources.paper_server._get_json", return_value=[]) as request,
|
|
):
|
|
result = PaperServerSearchTool().execute(publication_type="future-type")
|
|
self.assertEqual(request.call_args.kwargs["params"]["type"], "future-type")
|
|
self.assertEqual(result, "[]")
|
|
|
|
def test_search_rejects_unsafe_raw_type(self):
|
|
with patch("platform_sources.paper_server._get_json") as request:
|
|
result = PaperServerSearchTool().execute(publication_type="book&api_key=leak")
|
|
request.assert_not_called()
|
|
self.assertTrue(result.startswith("[Error]"))
|
|
|
|
def test_media_download_url_must_match_configured_origin(self):
|
|
self.assertEqual(
|
|
_media_url("/media/a.pdf", "https://paper.test"),
|
|
"https://paper.test/media/a.pdf",
|
|
)
|
|
with self.assertRaisesRegex(RuntimeError, "out-of-origin"):
|
|
_media_url("https://attacker.test/a.pdf", "https://paper.test")
|
|
|
|
def test_fetch_schema_limits_format(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tool = PaperServerFetchTool(working_dir=Path(tmp))
|
|
self.assertEqual(
|
|
tool.parameters["properties"]["format"]["enum"], ["pdf", "xml"]
|
|
)
|
|
|
|
def test_fetch_downloads_atomically_and_reuses_existing_file(self):
|
|
class FakeStreamResponse:
|
|
status_code = 200
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return False
|
|
|
|
def iter_bytes(self, chunk_size=0):
|
|
self.test_chunk_size = chunk_size
|
|
yield b"%PDF-test"
|
|
|
|
paper = {
|
|
"id": "p1",
|
|
"doi": "10.1/example",
|
|
"has_fulltext_pdf": True,
|
|
"pdf_url": "/media/example.pdf",
|
|
}
|
|
with (
|
|
tempfile.TemporaryDirectory() as tmp,
|
|
patch("platform_sources.paper_server._get_paper", return_value=paper),
|
|
patch(
|
|
"platform_sources.paper_server._config",
|
|
return_value=(
|
|
"https://paper.test",
|
|
"https://paper.test/api/resm/paper",
|
|
"secret",
|
|
),
|
|
),
|
|
patch(
|
|
"platform_sources.paper_server.httpx.stream", return_value=FakeStreamResponse()
|
|
) as stream,
|
|
):
|
|
tool = PaperServerFetchTool(working_dir=Path(tmp))
|
|
first = tool.execute(id_or_doi="p1", format="pdf")
|
|
second = tool.execute(id_or_doi="p1", format="pdf")
|
|
destination = Path(tmp) / "papers" / "10.1_example.pdf"
|
|
saved_bytes = destination.read_bytes()
|
|
|
|
self.assertEqual(saved_bytes, b"%PDF-test")
|
|
self.assertTrue(first.startswith("saved:"))
|
|
self.assertTrue(second.endswith("(existing)"))
|
|
self.assertEqual(stream.call_count, 1)
|
|
|
|
def test_run_python_no_longer_passes_paper_server_key(self):
|
|
with patch.dict(os.environ, {"PAPER_SERVER_API_KEY": "secret"}, clear=False):
|
|
env = RunPythonTool()._filtered_env()
|
|
self.assertNotIn("PAPER_SERVER_API_KEY", env)
|
|
|
|
def test_registry_exposes_host_tools_only_when_platform_key_exists(self):
|
|
from core.tool_registry import ToolContext, build_tools
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
common = dict(
|
|
tool_base=root,
|
|
ur_path=root,
|
|
working_dir_path=root,
|
|
task_id=uuid4(),
|
|
uid=uuid4(),
|
|
cfg={},
|
|
caps=SimpleNamespace(enable_run_python=False),
|
|
skills=SimpleNamespace(skills={}),
|
|
cancel_check=None,
|
|
scheduled_run=False,
|
|
deferred_actions=SimpleNamespace(),
|
|
ark_cfg=None,
|
|
img_provider="",
|
|
img_key="",
|
|
img_cfg=None,
|
|
img_provider_cfg=None,
|
|
video_variant="",
|
|
office_to_pdf_available=False,
|
|
)
|
|
gates = dict(
|
|
DOCUMENT_SEARCH_API_KEY="",
|
|
MP_API_KEY="",
|
|
PAPER_SERVER_API_KEY="",
|
|
)
|
|
|
|
def enter_common_patches(stack):
|
|
stack.enter_context(
|
|
patch(
|
|
"core.tool_registry._external_system_status_available",
|
|
return_value=False,
|
|
)
|
|
)
|
|
stack.enter_context(
|
|
patch(
|
|
"core.tool_registry._external_systems_available",
|
|
return_value=False,
|
|
)
|
|
)
|
|
stack.enter_context(
|
|
patch("core.tool_registry.smtp_configured", return_value=False)
|
|
)
|
|
stack.enter_context(
|
|
patch(
|
|
"core.tool_registry.wechat_push_available", return_value=False
|
|
)
|
|
)
|
|
stack.enter_context(
|
|
patch("core.tool_registry.lfasr_configured", return_value=False)
|
|
)
|
|
stack.enter_context(
|
|
patch("core.tool_registry.BochaConfig.load", return_value=None)
|
|
)
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch.dict(os.environ, gates, clear=False))
|
|
enter_common_patches(stack)
|
|
without_key = build_tools(ToolContext(**common))
|
|
gates["PAPER_SERVER_API_KEY"] = "secret"
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch.dict(os.environ, gates, clear=False))
|
|
enter_common_patches(stack)
|
|
with_key = build_tools(ToolContext(**common))
|
|
|
|
self.assertNotIn("paper_server_search", without_key)
|
|
self.assertTrue(
|
|
{"paper_server_search", "paper_server_get", "paper_server_fetch"}.issubset(with_key)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|