test_invoices.py•16.7 kB
"""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"