AIstudioProxyAPI / tests /api_utils /routers /test_api_keys.py
peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
12.8 kB
"""
High-quality tests for api_utils/routers/api_keys.py - API key management endpoints.
Focus: Test all 4 endpoints (get, add, test, delete) with success and error paths.
Strategy: Mock auth_utils module and file operations, test validation and exception handling.
"""
from unittest.mock import MagicMock, mock_open, patch
import pytest
from fastapi import HTTPException
from api_utils.routers.api_keys import (
ApiKeyRequest,
ApiKeyTestRequest,
add_api_key,
delete_api_key,
get_api_keys,
)
from api_utils.routers.api_keys import (
test_api_key as api_key_test_endpoint, # Alias doesn't start with 'test_'
)
@pytest.fixture
def mock_auth_utils():
"""Mock auth_utils module with API_KEYS set and KEY_FILE_PATH."""
with patch("api_utils.auth_utils") as mock_auth:
mock_auth.API_KEYS = set()
mock_auth.KEY_FILE_PATH = "/fake/path/key.txt"
mock_auth.initialize_keys = MagicMock()
mock_auth.verify_api_key = MagicMock()
yield mock_auth
@pytest.fixture
def mock_logger():
"""Mock logger instance."""
return MagicMock()
@pytest.mark.asyncio
async def test_get_api_keys_success_with_keys(mock_auth_utils, mock_logger):
"""
Test scenario: Successfully get API key list (with keys)
Expected: Return JSON response containing all keys (lines 18-26)
"""
# Setup: API_KEYS has 3 keys
mock_auth_utils.API_KEYS = {"key1", "key2", "key3"}
response = await get_api_keys(logger=mock_logger)
# Verify: initialize_keys called (line 22)
mock_auth_utils.initialize_keys.assert_called_once()
# Verify: Response structure (lines 23-26)
assert response.status_code == 200
content = bytes(response.body).decode()
assert '"success":true' in content.lower()
assert '"total_count":3' in content.lower()
@pytest.mark.asyncio
async def test_get_api_keys_success_empty(mock_auth_utils, mock_logger):
"""
Test scenario: Successfully get API key list (no keys)
Expected: Return empty list
"""
mock_auth_utils.API_KEYS = set()
response = await get_api_keys(logger=mock_logger)
# Verify: Empty keys list
assert response.status_code == 200
content = bytes(response.body).decode()
assert '"total_count":0' in content.lower()
@pytest.mark.asyncio
async def test_get_api_keys_exception_handling(mock_auth_utils, mock_logger):
"""
Test scenario: initialize_keys throws exception
Expected: Throw HTTPException 500 (lines 27-29)
"""
mock_auth_utils.initialize_keys.side_effect = RuntimeError("File permission error")
with pytest.raises(HTTPException) as exc_info:
await get_api_keys(logger=mock_logger)
# Verify: HTTPException 500
assert exc_info.value.status_code == 500
assert "File permission error" in exc_info.value.detail
# Verify: logger.error called (line 28)
assert mock_logger.error.call_count == 1
assert "Failed to get API key list" in mock_logger.error.call_args[0][0]
@pytest.mark.asyncio
async def test_add_api_key_success(mock_auth_utils, mock_logger):
"""
Test scenario: Successfully add new API key
Expected: Write to file and return success response (lines 35-61)
"""
mock_auth_utils.API_KEYS = set() # Initially empty
request = ApiKeyRequest(key="valid-key-123456")
# Mock file operations
mock_file = mock_open(read_data="")
with patch("builtins.open", mock_file):
response = await add_api_key(request=request, logger=mock_logger)
# Verify: initialize_keys called twice (lines 41, 53)
assert mock_auth_utils.initialize_keys.call_count == 2
# Verify: File write (lines 47-51)
mock_file.assert_called()
handle = mock_file()
# Key written to file
written_data = "".join(call.args[0] for call in handle.write.call_args_list)
assert "valid-key-123456" in written_data
# Verify: logger.info called (line 54)
assert mock_logger.info.call_count == 1
assert "API key added" in mock_logger.info.call_args[0][0]
# Verify: Response (lines 55-61)
assert response.status_code == 200
content = bytes(response.body).decode()
assert '"success":true' in content.lower()
assert '"message":"api key added successfully"' in content.lower()
@pytest.mark.asyncio
async def test_add_api_key_invalid_empty(mock_logger):
"""
Test scenario: Add empty key
Expected: Throw HTTPException 400 (lines 37-39)
"""
request = ApiKeyRequest(key=" ") # Whitespace only
with pytest.raises(HTTPException) as exc_info:
await add_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 400 (line 39)
assert exc_info.value.status_code == 400
assert "Invalid API key format" in exc_info.value.detail
@pytest.mark.asyncio
async def test_add_api_key_invalid_too_short(mock_logger):
"""
Test scenario: Add too short key (< 8 characters)
Expected: Throw HTTPException 400 (lines 38-39)
"""
request = ApiKeyRequest(key="short") # Only 5 characters
with pytest.raises(HTTPException) as exc_info:
await add_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 400
assert exc_info.value.status_code == 400
assert "Invalid API key format" in exc_info.value.detail
@pytest.mark.asyncio
async def test_add_api_key_duplicate(mock_auth_utils, mock_logger):
"""
Test scenario: Add existing key
Expected: Throw HTTPException 400 (lines 42-43)
"""
mock_auth_utils.API_KEYS = {"existing-key-123"}
request = ApiKeyRequest(key="existing-key-123")
with pytest.raises(HTTPException) as exc_info:
await add_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 400 (line 43)
assert exc_info.value.status_code == 400
assert "API key already exists" in exc_info.value.detail
@pytest.mark.asyncio
async def test_add_api_key_file_exception(mock_auth_utils, mock_logger):
"""
Test scenario: File write failed
Expected: Throw HTTPException 500 (lines 62-64)
"""
mock_auth_utils.API_KEYS = set()
request = ApiKeyRequest(key="valid-key-123456")
# Mock file open to raise exception
with patch("builtins.open", side_effect=IOError("Disk full")):
with pytest.raises(HTTPException) as exc_info:
await add_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 500 (line 64)
assert exc_info.value.status_code == 500
assert "Disk full" in exc_info.value.detail
# Verify: logger.error called (line 63)
assert mock_logger.error.call_count == 1
assert "Failed to add API key" in mock_logger.error.call_args[0][0]
@pytest.mark.asyncio
async def test_add_api_key_appends_newline_when_file_has_content(
mock_auth_utils, mock_logger
):
"""
Test scenario: File already has content, append newline when adding key
Expected: Write newline then write key (lines 48-51)
"""
mock_auth_utils.API_KEYS = set()
request = ApiKeyRequest(key="new-key-987654")
# Mock file with existing content
mock_file = mock_open(read_data="existing-key\n")
with patch("builtins.open", mock_file):
await add_api_key(request=request, logger=mock_logger)
# Verify: Newline written before key (line 50)
handle = mock_file()
write_calls = [call.args[0] for call in handle.write.call_args_list]
# Should have both newline and key
assert any("\n" in call for call in write_calls)
assert any("new-key-987654" in call for call in write_calls)
@pytest.mark.asyncio
async def test_test_api_key_valid(mock_auth_utils, mock_logger):
"""
Test scenario: Test valid API key
Expected: Return valid=True (lines 70-87)
"""
request = ApiKeyTestRequest(key="valid-key-123")
mock_auth_utils.verify_api_key.return_value = True
response = await api_key_test_endpoint(request=request, logger=mock_logger)
# Verify: verify_api_key called (line 77)
mock_auth_utils.verify_api_key.assert_called_once_with("valid-key-123")
# Verify: logger.info called (lines 78-80)
assert mock_logger.info.call_count == 1
log_msg = mock_logger.info.call_args[0][0]
assert "API key test" in log_msg
assert "Valid" in log_msg
# Verify: Response (lines 81-87)
assert response.status_code == 200
content = bytes(response.body).decode()
assert '"valid":true' in content.lower()
assert '"message":"key valid"' in content.lower()
@pytest.mark.asyncio
async def test_test_api_key_invalid(mock_auth_utils, mock_logger):
"""
Test scenario: Test invalid API key
Expected: Return valid=False
"""
request = ApiKeyTestRequest(key="invalid-key-999")
mock_auth_utils.verify_api_key.return_value = False
response = await api_key_test_endpoint(request=request, logger=mock_logger)
# Verify: verify_api_key called
mock_auth_utils.verify_api_key.assert_called_once_with("invalid-key-999")
# Verify: Response
assert response.status_code == 200
content = bytes(response.body).decode()
assert '"valid":false' in content.lower()
assert '"message":"key invalid or non-existent"' in content.lower()
@pytest.mark.asyncio
async def test_test_api_key_empty_validation(mock_logger):
"""
Test scenario: Test empty key
Expected: Throw HTTPException 400 (lines 73-74)
"""
request = ApiKeyTestRequest(key=" ") # Whitespace only
with pytest.raises(HTTPException) as exc_info:
await api_key_test_endpoint(request=request, logger=mock_logger)
# Verify: HTTPException 400
assert exc_info.value.status_code == 400
assert "API key cannot be empty" in exc_info.value.detail
@pytest.mark.asyncio
async def test_delete_api_key_success(mock_auth_utils, mock_logger):
"""
Test scenario: Successfully delete API key
Expected: Delete key from file and return success response (lines 93-119)
"""
mock_auth_utils.API_KEYS = {"key-to-delete", "key-to-keep"}
request = ApiKeyRequest(key="key-to-delete")
# Mock file operations
mock_file = mock_open(read_data="key-to-delete\nkey-to-keep\n")
with patch("builtins.open", mock_file):
response = await delete_api_key(request=request, logger=mock_logger)
# Verify: initialize_keys called twice (lines 99, 111)
assert mock_auth_utils.initialize_keys.call_count == 2
# Verify: File read and write (lines 105-109)
assert mock_file.call_count == 2 # Once for read, once for write
# Verify: logger.info called (line 112)
assert mock_logger.info.call_count == 1
assert "API key deleted" in mock_logger.info.call_args[0][0]
# Verify: Response (lines 113-119)
assert response.status_code == 200
content = bytes(response.body).decode()
assert '"success":true' in content.lower()
assert '"message":"api key deleted successfully"' in content.lower()
@pytest.mark.asyncio
async def test_delete_api_key_empty_validation(mock_logger):
"""
Test scenario: Delete empty key
Expected: Throw HTTPException 400 (lines 96-97)
"""
request = ApiKeyRequest(key=" ") # Whitespace only
with pytest.raises(HTTPException) as exc_info:
await delete_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 400
assert exc_info.value.status_code == 400
assert "API key cannot be empty" in exc_info.value.detail
@pytest.mark.asyncio
async def test_delete_api_key_not_found(mock_auth_utils, mock_logger):
"""
Test scenario: Delete non-existent key
Expected: Throw HTTPException 404 (lines 100-101)
"""
mock_auth_utils.API_KEYS = {"existing-key"}
request = ApiKeyRequest(key="non-existent-key")
with pytest.raises(HTTPException) as exc_info:
await delete_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 404 (line 101)
assert exc_info.value.status_code == 404
assert "API key does not exist" in exc_info.value.detail
@pytest.mark.asyncio
async def test_delete_api_key_file_exception(mock_auth_utils, mock_logger):
"""
Test scenario: File operation failed
Expected: Throw HTTPException 500 (lines 120-122)
"""
mock_auth_utils.API_KEYS = {"key-to-delete"}
request = ApiKeyRequest(key="key-to-delete")
# Mock file open to raise exception
with patch("builtins.open", side_effect=IOError("Permission denied")):
with pytest.raises(HTTPException) as exc_info:
await delete_api_key(request=request, logger=mock_logger)
# Verify: HTTPException 500 (line 122)
assert exc_info.value.status_code == 500
assert "Permission denied" in exc_info.value.detail
# Verify: logger.error called (line 121)
assert mock_logger.error.call_count == 1
assert "Failed to delete API key" in mock_logger.error.call_args[0][0]