fix(ci): fix black/ruff failures, add CoC and PR template
- Fix Python formatting (sync no longer strips blank lines from .py files) - Remove test_community_build.py (tests private sync module) - Fix ruff warnings in test files - Add CODE_OF_CONDUCT.md - Add .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
"""Tests for Authentication System (core/auth.py)."""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
|
||||
import pytest
|
||||
|
||||
from core.auth import AuthManager
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
"""Tests for Community Build Sync Script."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Import sync module from scripts
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts" / "community-build"))
|
||||
from sync import (
|
||||
SyncReport,
|
||||
apply_branding_transform,
|
||||
collect_files,
|
||||
parse_communityignore,
|
||||
scan_for_secrets,
|
||||
should_exclude,
|
||||
should_transform,
|
||||
sync,
|
||||
)
|
||||
|
||||
|
||||
class TestParseCommunityignore:
|
||||
"""Test .communityignore parsing."""
|
||||
|
||||
def test_parse_valid_file(self, tmp_path):
|
||||
ignore_file = tmp_path / ".communityignore"
|
||||
ignore_file.write_text("# comment\n\nfoo/\n*.pyc\nbar.txt\n")
|
||||
patterns = parse_communityignore(ignore_file)
|
||||
assert patterns == ["foo/", "*.pyc", "bar.txt"]
|
||||
|
||||
def test_parse_missing_file(self, tmp_path):
|
||||
patterns = parse_communityignore(tmp_path / "nonexistent")
|
||||
assert patterns == []
|
||||
|
||||
def test_skip_comments_and_empty(self, tmp_path):
|
||||
ignore_file = tmp_path / ".communityignore"
|
||||
ignore_file.write_text("# header\n\n# another\nreal_pattern\n\n")
|
||||
patterns = parse_communityignore(ignore_file)
|
||||
assert patterns == ["real_pattern"]
|
||||
|
||||
|
||||
class TestShouldExclude:
|
||||
"""Test file exclusion logic."""
|
||||
|
||||
def test_directory_pattern(self):
|
||||
assert should_exclude("docs/plans/file.md", ["docs/plans/"]) is True
|
||||
assert should_exclude("docs/other/file.md", ["docs/plans/"]) is False
|
||||
|
||||
def test_exact_match(self):
|
||||
assert should_exclude("MASTER_CONTEXT.md", ["MASTER_CONTEXT.md"]) is True
|
||||
assert should_exclude("README.md", ["MASTER_CONTEXT.md"]) is False
|
||||
|
||||
def test_glob_pattern(self):
|
||||
assert should_exclude("module.pyc", ["*.pyc"]) is True
|
||||
assert should_exclude("module.py", ["*.pyc"]) is False
|
||||
|
||||
def test_nested_directory(self):
|
||||
assert should_exclude("__pycache__/module.cpython-311.pyc", ["__pycache__/"]) is True
|
||||
|
||||
def test_no_patterns(self):
|
||||
assert should_exclude("any/file.txt", []) is False
|
||||
|
||||
def test_component_match(self):
|
||||
"""Pattern matching against directory components."""
|
||||
assert should_exclude("deep/__pycache__/file.pyc", ["__pycache__/"]) is True
|
||||
|
||||
|
||||
class TestScanForSecrets:
|
||||
"""Test secret detection."""
|
||||
|
||||
def test_no_secrets(self, tmp_path):
|
||||
f = tmp_path / "clean.py"
|
||||
f.write_text("x = 1\nprint('hello')\n")
|
||||
assert scan_for_secrets(f, "clean.py") == []
|
||||
|
||||
def test_detects_private_key(self, tmp_path):
|
||||
f = tmp_path / "bad.pem"
|
||||
f.write_text("-----BEGIN PRIVATE KEY-----\ndata\n-----END PRIVATE KEY-----\n")
|
||||
warnings = scan_for_secrets(f, "bad.pem")
|
||||
assert len(warnings) > 0
|
||||
|
||||
def test_allowlisted_file_skipped(self, tmp_path):
|
||||
f = tmp_path / "env.example"
|
||||
f.write_text('password = "super_secret_value"\n')
|
||||
assert scan_for_secrets(f, "env.example") == []
|
||||
|
||||
def test_detects_api_key_pattern(self, tmp_path):
|
||||
f = tmp_path / "config.py"
|
||||
f.write_text('api_key = "AKIAIOSFODNN7EXAMPLE12345"\n')
|
||||
warnings = scan_for_secrets(f, "config.py")
|
||||
assert len(warnings) > 0
|
||||
|
||||
|
||||
class TestSync:
|
||||
"""Test full sync operation."""
|
||||
|
||||
@pytest.fixture
|
||||
def source_tree(self, tmp_path):
|
||||
"""Create a mock source tree."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
|
||||
# Create .communityignore
|
||||
(src / ".communityignore").write_text("secret/\n*.log\nINTERNAL.md\n.communityignore\n")
|
||||
|
||||
# Create files that should be copied
|
||||
(src / "README.md").write_text("# Public Readme")
|
||||
(src / "core").mkdir()
|
||||
(src / "core" / "auth.py").write_text("class AuthManager: pass")
|
||||
|
||||
# Create files that should be excluded
|
||||
(src / "secret").mkdir()
|
||||
(src / "secret" / "keys.json").write_text('{"key": "value"}')
|
||||
(src / "INTERNAL.md").write_text("Internal docs")
|
||||
(src / "app.log").write_text("log data")
|
||||
|
||||
return src
|
||||
|
||||
def test_dry_run(self, source_tree, tmp_path):
|
||||
output = tmp_path / "output"
|
||||
report = sync(source_tree, output, dry_run=True)
|
||||
|
||||
assert report.total_copied > 0
|
||||
assert report.total_excluded > 0
|
||||
assert not output.exists() # Nothing should be created
|
||||
|
||||
def test_actual_sync(self, source_tree, tmp_path):
|
||||
output = tmp_path / "output"
|
||||
report = sync(source_tree, output)
|
||||
|
||||
assert (output / "README.md").exists()
|
||||
assert (output / "core" / "auth.py").exists()
|
||||
assert not (output / "secret").exists()
|
||||
assert not (output / "INTERNAL.md").exists()
|
||||
assert not (output / "app.log").exists()
|
||||
|
||||
def test_communityignore_excluded(self, source_tree, tmp_path):
|
||||
output = tmp_path / "output"
|
||||
report = sync(source_tree, output)
|
||||
|
||||
# .communityignore itself should be excluded
|
||||
assert not (output / ".communityignore").exists()
|
||||
assert ".communityignore" in report.excluded
|
||||
|
||||
|
||||
class TestSyncReport:
|
||||
"""Test report generation."""
|
||||
|
||||
def test_summary_format(self):
|
||||
report = SyncReport(
|
||||
copied=["a.py", "b.py"],
|
||||
excluded=["c.log"],
|
||||
source_dir="/src",
|
||||
output_dir="/out",
|
||||
timestamp="2026-02-17 12:00:00 UTC",
|
||||
)
|
||||
summary = report.summary()
|
||||
assert "Files copied: **2**" in summary
|
||||
assert "Files excluded: **1**" in summary
|
||||
|
||||
def test_summary_with_warnings(self):
|
||||
report = SyncReport(
|
||||
copied=["a.py"],
|
||||
excluded=[],
|
||||
secret_warnings=["file.py: found secret"],
|
||||
source_dir="/src",
|
||||
output_dir="/out",
|
||||
timestamp="now",
|
||||
)
|
||||
summary = report.summary()
|
||||
assert "Secret Warnings" in summary
|
||||
|
||||
def test_summary_has_review_checklist(self):
|
||||
report = SyncReport(
|
||||
copied=["a.py"],
|
||||
excluded=[],
|
||||
source_dir="/src",
|
||||
output_dir="/out",
|
||||
timestamp="now",
|
||||
)
|
||||
summary = report.summary()
|
||||
assert "Pre-Publish Review Checklist" in summary
|
||||
assert "APPROVED FOR PUBLISH" in summary
|
||||
|
||||
|
||||
class TestBrandingTransform:
|
||||
"""Test branding transform (B.3)."""
|
||||
|
||||
def test_replaces_package_name(self):
|
||||
content = 'name = "coolify-mcp-hub"'
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert changed is True
|
||||
assert 'name = "mcphub"' in transformed
|
||||
|
||||
def test_replaces_display_name(self):
|
||||
content = "Welcome to Coolify MCP Hub"
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert "MCP Hub" in transformed
|
||||
assert "Coolify" not in transformed
|
||||
|
||||
def test_replaces_urls(self):
|
||||
content = "Homepage: https://airano.ir"
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert "mcphub.dev" in transformed
|
||||
assert "airano.ir" not in transformed
|
||||
|
||||
def test_replaces_repo_urls(self):
|
||||
content = "https://gitea.airano.ir/dev/coolify-mcp-hub"
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert "github.com/airano-ir/mcphub" in transformed
|
||||
|
||||
def test_replaces_email(self):
|
||||
content = "Contact: gitea@airano.ir"
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert "hello@mcphub.dev" in transformed
|
||||
|
||||
def test_strips_private_comments(self):
|
||||
content = "line1\n# PRIVATE: internal note\nline3\n"
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert "PRIVATE" not in transformed
|
||||
assert "line1" in transformed
|
||||
assert "line3" in transformed
|
||||
|
||||
def test_no_changes_returns_false(self):
|
||||
content = "clean content with no markers\n"
|
||||
transformed, changed = apply_branding_transform(content)
|
||||
assert changed is False
|
||||
|
||||
def test_should_transform_pyproject(self):
|
||||
assert should_transform("pyproject.toml") is True
|
||||
|
||||
def test_should_transform_readme(self):
|
||||
assert should_transform("README.md") is True
|
||||
|
||||
def test_should_transform_docs(self):
|
||||
assert should_transform("docs/OAUTH_GUIDE.md") is True
|
||||
|
||||
def test_should_transform_python(self):
|
||||
assert should_transform("core/auth.py") is True
|
||||
assert should_transform("server.py") is True
|
||||
assert should_transform("plugins/wordpress/plugin.py") is True
|
||||
|
||||
def test_should_not_transform_binary(self):
|
||||
assert should_transform("core/data.bin") is False
|
||||
assert should_transform("images/logo.png") is False
|
||||
|
||||
def test_transform_applied_in_sync(self, tmp_path):
|
||||
"""Full sync should apply transforms to matching files."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
(src / ".communityignore").write_text(".communityignore\n")
|
||||
(src / "README.md").write_text("Welcome to Coolify MCP Hub\nhttps://airano.ir\n")
|
||||
(src / "core").mkdir()
|
||||
(src / "core" / "auth.py").write_text("# coolify-mcp-hub internal")
|
||||
|
||||
output = tmp_path / "output"
|
||||
report = sync(src, output)
|
||||
|
||||
# README.md should be transformed
|
||||
readme = (output / "README.md").read_text()
|
||||
assert "MCP Hub" in readme
|
||||
assert "Coolify" not in readme
|
||||
assert "mcphub.dev" in readme
|
||||
assert "README.md" in report.transformed
|
||||
|
||||
# auth.py SHOULD be transformed (Python files now in TRANSFORM_GLOBS)
|
||||
auth = (output / "core" / "auth.py").read_text()
|
||||
assert "mcphub" in auth # transformed
|
||||
@@ -6,7 +6,6 @@ language detection, translations, and utility functions.
|
||||
|
||||
import time
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
@@ -20,7 +19,6 @@ from core.dashboard.routes import (
|
||||
get_translations,
|
||||
)
|
||||
|
||||
|
||||
# --- Plugin Display Names ---
|
||||
|
||||
|
||||
@@ -49,8 +47,15 @@ class TestPluginDisplayNames:
|
||||
def test_all_nine_plugins_mapped(self):
|
||||
"""All 9 plugin types should be in the display names map."""
|
||||
expected = {
|
||||
"wordpress", "woocommerce", "wordpress_advanced", "gitea",
|
||||
"n8n", "supabase", "openpanel", "appwrite", "directus",
|
||||
"wordpress",
|
||||
"woocommerce",
|
||||
"wordpress_advanced",
|
||||
"gitea",
|
||||
"n8n",
|
||||
"supabase",
|
||||
"openpanel",
|
||||
"appwrite",
|
||||
"directus",
|
||||
}
|
||||
assert expected == set(PLUGIN_DISPLAY_NAMES.keys())
|
||||
|
||||
@@ -119,7 +124,9 @@ class TestTranslations:
|
||||
"""English and Farsi should have the same translation keys."""
|
||||
en_keys = set(DASHBOARD_TRANSLATIONS["en"].keys())
|
||||
fa_keys = set(DASHBOARD_TRANSLATIONS["fa"].keys())
|
||||
assert en_keys == fa_keys, f"Missing keys in fa: {en_keys - fa_keys}, extra in fa: {fa_keys - en_keys}"
|
||||
assert (
|
||||
en_keys == fa_keys
|
||||
), f"Missing keys in fa: {en_keys - fa_keys}, extra in fa: {fa_keys - en_keys}"
|
||||
|
||||
def test_no_empty_translations(self):
|
||||
"""No translation value should be empty."""
|
||||
@@ -197,7 +204,8 @@ class TestDashboardRateLimiting:
|
||||
@pytest.fixture
|
||||
def auth(self):
|
||||
return DashboardAuth(
|
||||
secret_key="test-secret", master_api_key="sk-master",
|
||||
secret_key="test-secret",
|
||||
master_api_key="sk-master",
|
||||
)
|
||||
|
||||
def test_within_limit(self, auth):
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Tests for Rate Limiter (core/rate_limiter.py)."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from core.rate_limiter import ClientRateLimitState, RateLimitConfig, RateLimiter, TokenBucket
|
||||
|
||||
@@ -4,7 +4,6 @@ import pytest
|
||||
|
||||
from core.site_manager import SiteConfig, SiteManager
|
||||
|
||||
|
||||
# --- SiteConfig Tests ---
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import pytest
|
||||
from plugins.base import BasePlugin
|
||||
from plugins.woocommerce.plugin import WooCommercePlugin
|
||||
|
||||
|
||||
# --- WooCommercePlugin Initialization ---
|
||||
|
||||
|
||||
@@ -102,7 +101,7 @@ class TestWooCommerceToolSpecs:
|
||||
"""Each spec should have name, method_name, description, schema, scope."""
|
||||
specs = WooCommercePlugin.get_tool_specifications()
|
||||
for spec in specs:
|
||||
assert "name" in spec, f"Missing 'name' in spec"
|
||||
assert "name" in spec, "Missing 'name' in spec"
|
||||
assert "method_name" in spec, f"Missing 'method_name' in {spec.get('name')}"
|
||||
assert "description" in spec, f"Missing 'description' in {spec.get('name')}"
|
||||
assert "schema" in spec, f"Missing 'schema' in {spec.get('name')}"
|
||||
@@ -113,26 +112,42 @@ class TestWooCommerceToolSpecs:
|
||||
specs = WooCommercePlugin.get_tool_specifications()
|
||||
valid_scopes = {"read", "write", "admin"}
|
||||
for spec in specs:
|
||||
assert spec["scope"] in valid_scopes, f"Invalid scope '{spec['scope']}' in {spec['name']}"
|
||||
assert (
|
||||
spec["scope"] in valid_scopes
|
||||
), f"Invalid scope '{spec['scope']}' in {spec['name']}"
|
||||
|
||||
def test_specs_unique_names(self):
|
||||
"""All tool names should be unique."""
|
||||
specs = WooCommercePlugin.get_tool_specifications()
|
||||
names = [s["name"] for s in specs]
|
||||
assert len(names) == len(set(names)), f"Duplicate names: {[n for n in names if names.count(n) > 1]}"
|
||||
assert len(names) == len(
|
||||
set(names)
|
||||
), f"Duplicate names: {[n for n in names if names.count(n) > 1]}"
|
||||
|
||||
def test_product_tools_present(self):
|
||||
"""Should include product management tools."""
|
||||
specs = WooCommercePlugin.get_tool_specifications()
|
||||
names = {s["name"] for s in specs}
|
||||
expected = {"list_products", "get_product", "create_product", "update_product", "delete_product"}
|
||||
expected = {
|
||||
"list_products",
|
||||
"get_product",
|
||||
"create_product",
|
||||
"update_product",
|
||||
"delete_product",
|
||||
}
|
||||
assert expected.issubset(names), f"Missing product tools: {expected - names}"
|
||||
|
||||
def test_order_tools_present(self):
|
||||
"""Should include order management tools."""
|
||||
specs = WooCommercePlugin.get_tool_specifications()
|
||||
names = {s["name"] for s in specs}
|
||||
expected = {"list_orders", "get_order", "create_order", "update_order_status", "delete_order"}
|
||||
expected = {
|
||||
"list_orders",
|
||||
"get_order",
|
||||
"create_order",
|
||||
"update_order_status",
|
||||
"delete_order",
|
||||
}
|
||||
assert expected.issubset(names), f"Missing order tools: {expected - names}"
|
||||
|
||||
def test_customer_tools_present(self):
|
||||
@@ -193,7 +208,7 @@ class TestWooCommerceHandlerDelegation:
|
||||
async def test_create_product_delegates(self, plugin):
|
||||
"""create_product should delegate to products handler."""
|
||||
plugin.products.create_product = AsyncMock(return_value={"id": 99})
|
||||
result = await plugin.create_product(name="Widget", regular_price="19.99")
|
||||
await plugin.create_product(name="Widget", regular_price="19.99")
|
||||
plugin.products.create_product.assert_called_once_with(name="Widget", regular_price="19.99")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -207,7 +222,7 @@ class TestWooCommerceHandlerDelegation:
|
||||
async def test_get_order_delegates(self, plugin):
|
||||
"""get_order should delegate to orders handler."""
|
||||
plugin.orders.get_order = AsyncMock(return_value={"id": 1, "status": "completed"})
|
||||
result = await plugin.get_order(order_id=1)
|
||||
await plugin.get_order(order_id=1)
|
||||
plugin.orders.get_order.assert_called_once_with(order_id=1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -221,14 +236,14 @@ class TestWooCommerceHandlerDelegation:
|
||||
async def test_create_coupon_delegates(self, plugin):
|
||||
"""create_coupon should delegate to coupons handler."""
|
||||
plugin.coupons.create_coupon = AsyncMock(return_value={"id": 5})
|
||||
result = await plugin.create_coupon(code="SAVE10", discount_type="percent")
|
||||
await plugin.create_coupon(code="SAVE10", discount_type="percent")
|
||||
plugin.coupons.create_coupon.assert_called_once_with(code="SAVE10", discount_type="percent")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_report_delegates(self, plugin):
|
||||
"""get_sales_report should delegate to reports handler."""
|
||||
plugin.reports.get_sales_report = AsyncMock(return_value={"total": 1000})
|
||||
result = await plugin.get_sales_report(period="month")
|
||||
await plugin.get_sales_report(period="month")
|
||||
plugin.reports.get_sales_report.assert_called_once_with(period="month")
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from plugins.base import BasePlugin, PluginRegistry
|
||||
from plugins.wordpress.client import AuthenticationError, ConfigurationError, WordPressClient
|
||||
from plugins.wordpress.plugin import WordPressPlugin
|
||||
|
||||
|
||||
# --- WordPressClient Tests ---
|
||||
|
||||
|
||||
@@ -87,7 +86,9 @@ class TestWordPressClientErrorParsing:
|
||||
|
||||
def test_parse_json_error(self, client):
|
||||
"""Should parse JSON error responses."""
|
||||
error_text = json.dumps({"code": "rest_forbidden", "message": "Sorry, you are not allowed."})
|
||||
error_text = json.dumps(
|
||||
{"code": "rest_forbidden", "message": "Sorry, you are not allowed."}
|
||||
)
|
||||
result = client._parse_error_response(403, error_text)
|
||||
assert result["error_code"] == "ACCESS_DENIED"
|
||||
assert result["status_code"] == 403
|
||||
@@ -159,10 +160,12 @@ class TestWordPressClientRequest:
|
||||
mock_response.json = AsyncMock(return_value={"id": 1})
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_session.request = MagicMock(return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_response),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
))
|
||||
mock_session.request = MagicMock(
|
||||
return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_response),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
)
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession") as mock_cls:
|
||||
mock_cls.return_value = AsyncMock(
|
||||
@@ -171,7 +174,8 @@ class TestWordPressClientRequest:
|
||||
)
|
||||
|
||||
result = await client.request(
|
||||
"GET", "posts",
|
||||
"GET",
|
||||
"posts",
|
||||
params={"status": "publish", "search": None, "tags": "", "ids": []},
|
||||
)
|
||||
assert result == {"id": 1}
|
||||
@@ -194,10 +198,12 @@ class TestWordPressClientRequest:
|
||||
)
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_session.request = MagicMock(return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_response),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
))
|
||||
mock_session.request = MagicMock(
|
||||
return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_response),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
)
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession") as mock_cls:
|
||||
mock_cls.return_value = AsyncMock(
|
||||
@@ -216,10 +222,12 @@ class TestWordPressClientRequest:
|
||||
mock_response.text = AsyncMock(return_value="Internal Server Error")
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_session.request = MagicMock(return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_response),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
))
|
||||
mock_session.request = MagicMock(
|
||||
return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_response),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
)
|
||||
)
|
||||
|
||||
with patch("aiohttp.ClientSession") as mock_cls:
|
||||
mock_cls.return_value = AsyncMock(
|
||||
@@ -236,8 +244,11 @@ class TestWordPressClientRequest:
|
||||
client.request = AsyncMock(return_value={"posts": []})
|
||||
result = await client.get("posts", params={"per_page": 10})
|
||||
client.request.assert_called_once_with(
|
||||
"GET", "posts", params={"per_page": 10},
|
||||
use_custom_namespace=False, use_woocommerce=False,
|
||||
"GET",
|
||||
"posts",
|
||||
params={"per_page": 10},
|
||||
use_custom_namespace=False,
|
||||
use_woocommerce=False,
|
||||
)
|
||||
assert result == {"posts": []}
|
||||
|
||||
@@ -263,8 +274,11 @@ class TestWordPressClientRequest:
|
||||
client.request = AsyncMock(return_value={"available": True})
|
||||
await client.get("products", use_woocommerce=True)
|
||||
client.request.assert_called_once_with(
|
||||
"GET", "products", params=None,
|
||||
use_custom_namespace=False, use_woocommerce=True,
|
||||
"GET",
|
||||
"products",
|
||||
params=None,
|
||||
use_custom_namespace=False,
|
||||
use_woocommerce=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -361,7 +375,7 @@ class TestWordPressToolSpecifications:
|
||||
"""Each spec should have name, method_name, description, schema, scope."""
|
||||
specs = WordPressPlugin.get_tool_specifications()
|
||||
for spec in specs:
|
||||
assert "name" in spec, f"Missing 'name' in spec"
|
||||
assert "name" in spec, "Missing 'name' in spec"
|
||||
assert "method_name" in spec, f"Missing 'method_name' in {spec.get('name')}"
|
||||
assert "description" in spec, f"Missing 'description' in {spec.get('name')}"
|
||||
assert "schema" in spec, f"Missing 'schema' in {spec.get('name')}"
|
||||
@@ -372,13 +386,17 @@ class TestWordPressToolSpecifications:
|
||||
specs = WordPressPlugin.get_tool_specifications()
|
||||
valid_scopes = {"read", "write", "admin"}
|
||||
for spec in specs:
|
||||
assert spec["scope"] in valid_scopes, f"Invalid scope '{spec['scope']}' in {spec['name']}"
|
||||
assert (
|
||||
spec["scope"] in valid_scopes
|
||||
), f"Invalid scope '{spec['scope']}' in {spec['name']}"
|
||||
|
||||
def test_specs_unique_names(self):
|
||||
"""All tool names should be unique."""
|
||||
specs = WordPressPlugin.get_tool_specifications()
|
||||
names = [s["name"] for s in specs]
|
||||
assert len(names) == len(set(names)), f"Duplicate tool names found: {[n for n in names if names.count(n) > 1]}"
|
||||
assert len(names) == len(
|
||||
set(names)
|
||||
), f"Duplicate tool names found: {[n for n in names if names.count(n) > 1]}"
|
||||
|
||||
def test_core_tools_present(self):
|
||||
"""Should include key WordPress tools."""
|
||||
@@ -442,21 +460,21 @@ class TestWordPressHandlerDelegation:
|
||||
async def test_list_media_delegates(self, plugin):
|
||||
"""list_media should delegate to media handler."""
|
||||
plugin.media.list_media = AsyncMock(return_value=[])
|
||||
result = await plugin.list_media()
|
||||
await plugin.list_media()
|
||||
plugin.media.list_media.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_categories_delegates(self, plugin):
|
||||
"""list_categories should delegate to taxonomy handler."""
|
||||
plugin.taxonomy.list_categories = AsyncMock(return_value=[])
|
||||
result = await plugin.list_categories()
|
||||
await plugin.list_categories()
|
||||
plugin.taxonomy.list_categories.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_comments_delegates(self, plugin):
|
||||
"""list_comments should delegate to comments handler."""
|
||||
plugin.comments.list_comments = AsyncMock(return_value=[])
|
||||
result = await plugin.list_comments()
|
||||
await plugin.list_comments()
|
||||
plugin.comments.list_comments.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user