- 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>
319 lines
12 KiB
Python
319 lines
12 KiB
Python
"""Tests for WooCommerce Plugin (plugins/woocommerce/).
|
|
|
|
Integration tests covering plugin initialization, configuration validation,
|
|
tool specifications, handler delegation, and health checks.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from plugins.base import BasePlugin
|
|
from plugins.woocommerce.plugin import WooCommercePlugin
|
|
|
|
# --- WooCommercePlugin Initialization ---
|
|
|
|
|
|
class TestWooCommercePluginInit:
|
|
"""Test WooCommerce plugin initialization."""
|
|
|
|
VALID_CONFIG = {
|
|
"url": "https://shop.example.com",
|
|
"username": "admin",
|
|
"app_password": "xxxx xxxx xxxx xxxx",
|
|
}
|
|
|
|
def test_create_with_valid_config(self):
|
|
"""Should initialize with valid credentials."""
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG)
|
|
assert plugin.project_id is not None
|
|
assert plugin.client is not None
|
|
assert isinstance(plugin, BasePlugin)
|
|
|
|
def test_handlers_initialized(self):
|
|
"""Should initialize all WooCommerce handlers."""
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG)
|
|
assert plugin.products is not None
|
|
assert plugin.orders is not None
|
|
assert plugin.customers is not None
|
|
assert plugin.coupons is not None
|
|
assert plugin.reports is not None
|
|
|
|
def test_plugin_name(self):
|
|
"""Should return 'woocommerce' as plugin name."""
|
|
assert WooCommercePlugin.get_plugin_name() == "woocommerce"
|
|
|
|
def test_required_config_keys(self):
|
|
"""Should require url, username, app_password."""
|
|
keys = WooCommercePlugin.get_required_config_keys()
|
|
assert "url" in keys
|
|
assert "username" in keys
|
|
assert "app_password" in keys
|
|
|
|
def test_missing_url_raises(self):
|
|
"""Should raise ValueError for missing URL."""
|
|
config = {"username": "admin", "app_password": "xxxx"}
|
|
with pytest.raises(ValueError, match="Missing required configuration"):
|
|
WooCommercePlugin(config)
|
|
|
|
def test_missing_credentials_raises(self):
|
|
"""Should raise ValueError for missing credentials."""
|
|
config = {"url": "https://shop.example.com"}
|
|
with pytest.raises(ValueError, match="Missing required configuration"):
|
|
WooCommercePlugin(config)
|
|
|
|
def test_custom_project_id(self):
|
|
"""Should accept custom project_id."""
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG, project_id="wc_myshop")
|
|
assert plugin.project_id == "wc_myshop"
|
|
|
|
def test_auto_generated_project_id(self):
|
|
"""Should auto-generate project_id from config."""
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG)
|
|
assert plugin.project_id.startswith("woocommerce")
|
|
|
|
def test_uses_wordpress_client(self):
|
|
"""Should create a WordPressClient (shared API client)."""
|
|
from plugins.wordpress.client import WordPressClient
|
|
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG)
|
|
assert isinstance(plugin.client, WordPressClient)
|
|
assert plugin.client.site_url == "https://shop.example.com"
|
|
|
|
|
|
# --- Tool Specifications ---
|
|
|
|
|
|
class TestWooCommerceToolSpecs:
|
|
"""Test WooCommerce tool specification generation."""
|
|
|
|
def test_specs_not_empty(self):
|
|
"""Should return non-empty tool specifications."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
assert len(specs) > 0
|
|
|
|
def test_specs_count(self):
|
|
"""Should return exactly 28 tool specs."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
assert len(specs) == 28
|
|
|
|
def test_specs_have_required_fields(self):
|
|
"""Each spec should have name, method_name, description, schema, scope."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
for spec in specs:
|
|
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')}"
|
|
assert "scope" in spec, f"Missing 'scope' in {spec.get('name')}"
|
|
|
|
def test_specs_scope_values(self):
|
|
"""All scopes should be valid (read, write, admin)."""
|
|
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']}"
|
|
|
|
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]}"
|
|
|
|
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",
|
|
}
|
|
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",
|
|
}
|
|
assert expected.issubset(names), f"Missing order tools: {expected - names}"
|
|
|
|
def test_customer_tools_present(self):
|
|
"""Should include customer tools."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
names = {s["name"] for s in specs}
|
|
assert "list_customers" in names
|
|
assert "create_customer" in names
|
|
|
|
def test_coupon_tools_present(self):
|
|
"""Should include coupon tools."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
names = {s["name"] for s in specs}
|
|
assert "list_coupons" in names
|
|
assert "create_coupon" in names
|
|
|
|
def test_report_tools_present(self):
|
|
"""Should include report tools."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
names = {s["name"] for s in specs}
|
|
assert "get_sales_report" in names
|
|
assert "get_top_sellers" in names
|
|
|
|
def test_no_wordpress_tools_leaked(self):
|
|
"""Should NOT include WordPress-core tools (posts, pages, etc)."""
|
|
specs = WooCommercePlugin.get_tool_specifications()
|
|
names = {s["name"] for s in specs}
|
|
wp_tools = {"list_posts", "create_post", "list_categories", "list_media"}
|
|
leaked = names & wp_tools
|
|
assert len(leaked) == 0, f"WordPress tools leaked into WooCommerce: {leaked}"
|
|
|
|
|
|
# --- Handler Delegation ---
|
|
|
|
|
|
class TestWooCommerceHandlerDelegation:
|
|
"""Test that plugin methods delegate to handlers correctly."""
|
|
|
|
VALID_CONFIG = {
|
|
"url": "https://shop.example.com",
|
|
"username": "admin",
|
|
"app_password": "xxxx xxxx xxxx xxxx",
|
|
}
|
|
|
|
@pytest.fixture
|
|
def plugin(self):
|
|
return WooCommercePlugin(self.VALID_CONFIG)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_products_delegates(self, plugin):
|
|
"""list_products should delegate to products handler."""
|
|
plugin.products.list_products = AsyncMock(return_value={"products": []})
|
|
result = await plugin.list_products(per_page=10)
|
|
plugin.products.list_products.assert_called_once_with(per_page=10)
|
|
assert result == {"products": []}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_product_delegates(self, plugin):
|
|
"""create_product should delegate to products handler."""
|
|
plugin.products.create_product = AsyncMock(return_value={"id": 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
|
|
async def test_list_orders_delegates(self, plugin):
|
|
"""list_orders should delegate to orders handler."""
|
|
plugin.orders.list_orders = AsyncMock(return_value=[])
|
|
await plugin.list_orders(status="processing")
|
|
plugin.orders.list_orders.assert_called_once_with(status="processing")
|
|
|
|
@pytest.mark.asyncio
|
|
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"})
|
|
await plugin.get_order(order_id=1)
|
|
plugin.orders.get_order.assert_called_once_with(order_id=1)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_customers_delegates(self, plugin):
|
|
"""list_customers should delegate to customers handler."""
|
|
plugin.customers.list_customers = AsyncMock(return_value=[])
|
|
await plugin.list_customers()
|
|
plugin.customers.list_customers.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_coupon_delegates(self, plugin):
|
|
"""create_coupon should delegate to coupons handler."""
|
|
plugin.coupons.create_coupon = AsyncMock(return_value={"id": 5})
|
|
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})
|
|
await plugin.get_sales_report(period="month")
|
|
plugin.reports.get_sales_report.assert_called_once_with(period="month")
|
|
|
|
|
|
# --- Health Check ---
|
|
|
|
|
|
class TestWooCommerceHealthCheck:
|
|
"""Test WooCommerce health check."""
|
|
|
|
VALID_CONFIG = {
|
|
"url": "https://shop.example.com",
|
|
"username": "admin",
|
|
"app_password": "xxxx xxxx xxxx xxxx",
|
|
}
|
|
|
|
@pytest.fixture
|
|
def plugin(self):
|
|
return WooCommercePlugin(self.VALID_CONFIG)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_healthy_when_wc_available(self, plugin):
|
|
"""Should report healthy when WooCommerce is available."""
|
|
plugin.client.check_woocommerce = AsyncMock(
|
|
return_value={"available": True, "version": "8.5.0"}
|
|
)
|
|
result = await plugin.health_check()
|
|
assert result["healthy"] is True
|
|
assert result["version"] == "8.5.0"
|
|
assert result["plugin_type"] == "woocommerce"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unhealthy_when_wc_unavailable(self, plugin):
|
|
"""Should report unhealthy when WooCommerce is not available."""
|
|
plugin.client.check_woocommerce = AsyncMock(
|
|
return_value={"available": False, "version": None}
|
|
)
|
|
result = await plugin.health_check()
|
|
assert result["healthy"] is False
|
|
assert "not available" in result["message"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unhealthy_on_exception(self, plugin):
|
|
"""Should report unhealthy on network errors."""
|
|
plugin.client.check_woocommerce = AsyncMock(side_effect=Exception("Connection refused"))
|
|
result = await plugin.health_check()
|
|
assert result["healthy"] is False
|
|
assert "Connection refused" in result["message"]
|
|
|
|
|
|
# --- Plugin Info ---
|
|
|
|
|
|
class TestWooCommercePluginInfo:
|
|
"""Test plugin info methods."""
|
|
|
|
VALID_CONFIG = {
|
|
"url": "https://shop.example.com",
|
|
"username": "admin",
|
|
"app_password": "xxxx xxxx xxxx xxxx",
|
|
}
|
|
|
|
def test_get_project_info(self):
|
|
"""Should return structured project info."""
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG, project_id="wc_shop1")
|
|
info = plugin.get_project_info()
|
|
assert info["project_id"] == "wc_shop1"
|
|
assert info["plugin_type"] == "woocommerce"
|
|
|
|
def test_legacy_get_tools_empty(self):
|
|
"""Legacy get_tools should return empty list."""
|
|
plugin = WooCommercePlugin(self.VALID_CONFIG)
|
|
assert plugin.get_tools() == []
|