Spaces:
Paused
Paused
File size: 12,823 Bytes
a5784e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | """
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]
|