import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from tools.search_tool import EuropePMCTool, PubMedTool, UnpaywallTool
from tools.wikipedia_tool import WikipediaTool, WikidataTool
from tools.database_tool import Neo4jTool, MongoTool
def _client_ctx(responses):
"""Build a mocked httpx.AsyncClient whose GET returns `responses` in order."""
client = MagicMock()
ctx = MagicMock()
it = iter(responses)
async def get(*args, **kwargs):
return next(it)
ctx.get = AsyncMock(side_effect=get)
client.__aenter__ = AsyncMock(return_value=ctx)
client.__aexit__ = AsyncMock(return_value=None)
return client
def _resp(status=200, json=None, text=""):
r = MagicMock()
r.status_code = status
r.json.return_value = json if json is not None else {}
r.text = text
r.raise_for_status = MagicMock()
if status >= 400:
r.raise_for_status.side_effect = RuntimeError(f"HTTP {status}")
return r
@pytest.mark.asyncio
async def test_europepmc_tool_returns_papers():
tool = EuropePMCTool()
mock_response = {
"resultList": {
"result": [
{
"title": "A Study on Federated Learning",
"authorString": "Alice A, Bob B",
"pubYear": "2023",
"abstractText": "We study federated learning.",
"citedByCount": 12,
"doi": "10.1234/test",
"pmcid": "PMC1234567",
"id": "PMC1234567",
}
]
}
}
with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(json=mock_response)])):
result = await tool.run(query="federated learning", limit=5)
assert result.success
assert len(result.data) == 1
assert result.data[0]["title"] == "A Study on Federated Learning"
assert result.data[0]["url"] == "https://europepmc.org/article/PMC/PMC1234567"
assert result.data[0]["citation_count"] == 12
@pytest.mark.asyncio
async def test_pubmed_tool_returns_papers():
tool = PubMedTool()
esearch_json = {"esearchresult": {"idlist": ["33000001"]}}
efetch_xml = """
33000001
Neural networks for medical imaging
We train CNNs on scans.
2021
JaneDoe
10.9999/medimg
"""
with patch(
"httpx.AsyncClient",
return_value=_client_ctx([_resp(json=esearch_json), _resp(text=efetch_xml)]),
):
result = await tool.run(query="medical imaging", limit=5)
assert result.success
assert len(result.data) == 1
assert result.data[0]["title"] == "Neural networks for medical imaging"
assert result.data[0]["abstract"] == "We train CNNs on scans."
assert result.data[0]["doi"] == "10.9999/medimg"
assert result.data[0]["url"].startswith("https://pubmed.ncbi.nlm.nih.gov/")
@pytest.mark.asyncio
async def test_pubmed_tool_no_ids_returns_empty():
tool = PubMedTool()
with patch(
"httpx.AsyncClient",
return_value=_client_ctx([_resp(json={"esearchresult": {"idlist": []}})]),
):
result = await tool.run(query="nothing found", limit=5)
assert result.success
assert result.data == []
@pytest.mark.asyncio
async def test_unpaywall_lookup_returns_oa_url():
tool = UnpaywallTool()
mock_response = {
"is_oa": True,
"best_oa_location": {"url": "https://oa.example.com/paper.pdf"},
}
with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(json=mock_response)])):
result = await tool.run("10.1234/test")
assert result.success
assert result.data["is_oa"] is True
assert result.data["url"] == "https://oa.example.com/paper.pdf"
@pytest.mark.asyncio
async def test_unpaywall_lookup_not_oa_404():
tool = UnpaywallTool()
with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(status=404)])):
result = await tool.run("10.1234/missing")
assert result.success
assert result.data["is_oa"] is False
assert result.data["url"] == ""
@pytest.mark.asyncio
async def test_wikipedia_tool_returns_snippets():
tool = WikipediaTool()
search_json = {
"query": {
"search": [
{"title": "Momentum Investing", "snippet": "Momentum investing basics"},
]
}
}
extracts_json = {
"query": {
"pages": {
"1": {"title": "Momentum Investing", "extract": "Momentum investing is a strategy..."}
}
}
}
with patch(
"httpx.AsyncClient",
return_value=_client_ctx([_resp(json=search_json), _resp(json=extracts_json)]),
):
result = await tool.run(query="momentum investing", limit=3)
assert result.success
assert len(result.data) == 1
assert result.data[0]["title"] == "Momentum Investing"
assert result.data[0]["url"] == "https://en.wikipedia.org/wiki/Momentum_Investing"
assert "Momentum investing is a strategy" in result.data[0]["snippet"]
@pytest.mark.asyncio
async def test_wikidata_tool_returns_entities():
tool = WikidataTool()
mock_response = {
"search": [
{"id": "Q42", "label": "Douglas Adams", "description": "English writer and humorist"},
]
}
with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(json=mock_response)])):
result = await tool.run(query="douglas adams", limit=3)
assert result.success
assert result.data[0]["id"] == "Q42"
assert result.data[0]["url"] == "https://www.wikidata.org/wiki/Q42"
@pytest.mark.asyncio
async def test_academic_tools_serialize_parallel_calls():
"""Concurrent keyless academic searches must be spaced out (rate-limit regression)."""
import asyncio
import time
import tools.search_tool as st
original_interval = st._ACADEMIC_MIN_INTERVAL
original_last = st._academic_last_request_at
st._ACADEMIC_MIN_INTERVAL = 0.2
st._academic_last_request_at = 0.0
starts = []
empty_ok = {"resultList": {"result": []}}
def make_ctx():
client = MagicMock()
ctx = MagicMock()
async def get(*args, **kwargs):
starts.append(time.monotonic())
r = MagicMock()
r.json.return_value = empty_ok
r.raise_for_status = MagicMock()
return r
ctx.get = AsyncMock(side_effect=get)
client.__aenter__ = AsyncMock(return_value=ctx)
client.__aexit__ = AsyncMock(return_value=None)
return client
try:
with patch("httpx.AsyncClient", return_value=make_ctx()):
tool = EuropePMCTool()
results = await asyncio.gather(*[tool.run(query="q", limit=5) for _ in range(3)])
finally:
st._ACADEMIC_MIN_INTERVAL = original_interval
st._academic_last_request_at = original_last
assert all(r.success for r in results)
assert len(starts) == 3
deltas = [starts[i + 1] - starts[i] for i in range(2)]
assert all(d >= 0.18 for d in deltas)
@pytest.mark.asyncio
async def test_neo4j_tool_query(mock_neo4j):
mock_neo4j.run.return_value = [{"n": "value"}]
tool = Neo4jTool(neo4j=mock_neo4j)
result = await tool.run(query="MATCH (n) RETURN n LIMIT 1")
assert result.success
assert result.data == [{"n": "value"}]