Merge branch 'main' of github.com:taylorwilsdon/google_workspace_mcp into feat/chat-attachment-download
This commit is contained in:
0
tests/core/__init__.py
Normal file
0
tests/core/__init__.py
Normal file
110
tests/core/test_comments.py
Normal file
110
tests/core/test_comments.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Tests for core comments module."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||
|
||||
from core.comments import _read_comments_impl
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_comments_includes_quoted_text():
|
||||
"""Verify that quotedFileContent.value is surfaced in the output."""
|
||||
mock_service = Mock()
|
||||
mock_service.comments.return_value.list.return_value.execute = Mock(
|
||||
return_value={
|
||||
"comments": [
|
||||
{
|
||||
"id": "c1",
|
||||
"content": "Needs a citation here.",
|
||||
"author": {"displayName": "Alice"},
|
||||
"createdTime": "2025-01-15T10:00:00Z",
|
||||
"modifiedTime": "2025-01-15T10:00:00Z",
|
||||
"resolved": False,
|
||||
"quotedFileContent": {
|
||||
"mimeType": "text/html",
|
||||
"value": "the specific text that was highlighted",
|
||||
},
|
||||
"replies": [],
|
||||
},
|
||||
{
|
||||
"id": "c2",
|
||||
"content": "General comment without anchor.",
|
||||
"author": {"displayName": "Bob"},
|
||||
"createdTime": "2025-01-16T09:00:00Z",
|
||||
"modifiedTime": "2025-01-16T09:00:00Z",
|
||||
"resolved": False,
|
||||
"replies": [],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
result = await _read_comments_impl(mock_service, "document", "doc123")
|
||||
|
||||
# Comment with anchor text should show the quoted text
|
||||
assert "Quoted text: the specific text that was highlighted" in result
|
||||
assert "Needs a citation here." in result
|
||||
|
||||
# Comment without anchor text should not have a "Quoted text" line between Bob's author and content
|
||||
# The output uses literal \n joins, so split on that
|
||||
parts = result.split("\\n")
|
||||
bob_section_started = False
|
||||
for part in parts:
|
||||
if "Author: Bob" in part:
|
||||
bob_section_started = True
|
||||
if bob_section_started and "Quoted text:" in part:
|
||||
pytest.fail("Comment without quotedFileContent should not show 'Quoted text'")
|
||||
if bob_section_started and "Content: General comment" in part:
|
||||
break
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_comments_empty():
|
||||
"""Verify empty comments returns appropriate message."""
|
||||
mock_service = Mock()
|
||||
mock_service.comments.return_value.list.return_value.execute = Mock(
|
||||
return_value={"comments": []}
|
||||
)
|
||||
|
||||
result = await _read_comments_impl(mock_service, "document", "doc123")
|
||||
assert "No comments found" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_comments_with_replies():
|
||||
"""Verify replies are included in output."""
|
||||
mock_service = Mock()
|
||||
mock_service.comments.return_value.list.return_value.execute = Mock(
|
||||
return_value={
|
||||
"comments": [
|
||||
{
|
||||
"id": "c1",
|
||||
"content": "Question?",
|
||||
"author": {"displayName": "Alice"},
|
||||
"createdTime": "2025-01-15T10:00:00Z",
|
||||
"modifiedTime": "2025-01-15T10:00:00Z",
|
||||
"resolved": False,
|
||||
"quotedFileContent": {"value": "some text"},
|
||||
"replies": [
|
||||
{
|
||||
"id": "r1",
|
||||
"content": "Answer.",
|
||||
"author": {"displayName": "Bob"},
|
||||
"createdTime": "2025-01-15T11:00:00Z",
|
||||
"modifiedTime": "2025-01-15T11:00:00Z",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
result = await _read_comments_impl(mock_service, "document", "doc123")
|
||||
assert "Question?" in result
|
||||
assert "Answer." in result
|
||||
assert "Bob" in result
|
||||
assert "Quoted text: some text" in result
|
||||
267
tests/gdocs/test_docs_markdown.py
Normal file
267
tests/gdocs/test_docs_markdown.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""Tests for the Google Docs to Markdown converter."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||
|
||||
from gdocs.docs_markdown import (
|
||||
convert_doc_to_markdown,
|
||||
format_comments_appendix,
|
||||
format_comments_inline,
|
||||
parse_drive_comments,
|
||||
)
|
||||
|
||||
|
||||
# --- Fixtures ---
|
||||
|
||||
SIMPLE_DOC = {
|
||||
"title": "Simple Test",
|
||||
"body": {
|
||||
"content": [
|
||||
{"sectionBreak": {"sectionStyle": {}}},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [{"textRun": {"content": "Hello world\n", "textStyle": {}}}],
|
||||
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [
|
||||
{"textRun": {"content": "This is ", "textStyle": {}}},
|
||||
{"textRun": {"content": "bold", "textStyle": {"bold": True}}},
|
||||
{"textRun": {"content": " and ", "textStyle": {}}},
|
||||
{"textRun": {"content": "italic", "textStyle": {"italic": True}}},
|
||||
{"textRun": {"content": " text.\n", "textStyle": {}}},
|
||||
],
|
||||
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
HEADINGS_DOC = {
|
||||
"title": "Headings",
|
||||
"body": {
|
||||
"content": [
|
||||
{"sectionBreak": {"sectionStyle": {}}},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [{"textRun": {"content": "Title\n", "textStyle": {}}}],
|
||||
"paragraphStyle": {"namedStyleType": "TITLE"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [{"textRun": {"content": "Heading one\n", "textStyle": {}}}],
|
||||
"paragraphStyle": {"namedStyleType": "HEADING_1"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [{"textRun": {"content": "Heading two\n", "textStyle": {}}}],
|
||||
"paragraphStyle": {"namedStyleType": "HEADING_2"},
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
TABLE_DOC = {
|
||||
"title": "Table Test",
|
||||
"body": {
|
||||
"content": [
|
||||
{"sectionBreak": {"sectionStyle": {}}},
|
||||
{
|
||||
"table": {
|
||||
"rows": 2,
|
||||
"columns": 2,
|
||||
"tableRows": [
|
||||
{
|
||||
"tableCells": [
|
||||
{"content": [{"paragraph": {"elements": [{"textRun": {"content": "Name\n", "textStyle": {}}}], "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"}}}]},
|
||||
{"content": [{"paragraph": {"elements": [{"textRun": {"content": "Age\n", "textStyle": {}}}], "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"}}}]},
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableCells": [
|
||||
{"content": [{"paragraph": {"elements": [{"textRun": {"content": "Alice\n", "textStyle": {}}}], "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"}}}]},
|
||||
{"content": [{"paragraph": {"elements": [{"textRun": {"content": "30\n", "textStyle": {}}}], "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"}}}]},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
LIST_DOC = {
|
||||
"title": "List Test",
|
||||
"lists": {
|
||||
"kix.list001": {
|
||||
"listProperties": {
|
||||
"nestingLevels": [
|
||||
{"glyphType": "GLYPH_TYPE_UNSPECIFIED", "glyphSymbol": "\u2022"},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"content": [
|
||||
{"sectionBreak": {"sectionStyle": {}}},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [{"textRun": {"content": "Item one\n", "textStyle": {}}}],
|
||||
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
|
||||
"bullet": {"listId": "kix.list001", "nestingLevel": 0},
|
||||
}
|
||||
},
|
||||
{
|
||||
"paragraph": {
|
||||
"elements": [{"textRun": {"content": "Item two\n", "textStyle": {}}}],
|
||||
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
|
||||
"bullet": {"listId": "kix.list001", "nestingLevel": 0},
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# --- Converter tests ---
|
||||
|
||||
|
||||
class TestTextFormatting:
|
||||
def test_plain_text(self):
|
||||
md = convert_doc_to_markdown(SIMPLE_DOC)
|
||||
assert "Hello world" in md
|
||||
|
||||
def test_bold(self):
|
||||
md = convert_doc_to_markdown(SIMPLE_DOC)
|
||||
assert "**bold**" in md
|
||||
|
||||
def test_italic(self):
|
||||
md = convert_doc_to_markdown(SIMPLE_DOC)
|
||||
assert "*italic*" in md
|
||||
|
||||
|
||||
class TestHeadings:
|
||||
def test_title(self):
|
||||
md = convert_doc_to_markdown(HEADINGS_DOC)
|
||||
assert "# Title" in md
|
||||
|
||||
def test_h1(self):
|
||||
md = convert_doc_to_markdown(HEADINGS_DOC)
|
||||
assert "# Heading one" in md
|
||||
|
||||
def test_h2(self):
|
||||
md = convert_doc_to_markdown(HEADINGS_DOC)
|
||||
assert "## Heading two" in md
|
||||
|
||||
|
||||
class TestTables:
|
||||
def test_table_header(self):
|
||||
md = convert_doc_to_markdown(TABLE_DOC)
|
||||
assert "| Name | Age |" in md
|
||||
|
||||
def test_table_separator(self):
|
||||
md = convert_doc_to_markdown(TABLE_DOC)
|
||||
assert "| --- | --- |" in md
|
||||
|
||||
def test_table_row(self):
|
||||
md = convert_doc_to_markdown(TABLE_DOC)
|
||||
assert "| Alice | 30 |" in md
|
||||
|
||||
|
||||
class TestLists:
|
||||
def test_unordered(self):
|
||||
md = convert_doc_to_markdown(LIST_DOC)
|
||||
assert "- Item one" in md
|
||||
assert "- Item two" in md
|
||||
|
||||
|
||||
class TestEmptyDoc:
|
||||
def test_empty(self):
|
||||
md = convert_doc_to_markdown({"title": "Empty", "body": {"content": []}})
|
||||
assert md.strip() == ""
|
||||
|
||||
|
||||
# --- Comment parsing tests ---
|
||||
|
||||
|
||||
class TestParseComments:
|
||||
def test_filters_resolved(self):
|
||||
response = {
|
||||
"comments": [
|
||||
{"content": "open", "resolved": False, "author": {"displayName": "A"}, "replies": []},
|
||||
{"content": "closed", "resolved": True, "author": {"displayName": "B"}, "replies": []},
|
||||
]
|
||||
}
|
||||
result = parse_drive_comments(response, include_resolved=False)
|
||||
assert len(result) == 1
|
||||
assert result[0]["content"] == "open"
|
||||
|
||||
def test_includes_resolved(self):
|
||||
response = {
|
||||
"comments": [
|
||||
{"content": "open", "resolved": False, "author": {"displayName": "A"}, "replies": []},
|
||||
{"content": "closed", "resolved": True, "author": {"displayName": "B"}, "replies": []},
|
||||
]
|
||||
}
|
||||
result = parse_drive_comments(response, include_resolved=True)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_anchor_text(self):
|
||||
response = {
|
||||
"comments": [
|
||||
{
|
||||
"content": "note",
|
||||
"resolved": False,
|
||||
"author": {"displayName": "A"},
|
||||
"quotedFileContent": {"value": "highlighted text"},
|
||||
"replies": [],
|
||||
}
|
||||
]
|
||||
}
|
||||
result = parse_drive_comments(response)
|
||||
assert result[0]["anchor_text"] == "highlighted text"
|
||||
|
||||
|
||||
# --- Comment formatting tests ---
|
||||
|
||||
|
||||
class TestInlineComments:
|
||||
def test_inserts_footnote(self):
|
||||
md = "Some text here."
|
||||
comments = [
|
||||
{"author": "Alice", "content": "Note.", "anchor_text": "text", "replies": [], "resolved": False}
|
||||
]
|
||||
result = format_comments_inline(md, comments)
|
||||
assert "text[^c1]" in result
|
||||
assert "[^c1]: **Alice**: Note." in result
|
||||
|
||||
def test_unmatched_goes_to_appendix(self):
|
||||
md = "No match."
|
||||
comments = [
|
||||
{"author": "Alice", "content": "Note.", "anchor_text": "missing", "replies": [], "resolved": False}
|
||||
]
|
||||
result = format_comments_inline(md, comments)
|
||||
assert "## Comments" in result
|
||||
assert "> missing" in result
|
||||
|
||||
|
||||
class TestAppendixComments:
|
||||
def test_structure(self):
|
||||
comments = [
|
||||
{"author": "Alice", "content": "Note.", "anchor_text": "some text", "replies": [], "resolved": False}
|
||||
]
|
||||
result = format_comments_appendix(comments)
|
||||
assert "## Comments" in result
|
||||
assert "> some text" in result
|
||||
assert "**Alice**: Note." in result
|
||||
|
||||
def test_empty(self):
|
||||
assert format_comments_appendix([]).strip() == ""
|
||||
Reference in New Issue
Block a user