test_tools.py•40.7 kB
"""Tests for MCP tools."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from chuk_mcp_linkedin.manager import Draft
@pytest.fixture
def mock_mcp():
"""Create a mock MCP server"""
mcp = MagicMock()
# Store registered tools so we can access them
mcp.registered_tools = {}
def tool_decorator(func):
mcp.registered_tools[func.__name__] = func
return func
mcp.tool = tool_decorator
return mcp
@pytest.fixture
def mock_manager():
"""Create a mock manager"""
manager = MagicMock()
manager.current_draft_id = "draft-123"
return manager
@pytest.fixture
def mock_linkedin_client():
"""Create a mock LinkedIn client"""
client = MagicMock()
client.test_connection = AsyncMock(return_value=True)
client.validate_config = MagicMock(return_value=(True, []))
client.create_text_post = AsyncMock(return_value={"id": "post-123"})
return client
class TestDraftTools:
"""Test draft management tools"""
@pytest.fixture(autouse=True)
def patch_manager(self, mock_manager):
"""Automatically patch get_current_manager for all tests in this class"""
with patch(
"chuk_mcp_linkedin.tools.draft_tools.get_current_manager", return_value=mock_manager
):
yield
def test_register_draft_tools(self, mock_mcp, mock_manager):
"""Test that draft tools are registered"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
tools = register_draft_tools(mock_mcp)
assert "linkedin_create" in tools
assert "linkedin_list" in tools
assert "linkedin_switch" in tools
assert "linkedin_get_info" in tools
assert "linkedin_delete" in tools
assert "linkedin_clear_all" in tools
@pytest.mark.asyncio
async def test_linkedin_create(self, mock_mcp, mock_manager):
"""Test creating a draft"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.create_draft.return_value = mock_draft
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_create"]("Test", "text", "professional")
assert "Created draft 'Test'" in result
assert "draft-123" in result
mock_manager.create_draft.assert_called_once_with(
name="Test", post_type="text", theme="professional"
)
@pytest.mark.asyncio
async def test_linkedin_list(self, mock_mcp, mock_manager):
"""Test listing drafts"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.list_drafts.return_value = [{"id": "draft-1", "name": "Test"}]
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_list"]()
assert "draft-1" in result
assert "Test" in result
mock_manager.list_drafts.assert_called_once()
@pytest.mark.asyncio
async def test_linkedin_switch_success(self, mock_mcp, mock_manager):
"""Test switching to a draft successfully"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.switch_draft.return_value = True
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_switch"]("draft-123")
assert "Switched to draft draft-123" in result
mock_manager.switch_draft.assert_called_once_with("draft-123")
@pytest.mark.asyncio
async def test_linkedin_switch_failure(self, mock_mcp, mock_manager):
"""Test switching to a non-existent draft"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.switch_draft.return_value = False
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_switch"]("nonexistent")
assert "not found" in result
@pytest.mark.asyncio
async def test_linkedin_get_info_with_draft_id(self, mock_mcp, mock_manager):
"""Test getting draft info with draft ID"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_draft.return_value = mock_draft
mock_manager.get_draft_stats.return_value = {"char_count": 100}
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_get_info"]("draft-123")
assert "draft-123" in result or "Test" in result
mock_manager.get_draft.assert_called_once_with("draft-123")
@pytest.mark.asyncio
async def test_linkedin_get_info_current_draft(self, mock_mcp, mock_manager):
"""Test getting info for current draft"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.current_draft_id = "draft-123"
mock_manager.get_draft.return_value = mock_draft
mock_manager.get_draft_stats.return_value = {"char_count": 100}
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_get_info"](None)
assert "draft-123" in result or "Test" in result
@pytest.mark.asyncio
async def test_linkedin_get_info_no_draft(self, mock_mcp, mock_manager):
"""Test getting info when no draft exists"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.current_draft_id = None
mock_manager.get_draft.return_value = None
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_get_info"](None)
assert "No draft found" in result
@pytest.mark.asyncio
async def test_linkedin_delete_success(self, mock_mcp, mock_manager):
"""Test deleting a draft successfully"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.delete_draft.return_value = True
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_delete"]("draft-123")
assert "Deleted draft draft-123" in result
@pytest.mark.asyncio
async def test_linkedin_delete_failure(self, mock_mcp, mock_manager):
"""Test deleting non-existent draft"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.delete_draft.return_value = False
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_delete"]("nonexistent")
assert "not found" in result
@pytest.mark.asyncio
async def test_linkedin_clear_all(self, mock_mcp, mock_manager):
"""Test clearing all drafts"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.clear_all_drafts.return_value = 5
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_clear_all"]()
assert "Cleared 5 drafts" in result
@pytest.mark.asyncio
async def test_linkedin_preview_url_success_memory(self, mock_mcp, mock_manager):
"""Test generating preview URL with memory storage"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_draft = Draft(
draft_id="draft-123", name="Test Draft", post_type="text", content={}, theme=None
)
mock_manager.get_draft.return_value = mock_draft
mock_manager.artifact_provider = "memory"
mock_manager.generate_preview_url = AsyncMock(
return_value="http://localhost:8000/preview/token123"
)
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_preview_url"](draft_id="draft-123")
assert "Preview URL: http://localhost:8000/preview/token123" in result
assert "Draft: Test Draft" in result
assert "Draft ID: draft-123" in result
assert "URL Type: token-based URL" in result
assert "linkedin-mcp http --port 8000" in result
mock_manager.generate_preview_url.assert_called_once_with(
draft_id="draft-123", base_url="http://localhost:8000", expires_in=3600
)
@pytest.mark.asyncio
async def test_linkedin_preview_url_success_s3(self, mock_mcp, mock_manager):
"""Test generating preview URL with S3 storage"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_draft = Draft(
draft_id="draft-456", name="S3 Draft", post_type="text", content={}, theme=None
)
mock_manager.get_draft.return_value = mock_draft
mock_manager.artifact_provider = "s3"
mock_manager.generate_preview_url = AsyncMock(
return_value="https://s3.amazonaws.com/signed-url"
)
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_preview_url"](draft_id="draft-456", expires_in=7200)
assert "Preview URL: https://s3.amazonaws.com/signed-url" in result
assert "Draft: S3 Draft" in result
assert "URL Type: signed URL (S3)" in result
assert "Signed URL expires in 7200 seconds" in result
mock_manager.generate_preview_url.assert_called_once_with(
draft_id="draft-456", base_url="http://localhost:8000", expires_in=7200
)
@pytest.mark.asyncio
async def test_linkedin_preview_url_no_draft_id(self, mock_mcp, mock_manager):
"""Test preview URL when no draft is selected"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.current_draft_id = None
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_preview_url"]()
assert "Error: No draft selected" in result
@pytest.mark.asyncio
async def test_linkedin_preview_url_generation_failed(self, mock_mcp, mock_manager):
"""Test preview URL when generation fails"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_manager.generate_preview_url = AsyncMock(return_value=None)
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_preview_url"](draft_id="draft-123")
assert "Error: Failed to generate preview URL" in result
@pytest.mark.asyncio
async def test_linkedin_preview_url_use_current_draft(self, mock_mcp, mock_manager):
"""Test preview URL uses current draft when draft_id not provided"""
from chuk_mcp_linkedin.tools.draft_tools import register_draft_tools
mock_draft = Draft(
draft_id="current-draft", name="Current", post_type="text", content={}, theme=None
)
mock_manager.current_draft_id = "current-draft"
mock_manager.get_draft.return_value = mock_draft
mock_manager.artifact_provider = "filesystem"
mock_manager.generate_preview_url = AsyncMock(
return_value="http://localhost:8000/preview/current"
)
tools = register_draft_tools(mock_mcp)
result = await tools["linkedin_preview_url"]()
assert "Draft ID: current-draft" in result
mock_manager.generate_preview_url.assert_called_once_with(
draft_id="current-draft", base_url="http://localhost:8000", expires_in=3600
)
class TestPublishingTools:
"""Test publishing tools"""
@pytest.fixture(autouse=True)
def patch_manager(self, mock_manager):
"""Automatically patch get_current_manager for all tests in this class"""
with patch(
"chuk_mcp_linkedin.tools.publishing_tools.get_current_manager",
return_value=mock_manager,
):
yield
def test_register_publishing_tools(self, mock_mcp, mock_manager, mock_linkedin_client):
"""Test that publishing tools are registered"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
assert "linkedin_publish" in tools
assert "linkedin_test_connection" in tools
assert len(tools) == 2 # Only two publishing tools with OAuth
@pytest.mark.asyncio
async def test_linkedin_publish_no_draft(self, mock_mcp, mock_manager, mock_linkedin_client):
"""Test publishing with no active draft"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_manager.get_current_draft.return_value = None
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"]()
assert result["status"] == "error"
assert result["error_type"] == "no_draft"
assert "No active draft" in result["error"]
@pytest.mark.asyncio
async def test_linkedin_publish_not_configured(
self, mock_mcp, mock_manager, mock_linkedin_client
):
"""Test publishing without OAuth token"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_draft = Draft(
draft_id="draft-123",
name="Test",
post_type="text",
content={"composed_text": "Test post"},
theme=None,
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"]()
assert result["status"] == "error"
assert result["error_type"] == "missing_oauth_token"
assert "Authentication required" in result["error"]
@pytest.mark.asyncio
async def test_linkedin_publish_no_content(self, mock_mcp, mock_manager, mock_linkedin_client):
"""Test publishing with no content"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"](_external_access_token="test_token")
assert result["status"] == "error"
assert result["error_type"] == "missing_content"
assert "No post content" in result["error"]
@pytest.mark.asyncio
async def test_linkedin_publish_dry_run(self, mock_mcp, mock_manager, mock_linkedin_client):
"""Test publishing in dry run mode"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_draft = Draft(
draft_id="draft-123",
name="Test",
post_type="text",
content={"composed_text": "Test post content"},
theme=None,
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"](dry_run=True, _external_access_token="test_token")
assert result["status"] == "dry_run"
assert result["character_count"] == 17
assert "Test post content" in result["full_content"]
@pytest.mark.asyncio
async def test_linkedin_publish_without_token(
self, mock_mcp, mock_manager, mock_linkedin_client
):
"""Test publishing without OAuth token"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_draft = Draft(
draft_id="draft-123",
name="Test",
post_type="text",
content={"composed_text": "Test post"},
theme=None,
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"](dry_run=False)
assert result["status"] == "error"
assert result["error_type"] == "missing_oauth_token"
assert "Authentication required" in result["error"]
@pytest.mark.asyncio
async def test_linkedin_publish_success(self, mock_mcp, mock_manager, mock_linkedin_client):
"""Test successful publishing with OAuth"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_draft = Draft(
draft_id="draft-123",
name="Test",
post_type="text",
content={"composed_text": "Test post"},
theme=None,
)
mock_manager.get_current_draft.return_value = mock_draft
# Mock httpx client for userinfo fetch
with patch("httpx.AsyncClient") as mock_httpx_client:
mock_client_instance = mock_httpx_client.return_value.__aenter__.return_value
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json = MagicMock(return_value={"sub": "test_person_id"})
mock_client_instance.get = AsyncMock(return_value=mock_response)
# Mock LinkedInClient for post creation
with patch("chuk_mcp_linkedin.api.LinkedInClient") as mock_client_class:
mock_linkedin_instance = mock_client_class.return_value
mock_linkedin_instance.create_text_post = AsyncMock(
return_value={"id": "urn:li:share:post-123"}
)
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"](
visibility="PUBLIC", dry_run=False, _external_access_token="test_token"
)
assert result["status"] == "published"
assert result["post_id"] == "urn:li:share:post-123"
assert "post_url" in result
@pytest.mark.asyncio
async def test_linkedin_publish_api_error(self, mock_mcp, mock_manager, mock_linkedin_client):
"""Test publishing with API error"""
from chuk_mcp_linkedin.api import LinkedInAPIError
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
mock_draft = Draft(
draft_id="draft-123",
name="Test",
post_type="text",
content={"composed_text": "Test post"},
theme=None,
)
mock_manager.get_current_draft.return_value = mock_draft
# Mock httpx client for userinfo fetch
with patch("httpx.AsyncClient") as mock_httpx_client:
mock_client_instance = mock_httpx_client.return_value.__aenter__.return_value
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json = MagicMock(return_value={"sub": "test_person_id"})
mock_client_instance.get = AsyncMock(return_value=mock_response)
# Mock LinkedInClient to raise error
with patch("chuk_mcp_linkedin.api.LinkedInClient") as mock_client_class:
mock_linkedin_instance = mock_client_class.return_value
mock_linkedin_instance.create_text_post = AsyncMock(
side_effect=LinkedInAPIError("API Error")
)
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_publish"](_external_access_token="test_token")
assert result["status"] == "error"
assert result["error_type"] == "linkedin_api_error"
@pytest.mark.asyncio
async def test_linkedin_test_connection_success(
self, mock_mcp, mock_manager, mock_linkedin_client
):
"""Test successful connection test with OAuth"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
# Mock httpx client for userinfo fetch
with patch("httpx.AsyncClient") as mock_httpx_client:
mock_client_instance = mock_httpx_client.return_value.__aenter__.return_value
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json = MagicMock(
return_value={
"sub": "test_person_id",
"name": "Test User",
"email": "test@example.com",
}
)
mock_client_instance.get = AsyncMock(return_value=mock_response)
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_test_connection"](_external_access_token="test_token")
assert result["status"] == "connected"
assert result["name"] == "Test User"
assert result["email"] == "test@example.com"
@pytest.mark.asyncio
async def test_linkedin_test_connection_failure(
self, mock_mcp, mock_manager, mock_linkedin_client
):
"""Test connection test without OAuth token"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_test_connection"]()
assert result["status"] == "error"
assert result["error_type"] == "missing_oauth_token"
assert "Authentication required" in result["error"]
@pytest.mark.asyncio
async def test_linkedin_test_connection_invalid_credentials(
self, mock_mcp, mock_manager, mock_linkedin_client
):
"""Test connection test with invalid OAuth token"""
from chuk_mcp_linkedin.tools.publishing_tools import register_publishing_tools
# Mock httpx client to raise an error
with patch("httpx.AsyncClient") as mock_httpx_client:
mock_client_instance = mock_httpx_client.return_value.__aenter__.return_value
mock_response = AsyncMock()
mock_response.raise_for_status = AsyncMock(side_effect=Exception("Unauthorized"))
mock_client_instance.get = AsyncMock(return_value=mock_response)
tools = register_publishing_tools(mock_mcp, mock_linkedin_client)
result = await tools["linkedin_test_connection"](_external_access_token="invalid_token")
assert result["status"] == "error"
assert result["error_type"] == "connection_failed"
class TestRegistryTools:
"""Test registry tools"""
def test_register_registry_tools(self, mock_mcp, mock_manager):
"""Test that registry tools are registered"""
from chuk_mcp_linkedin.tools.registry_tools import register_registry_tools
tools = register_registry_tools(mock_mcp)
assert "linkedin_list_components" in tools
assert "linkedin_get_component_info" in tools
assert "linkedin_get_recommendations" in tools
assert "linkedin_get_system_overview" in tools
@pytest.mark.asyncio
async def test_linkedin_list_components(self, mock_mcp, mock_manager):
"""Test listing components"""
from chuk_mcp_linkedin.tools.registry_tools import register_registry_tools
tools = register_registry_tools(mock_mcp)
result = await tools["linkedin_list_components"]()
# Should return JSON with components (can be list or dict)
data = json.loads(result)
assert isinstance(data, (list, dict))
@pytest.mark.asyncio
async def test_linkedin_get_component_info(self, mock_mcp, mock_manager):
"""Test getting component info"""
from chuk_mcp_linkedin.tools.registry_tools import register_registry_tools
tools = register_registry_tools(mock_mcp)
result = await tools["linkedin_get_component_info"]("hook")
# Should return JSON with component info
data = json.loads(result)
assert isinstance(data, dict)
@pytest.mark.asyncio
async def test_linkedin_get_recommendations(self, mock_mcp, mock_manager):
"""Test getting recommendations"""
from chuk_mcp_linkedin.tools.registry_tools import register_registry_tools
tools = register_registry_tools(mock_mcp)
result = await tools["linkedin_get_recommendations"]("engagement")
# Should return JSON with recommendations
data = json.loads(result)
assert isinstance(data, dict)
@pytest.mark.asyncio
async def test_linkedin_get_system_overview(self, mock_mcp, mock_manager):
"""Test getting system overview"""
from chuk_mcp_linkedin.tools.registry_tools import register_registry_tools
tools = register_registry_tools(mock_mcp)
result = await tools["linkedin_get_system_overview"]()
# Should return JSON with system overview
data = json.loads(result)
assert isinstance(data, dict)
class TestThemeTools:
"""Test theme tools"""
@pytest.fixture(autouse=True)
def patch_manager(self, mock_manager):
"""Automatically patch get_current_manager for all tests in this class"""
with patch(
"chuk_mcp_linkedin.tools.theme_tools.get_current_manager", return_value=mock_manager
):
yield
def test_register_theme_tools(self, mock_mcp, mock_manager):
"""Test that theme tools are registered"""
from chuk_mcp_linkedin.tools.theme_tools import register_theme_tools
tools = register_theme_tools(mock_mcp)
assert "linkedin_list_themes" in tools
assert "linkedin_get_theme" in tools
assert "linkedin_apply_theme" in tools
@pytest.mark.asyncio
async def test_linkedin_list_themes(self, mock_mcp, mock_manager):
"""Test listing themes"""
from chuk_mcp_linkedin.tools.theme_tools import register_theme_tools
tools = register_theme_tools(mock_mcp)
result = await tools["linkedin_list_themes"]()
# Should return JSON with themes (can be list or dict)
data = json.loads(result)
assert isinstance(data, (list, dict))
@pytest.mark.asyncio
async def test_linkedin_get_theme(self, mock_mcp, mock_manager):
"""Test getting theme info"""
from chuk_mcp_linkedin.tools.theme_tools import register_theme_tools
tools = register_theme_tools(mock_mcp)
# Use a valid theme name
result = await tools["linkedin_get_theme"]("thought_leader")
# Should return JSON with theme info
data = json.loads(result)
assert isinstance(data, dict)
@pytest.mark.asyncio
async def test_linkedin_apply_theme_no_draft(self, mock_mcp, mock_manager):
"""Test applying theme with no active draft"""
from chuk_mcp_linkedin.tools.theme_tools import register_theme_tools
mock_manager.get_current_draft.return_value = None
tools = register_theme_tools(mock_mcp)
result = await tools["linkedin_apply_theme"]("professional")
assert "No active draft" in result
@pytest.mark.asyncio
async def test_linkedin_apply_theme_success(self, mock_mcp, mock_manager):
"""Test successfully applying theme"""
from chuk_mcp_linkedin.tools.theme_tools import register_theme_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_theme_tools(mock_mcp)
result = await tools["linkedin_apply_theme"]("professional")
assert "Applied theme 'professional'" in result
mock_manager.update_draft.assert_called_once_with("draft-123", theme="professional")
class TestCompositionTools:
"""Test composition tools"""
@pytest.fixture(autouse=True)
def patch_manager(self, mock_manager):
"""Automatically patch get_current_manager for all tests in this class"""
with patch(
"chuk_mcp_linkedin.tools.composition_tools.get_current_manager",
return_value=mock_manager,
):
yield
def test_register_composition_tools(self, mock_mcp, mock_manager):
"""Test that composition tools are registered"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
tools = register_composition_tools(mock_mcp)
# Check content tools
assert "linkedin_add_hook" in tools
assert "linkedin_add_body" in tools
assert "linkedin_add_cta" in tools
assert "linkedin_add_hashtags" in tools
# Check chart tools
assert "linkedin_add_bar_chart" in tools
assert "linkedin_add_metrics_chart" in tools
assert "linkedin_add_comparison_chart" in tools
assert "linkedin_add_progress_chart" in tools
assert "linkedin_add_ranking_chart" in tools
# Check feature tools
assert "linkedin_add_quote" in tools
assert "linkedin_add_big_stat" in tools
assert "linkedin_add_timeline" in tools
assert "linkedin_add_key_takeaway" in tools
assert "linkedin_add_pro_con" in tools
assert "linkedin_add_checklist" in tools
assert "linkedin_add_before_after" in tools
assert "linkedin_add_tip_box" in tools
assert "linkedin_add_stats_grid" in tools
assert "linkedin_add_poll_preview" in tools
assert "linkedin_add_feature_list" in tools
assert "linkedin_add_numbered_list" in tools
# Check composition tools
assert "linkedin_compose_post" in tools
assert "linkedin_get_preview" in tools
assert "linkedin_preview_html" in tools
assert "linkedin_export_draft" in tools
@pytest.mark.asyncio
async def test_linkedin_add_hook_no_draft(self, mock_mcp, mock_manager):
"""Test adding hook with no active draft"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_manager.get_current_draft.return_value = None
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_hook"]("question", "Why is AI important?")
assert "No active draft" in result
@pytest.mark.asyncio
async def test_linkedin_add_hook_success(self, mock_mcp, mock_manager):
"""Test successfully adding hook"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_hook"]("question", "Why is AI important?")
assert "Added question hook" in result
# No longer calls update_draft on every component add
@pytest.mark.asyncio
async def test_linkedin_add_body_success(self, mock_mcp, mock_manager):
"""Test successfully adding body"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_body"]("Main content here", "linear")
assert "Added body" in result
assert "linear" in result
@pytest.mark.asyncio
async def test_linkedin_add_cta_success(self, mock_mcp, mock_manager):
"""Test successfully adding CTA"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_cta"]("direct", "Click here!")
assert "Added direct CTA" in result
@pytest.mark.asyncio
async def test_linkedin_add_bar_chart_validation_error(self, mock_mcp, mock_manager):
"""Test bar chart with validation error"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_bar_chart"]({}, "Title")
# Empty dict is valid (just no bars), validation happens in component
assert "Added bar chart with 0 bars" in result
@pytest.mark.asyncio
async def test_linkedin_add_bar_chart_success(self, mock_mcp, mock_manager):
"""Test successfully adding bar chart"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_bar_chart"]({"A": 10, "B": 20}, "Chart")
assert "Added bar chart" in result
assert "2" in result
@pytest.mark.asyncio
async def test_linkedin_compose_post_no_draft(self, mock_mcp, mock_manager):
"""Test composing post with no draft"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_manager.get_current_draft.return_value = None
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_compose_post"]()
assert "No active draft" in result
@pytest.mark.asyncio
async def test_linkedin_compose_post_success(self, mock_mcp, mock_manager):
"""Test successfully composing post"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123",
name="Test",
post_type="text",
content={"components": [{"component": "hook", "type": "question", "content": "Test?"}]},
theme=None,
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_compose_post"](optimize=False)
assert "Composed post" in result
@pytest.mark.asyncio
async def test_linkedin_get_preview_success(self, mock_mcp, mock_manager):
"""Test getting preview"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
# Add content first so there's something to preview
await tools["linkedin_add_body"]("Test content for preview")
result = await tools["linkedin_get_preview"]()
assert "Preview" in result
assert "chars" in result
@pytest.mark.asyncio
async def test_linkedin_preview_html_no_draft(self, mock_mcp, mock_manager):
"""Test HTML preview with no draft"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_manager.get_current_draft.return_value = None
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_preview_html"]()
assert "No active draft" in result
@pytest.mark.asyncio
async def test_linkedin_preview_html_success(self, mock_mcp, mock_manager):
"""Test successful HTML preview generation"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
mock_manager.generate_preview_url = AsyncMock(
return_value="http://localhost:8000/preview/abc123"
)
tools = register_composition_tools(mock_mcp)
with patch("webbrowser.open") as mock_browser:
result = await tools["linkedin_preview_html"](open_browser=True)
assert "Preview generated" in result
assert "http://localhost:8000/preview/abc123" in result
mock_browser.assert_called_once()
@pytest.mark.asyncio
async def test_linkedin_preview_html_no_browser(self, mock_mcp, mock_manager):
"""Test HTML preview without opening browser"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
mock_manager.generate_preview_url = AsyncMock(
return_value="http://localhost:8000/preview/abc123"
)
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_preview_html"](open_browser=False)
assert "Preview URL" in result
assert "http://localhost:8000/preview/abc123" in result
@pytest.mark.asyncio
async def test_linkedin_export_draft_no_draft(self, mock_mcp, mock_manager):
"""Test exporting with no draft"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_manager.get_current_draft.return_value = None
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_export_draft"]()
assert "No active draft" in result
@pytest.mark.asyncio
async def test_linkedin_export_draft_success(self, mock_mcp, mock_manager):
"""Test successful draft export"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
mock_manager.export_draft.return_value = '{"draft_id": "draft-123"}'
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_export_draft"]()
assert "draft-123" in result
@pytest.mark.asyncio
async def test_linkedin_add_separator(self, mock_mcp, mock_manager):
"""Test adding separator"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_separator"]("line")
assert "Added line separator" in result
@pytest.mark.asyncio
async def test_linkedin_add_hashtags(self, mock_mcp, mock_manager):
"""Test adding hashtags"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_add_hashtags"](["ai", "tech", "innovation"])
assert "Added 3 hashtags" in result
class TestToolsInit:
"""Test tools package initialization"""
def test_all_tools_importable(self):
"""Test that all tools can be imported from __init__"""
from chuk_mcp_linkedin.tools import (
register_composition_tools,
register_draft_tools,
register_publishing_tools,
register_registry_tools,
register_theme_tools,
)
assert register_draft_tools is not None
assert register_composition_tools is not None
assert register_theme_tools is not None
assert register_publishing_tools is not None
assert register_registry_tools is not None