mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
ab1a71c86c
* feat: add PostgreSQL CI integration tests Add --storage-backend pytest option and shared storage_backend fixture in conftest.py that creates SQLiteBackend or PostgreSQLBackend based on the flag. Migrate 13 storage test files to use shared fixture instead of local SQLiteBackend fixtures. Add test-postgres CI job with PostgreSQL 17 service container that runs the full test suite against real PostgreSQL. * fix: use TRUNCATE CASCADE for PG cleanup, wrap in try/finally TRUNCATE is faster than per-table DELETE and resets autoincrement sequences. try/except ensures reset_storage() always runs even if cleanup fails due to a corrupted connection from a failing test. * fix: document _engine coupling in PG cleanup comment
213 lines
8.3 KiB
Python
213 lines
8.3 KiB
Python
"""Tests for MCP server storage CRUD operations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from turnstone.core.storage._sqlite import SQLiteBackend
|
|
|
|
|
|
def _make_id() -> str:
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
class TestMcpServerStorage:
|
|
def test_create_and_get(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(
|
|
server_id=sid,
|
|
name="test-server",
|
|
transport="stdio",
|
|
command="echo",
|
|
args='["hello"]',
|
|
)
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["name"] == "test-server"
|
|
assert s["transport"] == "stdio"
|
|
assert s["command"] == "echo"
|
|
assert s["args"] == '["hello"]'
|
|
assert s["enabled"] is True
|
|
assert s["auto_approve"] is False
|
|
|
|
def test_get_by_name(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="named-srv", transport="stdio")
|
|
s = db.get_mcp_server_by_name("named-srv")
|
|
assert s is not None
|
|
assert s["server_id"] == sid
|
|
|
|
def test_get_by_name_not_found(self, db: SQLiteBackend) -> None:
|
|
assert db.get_mcp_server_by_name("nope") is None
|
|
|
|
def test_get_not_found(self, db: SQLiteBackend) -> None:
|
|
assert db.get_mcp_server("nonexistent") is None
|
|
|
|
def test_list_empty(self, db: SQLiteBackend) -> None:
|
|
assert db.list_mcp_servers() == []
|
|
|
|
def test_list_all(self, db: SQLiteBackend) -> None:
|
|
db.create_mcp_server(server_id=_make_id(), name="alpha", transport="stdio")
|
|
db.create_mcp_server(
|
|
server_id=_make_id(), name="beta", transport="streamable-http", url="http://x"
|
|
)
|
|
servers = db.list_mcp_servers()
|
|
assert len(servers) == 2
|
|
assert servers[0]["name"] == "alpha" # ordered by name
|
|
assert servers[1]["name"] == "beta"
|
|
|
|
def test_list_enabled_only(self, db: SQLiteBackend) -> None:
|
|
sid1 = _make_id()
|
|
sid2 = _make_id()
|
|
db.create_mcp_server(server_id=sid1, name="enabled-srv", transport="stdio", enabled=True)
|
|
db.create_mcp_server(server_id=sid2, name="disabled-srv", transport="stdio", enabled=False)
|
|
enabled = db.list_mcp_servers(enabled_only=True)
|
|
assert len(enabled) == 1
|
|
assert enabled[0]["name"] == "enabled-srv"
|
|
|
|
def test_update_basic_fields(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="orig", transport="stdio", command="echo")
|
|
ok = db.update_mcp_server(sid, name="renamed", command="cat")
|
|
assert ok is True
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["name"] == "renamed"
|
|
assert s["command"] == "cat"
|
|
|
|
def test_update_boolean_conversion(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="booltest", transport="stdio")
|
|
db.update_mcp_server(sid, auto_approve=True, enabled=False)
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["auto_approve"] is True
|
|
assert s["enabled"] is False
|
|
|
|
def test_update_not_found(self, db: SQLiteBackend) -> None:
|
|
ok = db.update_mcp_server("nonexistent", name="x")
|
|
assert ok is False
|
|
|
|
def test_update_ignores_disallowed_fields(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="guard", transport="stdio", created_by="admin")
|
|
original = db.get_mcp_server(sid)
|
|
assert original is not None
|
|
original_created = original["created"]
|
|
# created_by and created are not in the mutable allowlist
|
|
db.update_mcp_server(sid, created_by="evil", created="2000-01-01T00:00:00")
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["created_by"] == "admin" # unchanged
|
|
assert s["created"] == original_created # unchanged
|
|
|
|
def test_delete(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="delme", transport="stdio")
|
|
ok = db.delete_mcp_server(sid)
|
|
assert ok is True
|
|
assert db.get_mcp_server(sid) is None
|
|
|
|
def test_delete_not_found(self, db: SQLiteBackend) -> None:
|
|
ok = db.delete_mcp_server("nonexistent")
|
|
assert ok is False
|
|
|
|
def test_create_duplicate_name(self, db: SQLiteBackend) -> None:
|
|
db.create_mcp_server(server_id=_make_id(), name="unique", transport="stdio")
|
|
# Second create with same name but different ID should be no-op (OR IGNORE)
|
|
sid2 = _make_id()
|
|
db.create_mcp_server(server_id=sid2, name="unique", transport="stdio")
|
|
# OR IGNORE silently drops the conflicting insert
|
|
assert db.get_mcp_server(sid2) is None
|
|
|
|
def test_create_idempotent_same_id(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="idem", transport="stdio", command="v1")
|
|
db.create_mcp_server(server_id=sid, name="idem", transport="stdio", command="v2")
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["command"] == "v1" # original preserved, second ignored
|
|
|
|
def test_http_transport_fields(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(
|
|
server_id=sid,
|
|
name="http-srv",
|
|
transport="streamable-http",
|
|
url="https://example.com/mcp",
|
|
headers='{"Authorization":"Bearer xyz"}',
|
|
)
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["transport"] == "streamable-http"
|
|
assert s["url"] == "https://example.com/mcp"
|
|
assert "Authorization" in s["headers"]
|
|
|
|
# -- Registry columns -------------------------------------------------------
|
|
|
|
def test_create_with_registry_columns(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(
|
|
server_id=sid,
|
|
name="reg-server",
|
|
transport="streamable-http",
|
|
url="https://example.com/mcp",
|
|
registry_name="io.example/mcp-server",
|
|
registry_version="1.0.0",
|
|
registry_meta='{"description":"test"}',
|
|
)
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["registry_name"] == "io.example/mcp-server"
|
|
assert s["registry_version"] == "1.0.0"
|
|
assert s["registry_meta"] == '{"description":"test"}'
|
|
|
|
def test_create_without_registry_columns(self, db: SQLiteBackend) -> None:
|
|
"""Non-registry servers should have None/empty defaults."""
|
|
sid = _make_id()
|
|
db.create_mcp_server(server_id=sid, name="plain", transport="stdio")
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["registry_name"] is None
|
|
assert s["registry_version"] == ""
|
|
assert s["registry_meta"] == "{}"
|
|
|
|
def test_get_by_registry_name(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(
|
|
server_id=sid,
|
|
name="reg-test",
|
|
transport="stdio",
|
|
registry_name="io.example/test",
|
|
)
|
|
s = db.get_mcp_server_by_registry_name("io.example/test")
|
|
assert s is not None
|
|
assert s["server_id"] == sid
|
|
|
|
def test_get_by_registry_name_not_found(self, db: SQLiteBackend) -> None:
|
|
assert db.get_mcp_server_by_registry_name("nonexistent") is None
|
|
|
|
def test_null_registry_name_no_conflict(self, db: SQLiteBackend) -> None:
|
|
"""Multiple servers with NULL registry_name should coexist."""
|
|
db.create_mcp_server(server_id=_make_id(), name="a", transport="stdio")
|
|
db.create_mcp_server(server_id=_make_id(), name="b", transport="stdio")
|
|
servers = db.list_mcp_servers()
|
|
assert len(servers) == 2
|
|
|
|
def test_update_registry_columns(self, db: SQLiteBackend) -> None:
|
|
sid = _make_id()
|
|
db.create_mcp_server(
|
|
server_id=sid,
|
|
name="upgradable",
|
|
transport="stdio",
|
|
registry_name="io.example/up",
|
|
registry_version="1.0.0",
|
|
)
|
|
ok = db.update_mcp_server(sid, registry_version="2.0.0")
|
|
assert ok is True
|
|
s = db.get_mcp_server(sid)
|
|
assert s is not None
|
|
assert s["registry_version"] == "2.0.0"
|