Spaces:
Paused
Paused
| """ | |
| 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_' | |
| ) | |
| 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 | |
| def mock_logger(): | |
| """Mock logger instance.""" | |
| return MagicMock() | |
| 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() | |
| 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() | |
| 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] | |
| 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() | |
| 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 | |
| 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 | |
| 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 | |
| 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] | |
| 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) | |
| 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() | |
| 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() | |
| 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 | |
| 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() | |
| 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 | |
| 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 | |
| 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] | |