File size: 14,874 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
380
381
382
383
384
385
386
387
388
389
390
391
"""
Tests for api_utils/page_response.py - Response element location.

Test Strategy:
- Mock only external boundaries: Playwright page and expect_async
- Use fixtures to reduce mock duplication (page with locator chain)
- Organize into test classes by error type
- Test success path, timeout handling, error paths

Coverage Target: 100% (simple 33-line file)
Mock Budget: <40 (down from ~56)
"""

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import HTTPException
from playwright.async_api import Error as PlaywrightAsyncError

from api_utils.page_response import locate_response_elements
from models.exceptions import ClientDisconnectedError


@pytest.fixture
def mock_page_response_setup():
    """Fixture providing common mocks for page_response tests."""
    logger = MagicMock()
    page = MagicMock()
    check_client_disconnected = MagicMock()

    # Mock locator chain: page.locator().last.locator()
    response_container_locator = MagicMock()
    response_element_locator = MagicMock()
    page.locator.return_value.last = response_container_locator
    response_container_locator.locator.return_value = response_element_locator

    return {
        "logger": logger,
        "page": page,
        "check_disconnect": check_client_disconnected,
        "container_locator": response_container_locator,
        "element_locator": response_element_locator,
    }


class TestLocateResponseElementsSuccess:
    """Tests for successful response element location."""

    @pytest.mark.asyncio
    async def test_successful_location_completes_without_error(
        self, mock_page_response_setup
    ):
        """Test normal success path completes without raising."""
        setup = mock_page_response_setup

        # Mock expect_async to succeed immediately
        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            # Should not raise
            await locate_response_elements(
                setup["page"],
                "req1",
                setup["logger"],
                setup["check_disconnect"],
            )

    @pytest.mark.asyncio
    async def test_success_logs_start_and_completion_messages(
        self, mock_page_response_setup
    ):
        """Test success path logs correct messages (lines 16, 24)."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            await locate_response_elements(
                setup["page"],
                "req1",
                setup["logger"],
                setup["check_disconnect"],
            )

            # Verify logger.info called twice (lines 16, 24)
            assert setup["logger"].info.call_count == 2
            assert (
                "Locating response elements..."
                in setup["logger"].info.call_args_list[0][0][0]
            )
            assert (
                "Response elements located."
                in setup["logger"].info.call_args_list[1][0][0]
            )

    @pytest.mark.asyncio
    async def test_success_checks_client_disconnect_after_container(
        self, mock_page_response_setup
    ):
        """Test disconnect check happens after container attached (line 22)."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            await locate_response_elements(
                setup["page"],
                "req1",
                setup["logger"],
                setup["check_disconnect"],
            )

            # Verify check_client_disconnected called (line 22)
            setup["check_disconnect"].assert_called_once_with(
                "After Response Container Attached: "
            )

    @pytest.mark.asyncio
    async def test_success_waits_for_both_container_and_element(
        self, mock_page_response_setup
    ):
        """Test both locators are awaited with expect_async."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            await locate_response_elements(
                setup["page"],
                "req1",
                setup["logger"],
                setup["check_disconnect"],
            )

            # Verify expect_async called twice (container + element)
            assert mock_expect.call_count == 2
            # Verify to_be_attached called twice
            assert mock_expect_result.to_be_attached.call_count == 2

    @pytest.mark.asyncio
    async def test_locator_chain_uses_correct_selectors(self, mock_page_response_setup):
        """Test correct selector usage (lines 17-18)."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            await locate_response_elements(
                setup["page"],
                "req1",
                setup["logger"],
                setup["check_disconnect"],
            )

            # Verify page.locator called with RESPONSE_CONTAINER_SELECTOR
            from config import RESPONSE_CONTAINER_SELECTOR

            setup["page"].locator.assert_called_once_with(RESPONSE_CONTAINER_SELECTOR)

            # Verify container.locator called with RESPONSE_TEXT_SELECTOR
            from config import RESPONSE_TEXT_SELECTOR

            setup["container_locator"].locator.assert_called_once_with(
                RESPONSE_TEXT_SELECTOR
            )

    @pytest.mark.asyncio
    async def test_timeout_values_are_correct(self, mock_page_response_setup):
        """Test timeout parameters: container=20000ms, element=90000ms (lines 21, 23)."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            await locate_response_elements(
                setup["page"],
                "req1",
                setup["logger"],
                setup["check_disconnect"],
            )

            # Verify timeout parameters
            calls = mock_expect_result.to_be_attached.call_args_list
            assert calls[0][1]["timeout"] == 20000  # Container timeout
            assert calls[1][1]["timeout"] == 90000  # Element timeout


class TestLocateResponseElementsTimeouts:
    """Tests for timeout error handling."""

    @pytest.mark.asyncio
    async def test_container_timeout_raises_http_502(self, mock_page_response_setup):
        """Test PlaywrightAsyncError during container wait raises 502 (lines 25-28)."""
        setup = mock_page_response_setup

        # Mock expect_async to raise PlaywrightAsyncError on first call
        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock(
                side_effect=PlaywrightAsyncError("Timeout 20000ms exceeded")
            )
            mock_expect.return_value = mock_expect_result

            with pytest.raises(HTTPException) as exc_info:
                await locate_response_elements(
                    setup["page"],
                    "req1",
                    setup["logger"],
                    setup["check_disconnect"],
                )

            # Verify HTTPException status code 502 (upstream error)
            assert exc_info.value.status_code == 502
            assert (
                "Failed to locate AI Studio response elements" in exc_info.value.detail
            )
            assert "Timeout 20000ms exceeded" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_element_timeout_raises_http_502(self, mock_page_response_setup):
        """Test asyncio.TimeoutError during element wait raises 502 (lines 25-28)."""
        setup = mock_page_response_setup

        # Mock expect_async: succeed on first call (container), fail on second (element)
        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result_container = AsyncMock()
            mock_expect_result_container.to_be_attached = AsyncMock()

            mock_expect_result_element = AsyncMock()
            mock_expect_result_element.to_be_attached = AsyncMock(
                side_effect=asyncio.TimeoutError("90000ms timeout")
            )

            # First call returns container result, second call returns element result
            mock_expect.side_effect = [
                mock_expect_result_container,
                mock_expect_result_element,
            ]

            with pytest.raises(HTTPException) as exc_info:
                await locate_response_elements(
                    setup["page"],
                    "req1",
                    setup["logger"],
                    setup["check_disconnect"],
                )

            # Verify HTTPException status code 502 (upstream error)
            assert exc_info.value.status_code == 502
            assert (
                "Failed to locate AI Studio response elements" in exc_info.value.detail
            )

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        "exception_type,error_message",
        [
            (PlaywrightAsyncError, "Playwright timeout error"),
            (asyncio.TimeoutError, "Async timeout error"),
        ],
    )
    async def test_timeout_error_types_both_raise_502(
        self, mock_page_response_setup, exception_type, error_message
    ):
        """Test both PlaywrightAsyncError and asyncio.TimeoutError raise 502."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock(
                side_effect=exception_type(error_message)
            )
            mock_expect.return_value = mock_expect_result

            with pytest.raises(HTTPException) as exc_info:
                await locate_response_elements(
                    setup["page"],
                    "req1",
                    setup["logger"],
                    setup["check_disconnect"],
                )

            assert exc_info.value.status_code == 502
            assert error_message in exc_info.value.detail


class TestLocateResponseElementsErrors:
    """Tests for error handling (client disconnect, generic errors)."""

    @pytest.mark.asyncio
    async def test_client_disconnect_raises_http_500(self, mock_page_response_setup):
        """Test ClientDisconnectedError raises 500 (generic Exception handler)."""
        setup = mock_page_response_setup
        setup["check_disconnect"].side_effect = ClientDisconnectedError(
            "Client disconnected"
        )

        # Mock expect_async to succeed (but check_disconnect raises first)
        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect_result = AsyncMock()
            mock_expect_result.to_be_attached = AsyncMock()
            mock_expect.return_value = mock_expect_result

            with pytest.raises(HTTPException) as exc_info:
                await locate_response_elements(
                    setup["page"],
                    "req1",
                    setup["logger"],
                    setup["check_disconnect"],
                )

            # Verify HTTPException status code 500 (server error from generic handler)
            assert exc_info.value.status_code == 500
            assert (
                "Unexpected error while locating response elements"
                in exc_info.value.detail
            )
            # Verify check_disconnect was called (line 22)
            setup["check_disconnect"].assert_called_once()

    @pytest.mark.asyncio
    async def test_generic_exception_raises_http_500(self, mock_page_response_setup):
        """Test unexpected exception raises 500 with error details (lines 29-32)."""
        setup = mock_page_response_setup

        # Mock expect_async to raise generic exception
        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect.side_effect = ValueError("Unexpected validation error")

            with pytest.raises(HTTPException) as exc_info:
                await locate_response_elements(
                    setup["page"],
                    "req1",
                    setup["logger"],
                    setup["check_disconnect"],
                )

            # Verify HTTPException status code 500 (server error)
            assert exc_info.value.status_code == 500
            assert (
                "Unexpected error while locating response elements"
                in exc_info.value.detail
            )
            assert "Unexpected validation error" in exc_info.value.detail

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        "exception_class,error_msg",
        [
            (ValueError, "Validation failed"),
            (RuntimeError, "Runtime issue"),
            (AttributeError, "Missing attribute"),
        ],
    )
    async def test_various_generic_exceptions_raise_500(
        self, mock_page_response_setup, exception_class, error_msg
    ):
        """Test various generic exceptions are caught and raise 500."""
        setup = mock_page_response_setup

        with patch("api_utils.page_response.expect_async") as mock_expect:
            mock_expect.side_effect = exception_class(error_msg)

            with pytest.raises(HTTPException) as exc_info:
                await locate_response_elements(
                    setup["page"],
                    "req1",
                    setup["logger"],
                    setup["check_disconnect"],
                )

            assert exc_info.value.status_code == 500
            assert (
                "Unexpected error while locating response elements"
                in exc_info.value.detail
            )
            assert error_msg in exc_info.value.detail