Consolidate Persimmons shared backend and tests

This commit is contained in:
matcha committed 2026-10-04 02:58:04 +00:00
1 parent f0b9d468f2
commit 2e92247d80
1 file changed
+102
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import os
import json
import secrets
import unittest
from unittest.mock import AsyncMock, patch
import httpx
os.environ.setdefault("GITEA_TOKEN", "test-token")
os.environ.setdefault("GITEA_OAUTH_CLIENT_ID", "test-client")
os.environ.setdefault("GITEA_OAUTH_CLIENT_SECRET", "test-secret")
os.environ.setdefault("JWT_SIGNING_KEY", "test-signing-key-for-unit-tests")
from tea_mcp import server
from tea_mcp.persimmons import create_connection, create_juice
from fastmcp import Client, FastMCP
from tests.test_persimmons import FakeGitea
class JuiceHttpTests(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.client = httpx.AsyncClient(transport=httpx.ASGITransport(app=server.app), base_url="https://example.invalid")
async def asyncTearDown(self):
await self.client.aclose()
async def test_unauthorized_routes_do_not_access_storage_or_duplicate_proxy_cors(self):
with patch.object(server, "persimmons_actor", AsyncMock(return_value=None)), patch.object(server, "persimmons_requester") as requester:
for operation, method in (("list", "GET"), ("create", "POST"), ("revoke", "POST")):
response = await self.client.request(method, "/chat/persimmons/juice/" + operation,
headers={"Origin": "https://persimmons.app"})
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json(), {"error": "unauthorized"})
self.assertEqual(response.headers["cache-control"], "no-store")
self.assertNotIn("access-control-allow-origin", response.headers)
requester.assert_not_called()
async def test_authenticated_lifecycle_and_validation(self):
store = FakeGitea()
source = await create_connection(store.request, {
"provider": "Example", "credential_type": "Token", "credential": "hidden-source",
"vault_kind": "user",
})
with patch.object(server, "persimmons_actor", AsyncMock(return_value="example-user")), patch.object(server, "persimmons_requester", return_value=store.request):
response = await self.client.post("/chat/persimmons/juice/create", json={
"source_id": source["id"], "subject": "agent:example", "scopes": ["read"],
})
self.assertEqual(response.status_code, 201)
juice = response.json()
listed = await self.client.get("/chat/persimmons/juice/list", params={"source_id": source["id"]})
self.assertEqual(listed.json(), {"juice": [juice]})
revoked = await self.client.post("/chat/persimmons/juice/revoke", json={"id": juice["id"]})
self.assertEqual(revoked.status_code, 200)
self.assertEqual(revoked.json()["status"], "revoked")
self.assertNotIn("hidden-source", response.text + listed.text + revoked.text)
invalid = await self.client.post("/chat/persimmons/juice/create", json=[])
self.assertEqual(invalid.status_code, 400)
async def test_storage_errors_do_not_leak_details(self):
with patch.object(server, "persimmons_actor", AsyncMock(return_value="example-user")), patch.object(server, "persimmons_requester", return_value=AsyncMock(side_effect=RuntimeError("private detail"))):
response = await self.client.get("/chat/persimmons/juice/list")
self.assertEqual(response.status_code, 502)
self.assertNotIn("private detail", response.text)
class JuiceMcpTests(unittest.IsolatedAsyncioTestCase):
async def test_registered_agent_route_never_reads_or_returns_source_value(self):
store = FakeGitea()
sentinel = secrets.token_hex(24)
source = await create_connection(store.request, {
"provider": "Example", "credential_type": "Token", "credential": sentinel,
"vault_kind": "user",
})
juice = await create_juice(store.request, {
"source_id": source["id"], "subject": "agent:example", "scopes": ["read"],
})
key = "PERSIM_JUICE_" + juice["id"]
poisoned = json.loads(store.user_variables[key])
poisoned["credential"] = sentinel
store.user_variables[key] = json.dumps(poisoned)
calls = []
async def requester(method, path, **kwargs):
calls.append((method, path))
self.assertEqual(method, "GET")
self.assertNotIn("/secrets", path)
return await store.request(method, path, **kwargs)
# Exercise the registered tool through MCP without mounting unrelated
# runtime proxies or starting the production application's background jobs.
isolated = FastMCP("Juice route test")
isolated.add_tool(await server.mcp.local_provider.get_tool("persimmons_juice_list"))
with patch.object(server, "actor_gitea_request", requester):
async with Client(isolated) as client:
result = await client.call_tool("persimmons_juice_list", {"source_id": source["id"]})
self.assertFalse(result.is_error)
self.assertEqual(result.data, {"juice": [juice]})
# Use a boolean assertion so a regression cannot echo the sentinel in logs.
self.assertTrue(sentinel not in str(result))
self.assertTrue(calls)