Newer
Older
gnexus-tasks / backend / tests / test_bearer_auth.py
"""Bearer MCP-токен на REST /api/*: аутентификация расширения и агентов.

Сессии SSO из браузера не видны запросам из extension-контекста (SameSite=Lax
режет cross-site куки), поэтому расширение ходит с Bearer MCP-токеном
(Настройки → «Токены MCP») — тем же, что у ИИ-агентов на /mcp.
"""

from fastapi.testclient import TestClient

from app.dependencies import require_user


def _create_token(client: TestClient, label: str = "расширение") -> str:
    res = client.post("/api/mcp-tokens", json={"label": label})
    assert res.status_code == 200, res.text
    return res.json()["token"]


def test_bearer_grants_api_access(client: TestClient):
    token = _create_token(client)
    # Оверрайд require_user снят: настоящая зависимость читает Bearer.
    saved = client.app.dependency_overrides.pop(require_user, None)  # type: ignore[attr-defined]
    try:
        res = client.get("/api/tasks", headers={"Authorization": f"Bearer {token}"})
        assert res.status_code == 200, res.text
        res = client.get("/api/projects", headers={"Authorization": f"Bearer {token}"})
        assert res.status_code == 200
    finally:
        if saved is not None:
            client.app.dependency_overrides[require_user] = saved


def test_bearer_wrong_token_401(client: TestClient) -> None:
    saved = client.app.dependency_overrides.pop(require_user, None)  # type: ignore[attr-defined]
    try:
        res = client.get("/api/tasks", headers={"Authorization": "Bearer gnt_nope"})
        assert res.status_code == 401
    finally:
        if saved is not None:
            client.app.dependency_overrides[require_user] = saved


def test_bearer_revoked_token_401(client: TestClient) -> None:
    res = client.post("/api/mcp-tokens", json={"label": "отзываем"})
    token, token_id = res.json()["token"], res.json()["id"]
    assert client.delete(f"/api/mcp-tokens/{token_id}").status_code == 200
    saved = client.app.dependency_overrides.pop(require_user, None)  # type: ignore[attr-defined]
    try:
        res = client.get("/api/tasks", headers={"Authorization": f"Bearer {token}"})
        assert res.status_code == 401
    finally:
        if saved is not None:
            client.app.dependency_overrides[require_user] = saved


def test_bearer_auth_me_returns_profile(client: TestClient) -> None:
    token = _create_token(client)
    saved = client.app.dependency_overrides.pop(require_user, None)  # type: ignore[attr-defined]
    try:
        res = client.get("/auth/me", headers={"Authorization": f"Bearer {token}"})
        assert res.status_code == 200, res.text
        assert res.json()["user"]["user_id"] == "1"
    finally:
        if saved is not None:
            client.app.dependency_overrides[require_user] = saved


def test_bearer_scoped_to_owner(client: TestClient) -> None:
    """Задача сессии видна через bearer того же пользователя (скоуп по user_id)."""
    token = _create_token(client)
    created = client.post("/api/tasks", json={"title": "скоуп"}).json()
    saved = client.app.dependency_overrides.pop(require_user, None)  # type: ignore[attr-defined]
    try:
        res = client.get("/api/tasks", headers={"Authorization": f"Bearer {token}"})
        assert [t["id"] for t in res.json()] == [created["id"]]
    finally:
        if saved is not None:
            client.app.dependency_overrides[require_user] = saved