mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
497 lines
17 KiB
Python
497 lines
17 KiB
Python
"""Tests for the custom secrets API endpoints."""
|
|
# flake8: noqa: E501
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from pydantic import SecretStr
|
|
|
|
from openhands.integrations.provider import (
|
|
CustomSecret,
|
|
ProviderToken,
|
|
ProviderType,
|
|
)
|
|
from openhands.server.routes.secrets import app as secrets_app
|
|
from openhands.storage import get_file_store
|
|
from openhands.storage.data_models.user_secrets import UserSecrets
|
|
from openhands.storage.secrets.file_secrets_store import FileSecretsStore
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client():
|
|
"""Create a test client for the settings API."""
|
|
app = FastAPI()
|
|
app.include_router(secrets_app)
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir(tmp_path_factory: pytest.TempPathFactory) -> str:
|
|
return str(tmp_path_factory.mktemp('secrets_store'))
|
|
|
|
|
|
@pytest.fixture
|
|
def file_secrets_store(temp_dir):
|
|
file_store = get_file_store('local', temp_dir)
|
|
store = FileSecretsStore(file_store)
|
|
with patch(
|
|
'openhands.storage.secrets.file_secrets_store.FileSecretsStore.get_instance',
|
|
AsyncMock(return_value=store),
|
|
):
|
|
yield store
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_custom_secrets_names(test_client, file_secrets_store):
|
|
"""Test loading custom secrets names."""
|
|
|
|
# Create initial settings with custom secrets
|
|
custom_secrets = {
|
|
'API_KEY': CustomSecret(secret=SecretStr('api-key-value')),
|
|
'DB_PASSWORD': CustomSecret(secret=SecretStr('db-password-value')),
|
|
}
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(
|
|
custom_secrets=custom_secrets, provider_tokens=provider_tokens
|
|
)
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the GET request
|
|
response = test_client.get('/api/secrets')
|
|
print(response)
|
|
assert response.status_code == 200
|
|
|
|
# Check the response
|
|
data = response.json()
|
|
assert 'custom_secrets' in data
|
|
# Extract just the names from the list of custom secrets
|
|
secret_names = [secret['name'] for secret in data['custom_secrets']]
|
|
assert sorted(secret_names) == ['API_KEY', 'DB_PASSWORD']
|
|
|
|
# Verify that the original settings were not modified
|
|
stored_settings = await file_secrets_store.load()
|
|
assert (
|
|
stored_settings.custom_secrets['API_KEY'].secret.get_secret_value()
|
|
== 'api-key-value'
|
|
)
|
|
assert (
|
|
stored_settings.custom_secrets['DB_PASSWORD'].secret.get_secret_value()
|
|
== 'db-password-value'
|
|
)
|
|
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_custom_secrets_names_empty(test_client, file_secrets_store):
|
|
"""Test loading custom secrets names when there are no custom secrets."""
|
|
# Create initial settings with no custom secrets
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(provider_tokens=provider_tokens, custom_secrets={})
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the GET request
|
|
response = test_client.get('/api/secrets')
|
|
assert response.status_code == 200
|
|
|
|
# Check the response
|
|
data = response.json()
|
|
assert 'custom_secrets' in data
|
|
assert data['custom_secrets'] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_custom_secret(test_client, file_secrets_store):
|
|
"""Test adding a new custom secret."""
|
|
|
|
# Create initial settings with provider tokens but no custom secrets
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(provider_tokens=provider_tokens)
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the POST request to add a custom secret
|
|
add_secret_data = {'name': 'API_KEY', 'value': 'api-key-value', 'description': None}
|
|
response = test_client.post('/api/secrets', json=add_secret_data)
|
|
assert response.status_code == 201
|
|
|
|
# Verify that the settings were stored with the new secret
|
|
stored_settings = await file_secrets_store.load()
|
|
|
|
# Check that the secret was added
|
|
assert 'API_KEY' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['API_KEY'].secret.get_secret_value()
|
|
== 'api-key-value'
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_custom_secret_with_no_existing_secrets(
|
|
test_client, file_secrets_store
|
|
):
|
|
"""Test creating a custom secret when there are no existing secrets at all."""
|
|
|
|
# Don't store any initial settings - this simulates a completely new user
|
|
# or a situation where the secrets store is empty
|
|
|
|
# Make the POST request to add a custom secret
|
|
add_secret_data = {
|
|
'name': 'NEW_API_KEY',
|
|
'value': 'new-api-key-value',
|
|
'description': 'Test API Key',
|
|
}
|
|
response = test_client.post('/api/secrets', json=add_secret_data)
|
|
assert response.status_code == 201
|
|
|
|
# Verify that the settings were stored with the new secret
|
|
stored_settings = await file_secrets_store.load()
|
|
|
|
# Check that the secret was added
|
|
assert 'NEW_API_KEY' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['NEW_API_KEY'].secret.get_secret_value()
|
|
== 'new-api-key-value'
|
|
)
|
|
assert stored_settings.custom_secrets['NEW_API_KEY'].description == 'Test API Key'
|
|
|
|
# Check that provider_tokens is an empty dict, not None
|
|
assert stored_settings.provider_tokens == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_existing_custom_secret(test_client, file_secrets_store):
|
|
"""Test updating an existing custom secret's name and description (cannot change value once set)."""
|
|
|
|
# Create initial settings with a custom secret
|
|
custom_secrets = {'API_KEY': CustomSecret(secret=SecretStr('old-api-key'))}
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(
|
|
custom_secrets=custom_secrets, provider_tokens=provider_tokens
|
|
)
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the PUT request to update the custom secret
|
|
update_secret_data = {
|
|
'name': 'API_KEY',
|
|
'description': None,
|
|
}
|
|
response = test_client.put('/api/secrets/API_KEY', json=update_secret_data)
|
|
assert response.status_code == 200
|
|
|
|
# Verify that the settings were stored with the updated secret
|
|
stored_settings = await file_secrets_store.load()
|
|
|
|
# Check that the secret was updated
|
|
assert 'API_KEY' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['API_KEY'].secret.get_secret_value()
|
|
== 'old-api-key'
|
|
)
|
|
|
|
# Check that other settings were preserved
|
|
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_multiple_custom_secrets(test_client, file_secrets_store):
|
|
"""Test adding multiple custom secrets at once."""
|
|
|
|
# Create initial settings with one custom secret
|
|
custom_secrets = {
|
|
'EXISTING_SECRET': CustomSecret(secret=SecretStr('existing-value'))
|
|
}
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(
|
|
custom_secrets=custom_secrets, provider_tokens=provider_tokens
|
|
)
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the POST request to add first custom secret
|
|
add_secret_data1 = {
|
|
'name': 'API_KEY',
|
|
'value': 'api-key-value',
|
|
'description': None,
|
|
}
|
|
response1 = test_client.post('/api/secrets', json=add_secret_data1)
|
|
assert response1.status_code == 201
|
|
|
|
# Make the POST request to add second custom secret
|
|
add_secret_data2 = {
|
|
'name': 'DB_PASSWORD',
|
|
'value': 'db-password-value',
|
|
'description': None,
|
|
}
|
|
response = test_client.post('/api/secrets', json=add_secret_data2)
|
|
assert response.status_code == 201
|
|
|
|
# Verify that the settings were stored with the new secrets
|
|
stored_settings = await file_secrets_store.load()
|
|
|
|
# Check that the new secrets were added
|
|
assert 'API_KEY' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['API_KEY'].secret.get_secret_value()
|
|
== 'api-key-value'
|
|
)
|
|
assert 'DB_PASSWORD' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['DB_PASSWORD'].secret.get_secret_value()
|
|
== 'db-password-value'
|
|
)
|
|
|
|
# Check that existing secrets were preserved
|
|
assert 'EXISTING_SECRET' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['EXISTING_SECRET'].secret.get_secret_value()
|
|
== 'existing-value'
|
|
)
|
|
|
|
# Check that other settings were preserved
|
|
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_custom_secret(test_client, file_secrets_store):
|
|
"""Test deleting a custom secret."""
|
|
|
|
# Create initial settings with multiple custom secrets
|
|
custom_secrets = {
|
|
'API_KEY': CustomSecret(secret=SecretStr('api-key-value')),
|
|
'DB_PASSWORD': CustomSecret(secret=SecretStr('db-password-value')),
|
|
}
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(
|
|
custom_secrets=custom_secrets, provider_tokens=provider_tokens
|
|
)
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the DELETE request to delete a custom secret
|
|
response = test_client.delete('/api/secrets/API_KEY')
|
|
assert response.status_code == 200
|
|
|
|
# Verify that the settings were stored without the deleted secret
|
|
stored_settings = await file_secrets_store.load()
|
|
|
|
# Check that the specified secret was deleted
|
|
assert 'API_KEY' not in stored_settings.custom_secrets
|
|
|
|
# Check that other secrets were preserved
|
|
assert 'DB_PASSWORD' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['DB_PASSWORD'].secret.get_secret_value()
|
|
== 'db-password-value'
|
|
)
|
|
|
|
# Check that other settings were preserved
|
|
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_nonexistent_custom_secret(test_client, file_secrets_store):
|
|
"""Test deleting a custom secret that doesn't exist."""
|
|
|
|
# Create initial settings with a custom secret
|
|
custom_secrets = {
|
|
'API_KEY': CustomSecret(secret=SecretStr('api-key-value'), description='')
|
|
}
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(
|
|
custom_secrets=custom_secrets, provider_tokens=provider_tokens
|
|
)
|
|
|
|
# Store the initial settings
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Make the DELETE request to delete a nonexistent custom secret
|
|
response = test_client.delete('/api/secrets/NONEXISTENT_KEY')
|
|
assert response.status_code == 404
|
|
|
|
# Verify that the settings were stored without changes to existing secrets
|
|
stored_settings = await file_secrets_store.load()
|
|
|
|
# Check that the existing secret was preserved
|
|
assert 'API_KEY' in stored_settings.custom_secrets
|
|
assert (
|
|
stored_settings.custom_secrets['API_KEY'].secret.get_secret_value()
|
|
== 'api-key-value'
|
|
)
|
|
|
|
# Check that other settings were preserved
|
|
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_git_providers_with_host(test_client, file_secrets_store):
|
|
"""Test adding git providers with host parameter."""
|
|
# Create initial user secrets
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
|
}
|
|
user_secrets = UserSecrets(provider_tokens=provider_tokens)
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Mock check_provider_tokens to return empty string (no error)
|
|
with patch(
|
|
'openhands.server.routes.secrets.check_provider_tokens',
|
|
AsyncMock(return_value=''),
|
|
):
|
|
# Add a GitHub provider with a host
|
|
add_provider_data = {
|
|
'provider_tokens': {
|
|
'github': {'token': 'new-github-token', 'host': 'github.enterprise.com'}
|
|
}
|
|
}
|
|
response = test_client.post('/api/add-git-providers', json=add_provider_data)
|
|
assert response.status_code == 200
|
|
|
|
# Verify that the settings were stored with the new provider token and host
|
|
stored_secrets = await file_secrets_store.load()
|
|
assert ProviderType.GITHUB in stored_secrets.provider_tokens
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
|
|
== 'new-github-token'
|
|
)
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITHUB].host
|
|
== 'github.enterprise.com'
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_git_providers_update_host_only(test_client, file_secrets_store):
|
|
"""Test updating only the host for an existing provider token."""
|
|
# Create initial user secrets with a token
|
|
provider_tokens = {
|
|
ProviderType.GITHUB: ProviderToken(
|
|
token=SecretStr('github-token'), host='github.com'
|
|
)
|
|
}
|
|
user_secrets = UserSecrets(provider_tokens=provider_tokens)
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Mock check_provider_tokens to return empty string (no error)
|
|
with patch(
|
|
'openhands.server.routes.secrets.check_provider_tokens',
|
|
AsyncMock(return_value=''),
|
|
):
|
|
# Update only the host
|
|
update_host_data = {
|
|
'provider_tokens': {
|
|
'github': {
|
|
'token': '', # Empty token means keep existing token
|
|
'host': 'github.enterprise.com',
|
|
}
|
|
}
|
|
}
|
|
response = test_client.post('/api/add-git-providers', json=update_host_data)
|
|
assert response.status_code == 200
|
|
|
|
# Verify that the host was updated but the token remains the same
|
|
stored_secrets = await file_secrets_store.load()
|
|
assert ProviderType.GITHUB in stored_secrets.provider_tokens
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
|
|
== 'github-token'
|
|
)
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITHUB].host
|
|
== 'github.enterprise.com'
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_git_providers_invalid_token_with_host(
|
|
test_client, file_secrets_store
|
|
):
|
|
"""Test adding an invalid token with a host."""
|
|
# Create initial user secrets
|
|
user_secrets = UserSecrets()
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Mock validate_provider_token to return None (invalid token)
|
|
with patch(
|
|
'openhands.integrations.utils.validate_provider_token',
|
|
AsyncMock(return_value=None),
|
|
):
|
|
# Try to add an invalid GitHub provider with a host
|
|
add_provider_data = {
|
|
'provider_tokens': {
|
|
'github': {'token': 'invalid-token', 'host': 'github.enterprise.com'}
|
|
}
|
|
}
|
|
response = test_client.post('/api/add-git-providers', json=add_provider_data)
|
|
assert response.status_code == 401
|
|
assert 'Invalid token' in response.json()['error']
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_multiple_git_providers_with_hosts(test_client, file_secrets_store):
|
|
"""Test adding multiple git providers with different hosts."""
|
|
# Create initial user secrets
|
|
user_secrets = UserSecrets()
|
|
await file_secrets_store.store(user_secrets)
|
|
|
|
# Mock check_provider_tokens to return empty string (no error)
|
|
with patch(
|
|
'openhands.server.routes.secrets.check_provider_tokens',
|
|
AsyncMock(return_value=''),
|
|
):
|
|
# Add multiple providers with hosts
|
|
add_providers_data = {
|
|
'provider_tokens': {
|
|
'github': {'token': 'github-token', 'host': 'github.enterprise.com'},
|
|
'gitlab': {'token': 'gitlab-token', 'host': 'gitlab.enterprise.com'},
|
|
}
|
|
}
|
|
response = test_client.post('/api/add-git-providers', json=add_providers_data)
|
|
assert response.status_code == 200
|
|
|
|
# Verify that both providers were stored with their respective hosts
|
|
stored_secrets = await file_secrets_store.load()
|
|
assert ProviderType.GITHUB in stored_secrets.provider_tokens
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
|
|
== 'github-token'
|
|
)
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITHUB].host
|
|
== 'github.enterprise.com'
|
|
)
|
|
|
|
assert ProviderType.GITLAB in stored_secrets.provider_tokens
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITLAB].token.get_secret_value()
|
|
== 'gitlab-token'
|
|
)
|
|
assert (
|
|
stored_secrets.provider_tokens[ProviderType.GITLAB].host
|
|
== 'gitlab.enterprise.com'
|
|
)
|