arcade-mcp/toolkits/notion/tests/test_tools_pages.py
Eric Gustin 4598e1e4d4
Updates to Notion toolkit (#476)
### Adds one new tool to the Notion toolkit
* **AppendContentToEndOfPage**: Append markdown content to the end of a
Notion page by its ID or title

### Fixes a bug
* The **CreatePage** would previously fail if the content provided was
broken into more than 100 blocks. I added 'chunking' logic to support >
100 blocks.
2025-07-18 17:35:12 -07:00

246 lines
8.1 KiB
Python

import pytest
# Simulates a single block with no children
fake_get_next_page_simple = (
{
"results": [
{
"object": "block",
"id": "block1",
"has_children": False,
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"plain_text": "Hello World",
"type": "text",
"text": {"content": "Hello World", "link": None},
"annotations": {
"bold": False,
"italic": False,
"underline": False,
"strikethrough": False,
"code": False,
"color": "default",
},
"href": None,
}
]
},
}
]
},
False,
None,
)
# Simulates a parent block with a child block
fake_get_next_page_nested = (
{
"results": [
{
"object": "block",
"id": "parent_block",
"has_children": True,
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"plain_text": "Parent Block",
"type": "text",
"text": {"content": "Parent Block", "link": None},
"annotations": {
"bold": False,
"italic": False,
"underline": False,
"strikethrough": False,
"code": False,
"color": "default",
},
"href": None,
}
]
},
}
]
},
False,
None,
)
fake_get_next_page_parent_block = (
{
"results": [
{
"object": "block",
"id": "child_block",
"has_children": False,
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"plain_text": "Child Block",
"type": "text",
"text": {"content": "Child Block", "link": None},
"annotations": {
"bold": False,
"italic": False,
"underline": False,
"strikethrough": False,
"code": False,
"color": "default",
},
"href": None,
}
]
},
}
]
},
False,
None,
)
@pytest.fixture
def setup_notion_pages(monkeypatch):
from arcade_notion_toolkit.tools import pages
monkeypatch.setattr(pages, "get_headers", lambda ctx: {"Authorization": "Bearer test"})
monkeypatch.setattr(
pages, "get_url", lambda endpoint, block_id=None: f"https://dummy/{block_id}"
)
return pages
@pytest.mark.asyncio
async def test_get_page_content_by_id_simple(mock_context, monkeypatch, setup_notion_pages):
pages = setup_notion_pages
# Patch get_object_metadata to return a dummy page with title 'Test Page'
async def fake_get_object_metadata(context, object_id=None, **kwargs):
return {
"id": object_id,
"object": "page",
"properties": {"title": {"title": [{"plain_text": "Test Page"}]}},
}
monkeypatch.setattr(pages, "get_object_metadata", fake_get_object_metadata)
# Patch get_next_page to
async def fake_get_next_page(client, url, headers, params, cursor):
return fake_get_next_page_simple
monkeypatch.setattr(pages, "get_next_page", fake_get_next_page)
# Call the function under test
result = await pages.get_page_content_by_id(mock_context, "test_page_id")
expected = "# Test Page\nHello World \n"
assert result == expected
@pytest.mark.asyncio
async def test_get_page_content_by_id_nested(mock_context, monkeypatch, setup_notion_pages):
pages = setup_notion_pages
# Patch get_object_metadata to return a dummy page with title 'Test Nested'
async def fake_get_object_metadata(context, object_id=None, **kwargs):
return {
"id": object_id,
"object": "page",
"properties": {"title": {"title": [{"plain_text": "Test Nested"}]}},
}
monkeypatch.setattr(pages, "get_object_metadata", fake_get_object_metadata)
# Patch get_next_page
async def fake_get_next_page(client, url, headers, params, cursor):
if url == "https://dummy/test_nested":
return fake_get_next_page_nested
elif url == "https://dummy/parent_block":
return fake_get_next_page_parent_block
return ({"results": []}, False, None)
monkeypatch.setattr(pages, "get_next_page", fake_get_next_page)
# Call the function under test
result = await pages.get_page_content_by_id(mock_context, "test_nested")
expected = "# Test Nested\nParent Block \n Child Block \n"
assert result == expected
@pytest.mark.asyncio
async def test_append_content_to_end_of_page_with_large_content(
mock_context, monkeypatch, setup_notion_pages
):
pages = setup_notion_pages
# Mock is_page_id to return True
monkeypatch.setattr(pages, "is_page_id", lambda x: True)
# Create 150 dummy blocks (more than the 100 chunk size)
dummy_blocks = []
for i in range(150):
dummy_blocks.append({
"object": "block",
"type": "paragraph",
"paragraph": {"rich_text": [{"type": "text", "text": {"content": f"Block {i}"}}]},
})
# Mock convert_markdown_to_blocks to return our 150 blocks
def fake_convert_markdown_to_blocks(content):
return dummy_blocks
monkeypatch.setattr(pages, "convert_markdown_to_blocks", fake_convert_markdown_to_blocks)
# Mock get_page_url to return a dummy URL
async def fake_get_page_url(context, page_id):
return f"https://notion.so/page/{page_id}"
monkeypatch.setattr(pages, "get_page_url", fake_get_page_url)
# Track the HTTP requests
request_count = 0
request_payloads = []
class MockResponse:
def raise_for_status(self):
pass
class MockClient:
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
async def patch(self, url, headers, json):
nonlocal request_count
request_count += 1
request_payloads.append(json)
return MockResponse()
# Mock httpx.AsyncClient
monkeypatch.setattr(pages.httpx, "AsyncClient", MockClient)
_ = await pages.append_content_to_end_of_page(
mock_context, "test_page_id", "Large content that will be converted to 150 blocks"
)
# Verify chunking behavior: 150 blocks should be split into 2 requests
# First request: 100 blocks (0-99)
# Second request: 50 blocks (100-149)
assert request_count == 2
assert len(request_payloads) == 2
# Verify first chunk has 100 blocks
first_chunk = request_payloads[0]["children"]
assert len(first_chunk) == 100
assert first_chunk[0]["paragraph"]["rich_text"][0]["text"]["content"] == "Block 0"
assert first_chunk[99]["paragraph"]["rich_text"][0]["text"]["content"] == "Block 99"
# Verify second chunk has 50 blocks
second_chunk = request_payloads[1]["children"]
assert len(second_chunk) == 50
assert second_chunk[0]["paragraph"]["rich_text"][0]["text"]["content"] == "Block 100"
assert second_chunk[49]["paragraph"]["rich_text"][0]["text"]["content"] == "Block 149"