Skip to main content
Glama

LinkedIn Content Creation MCP Server

by chrishayuk
test_tools.py40.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

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-linkedin'

If you have feedback or need assistance with the MCP directory API, please join our Discord server