xsecuremcp / tests /contracts /test_contract_http.py
svkrishna
🤖 Implement Story 2: Inter-Agent Contract Module
4d56236
Raw
History Blame Contribute Delete
11.5 kB
"""Tests for contract HTTP endpoints."""
import pytest
from datetime import datetime, timedelta
from uuid import UUID
from fastmcp import FastMCP
from fastmcp.contracts import ContractEngine, ContractState
from fastmcp.contracts.contract import (
Clause, ContractCreateRequest, ContractProposeRequest,
ContractSignRequest, ContractRevokeRequest
)
from fastmcp.contracts.crypto import generate_key_pair, Ed25519Signer
from starlette.routing import Route
class TestContractHTTPEndpoint:
"""Test the contract management HTTP endpoints."""
@pytest.fixture
def server_with_contracts(self):
"""Create a server with contract engine enabled."""
server = FastMCP("Test Contract Server")
# Use in-memory database for testing
contract_engine = server.enable_contract_engine(database_url="sqlite:///:memory:")
return server
@pytest.fixture
def app(self, server_with_contracts):
"""Create the HTTP app with contract endpoints."""
return server_with_contracts.http_app(transport="sse")
def test_contract_endpoints_exist(self, app):
"""Test that all contract endpoints exist in the app."""
expected_paths = [
"/contracts",
"/contracts/{id}",
"/contracts/{id}/propose",
"/contracts/{id}/sign",
"/contracts/{id}/revoke",
"/contracts/statistics"
]
# Check that all contract routes exist
contract_routes_found = set()
for route in app.routes:
if isinstance(route, Route):
if route.path in expected_paths:
contract_routes_found.add(route.path)
assert len(contract_routes_found) == len(expected_paths), f"Expected {len(expected_paths)} unique contract routes, found {len(contract_routes_found)}: {sorted(contract_routes_found)}"
def test_contract_engine_integration(self, server_with_contracts):
"""Test that contract engine is properly integrated with the server."""
# Check that contract engine is enabled
assert server_with_contracts.get_contract_engine() is not None
# Check that contract engine is the right type
contract_engine = server_with_contracts.get_contract_engine()
assert isinstance(contract_engine, ContractEngine)
@pytest.mark.asyncio
async def test_create_contract_endpoint(self, app):
"""Test the create contract endpoint."""
import httpx
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [
{
"title": "Test Clause",
"content": "This is a test clause",
"type": "test"
}
],
"parties": [
{
"id": "party1",
"name": "Test Party",
"type": "provider"
}
],
"is_hipaa_compliant": False
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.post("/contracts", json=contract_data)
assert response.status_code == 201
data = response.json()
assert data["title"] == contract_data["title"]
assert data["description"] == contract_data["description"]
assert data["state"] == "draft"
assert data["is_hipaa_compliant"] is False
@pytest.mark.asyncio
async def test_get_contract_endpoint(self, app):
"""Test the get contract endpoint."""
import httpx
# First create a contract
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [],
"parties": []
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
# Create contract
create_response = await client.post("/contracts", json=contract_data)
assert create_response.status_code == 201
contract_id = create_response.json()["id"]
# Get contract
get_response = await client.get(f"/contracts/{contract_id}")
assert get_response.status_code == 200
data = get_response.json()
assert data["id"] == contract_id
assert data["title"] == contract_data["title"]
@pytest.mark.asyncio
async def test_get_contract_not_found(self, app):
"""Test getting a non-existent contract."""
import httpx
fake_id = "12345678-1234-1234-1234-123456789012"
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.get(f"/contracts/{fake_id}")
assert response.status_code == 404
data = response.json()
assert "not found" in data["error"].lower()
@pytest.mark.asyncio
async def test_list_contracts_endpoint(self, app):
"""Test the list contracts endpoint."""
import httpx
# Create multiple contracts
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [],
"parties": []
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
# Create contracts
await client.post("/contracts", json=contract_data)
await client.post("/contracts", json=contract_data)
# List contracts
response = await client.get("/contracts")
assert response.status_code == 200
data = response.json()
assert "contracts" in data
assert "count" in data
assert data["count"] == 2
assert len(data["contracts"]) == 2
@pytest.mark.asyncio
async def test_propose_contract_endpoint(self, app):
"""Test the propose contract endpoint."""
import httpx
# First create a contract
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [],
"parties": []
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
# Create contract
create_response = await client.post("/contracts", json=contract_data)
contract_id = create_response.json()["id"]
# Propose contract
proposal_data = {
"proposed_to": ["party1", "party2"],
"message": "Please review and sign"
}
response = await client.post(f"/contracts/{contract_id}/propose", json=proposal_data)
assert response.status_code == 200
data = response.json()
assert data["state"] == "proposed"
assert data["proposed_at"] is not None
@pytest.mark.asyncio
async def test_sign_contract_endpoint(self, app):
"""Test the sign contract endpoint."""
import httpx
# First create and propose a contract
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [],
"parties": [{"id": "party1", "name": "Test Party", "type": "provider"}]
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
# Create contract
create_response = await client.post("/contracts", json=contract_data)
contract_id = create_response.json()["id"]
# Propose contract
proposal_data = {"proposed_to": ["party1"]}
await client.post(f"/contracts/{contract_id}/propose", json=proposal_data)
# Generate key pair for signing
public_key, private_key = generate_key_pair()
signer = Ed25519Signer.from_private_key_b64(private_key)
# Get contract to get content hash
contract_response = await client.get(f"/contracts/{contract_id}")
contract_data_response = contract_response.json()
content_hash = contract_data_response["content_hash"]
# Create signature
signing_message = f"{contract_id}:{content_hash}:party1:provider"
signature = signer.sign(signing_message)
# Sign contract
sign_data = {
"signer_id": "party1",
"signer_type": "provider",
"public_key": public_key,
"signature": signature
}
response = await client.post(f"/contracts/{contract_id}/sign", json=sign_data)
assert response.status_code == 200
data = response.json()
assert len(data["signatures"]) == 1
assert data["signatures"][0]["signer_id"] == "party1"
@pytest.mark.asyncio
async def test_revoke_contract_endpoint(self, app):
"""Test the revoke contract endpoint."""
import httpx
# First create a contract
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [],
"parties": []
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
# Create contract
create_response = await client.post("/contracts", json=contract_data)
contract_id = create_response.json()["id"]
# Revoke contract
revoke_data = {
"reason": "Contract terms violated",
"revoked_by": "admin"
}
response = await client.post(f"/contracts/{contract_id}/revoke", json=revoke_data)
assert response.status_code == 200
data = response.json()
assert data["state"] == "revoked"
assert data["revoked_at"] is not None
@pytest.mark.asyncio
async def test_contract_statistics_endpoint(self, app):
"""Test the contract statistics endpoint."""
import httpx
# Create some contracts
contract_data = {
"title": "Test Contract",
"description": "A test contract",
"clauses": [],
"parties": []
}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
# Create contracts
await client.post("/contracts", json=contract_data)
await client.post("/contracts", json=contract_data)
# Get statistics
response = await client.get("/contracts/statistics")
assert response.status_code == 200
data = response.json()
assert "total_contracts" in data
assert "by_state" in data
assert "hipaa_compliant" in data
assert "signed_contracts" in data
assert data["total_contracts"] == 2