File size: 11,210 Bytes
7d7a6cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""Tests for the ledger HTTP endpoints."""

import pytest
import httpx
from fastmcp import FastMCP
from fastmcp.ledger import ProvenanceLedger, LedgerEvent, EventType


@pytest.fixture
def server_with_ledger():
    """Create a FastMCP server with ledger enabled."""
    server = FastMCP("TestLedgerServer")
    ledger = server.enable_ledger(database_url="sqlite:///:memory:")
    return server, ledger


@pytest.fixture
async def client(server_with_ledger):
    """Create an HTTP client for testing."""
    server, ledger = server_with_ledger
    from fastmcp.server.http import create_streamable_http_app
    app = create_streamable_http_app(server, streamable_http_path="/")
    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
        # Store server and ledger in client for test access
        client.server = server
        client.ledger = ledger
        yield client


class TestLedgerHTTPEndpoints:
    """Test the ledger HTTP endpoints."""
    
    async def test_append_event_endpoint(self, client):
        """Test the append event endpoint."""
        event_data = {
            "event_type": "tool_call",
            "actor_id": "user123",
            "resource_id": "resource456",
            "action": "execute_tool",
            "metadata": {"tool_name": "test_tool"},
            "data_hash": "sha256_hash"
        }
        
        response = await client.post("/ledger/events", json=event_data)
        
        assert response.status_code == 201
        data = response.json()
        assert "entry_id" in data
        assert "sequence_number" in data
        assert "entry_hash" in data
        assert "block_id" in data
        assert "created_at" in data
        assert data["sequence_number"] == 1
    
    async def test_append_event_invalid_data(self, client):
        """Test append event with invalid data."""
        invalid_data = {
            "event_type": "invalid_type",
            "actor_id": "user123",
            "action": "test_action"
        }
        
        response = await client.post("/ledger/events", json=invalid_data)
        
        assert response.status_code == 400
        data = response.json()
        assert "error" in data
        assert "Invalid event data" in data["error"]
    
    async def test_verify_block_endpoint(self, client):
        """Test the verify block endpoint."""
        # First, add some events to create a block
        for i in range(3):
            event_data = {
                "event_type": "tool_call",
                "actor_id": f"user{i}",
                "action": f"action_{i}"
            }
            await client.post("/ledger/events", json=event_data)
        
        # Manually seal the block for testing
        client.ledger.seal_current_block()
    
        # Verify the block
        response = await client.get("/ledger/verify/1")
        
        assert response.status_code == 200
        data = response.json()
        assert data["block_number"] == 1
        assert data["verified"] is True
        assert "block_id" in data
        assert "entry_count" in data
        assert "merkle_root" in data
    
    async def test_verify_nonexistent_block(self, client):
        """Test verifying a non-existent block."""
        response = await client.get("/ledger/verify/999")
        
        assert response.status_code == 400
        data = response.json()
        assert "error" in data
        assert "Block integrity verification failed" in data["error"]
    
    async def test_get_entry_endpoint(self, client):
        """Test the get entry endpoint."""
        # First, add an event
        event_data = {
            "event_type": "tool_call",
            "actor_id": "user123",
            "action": "test_action"
        }
        await client.post("/ledger/events", json=event_data)
        
        # Get the entry
        response = await client.get("/ledger/entries/1")
        
        assert response.status_code == 200
        data = response.json()
        assert data["sequence_number"] == 1
        assert "entry_hash" in data
        assert "previous_hash" in data
        assert "event" in data
        assert data["event"]["actor_id"] == "user123"
        assert data["event"]["action"] == "test_action"
    
    async def test_get_nonexistent_entry(self, client):
        """Test getting a non-existent entry."""
        response = await client.get("/ledger/entries/999")
        
        assert response.status_code == 404
        data = response.json()
        assert "error" in data
        assert "Entry not found" in data["error"]
    
    async def test_get_block_endpoint(self, client):
        """Test the get block endpoint."""
        # First, add some events to create a block
        for i in range(3):
            event_data = {
                "event_type": "tool_call",
                "actor_id": f"user{i}",
                "action": f"action_{i}"
            }
            await client.post("/ledger/events", json=event_data)
        
        # Get the block
        response = await client.get("/ledger/blocks/1")
        
        assert response.status_code == 200
        data = response.json()
        assert data["block_number"] == 1
        assert "entry_count" in data
        assert "merkle_root" in data
        assert "entries" in data
        assert len(data["entries"]) == 3
    
    async def test_get_nonexistent_block(self, client):
        """Test getting a non-existent block."""
        response = await client.get("/ledger/blocks/999")
        
        assert response.status_code == 404
        data = response.json()
        assert "error" in data
        assert "Block not found" in data["error"]
    
    async def test_verify_chain_endpoint(self, client):
        """Test the verify chain endpoint."""
        # Add some events
        for i in range(3):
            event_data = {
                "event_type": "tool_call",
                "actor_id": f"user{i}",
                "action": f"action_{i}"
            }
            await client.post("/ledger/events", json=event_data)
        
        # Verify the chain
        response = await client.get("/ledger/verify-chain")
        
        assert response.status_code == 200
        data = response.json()
        assert data["verified"] is True
        assert "start_sequence" in data
        assert "end_sequence" in data
    
    async def test_verify_chain_with_range(self, client):
        """Test verify chain with specific range."""
        # Add some events
        for i in range(5):
            event_data = {
                "event_type": "tool_call",
                "actor_id": f"user{i}",
                "action": f"action_{i}"
            }
            await client.post("/ledger/events", json=event_data)
        
        # Verify partial chain
        response = await client.get("/ledger/verify-chain?start_sequence=2&end_sequence=4")
        
        assert response.status_code == 200
        data = response.json()
        assert data["verified"] is True
        assert data["start_sequence"] == 2
        assert data["end_sequence"] == 4
    
    async def test_get_merkle_proof_endpoint(self, client):
        """Test the get Merkle proof endpoint."""
        # Add some events to create a block
        for i in range(3):
            event_data = {
                "event_type": "tool_call",
                "actor_id": f"user{i}",
                "action": f"action_{i}"
            }
            await client.post("/ledger/events", json=event_data)
        
        # Manually seal the block for testing
        client.ledger.seal_current_block()
    
        # Get Merkle proof for first entry
        response = await client.get("/ledger/proof/1")
        
        assert response.status_code == 200
        data = response.json()
        assert data["sequence_number"] == 1
        assert "entry_hash" in data
        assert "block_number" in data
        assert "merkle_root" in data
        assert "proof" in data
        assert "leaf_hash" in data["proof"]
        assert "path" in data["proof"]
        assert "root_hash" in data["proof"]
        assert data["verified"] is True
    
    async def test_get_merkle_proof_nonexistent_entry(self, client):
        """Test getting Merkle proof for non-existent entry."""
        response = await client.get("/ledger/proof/999")
        
        assert response.status_code == 404
        data = response.json()
        assert "error" in data
        assert "Entry not found" in data["error"]
    
    async def test_get_ledger_statistics_endpoint(self, client):
        """Test the get ledger statistics endpoint."""
        # Initially empty
        response = await client.get("/ledger/statistics")
        
        assert response.status_code == 200
        data = response.json()
        assert data["total_entries"] == 0
        assert data["total_blocks"] == 0
        assert data["current_sequence"] == 0
        
        # Add some events
        for i in range(3):
            event_data = {
                "event_type": "tool_call",
                "actor_id": f"user{i}",
                "action": f"action_{i}"
            }
            await client.post("/ledger/events", json=event_data)
        
        # Check updated statistics
        response = await client.get("/ledger/statistics")
        
        assert response.status_code == 200
        data = response.json()
        assert data["total_entries"] == 3
        assert data["total_blocks"] == 1
        assert data["current_sequence"] == 3
    
    async def test_multiple_event_types(self, client):
        """Test appending different event types."""
        event_types = [
            "tool_call",
            "policy_decision", 
            "data_flow",
            "contract_action",
            "authentication",
            "authorization",
            "system_event"
        ]
        
        for event_type in event_types:
            event_data = {
                "event_type": event_type,
                "actor_id": "test_actor",
                "action": "test_action"
            }
            
            response = await client.post("/ledger/events", json=event_data)
            assert response.status_code == 201
    
    async def test_event_with_complex_metadata(self, client):
        """Test event with complex metadata."""
        event_data = {
            "event_type": "tool_call",
            "actor_id": "test_actor",
            "action": "test_action",
            "metadata": {
                "nested": {"key": "value"},
                "list": [1, 2, 3],
                "boolean": True,
                "null": None
            }
        }
        
        response = await client.post("/ledger/events", json=event_data)
        
        assert response.status_code == 201
        data = response.json()
        assert data["sequence_number"] == 1
        
        # Verify the event was stored correctly
        entry_response = await client.get("/ledger/entries/1")
        assert entry_response.status_code == 200
        entry_data = entry_response.json()
        assert entry_data["event"]["metadata"] == event_data["metadata"]