File size: 12,113 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
"""
High-quality tests for stream/proxy_connector.py - Proxy connection handling.

Focus: Test proxy connector initialization and connection creation for various proxy types.
Strategy: Mock only external I/O boundaries (asyncio.open_connection, Proxy.from_url).
"""

import ssl as ssl_module
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from stream.proxy_connector import ProxyConnector

# ============================================================================
# __init__() Tests
# ============================================================================


def test_proxy_connector_init_without_proxy():
    """Test scenario: Initialization without proxy URL"""
    connector = ProxyConnector()
    assert connector.proxy_url is None
    assert connector.connector is None


def test_proxy_connector_init_with_http_proxy():
    """Test scenario: HTTP proxy initialization"""
    connector = ProxyConnector("http://proxy.example.com:8080")
    assert connector.proxy_url == "http://proxy.example.com:8080"
    assert connector.connector == "SocksConnector"


def test_proxy_connector_init_with_https_proxy():
    """Test scenario: HTTPS proxy initialization"""
    connector = ProxyConnector("https://proxy.example.com:443")
    assert connector.proxy_url == "https://proxy.example.com:443"
    assert connector.connector == "SocksConnector"


def test_proxy_connector_init_with_socks4_proxy():
    """Test scenario: SOCKS4 proxy initialization"""
    connector = ProxyConnector("socks4://proxy.example.com:1080")
    assert connector.proxy_url == "socks4://proxy.example.com:1080"
    assert connector.connector == "SocksConnector"


def test_proxy_connector_init_with_socks5_proxy():
    """Test scenario: SOCKS5 proxy initialization"""
    connector = ProxyConnector("socks5://proxy.example.com:1080")
    assert connector.proxy_url == "socks5://proxy.example.com:1080"
    assert connector.connector == "SocksConnector"


def test_proxy_connector_init_with_invalid_proxy_type():
    """Test scenario: Unsupported proxy type"""
    with pytest.raises(ValueError, match="Unsupported proxy type: ftp"):
        ProxyConnector("ftp://proxy.example.com:21")


def test_proxy_connector_init_with_mixed_case_proxy_type():
    """Test scenario: Mixed case proxy type (should be case-insensitive)"""
    connector = ProxyConnector("HTTP://proxy.example.com:8080")
    assert connector.connector == "SocksConnector"

    connector2 = ProxyConnector("SOCKS5://proxy.example.com:1080")
    assert connector2.connector == "SocksConnector"


# ============================================================================
# _setup_connector() Tests
# ============================================================================


def test_setup_connector_with_no_proxy_url():
    """Test scenario: set TCPConnector when proxy_url is None"""
    from aiohttp import TCPConnector

    connector = ProxyConnector()
    # Mock TCPConnector to avoid requiring event loop
    with patch("stream.proxy_connector.TCPConnector") as mock_tcp:
        mock_tcp.return_value = MagicMock(spec=TCPConnector)
        # Manually call _setup_connector with proxy_url=None
        connector._setup_connector()
        # Should call TCPConnector
        mock_tcp.assert_called_once()
        assert connector.connector is mock_tcp.return_value


def test_setup_connector_with_socks_proxy():
    """Test scenario: set SocksConnector for SOCKS proxy"""
    connector = ProxyConnector("socks5://localhost:1080")
    # Already called in __init__
    assert connector.connector == "SocksConnector"


def test_setup_connector_with_http_proxy():
    """Test scenario: set SocksConnector for HTTP proxy"""
    connector = ProxyConnector("http://localhost:8080")
    # Already called in __init__
    assert connector.connector == "SocksConnector"


def test_setup_connector_with_invalid_scheme():
    """Test scenario: Invalid scheme raises ValueError"""
    with pytest.raises(ValueError, match="Unsupported proxy type"):
        ProxyConnector("rtsp://example.com:554")


# ============================================================================
# create_connection() Tests - Direct Connection (No Proxy)
# ============================================================================


@pytest.mark.asyncio
async def test_create_connection_direct_no_ssl():
    """Test scenario: No proxy, direct connection, no SSL"""
    connector = ProxyConnector()

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()

    with patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open:
        mock_open.return_value = (mock_reader, mock_writer)

        reader, writer = await connector.create_connection("example.com", 80, ssl=None)

        # Verify: asyncio.open_connection called correctly
        mock_open.assert_called_once_with("example.com", 80, ssl=None)
        assert reader is mock_reader
        assert writer is mock_writer


@pytest.mark.asyncio
async def test_create_connection_direct_with_ssl():
    """Test scenario: No proxy, direct connection, SSL enabled"""
    connector = ProxyConnector()

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()
    mock_ssl_context = MagicMock()

    with patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open:
        mock_open.return_value = (mock_reader, mock_writer)

        reader, writer = await connector.create_connection(
            "example.com", 443, ssl=mock_ssl_context
        )

        # Verify: asyncio.open_connection uses SSL context
        mock_open.assert_called_once_with("example.com", 443, ssl=mock_ssl_context)
        assert reader is mock_reader
        assert writer is mock_writer


# ============================================================================
# create_connection() Tests - SOCKS Proxy
# ============================================================================


@pytest.mark.asyncio
async def test_create_connection_socks_no_ssl():
    """Test scenario: SOCKS proxy, no SSL"""
    connector = ProxyConnector("socks5://localhost:1080")

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()
    mock_sock = MagicMock()
    mock_proxy = MagicMock()
    mock_proxy.connect = AsyncMock(return_value=mock_sock)

    with (
        patch(
            "stream.proxy_connector.Proxy.from_url", return_value=mock_proxy
        ) as mock_from_url,
        patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open,
    ):
        mock_open.return_value = (mock_reader, mock_writer)

        reader, writer = await connector.create_connection("example.com", 80, ssl=None)

        # Verify: Proxy.from_url is called
        mock_from_url.assert_called_once_with("socks5://localhost:1080")

        # Verify: proxy.connect is called
        mock_proxy.connect.assert_called_once_with(
            dest_host="example.com", dest_port=80
        )

        # Verify: asyncio.open_connection uses sock, no SSL
        mock_open.assert_called_once_with(
            host=None, port=None, sock=mock_sock, ssl=None
        )

        assert reader is mock_reader
        assert writer is mock_writer


@pytest.mark.asyncio
async def test_create_connection_socks_with_ssl():
    """Test scenario: SOCKS proxy, SSL enabled"""
    connector = ProxyConnector("socks5://localhost:1080")

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()
    mock_sock = MagicMock()
    mock_proxy = MagicMock()
    mock_proxy.connect = AsyncMock(return_value=mock_sock)

    with (
        patch("stream.proxy_connector.Proxy.from_url", return_value=mock_proxy),
        patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open,
    ):
        mock_open.return_value = (mock_reader, mock_writer)

        # 传入 ssl=True 来触发 SSL 上下文创建
        reader, writer = await connector.create_connection("example.com", 443, ssl=True)

        # Verify: proxy.connect is called
        mock_proxy.connect.assert_called_once_with(
            dest_host="example.com", dest_port=443
        )

        # Verify: asyncio.open_connection uses sock and SSL context
        mock_open.assert_called_once()
        call_kwargs = mock_open.call_args[1]
        assert call_kwargs["host"] is None
        assert call_kwargs["port"] is None
        assert call_kwargs["sock"] is mock_sock
        assert isinstance(call_kwargs["ssl"], ssl_module.SSLContext)
        assert call_kwargs["server_hostname"] == "example.com"

        # Verify: SSL context configuration
        ssl_ctx = call_kwargs["ssl"]
        assert ssl_ctx.check_hostname is False
        assert ssl_ctx.verify_mode == ssl_module.CERT_NONE

        assert reader is mock_reader
        assert writer is mock_writer


@pytest.mark.asyncio
async def test_create_connection_socks_with_custom_ssl_context():
    """Test scenario: SOCKS proxy, custom SSL context"""
    connector = ProxyConnector("socks5://localhost:1080")

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()
    mock_sock = MagicMock()
    mock_proxy = MagicMock()
    mock_proxy.connect = AsyncMock(return_value=mock_sock)

    # 创建自定义 SSL 上下文
    custom_ssl = ssl_module.SSLContext(ssl_module.PROTOCOL_TLS_CLIENT)

    with (
        patch("stream.proxy_connector.Proxy.from_url", return_value=mock_proxy),
        patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open,
    ):
        mock_open.return_value = (mock_reader, mock_writer)

        # 传入自定义 SSL 上下文(非 None 非 True)
        reader, writer = await connector.create_connection(
            "example.com", 443, ssl=custom_ssl
        )

        # Verify: asyncio.open_connection uses custom SSL context
        # 注意: 代码中 ssl != None 时会创建新的 SSL 上下文,而不是使用传入的
        mock_open.assert_called_once()
        call_kwargs = mock_open.call_args[1]
        assert call_kwargs["sock"] is mock_sock
        # 代码会创建新的 SSLContext,不使用传入的
        assert isinstance(call_kwargs["ssl"], ssl_module.SSLContext)
        assert call_kwargs["server_hostname"] == "example.com"


@pytest.mark.asyncio
async def test_create_connection_http_proxy():
    """Test scenario: HTTP proxy connection"""
    connector = ProxyConnector("http://proxy.example.com:8080")

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()
    mock_sock = MagicMock()
    mock_proxy = MagicMock()
    mock_proxy.connect = AsyncMock(return_value=mock_sock)

    with (
        patch(
            "stream.proxy_connector.Proxy.from_url", return_value=mock_proxy
        ) as mock_from_url,
        patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open,
    ):
        mock_open.return_value = (mock_reader, mock_writer)

        reader, writer = await connector.create_connection("target.com", 80)

        # Verify: Proxy.from_url uses HTTP proxy URL
        mock_from_url.assert_called_once_with("http://proxy.example.com:8080")

        # Verify: Connection to target host
        mock_proxy.connect.assert_called_once_with(dest_host="target.com", dest_port=80)


@pytest.mark.asyncio
async def test_create_connection_socks_proxy_with_auth():
    """Test scenario: SOCKS proxy with authentication"""
    proxy_url = "socks5://user:pass@localhost:1080"
    connector = ProxyConnector(proxy_url)

    mock_reader = AsyncMock()
    mock_writer = AsyncMock()
    mock_sock = MagicMock()
    mock_proxy = MagicMock()
    mock_proxy.connect = AsyncMock(return_value=mock_sock)

    with (
        patch(
            "stream.proxy_connector.Proxy.from_url", return_value=mock_proxy
        ) as mock_from_url,
        patch("asyncio.open_connection", new_callable=AsyncMock) as mock_open,
    ):
        mock_open.return_value = (mock_reader, mock_writer)

        reader, writer = await connector.create_connection("example.com", 80)

        # Verify: Proxy.from_url receives URL with authentication
        mock_from_url.assert_called_once_with(proxy_url)

        mock_proxy.connect.assert_called_once_with(
            dest_host="example.com", dest_port=80
        )