mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-05 03:12:14 +00:00
110 lines
3.7 KiB
Python
110 lines
3.7 KiB
Python
"""Tests for the Prowler documentation search tool.
|
|
|
|
Mintlify moved the docs search to a new endpoint that answers with page
|
|
sections, so a result is a part of a page and has to read as one.
|
|
"""
|
|
|
|
import json
|
|
|
|
from fastmcp import Client
|
|
|
|
SEARCH = "/api/search/prowler"
|
|
|
|
|
|
def search_match(
|
|
path: str = "getting-started/installation",
|
|
*,
|
|
header: str = "Requirements",
|
|
breadcrumbs: tuple[str, ...] = ("Get Started", "Installation"),
|
|
anchor: str | None = "requirements",
|
|
score: float = 4.9,
|
|
):
|
|
"""One match as Mintlify answers with it: a section of a page, not the page."""
|
|
return {
|
|
"page": path,
|
|
"header": header,
|
|
"content": "Prowler runs on Python 3.9 or later.",
|
|
"metadata": {
|
|
"title": header,
|
|
"breadcrumbs": list(breadcrumbs),
|
|
"icon": "",
|
|
"hash": anchor,
|
|
"openapi": "",
|
|
},
|
|
"score": score,
|
|
}
|
|
|
|
|
|
def stub_search_hit(docs_router, *matches):
|
|
"""Serve the search endpoint, with one default match when none are given."""
|
|
if not matches:
|
|
matches = (search_match(),)
|
|
return docs_router.add("POST", SEARCH, json={"results": list(matches)})
|
|
|
|
|
|
async def test_search_returns_the_matching_sections(mcp_root_server, docs_router):
|
|
"""Every field of a result, since the shape of one changed with the endpoint."""
|
|
stub_search_hit(docs_router)
|
|
|
|
async with Client(mcp_root_server) as client:
|
|
result = await client.call_tool("prowler_docs_search", {"term": "install"})
|
|
|
|
match = result.data[0]
|
|
assert match["path"] == "getting-started/installation"
|
|
# The page's title, so a result reads as more than the heading it matched.
|
|
assert match["title"] == "Installation"
|
|
assert match["section"] == "Requirements"
|
|
assert match["breadcrumbs"] == ["Get Started", "Installation"]
|
|
assert match["excerpt"] == "Prowler runs on Python 3.9 or later."
|
|
assert match["score"] == 4.9
|
|
# Anchored: a match is a section, and the page it is on can be a long one.
|
|
assert match["url"] == (
|
|
"https://docs.prowler.com/getting-started/installation#requirements"
|
|
)
|
|
|
|
|
|
async def test_the_search_query_is_sent_as_the_api_expects_it(
|
|
mcp_root_server, docs_router
|
|
):
|
|
"""The endpoint takes a POST body, not the payload the old one took."""
|
|
stub_search_hit(docs_router)
|
|
|
|
async with Client(mcp_root_server) as client:
|
|
await client.call_tool("prowler_docs_search", {"term": "install"})
|
|
|
|
request = docs_router.request_for("POST", SEARCH)
|
|
assert json.loads(request.content) == {"query": "install", "filters": {}}
|
|
|
|
|
|
async def test_a_section_with_no_anchor_links_to_the_page(mcp_root_server, docs_router):
|
|
"""The API sends "" for a page's first section and null for pages without anchors."""
|
|
stub_search_hit(
|
|
docs_router,
|
|
search_match(anchor=""),
|
|
search_match(path="getting-started/requirements", anchor=None),
|
|
)
|
|
|
|
async with Client(mcp_root_server) as client:
|
|
result = await client.call_tool("prowler_docs_search", {"term": "install"})
|
|
|
|
assert result.data[0]["url"] == (
|
|
"https://docs.prowler.com/getting-started/installation"
|
|
)
|
|
assert result.data[1]["url"] == (
|
|
"https://docs.prowler.com/getting-started/requirements"
|
|
)
|
|
|
|
|
|
async def test_page_size_caps_a_response_the_api_did_not_size(
|
|
mcp_root_server, docs_router
|
|
):
|
|
"""The endpoint takes no size argument, so the cap has to be applied here."""
|
|
stub_search_hit(docs_router, *(search_match() for _ in range(6)))
|
|
|
|
async with Client(mcp_root_server) as client:
|
|
result = await client.call_tool(
|
|
"prowler_docs_search", {"term": "install", "page_size": 2}
|
|
)
|
|
|
|
assert len(result.data) == 2
|