Files
bvandeusen b255a0f90e
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 1m14s
refactor: rename package fabledassistant -> scribe (code-only)
Renames src/fabledassistant -> src/scribe and all imports, plus the
default DB name and DB user/password (fabled -> scribe) in config +
compose. 952 refs / 154 files. Reverses the old 'internal name stays
fabledassistant' convention.

Code-only: live databases are still physically named 'fabledassistant'.
Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the
DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git
host (fabledsword), MCP (fabled-git) and the image name (fabledscribe)
are intentionally unchanged.

ruff check src/ clean locally; CI (typecheck + pytest) is the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:48:35 -04:00

95 lines
3.5 KiB
Python

"""Tests for MCP auth: bearer-token validation that reuses api_keys."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.mcp.auth import resolve_bearer, resolve_bearer_to_user_id
@pytest.mark.asyncio
async def test_resolve_bearer_missing_header_returns_none():
assert await resolve_bearer_to_user_id(None) is None
@pytest.mark.asyncio
async def test_resolve_bearer_malformed_header_returns_none():
assert await resolve_bearer_to_user_id("Token abc") is None
assert await resolve_bearer_to_user_id("Bearer") is None
assert await resolve_bearer_to_user_id("Bearer ") is None
assert await resolve_bearer_to_user_id("") is None
@pytest.mark.asyncio
async def test_resolve_bearer_unknown_token_returns_none():
with patch(
"scribe.mcp.auth.lookup_key",
AsyncMock(return_value=None),
):
assert await resolve_bearer_to_user_id("Bearer fmcp_doesnotexist") is None
@pytest.mark.asyncio
async def test_resolve_bearer_valid_token_returns_user_id():
fake_key = MagicMock()
fake_key.user_id = 42
with patch(
"scribe.mcp.auth.lookup_key",
AsyncMock(return_value=fake_key),
):
uid = await resolve_bearer_to_user_id("Bearer fmcp_validkey")
assert uid == 42
@pytest.mark.asyncio
async def test_resolve_bearer_calls_lookup_with_stripped_token():
"""The Bearer prefix and any trailing whitespace must be stripped before lookup."""
fake_key = MagicMock()
fake_key.user_id = 1
mock_lookup = AsyncMock(return_value=fake_key)
with patch("scribe.mcp.auth.lookup_key", mock_lookup):
await resolve_bearer_to_user_id("Bearer fmcp_abc123 ")
mock_lookup.assert_awaited_once_with("fmcp_abc123")
# ── resolve_bearer (user_id + scope) ────────────────────────────────────
@pytest.mark.asyncio
async def test_resolve_bearer_returns_user_id_and_scope():
fake_key = MagicMock()
fake_key.user_id = 9
fake_key.scope = "read"
with patch("scribe.mcp.auth.lookup_key", AsyncMock(return_value=fake_key)):
assert await resolve_bearer("Bearer fmcp_x") == (9, "read")
@pytest.mark.asyncio
async def test_resolve_bearer_none_for_invalid():
with patch("scribe.mcp.auth.lookup_key", AsyncMock(return_value=None)):
assert await resolve_bearer("Bearer nope") is None
assert await resolve_bearer(None) is None
# ── read-only scope gate ────────────────────────────────────────────────
def test_body_calls_write_tool_classifies_correctly():
import json
from scribe.mcp.server import _body_calls_write_tool
def call(name):
return json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": name, "arguments": {}}}).encode()
# Write-class tools are gated.
assert _body_calls_write_tool(call("create_note")) is True
assert _body_calls_write_tool(call("delete_project")) is True
assert _body_calls_write_tool(call("purge_trash")) is True
# An unknown/new tool defaults to write (default-deny for read keys).
assert _body_calls_write_tool(call("brand_new_tool")) is True
# Read tools and non-call methods pass.
assert _body_calls_write_tool(call("list_notes")) is False
assert _body_calls_write_tool(call("get_recent")) is False
assert _body_calls_write_tool(
json.dumps({"method": "tools/list"}).encode()
) is False
assert _body_calls_write_tool(b"not json") is False