diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/integration/test_minimal_creates.py b/tests/integration/test_minimal_creates.py new file mode 100644 index 0000000..6024014 --- /dev/null +++ b/tests/integration/test_minimal_creates.py @@ -0,0 +1,145 @@ +"""Integration-style tests for creating minimal Dolibarr entities.""" + +import re +import sys +from pathlib import Path +from typing import Dict, Tuple + +import pytest +import pytest_asyncio +from aiohttp import web + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src")) + +from dolibarr_mcp import Config, DolibarrClient + + +@pytest_asyncio.fixture +async def dolibarr_api_server(unused_tcp_port: int) -> Tuple[str, Dict, Dict]: + """Spin up a lightweight fake Dolibarr API for integration-style tests.""" + created: Dict[str, Dict[int, Dict]] = {"products": {}, "projects": {}} + statuses: Dict[str, list] = {"products": [], "projects": []} + + async def create_product(request: web.Request) -> web.Response: + data = await request.json() + product_id = len(created["products"]) + 1 + created["products"][product_id] = data + statuses["products"].append(201) + return web.json_response({"id": product_id, "ref": data.get("ref")}, status=201) + + async def delete_product(request: web.Request) -> web.Response: + product_id = int(request.match_info["product_id"]) + created["products"].pop(product_id, None) + return web.json_response({"success": True}) + + async def create_project(request: web.Request) -> web.Response: + data = await request.json() + project_id = len(created["projects"]) + 1 + created["projects"][project_id] = data + statuses["projects"].append(201) + return web.json_response({"id": project_id, "ref": data.get("ref")}, status=201) + + async def delete_project(request: web.Request) -> web.Response: + project_id = int(request.match_info["project_id"]) + created["projects"].pop(project_id, None) + return web.json_response({"success": True}) + + app = web.Application() + app.router.add_post("/api/index.php/products", create_product) + app.router.add_delete("/api/index.php/products/{product_id}", delete_product) + app.router.add_post("/api/index.php/projects", create_project) + app.router.add_delete("/api/index.php/projects/{project_id}", delete_project) + + runner = web.AppRunner(app) + await runner.setup() + port = unused_tcp_port + site = web.TCPSite(runner, "127.0.0.1", port) + await site.start() + + base_url = f"http://127.0.0.1:{port}/api/index.php" + + try: + yield base_url, created, statuses + finally: + await runner.cleanup() + + +@pytest.mark.asyncio +async def test_create_minimal_product_returns_created_id_and_ref(dolibarr_api_server): + """POST minimal product payload and confirm 201 + echoed ref.""" + base_url, created, statuses = dolibarr_api_server + config = Config(dolibarr_url=base_url, dolibarr_api_key="test-key") + + async with DolibarrClient(config) as client: + payload = { + "ref": "FREELANCE_HOUR_TEST", + "label": "Freelance hourly rate test — Gino test", + "type": "service", + "price": 110.00, + "tva_tx": 19.0, + } + + product_id = await client.create_product(payload) + + assert statuses["products"][-1] == 201 + assert product_id == 1 + assert created["products"][product_id]["ref"] == payload["ref"] + + cleanup_response = await client.delete_product(product_id) + assert cleanup_response.get("success") is True + assert product_id not in created["products"] + + +@pytest.mark.asyncio +async def test_create_minimal_project_returns_created_id_and_ref(dolibarr_api_server): + """POST minimal project payload and confirm 201 + echoed ref.""" + base_url, created, statuses = dolibarr_api_server + config = Config(dolibarr_url=base_url, dolibarr_api_key="test-key") + + async with DolibarrClient(config) as client: + payload = { + "ref": "PERCISION_TEST_PROJECT", + "name": "Percision test — Project test", + "socid": 8, + } + + project_id = await client.create_project(payload) + + assert statuses["projects"][-1] == 201 + assert project_id == 1 + assert created["projects"][project_id]["ref"] == payload["ref"] + + cleanup_response = await client.delete_project(project_id) + assert cleanup_response.get("success") is True + assert project_id not in created["projects"] + + +@pytest.mark.asyncio +async def test_autogenerated_reference_uses_prefix_when_enabled(dolibarr_api_server): + """When auto-generation is enabled, ensure generated refs include the prefix.""" + base_url, created, statuses = dolibarr_api_server + config = Config( + dolibarr_url=base_url, + dolibarr_api_key="test-key", + allow_ref_autogen=True, + ref_autogen_prefix="AUTO", + ) + + async with DolibarrClient(config) as client: + payload = { + "name": "Percision test — Project test", + "socid": 8, + } + + project_id = await client.create_project(payload) + + generated_ref = created["projects"][project_id]["ref"] + + assert statuses["projects"][-1] == 201 + assert project_id == 1 + assert generated_ref.startswith(f"{config.ref_autogen_prefix}_") + assert re.match(rf"{config.ref_autogen_prefix}_[0-9]{{14}}_[0-9a-f]{{8}}", generated_ref) + + cleanup_response = await client.delete_project(project_id) + assert cleanup_response.get("success") is True + assert project_id not in created["projects"]