File size: 9,332 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
"""
High-quality tests for api_utils/routers/info.py - API info endpoint.

Focus: Test get_api_info endpoint with various request configurations.
Strategy: Mock only dependencies (auth_utils.API_KEYS, get_current_ai_studio_model_id), test actual logic.
"""

from unittest.mock import patch

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from api_utils.routers.info import get_api_info


@pytest.fixture
def app():
    """Create test FastAPI app with info endpoint."""
    app = FastAPI()
    app.get("/info")(get_api_info)
    return app


@pytest.fixture
def client(app):
    """Create test client."""
    return TestClient(app)


def test_get_api_info_no_auth_required(client):
    """
    Test scenario: API requires no authentication, returns basic info
    Expected: api_key_required=False, auth_header=None, supported_auth_methods=[]
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", []),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "gemini-2.0-flash"),
    ):
        response = client.get("/info")

        assert response.status_code == 200
        data = response.json()

        # Verify basic fields
        assert data["model_name"] == "gemini-2.0-flash"  # Use MODEL_NAME as fallback
        assert data["openai_compatible"] is True
        assert data["api_key_required"] is False
        assert data["api_key_count"] == 0
        assert data["auth_header"] is None
        assert data["supported_auth_methods"] == []
        assert data["message"] == "API Key is not required."

        # Verify URL construction
        assert data["server_base_url"].startswith("http")
        assert data["api_base_url"].endswith("/v1")


def test_get_api_info_with_auth_required(client):
    """
    Test scenario: API requires authentication, 3 keys configured
    Expected: api_key_required=True, contains authentication header info
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", ["key1", "key2", "key3"]),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id",
            return_value="gemini-1.5-pro",
        ),
    ):
        response = client.get("/info")

        assert response.status_code == 200
        data = response.json()

        assert data["api_key_required"] is True
        assert data["api_key_count"] == 3
        assert (
            data["auth_header"] == "Authorization: Bearer <token> or X-API-Key: <token>"
        )
        assert data["supported_auth_methods"] == [
            "Authorization: Bearer",
            "X-API-Key",
        ]
        assert data["message"] == "API Key is required. 3 valid key(s) configured."


def test_get_api_info_with_custom_model_id(app, client):
    """
    Test scenario: Use custom model ID
    Expected: Return model ID provided by dependency
    """
    from api_utils.dependencies import get_current_ai_studio_model_id

    # Use FastAPI dependency override
    app.dependency_overrides[get_current_ai_studio_model_id] = (
        lambda: "gemini-2.0-flash-thinking-exp"
    )

    with patch("api_utils.auth_utils.API_KEYS", []):
        response = client.get("/info")

        assert response.status_code == 200
        data = response.json()

        assert data["model_name"] == "gemini-2.0-flash-thinking-exp"

    # Clean up override
    app.dependency_overrides.clear()


def test_get_api_info_with_custom_host_header(client):
    """
    Test scenario: Request contains custom Host header
    Expected: Use Host header to construct URL
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", []),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "gemini-1.5-pro"),
    ):
        response = client.get("/info", headers={"host": "api.example.com:8080"})

        assert response.status_code == 200
        data = response.json()

        assert "api.example.com:8080" in data["server_base_url"]
        assert "api.example.com:8080" in data["api_base_url"]


def test_get_api_info_with_x_forwarded_proto_https(client):
    """
    Test scenario: Request via HTTPS reverse proxy, with X-Forwarded-Proto header
    Expected: Use https as scheme
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", []),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "gemini-1.5-pro"),
    ):
        response = client.get(
            "/info",
            headers={"x-forwarded-proto": "https", "host": "api.example.com"},
        )

        assert response.status_code == 200
        data = response.json()

        assert data["server_base_url"].startswith("https://")
        assert data["api_base_url"].startswith("https://")


def test_get_api_info_with_custom_port_via_env(client):
    """
    Test scenario: Set custom port via environment variable
    Expected: Use SERVER_PORT_INFO environment variable value
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", []),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "gemini-1.5-pro"),
        patch("api_utils.routers.info.get_environment_variable", return_value="9999"),
    ):
        # No port info provided, should read from environment variable
        response = client.get("/info")

        assert response.status_code == 200
        response.json()

        # TestClient uses testserver by default, but if request.url.port is None,
        # it will fall back to SERVER_PORT_INFO
        # Since TestClient might provide a port, we mainly verify the logic executes correctly


def test_get_api_info_url_construction_with_port(client):
    """
    Test scenario: URL contains port number
    Expected: Correctly construct base URL with port
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", []),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "gemini-1.5-pro"),
    ):
        response = client.get("/info", headers={"host": "localhost:2048"})

        assert response.status_code == 200
        data = response.json()

        assert "localhost:2048" in data["server_base_url"]
        assert data["api_base_url"] == f"{data['server_base_url']}/v1"


def test_get_api_info_with_one_api_key(client):
    """
    Test scenario: Only 1 API key configured
    Expected: api_key_count=1, message singular form
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", ["single_key"]),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "gemini-1.5-pro"),
    ):
        response = client.get("/info")

        assert response.status_code == 200
        data = response.json()

        assert data["api_key_count"] == 1
        assert data["message"] == "API Key is required. 1 valid key(s) configured."


def test_get_api_info_model_fallback_to_default(client):
    """
    Test scenario: current_ai_studio_model_id is None, use MODEL_NAME
    Expected: effective_model_name = MODEL_NAME
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", []),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id", return_value=None
        ),
        patch("api_utils.routers.info.MODEL_NAME", "default-model-name"),
    ):
        response = client.get("/info")

        assert response.status_code == 200
        data = response.json()

        assert data["model_name"] == "default-model-name"


def test_get_api_info_response_structure(client):
    """
    Test scenario: Verify complete response structure
    Expected: Contains all required fields
    """
    with (
        patch("api_utils.auth_utils.API_KEYS", ["key1", "key2"]),
        patch(
            "api_utils.routers.info.get_current_ai_studio_model_id",
            return_value="test-model",
        ),
    ):
        response = client.get("/info")

        assert response.status_code == 200
        data = response.json()

        # Verify all required fields exist
        required_fields = [
            "model_name",
            "api_base_url",
            "server_base_url",
            "api_key_required",
            "api_key_count",
            "auth_header",
            "openai_compatible",
            "supported_auth_methods",
            "message",
        ]

        for field in required_fields:
            assert field in data, f"Missing required field: {field}"

        # Verify types
        assert isinstance(data["model_name"], str)
        assert isinstance(data["api_base_url"], str)
        assert isinstance(data["server_base_url"], str)
        assert isinstance(data["api_key_required"], bool)
        assert isinstance(data["api_key_count"], int)
        assert isinstance(data["openai_compatible"], bool)
        assert isinstance(data["supported_auth_methods"], list)
        assert isinstance(data["message"], str)