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]