Compare commits

...

7 Commits

Author SHA1 Message Date
openhands 58b8c66215 Fix enterprise linting: use single quotes
Apply enterprise linting rules (single quotes) to test file.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 19:18:43 +00:00
openhands 48ed801b27 Fix failing tests: mock module-level GitHub dependencies
The test file was failing because importing GitHubTokenResponse from
server.routes.integration.github causes Python to execute the entire
module, including line 32 which instantiates GitHubDataCollector().
This requires GitHub App credentials (GITHUB_APP_CLIENT_ID and
GITHUB_APP_PRIVATE_KEY) that are not available in CI.

Changes:
- Add autouse fixture to mock integrations.github.data_collector and
  integrations.github.github_manager modules before importing
- Fix test assertion for empty provider tokens - empty dict {} is falsy,
  so it triggers 'No provider tokens' error, not 'No GitHub token'
- Apply ruff formatting to the test file

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 19:13:31 +00:00
chuckbutkus 756caebd27 Merge branch 'main' into add-github-token-endpoint 2026-04-28 14:58:55 -04:00
chuckbutkus 8d573d9cc6 Lint fix 2026-04-28 13:51:24 -04:00
chuckbutkus fd81234ac4 Lint fixes 2026-04-28 13:46:02 -04:00
chuckbutkus 080ea0db5e Merge branch 'main' into add-github-token-endpoint 2026-04-28 13:09:03 -04:00
openhands 5156c580fe Add /integration/github/token endpoint
This endpoint returns a GitHub access token for the authenticated user,
refreshing the token if necessary.

- Add GitHubTokenResponse Pydantic model
- Add GET /integration/github/token endpoint
- Add unit tests for the endpoint

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-28 17:00:19 +00:00
2 changed files with 237 additions and 1 deletions
+44 -1
View File
@@ -2,21 +2,25 @@ import asyncio
import hashlib
import hmac
import os
from typing import cast
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
from fastapi.responses import JSONResponse
from integrations.github.data_collector import GitHubDataCollector
from integrations.github.github_manager import GithubManager
from integrations.models import Message, SourceType
from pydantic import BaseModel
from server.auth.constants import (
AUTOMATION_EVENT_FORWARDING_ENABLED,
GITHUB_APP_WEBHOOK_SECRET,
)
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from server.services.automation_event_service import AutomationEventService
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderType
from openhands.server.user_auth.user_auth import get_user_auth
# Environment variable to disable GitHub webhooks
GITHUB_WEBHOOKS_ENABLED = os.environ.get('GITHUB_WEBHOOKS_ENABLED', '1') in (
@@ -105,3 +109,42 @@ async def github_events(
except Exception as e:
logger.exception(f'Error processing GitHub event: {e}')
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
class GitHubTokenResponse(BaseModel):
"""Response model for the GitHub token endpoint."""
access_token: str
@github_integration_router.get('/github/token')
async def get_github_token(request: Request) -> GitHubTokenResponse:
"""Get the GitHub access token for the authenticated user.
This endpoint retrieves the user's GitHub OAuth token, refreshing it
if necessary. The token can be used for GitHub API operations.
Returns:
GitHubTokenResponse containing the access token.
Raises:
HTTPException 401: If the user is not authenticated.
HTTPException 404: If no GitHub token is available for the user.
"""
user_auth = cast(SaasUserAuth, await get_user_auth(request))
provider_tokens = await user_auth.get_provider_tokens()
if not provider_tokens:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='No provider tokens available for this user.',
)
github_token = provider_tokens.get(ProviderType.GITHUB)
if not github_token or not github_token.token:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='No GitHub token available for this user.',
)
return GitHubTokenResponse(access_token=github_token.token.get_secret_value())
@@ -0,0 +1,193 @@
"""Unit tests for GitHub integration routes.
Tests for:
- get_github_token endpoint
"""
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pydantic import SecretStr
@pytest.fixture(autouse=True)
def mock_github_dependencies():
"""Mock module-level dependencies before importing the github module.
The github.py module instantiates GitHubDataCollector at module level,
which requires GitHub App credentials. We mock these dependencies to
allow importing the module in test environments without credentials.
"""
# Store original modules if they exist
original_modules = {}
modules_to_mock = [
'integrations.github.data_collector',
'integrations.github.github_manager',
'server.routes.integration.github',
]
for mod in modules_to_mock:
if mod in sys.modules:
original_modules[mod] = sys.modules[mod]
del sys.modules[mod]
# Create mock GitHubDataCollector that doesn't require credentials
mock_data_collector_module = MagicMock()
mock_data_collector_instance = MagicMock()
mock_data_collector_module.GitHubDataCollector.return_value = (
mock_data_collector_instance
)
sys.modules['integrations.github.data_collector'] = mock_data_collector_module
# Create mock GithubManager
mock_github_manager_module = MagicMock()
mock_github_manager_instance = MagicMock()
mock_github_manager_module.GithubManager.return_value = mock_github_manager_instance
sys.modules['integrations.github.github_manager'] = mock_github_manager_module
yield
# Clean up the mocked modules
for mod in modules_to_mock:
if mod in sys.modules:
del sys.modules[mod]
# Restore original modules
for mod, original in original_modules.items():
sys.modules[mod] = original
class TestGitHubTokenResponse:
"""Test suite for GitHubTokenResponse model."""
def test_github_token_response_with_valid_token(self):
"""GitHubTokenResponse should accept a valid access_token."""
from server.routes.integration.github import GitHubTokenResponse
response = GitHubTokenResponse(access_token='ghp_test_token_12345')
assert response.access_token == 'ghp_test_token_12345'
def test_github_token_response_model_dump(self):
"""GitHubTokenResponse model_dump should include access_token."""
from server.routes.integration.github import GitHubTokenResponse
response = GitHubTokenResponse(access_token='ghp_test_token_12345')
data = response.model_dump()
assert data['access_token'] == 'ghp_test_token_12345'
class TestGetGitHubToken:
"""Test suite for get_github_token endpoint."""
@pytest.fixture
def mock_request(self):
"""Create a mock request object."""
request = MagicMock()
request.state = MagicMock()
return request
@pytest.fixture
def mock_saas_user_auth(self):
"""Create a mock SaasUserAuth object."""
from openhands.integrations.provider import ProviderToken, ProviderType
mock_auth = AsyncMock()
mock_auth.get_provider_tokens = AsyncMock(
return_value={
ProviderType.GITHUB: ProviderToken(
token=SecretStr('ghp_test_token_12345')
)
}
)
return mock_auth
@pytest.mark.asyncio
async def test_get_github_token_success(self, mock_request, mock_saas_user_auth):
"""Should return GitHub token when user has a valid token."""
from server.routes.integration.github import (
GitHubTokenResponse,
get_github_token,
)
with patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_saas_user_auth,
):
result = await get_github_token(mock_request)
assert isinstance(result, GitHubTokenResponse)
assert result.access_token == 'ghp_test_token_12345'
mock_saas_user_auth.get_provider_tokens.assert_called_once()
@pytest.mark.asyncio
async def test_get_github_token_no_provider_tokens(self, mock_request):
"""Should raise 404 when user has no provider tokens."""
from fastapi import HTTPException
from server.routes.integration.github import get_github_token
mock_auth = AsyncMock()
mock_auth.get_provider_tokens = AsyncMock(return_value=None)
with (
patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_auth,
),
pytest.raises(HTTPException) as exc_info,
):
await get_github_token(mock_request)
assert exc_info.value.status_code == 404
assert 'No provider tokens' in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_github_token_no_github_token(self, mock_request):
"""Should raise 404 when user has provider tokens but no GitHub token."""
from fastapi import HTTPException
from server.routes.integration.github import get_github_token
from openhands.integrations.provider import ProviderToken, ProviderType
mock_auth = AsyncMock()
# Return GitLab token but no GitHub token
mock_auth.get_provider_tokens = AsyncMock(
return_value={
ProviderType.GITLAB: ProviderToken(
token=SecretStr('glpat_test_token_12345')
)
}
)
with (
patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_auth,
),
pytest.raises(HTTPException) as exc_info,
):
await get_github_token(mock_request)
assert exc_info.value.status_code == 404
assert 'No GitHub token' in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_github_token_empty_provider_tokens(self, mock_request):
"""Should raise 404 when user has empty provider tokens dict."""
from fastapi import HTTPException
from server.routes.integration.github import get_github_token
mock_auth = AsyncMock()
mock_auth.get_provider_tokens = AsyncMock(return_value={})
with (
patch(
'server.routes.integration.github.get_user_auth',
return_value=mock_auth,
),
pytest.raises(HTTPException) as exc_info,
):
await get_github_token(mock_request)
assert exc_info.value.status_code == 404
# Empty dict is falsy, so it triggers the "no provider tokens" error
assert 'No provider tokens' in exc_info.value.detail