feat: add pytest testing infrastructure

- Add test optional dependencies to pyproject.toml
- Add conftest.py with fixtures for mocking settings and MCP client
- Add test_server.py with tests for tryton_call and MCP tools
This commit is contained in:
2026-03-09 16:04:34 -05:00
parent 45e73b6856
commit c670ea5bc9
4 changed files with 233 additions and 1 deletions

View File

@@ -10,6 +10,13 @@ dependencies = [
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
] ]
[project.optional-dependencies]
test = [
"pytest>=8.0.0",
"pytest-mock>=3.14.0",
"pytest-asyncio>=0.25.0",
]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
include = ["*"] include = ["*"]

25
tests/conftest.py Normal file
View File

@@ -0,0 +1,25 @@
import pytest
import pytest_asyncio
from unittest.mock import MagicMock, patch
from fastmcp import Client
@pytest.fixture
def mock_settings():
with patch("tryton_mcp.server.settings") as mock:
mock_client = MagicMock()
mock.get_client.return_value = mock_client
yield mock
@pytest.fixture
def mcp_server():
from tryton_mcp.server import mcp
return mcp
@pytest_asyncio.fixture
async def mcp_client(mcp_server):
async with Client(mcp_server) as client:
yield client

141
tests/test_server.py Normal file
View File

@@ -0,0 +1,141 @@
import pytest
from unittest.mock import MagicMock, patch
import datetime
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "src"))
@pytest.mark.asyncio
class TestTrytonCall:
async def test_tryton_call_success(self, mock_settings):
from tryton_mcp.server import tryton_call
mock_client = mock_settings.get_client.return_value
mock_client.call.return_value = [{"id": 1, "name": "Test"}]
result = tryton_call("model.test.read", [[1], ["name"]])
assert result["success"] is True
assert result["data"] == [{"id": 1, "name": "Test"}]
mock_client.call.assert_called_once_with("model.test.read", [[1], ["name"]])
async def test_tryton_call_failure(self, mock_settings):
from tryton_mcp.server import tryton_call
mock_client = mock_settings.get_client.return_value
mock_client.call.side_effect = Exception("Connection error")
result = tryton_call("model.test.read", [[1], ["name"]])
assert result["success"] is False
assert "Connection error" in result["error"]
@pytest.mark.asyncio
class TestCreateSchedule:
async def test_create_schedule_success(self, mcp_client, mock_settings):
from tryton_mcp.server import create_schedule
mock_client = mock_settings.get_client.return_value
mock_client.call.return_value = [1]
result = await mcp_client.call_tool(
"create_schedule",
{
"professional": 6,
"description": "Test appointment",
"customer": 4,
"date": "2026-03-15T10:00:00",
"service_center": 11,
},
)
assert result.content is not None
assert len(result.content) == 1
assert result.content[0].text == "[1]"
mock_client.call.assert_called_once()
async def test_create_schedule_failure(self, mcp_client, mock_settings):
from tryton_mcp.server import create_schedule
mock_client = mock_settings.get_client.return_value
mock_client.call.side_effect = Exception("Create failed")
with pytest.raises(Exception, match="Failed to create schedule"):
await mcp_client.call_tool(
"create_schedule",
{
"professional": 6,
"description": "Test appointment",
"customer": 4,
"date": "2026-03-15T10:00:00",
"service_center": 11,
},
)
@pytest.mark.asyncio
class TestFindServiceCenters:
async def test_find_service_centers_success(self, mcp_client, mock_settings):
from tryton_mcp.server import find_service_centers
mock_client = mock_settings.get_client.return_value
mock_client.call.return_value = [
{"id": 11, "name": "Center 1", "address.street": "Street 1"}
]
result = await mcp_client.call_tool("find_service_centers", {})
result_data = json.loads(result.content[0].text)
assert result_data == [
{"id": 11, "name": "Center 1", "address.street": "Street 1"}
]
call_args = mock_client.call.call_args[0]
assert call_args[0] == "model.naliia.service_center.search_read"
assert call_args[1][0] == [[]]
async def test_find_service_centers_failure(self, mcp_client, mock_settings):
from tryton_mcp.server import find_service_centers
mock_client = mock_settings.get_client.return_value
mock_client.call.side_effect = Exception("Search failed")
with pytest.raises(Exception, match="Failed to find service centers"):
await mcp_client.call_tool("find_service_centers", {})
@pytest.mark.asyncio
class TestFindProductsAndServices:
async def test_find_products_and_services_success(self, mcp_client, mock_settings):
from tryton_mcp.server import find_products_and_services
mock_client = mock_settings.get_client.return_value
mock_client.call.return_value = [
{"id": 1, "name": "Product 1", "list_price": 100.0}
]
result = await mcp_client.call_tool("find_products_and_services", {})
result_data = json.loads(result.content[0].text)
assert result_data == [{"id": 1, "name": "Product 1", "list_price": 100.0}]
mock_client.call.assert_called_once()
call_args = mock_client.call.call_args[0]
assert call_args[0] == "model.product.product.search_read"
assert call_args[1][0] == [["active", "=", True], ["salable", "=", True]]
async def test_find_products_and_services_failure(self, mcp_client, mock_settings):
from tryton_mcp.server import find_products_and_services
mock_client = mock_settings.get_client.return_value
mock_client.call.side_effect = Exception("Search failed")
with pytest.raises(Exception, match="Failed to find products"):
await mcp_client.call_tool("find_products_and_services", {})

61
uv.lock generated
View File

@@ -91,6 +91,8 @@ sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
{ url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
{ url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" },
{ url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
] ]
@@ -418,6 +420,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
] ]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]] [[package]]
name = "jaraco-classes" name = "jaraco-classes"
version = "3.4.0" version = "3.4.0"
@@ -664,6 +675,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" },
] ]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]] [[package]]
name = "prometheus-client" name = "prometheus-client"
version = "0.24.1" version = "0.24.1"
@@ -839,6 +859,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
] ]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-mock"
version = "3.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -1092,19 +1140,30 @@ wheels = [
] ]
[[package]] [[package]]
name = "trytonmcp" name = "tryton-mcp"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "fastmcp", extra = ["tasks"] }, { name = "fastmcp", extra = ["tasks"] },
{ name = "python-dotenv" },
{ name = "sabatron-tryton-rpc-client" }, { name = "sabatron-tryton-rpc-client" },
] ]
[package.optional-dependencies]
test = [
{ name = "pytest" },
{ name = "pytest-mock" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "fastmcp", extras = ["tasks"], specifier = ">=3.1.0" }, { name = "fastmcp", extras = ["tasks"], specifier = ">=3.1.0" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
{ name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.14.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "sabatron-tryton-rpc-client", specifier = ">=7.4.0" }, { name = "sabatron-tryton-rpc-client", specifier = ">=7.4.0" },
] ]
provides-extras = ["test"]
[[package]] [[package]]
name = "typer" name = "typer"