Files
leo-claude-mktplace/plugins/projman/mcp-servers/wikijs/tests/test_wikijs_client.py
lmiranda d84425cbb0 refactor: bundle MCP servers inside plugins for cache compatibility
Claude Code only caches the plugin directory when installed from a
marketplace, not parent directories. This broke the shared mcp-servers/
architecture because relative paths like ../../mcp-servers/ resolved
to non-existent locations in the cache.

Changes:
- Move gitea and wikijs MCP servers into plugins/projman/mcp-servers/
- Move netbox MCP server into plugins/cmdb-assistant/mcp-servers/
- Update .mcp.json files to use ${CLAUDE_PLUGIN_ROOT}/mcp-servers/
- Update setup.sh to handle new bundled structure
- Add netbox.env config template to setup.sh
- Update CLAUDE.md and CANONICAL-PATHS.md documentation

This ensures plugins work correctly when installed and cached.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 17:23:02 -05:00

356 lines
11 KiB
Python

"""
Tests for Wiki.js GraphQL client.
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from mcp_server.wikijs_client import WikiJSClient
@pytest.fixture
def client():
"""Create WikiJSClient instance for testing"""
return WikiJSClient(
api_url="http://wiki.test.com/graphql",
api_token="test_token_123",
base_path="/test-company",
project="projects/test-project"
)
@pytest.fixture
def company_client():
"""Create WikiJSClient in company mode"""
return WikiJSClient(
api_url="http://wiki.test.com/graphql",
api_token="test_token_123",
base_path="/test-company",
project=None # Company mode
)
def test_client_initialization(client):
"""Test client initializes with correct settings"""
assert client.api_url == "http://wiki.test.com/graphql"
assert client.api_token == "test_token_123"
assert client.base_path == "/test-company"
assert client.project == "projects/test-project"
assert client.mode == 'project'
def test_company_mode_initialization(company_client):
"""Test client initializes in company mode"""
assert company_client.mode == 'company'
assert company_client.project is None
def test_get_full_path_project_mode(client):
"""Test path construction in project mode"""
path = client._get_full_path("documentation/api")
assert path == "/test-company/projects/test-project/documentation/api"
def test_get_full_path_company_mode(company_client):
"""Test path construction in company mode"""
path = company_client._get_full_path("shared/architecture")
assert path == "/test-company/shared/architecture"
@pytest.mark.asyncio
async def test_search_pages(client):
"""Test searching pages"""
mock_response = {
'data': {
'pages': {
'search': {
'results': [
{
'id': 1,
'path': '/test-company/projects/test-project/doc1',
'title': 'Document 1',
'tags': ['api', 'documentation']
},
{
'id': 2,
'path': '/test-company/projects/test-project/doc2',
'title': 'Document 2',
'tags': ['guide', 'tutorial']
}
]
}
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
results = await client.search_pages("documentation")
assert len(results) == 2
assert results[0]['title'] == 'Document 1'
@pytest.mark.asyncio
async def test_get_page(client):
"""Test getting a specific page"""
mock_response = {
'data': {
'pages': {
'single': {
'id': 1,
'path': '/test-company/projects/test-project/doc1',
'title': 'Document 1',
'content': '# Test Content',
'tags': ['api'],
'isPublished': True
}
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
page = await client.get_page("doc1")
assert page is not None
assert page['title'] == 'Document 1'
assert page['content'] == '# Test Content'
@pytest.mark.asyncio
async def test_create_page(client):
"""Test creating a new page"""
mock_response = {
'data': {
'pages': {
'create': {
'responseResult': {
'succeeded': True,
'errorCode': None,
'message': 'Page created successfully'
},
'page': {
'id': 1,
'path': '/test-company/projects/test-project/new-doc',
'title': 'New Document'
}
}
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
page = await client.create_page(
path="new-doc",
title="New Document",
content="# Content",
tags=["test"]
)
assert page['id'] == 1
assert page['title'] == 'New Document'
@pytest.mark.asyncio
async def test_update_page(client):
"""Test updating a page"""
mock_response = {
'data': {
'pages': {
'update': {
'responseResult': {
'succeeded': True,
'message': 'Page updated'
},
'page': {
'id': 1,
'path': '/test-company/projects/test-project/doc1',
'title': 'Updated Title'
}
}
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
page = await client.update_page(
page_id=1,
title="Updated Title"
)
assert page['title'] == 'Updated Title'
@pytest.mark.asyncio
async def test_list_pages(client):
"""Test listing pages"""
mock_response = {
'data': {
'pages': {
'list': [
{'id': 1, 'path': '/test-company/projects/test-project/doc1', 'title': 'Doc 1'},
{'id': 2, 'path': '/test-company/projects/test-project/doc2', 'title': 'Doc 2'},
{'id': 3, 'path': '/test-company/other-project/doc3', 'title': 'Doc 3'}
]
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
# List all pages in current project
pages = await client.list_pages("")
# Should only return pages from test-project
assert len(pages) == 2
@pytest.mark.asyncio
async def test_create_lesson(client):
"""Test creating a lesson learned"""
mock_response = {
'data': {
'pages': {
'create': {
'responseResult': {
'succeeded': True,
'message': 'Lesson created'
},
'page': {
'id': 1,
'path': '/test-company/projects/test-project/lessons-learned/sprints/test-lesson',
'title': 'Test Lesson'
}
}
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
lesson = await client.create_lesson(
title="Test Lesson",
content="# Lesson Content",
tags=["testing", "sprint-16"],
category="sprints"
)
assert lesson['id'] == 1
assert 'lessons-learned' in lesson['path']
@pytest.mark.asyncio
async def test_search_lessons(client):
"""Test searching lessons learned"""
mock_response = {
'data': {
'pages': {
'search': {
'results': [
{
'id': 1,
'path': '/test-company/projects/test-project/lessons-learned/sprints/lesson1',
'title': 'Lesson 1',
'tags': ['testing']
},
{
'id': 2,
'path': '/test-company/projects/test-project/documentation/doc1',
'title': 'Doc 1',
'tags': ['guide']
}
]
}
}
}
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
lessons = await client.search_lessons(query="testing")
# Should only return lessons-learned pages
assert len(lessons) == 1
assert 'lessons-learned' in lessons[0]['path']
@pytest.mark.asyncio
async def test_graphql_error_handling(client):
"""Test handling of GraphQL errors"""
mock_response = {
'errors': [
{'message': 'Page not found'},
{'message': 'Invalid query'}
]
}
with patch('httpx.AsyncClient') as mock_client:
mock_instance = MagicMock()
mock_instance.__aenter__.return_value = mock_instance
mock_instance.__aexit__.return_value = None
mock_instance.post = AsyncMock(return_value=MagicMock(
json=lambda: mock_response,
raise_for_status=lambda: None
))
mock_client.return_value = mock_instance
with pytest.raises(ValueError, match="GraphQL errors"):
await client.search_pages("test")