We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/finizi-app/finizi-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Comprehensive tests for invoice management functionality."""
import json
import pytest
from unittest.mock import Mock, AsyncMock, patch
import httpx
from src.finizi_b4b_mcp.tools.invoices import (
list_invoices,
get_invoice,
import_invoice_xml,
get_invoice_statistics
)
from src.finizi_b4b_mcp.utils.errors import MCPValidationError, MCPAuthenticationError, MCPAuthorizationError
# Load mock responses
with open("tests/fixtures/mock_responses.json") as f:
MOCK_RESPONSES = json.load(f)
# Tests for list_invoices function
@pytest.mark.asyncio
async def test_list_invoices_success():
"""Test listing invoices with pagination."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = MOCK_RESPONSES["invoices"]["list_success"]
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_api
result = await list_invoices(
entity_id="123e4567-e89b-12d3-a456-426614174000",
page=1,
per_page=20,
ctx=ctx
)
assert result["success"] is True
assert "data" in result
assert len(result["data"]["items"]) == 2
assert result["data"]["total"] == 2
assert result["data"]["items"][0]["invoice_number"] == "INV-2024-001"
@pytest.mark.asyncio
async def test_list_invoices_with_filters():
"""Test listing invoices with date and status filters."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = {
"items": [MOCK_RESPONSES["invoices"]["list_success"]["items"][0]],
"total": 1,
"page": 1,
"per_page": 20,
"pages": 1
}
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_api
result = await list_invoices(
entity_id="123e4567-e89b-12d3-a456-426614174000",
page=1,
per_page=20,
date_from="2024-01-01",
date_to="2024-01-31",
status=1,
ctx=ctx
)
assert result["success"] is True
assert len(result["data"]["items"]) == 1
@pytest.mark.asyncio
async def test_list_invoices_forbidden():
"""Test 403 error when access is denied to entity invoices."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 403
mock_response.json.return_value = MOCK_RESPONSES["errors"]["forbidden_403"]
mock_response.text = "Forbidden"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(side_effect=httpx.HTTPStatusError(
"Forbidden",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await list_invoices(
entity_id="123e4567-e89b-12d3-a456-426614174000",
page=1,
per_page=20,
ctx=ctx
)
assert result["success"] is False
assert "Access denied" in result["error"]
@pytest.mark.asyncio
async def test_list_invoices_not_authenticated():
"""Test listing invoices without authentication."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {}
result = await list_invoices(
entity_id="123e4567-e89b-12d3-a456-426614174000",
page=1,
per_page=20,
ctx=ctx
)
# Updated to match actual error format from invoices.py
assert result["success"] is False
assert ("authentication" in result["error"].lower() or "no authentication session" in result["error"].lower())
# Tests for get_invoice function
@pytest.mark.asyncio
async def test_get_invoice_success():
"""Test retrieving detailed invoice information."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = MOCK_RESPONSES["invoices"]["get_success"]
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_api
result = await get_invoice(
entity_id="123e4567-e89b-12d3-a456-426614174000",
invoice_id="inv-123e4567-e89b-12d3-a456-426614174000",
ctx=ctx
)
# Invoice responses are wrapped in success/data structure
assert result["success"] is True
assert "data" in result
assert result["data"]["invoice_number"] == "INV-2024-001"
assert result["data"]["vendor_name"] == "Supplier Co."
assert len(result["data"]["line_items"]) == 1
assert result["data"]["line_items"][0]["product_name"] == "Product A"
@pytest.mark.asyncio
async def test_get_invoice_not_found():
"""Test 404 error when invoice doesn't exist."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.return_value = MOCK_RESPONSES["errors"]["not_found_404"]
mock_response.text = "Not Found"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(side_effect=httpx.HTTPStatusError(
"Not Found",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await get_invoice(
entity_id="123e4567-e89b-12d3-a456-426614174000",
invoice_id="inv-nonexistent",
ctx=ctx
)
# Invoice responses use success/error structure
assert result["success"] is False
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_get_invoice_forbidden():
"""Test 403 error when access is denied to invoice."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 403
mock_response.json.return_value = MOCK_RESPONSES["errors"]["forbidden_403"]
mock_response.text = "Forbidden"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(side_effect=httpx.HTTPStatusError(
"Forbidden",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await get_invoice(
entity_id="123e4567-e89b-12d3-a456-426614174000",
invoice_id="inv-123e4567-e89b-12d3-a456-426614174000",
ctx=ctx
)
# Invoice responses use success/error structure
assert result["success"] is False
assert ("access" in result["error"].lower() or "forbidden" in result["error"].lower())
# Tests for import_invoice_xml function
@pytest.mark.asyncio
async def test_import_invoice_xml_success():
"""Test successful XML invoice import."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = MOCK_RESPONSES["invoices"]["import_xml_success"]
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<InvoiceNumber>INV-XML-2024-001</InvoiceNumber>
<TotalAmount>5000000</TotalAmount>
</Invoice>"""
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.post = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_api
result = await import_invoice_xml(
entity_id="123e4567-e89b-12d3-a456-426614174000",
xml_content=xml_content,
ctx=ctx
)
assert result["success"] is True
assert result["data"]["invoice_number"] == "INV-XML-2024-001"
assert result["data"]["parsing_status"] == "success"
assert result["data"]["total_amount"] == 5000000
@pytest.mark.asyncio
async def test_import_invoice_xml_parsing_error():
"""Test 400 error when XML parsing fails."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 400
mock_response.json.return_value = {"detail": "Invalid XML format"}
mock_response.text = "Bad Request"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.post = AsyncMock(side_effect=httpx.HTTPStatusError(
"Bad Request",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await import_invoice_xml(
entity_id="123e4567-e89b-12d3-a456-426614174000",
xml_content="<Invalid>XML</Invalid",
ctx=ctx
)
assert result["success"] is False
assert "Failed to parse XML" in result["error"]
@pytest.mark.asyncio
async def test_import_invoice_xml_empty_content():
"""Test validation error when XML content is empty."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
result = await import_invoice_xml(
entity_id="123e4567-e89b-12d3-a456-426614174000",
xml_content="",
ctx=ctx
)
assert result["success"] is False
assert "XML content cannot be empty" in result["error"]
@pytest.mark.asyncio
async def test_import_invoice_xml_forbidden():
"""Test 403 error when import permission is denied."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 403
mock_response.json.return_value = MOCK_RESPONSES["errors"]["forbidden_403"]
mock_response.text = "Forbidden"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.post = AsyncMock(side_effect=httpx.HTTPStatusError(
"Forbidden",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await import_invoice_xml(
entity_id="123e4567-e89b-12d3-a456-426614174000",
xml_content="<Invoice></Invoice>",
ctx=ctx
)
assert result["success"] is False
assert "Access denied" in result["error"]
# Tests for get_invoice_statistics function
@pytest.mark.asyncio
async def test_get_invoice_statistics_success():
"""Test retrieving invoice statistics."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = MOCK_RESPONSES["invoices"]["statistics_success"]
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_api
result = await get_invoice_statistics(
entity_id="123e4567-e89b-12d3-a456-426614174000",
ctx=ctx
)
assert result["success"] is True
assert result["data"]["total_invoices"] == 150
assert result["data"]["total_amount"] == 500000000
assert result["data"]["total_vat"] == 50000000
assert "by_status" in result["data"]
assert result["data"]["by_status"]["active"] == 120
@pytest.mark.asyncio
async def test_get_invoice_statistics_with_filters():
"""Test statistics with year and month filters."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = {
"total_invoices": 40,
"total_amount": 150000000,
"total_vat": 15000000,
"by_status": {
"draft": 2,
"active": 35,
"cancelled": 2,
"archived": 1
}
}
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_api
result = await get_invoice_statistics(
entity_id="123e4567-e89b-12d3-a456-426614174000",
year=2024,
month=2,
ctx=ctx
)
assert result["success"] is True
assert result["data"]["total_invoices"] == 40
assert result["data"]["total_amount"] == 150000000
@pytest.mark.asyncio
async def test_get_invoice_statistics_invalid_month():
"""Test validation error with invalid month."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
result = await get_invoice_statistics(
entity_id="123e4567-e89b-12d3-a456-426614174000",
year=2024,
month=13,
ctx=ctx
)
assert result["success"] is False
assert "Month must be between 1 and 12" in result["error"]
@pytest.mark.asyncio
async def test_get_invoice_statistics_forbidden():
"""Test 403 error when statistics access is denied."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 403
mock_response.json.return_value = MOCK_RESPONSES["errors"]["forbidden_403"]
mock_response.text = "Forbidden"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(side_effect=httpx.HTTPStatusError(
"Forbidden",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await get_invoice_statistics(
entity_id="123e4567-e89b-12d3-a456-426614174000",
ctx=ctx
)
assert result["success"] is False
assert "Access denied" in result["error"]
@pytest.mark.asyncio
async def test_get_invoice_statistics_entity_not_found():
"""Test 404 error when entity doesn't exist."""
ctx = Mock()
ctx.session = Mock()
ctx.session.metadata = {"user_token": "fake_token"}
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.return_value = MOCK_RESPONSES["errors"]["not_found_404"]
mock_response.text = "Not Found"
with patch('src.finizi_b4b_mcp.tools.invoices.get_api_client') as mock_client:
mock_api = Mock()
mock_api.get = AsyncMock(side_effect=httpx.HTTPStatusError(
"Not Found",
request=Mock(),
response=mock_response
))
mock_client.return_value = mock_api
result = await get_invoice_statistics(
entity_id="nonexistent-entity-id",
ctx=ctx
)
# Invoice responses use success/error structure
assert result["success"] is False
assert "not found" in result["error"].lower()
# Tests for error classes
def test_authentication_error_default_message():
"""Test authentication error with default message."""
error = MCPAuthenticationError()
assert "Authentication required" in str(error)
def test_authentication_error_custom_message():
"""Test authentication error with custom message."""
error = MCPAuthenticationError("Custom auth error")
assert str(error) == "Custom auth error"
def test_authorization_error_default_message():
"""Test authorization error with default message."""
error = MCPAuthorizationError()
assert "do not have permission" in str(error)
def test_authorization_error_custom_message():
"""Test authorization error with custom message."""
error = MCPAuthorizationError("Custom authz error")
assert str(error) == "Custom authz error"
def test_validation_error_without_field():
"""Test validation error without field name."""
error = MCPValidationError("Invalid input")
assert "Invalid input" in str(error)
assert error.field is None
def test_validation_error_with_field():
"""Test validation error with field name."""
error = MCPValidationError("Invalid value", field="amount")
assert "amount" in str(error)
assert "Invalid value" in str(error)
assert error.field == "amount"