vibe/tests/core/test_auth_github.py
Mathias Gesbert 51fecc67d9
2.1.0 (#317)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Nicolas Karolak <nicolas@karolak.fr>
2026-02-11 18:17:30 +01:00

286 lines
11 KiB
Python

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from vibe.core.auth.github import (
DeviceFlowHandle,
DeviceFlowInfo,
GitHubAuthError,
GitHubAuthProvider,
)
class TestDeviceFlowModels:
def test_device_flow_info(self) -> None:
info = DeviceFlowInfo(
user_code="ABC-123", verification_uri="https://example.com"
)
assert info.user_code == "ABC-123"
assert info.verification_uri == "https://example.com"
def test_device_flow_handle(self) -> None:
info = DeviceFlowInfo(
user_code="ABC-123", verification_uri="https://example.com"
)
handle = DeviceFlowHandle(device_code="dc_123", expires_in=900, info=info)
assert handle.device_code == "dc_123"
assert handle.expires_in == 900
assert handle.info.user_code == "ABC-123"
class TestGitHubAuthProviderContextManager:
@pytest.mark.asyncio
async def test_creates_client_on_enter(self) -> None:
provider = GitHubAuthProvider()
assert provider._client is None
async with provider:
assert provider._client is not None
assert provider._client is None
@pytest.mark.asyncio
async def test_uses_provided_client(self) -> None:
external_client = httpx.AsyncClient()
provider = GitHubAuthProvider(client=external_client)
async with provider:
assert provider._client is external_client
assert provider._client is external_client
await external_client.aclose()
class TestGitHubAuthProviderGetToken:
def test_returns_token_from_keyring(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
mock_keyring.get_password.return_value = "ghp_test_token"
provider = GitHubAuthProvider()
token = provider.get_token()
assert token == "ghp_test_token"
def test_returns_none_on_keyring_error(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
import keyring.errors
mock_keyring.errors = keyring.errors
mock_keyring.get_password.side_effect = keyring.errors.KeyringError("error")
provider = GitHubAuthProvider()
token = provider.get_token()
assert token is None
def test_returns_none_when_no_token(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
mock_keyring.get_password.return_value = None
provider = GitHubAuthProvider()
token = provider.get_token()
assert token is None
class TestGitHubAuthProviderHasToken:
def test_returns_true_when_token_exists(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
mock_keyring.get_password.return_value = "ghp_token"
provider = GitHubAuthProvider()
assert provider.has_token() is True
def test_returns_false_when_no_token(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
mock_keyring.get_password.return_value = None
provider = GitHubAuthProvider()
assert provider.has_token() is False
class TestGitHubAuthProviderStartDeviceFlow:
@pytest.fixture
def mock_client(self) -> MagicMock:
return MagicMock(spec=httpx.AsyncClient)
@pytest.fixture
def provider(self, mock_client: MagicMock) -> GitHubAuthProvider:
return GitHubAuthProvider(client=mock_client)
@pytest.mark.asyncio
async def test_start_device_flow_success(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {
"device_code": "dc_123",
"user_code": "ABC-123",
"verification_uri": "https://github.com/login/device",
"expires_in": 900,
}
mock_client.post = AsyncMock(return_value=mock_response)
with patch("vibe.core.auth.github.webbrowser") as mock_browser:
handle = await provider.start_device_flow(open_browser=True)
mock_browser.open.assert_called_once_with("https://github.com/login/device")
assert handle.device_code == "dc_123"
assert handle.info.user_code == "ABC-123"
assert handle.expires_in == 900
@pytest.mark.asyncio
async def test_start_device_flow_without_browser(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {
"device_code": "dc_123",
"user_code": "ABC-123",
"verification_uri": "https://github.com/login/device",
"expires_in": 900,
}
mock_client.post = AsyncMock(return_value=mock_response)
with patch("vibe.core.auth.github.webbrowser") as mock_browser:
await provider.start_device_flow(open_browser=False)
mock_browser.open.assert_not_called()
@pytest.mark.asyncio
async def test_start_device_flow_failure(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = False
mock_response.text = "Bad request"
mock_client.post = AsyncMock(return_value=mock_response)
with pytest.raises(GitHubAuthError, match="Failed to initiate device flow"):
await provider.start_device_flow()
class TestGitHubAuthProviderPollForToken:
@pytest.fixture
def mock_client(self) -> MagicMock:
return MagicMock(spec=httpx.AsyncClient)
@pytest.fixture
def provider(self, mock_client: MagicMock) -> GitHubAuthProvider:
return GitHubAuthProvider(client=mock_client)
@pytest.mark.asyncio
async def test_poll_returns_token_on_success(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.json.return_value = {"access_token": "ghp_new_token"}
mock_client.post = AsyncMock(return_value=mock_response)
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
token = await provider._poll_for_token(
mock_client, "dc_123", expires_in=10, interval=1
)
assert token == "ghp_new_token"
@pytest.mark.asyncio
async def test_poll_handles_slow_down(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
responses = [
MagicMock(
json=MagicMock(return_value={"error": "slow_down", "interval": 5})
),
MagicMock(json=MagicMock(return_value={"access_token": "ghp_token"})),
]
mock_client.post = AsyncMock(side_effect=responses)
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
token = await provider._poll_for_token(
mock_client, "dc_123", expires_in=30, interval=1
)
assert token == "ghp_token"
@pytest.mark.asyncio
async def test_poll_raises_on_expired_token(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.json.return_value = {"error": "expired_token"}
mock_client.post = AsyncMock(return_value=mock_response)
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
with pytest.raises(GitHubAuthError, match="expired_token"):
await provider._poll_for_token(
mock_client, "dc_123", expires_in=10, interval=1
)
@pytest.mark.asyncio
async def test_poll_raises_on_access_denied(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.json.return_value = {"error": "access_denied"}
mock_client.post = AsyncMock(return_value=mock_response)
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
with pytest.raises(GitHubAuthError, match="access_denied"):
await provider._poll_for_token(
mock_client, "dc_123", expires_in=10, interval=1
)
@pytest.mark.asyncio
async def test_poll_raises_on_timeout(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.json.return_value = {"error": "authorization_pending"}
mock_client.post = AsyncMock(return_value=mock_response)
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
with pytest.raises(GitHubAuthError, match="timed out"):
await provider._poll_for_token(
mock_client, "dc_123", expires_in=2, interval=1
)
class TestGitHubAuthProviderSaveToken:
def test_save_token_success(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
provider = GitHubAuthProvider()
provider._save_token("ghp_token")
mock_keyring.set_password.assert_called_once_with(
"vibe", "github_token", "ghp_token"
)
def test_save_token_raises_on_keyring_error(self) -> None:
with patch("vibe.core.auth.github.keyring") as mock_keyring:
import keyring.errors
mock_keyring.errors = keyring.errors
mock_keyring.set_password.side_effect = keyring.errors.KeyringError("error")
provider = GitHubAuthProvider()
with pytest.raises(GitHubAuthError, match="Failed to save token"):
provider._save_token("ghp_token")
class TestGitHubAuthProviderWaitForToken:
@pytest.fixture
def mock_client(self) -> MagicMock:
return MagicMock(spec=httpx.AsyncClient)
@pytest.fixture
def provider(self, mock_client: MagicMock) -> GitHubAuthProvider:
return GitHubAuthProvider(client=mock_client)
@pytest.mark.asyncio
async def test_wait_for_token_polls_and_saves(
self, provider: GitHubAuthProvider, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.json.return_value = {"access_token": "ghp_token"}
mock_client.post = AsyncMock(return_value=mock_response)
info = DeviceFlowInfo(user_code="ABC", verification_uri="https://example.com")
handle = DeviceFlowHandle(device_code="dc_123", expires_in=10, info=info)
with (
patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock),
patch("vibe.core.auth.github.keyring") as mock_keyring,
):
token = await provider.wait_for_token(handle)
assert token == "ghp_token"
mock_keyring.set_password.assert_called_once()