2
2
import os
3
3
from http .server import HTTPServer
4
4
from pathlib import Path
5
- from typing import Any , Iterator , List , Optional
5
+ from typing import Any , Iterator , List , Optional , Tuple
6
+ from unittest .mock import patch
6
7
from urllib .parse import urlparse
7
8
from urllib .request import getproxies
8
9
9
10
import pytest
10
11
from pip ._vendor import requests
11
12
12
13
from pip import __version__
13
- from pip ._internal .exceptions import DiagnosticPipError
14
+ from pip ._internal .exceptions import (
15
+ ConnectionFailedError ,
16
+ ConnectionTimeoutError ,
17
+ DiagnosticPipError ,
18
+ ProxyConnectionError ,
19
+ SSLVerificationError ,
20
+ )
14
21
from pip ._internal .models .link import Link
15
22
from pip ._internal .network .session import (
16
23
CI_ENVIRONMENT_VARIABLES ,
17
24
PipSession ,
18
25
user_agent ,
19
26
)
20
27
from pip ._internal .utils .logging import VERBOSE
28
+ from tests .lib .output import render_to_text
21
29
from tests .lib .server import InstantCloseHTTPHandler , server_running
22
30
23
31
32
+ def render_diagnostic_error (error : DiagnosticPipError ) -> Tuple [str , Optional [str ]]:
33
+ message = render_to_text (error .message ).rstrip ()
34
+ if error .context is None :
35
+ return (message , None )
36
+ return (message , render_to_text (error .context ).rstrip ())
37
+
38
+
24
39
def get_user_agent () -> str :
25
40
# These tests are testing the computation of the user agent, so we want to
26
41
# avoid reusing cached values.
@@ -287,6 +302,13 @@ def test_proxy(self, proxy: Optional[str]) -> None:
287
302
)
288
303
289
304
305
+ @pytest .fixture (scope = "module" )
306
+ def instant_close_http_server () -> Iterator [str ]:
307
+ with HTTPServer (("localhost" , 0 ), InstantCloseHTTPHandler ) as server :
308
+ with server_running (server ):
309
+ yield f"http://{ server .server_name } :{ server .server_port } /"
310
+
311
+
290
312
@pytest .mark .network
291
313
class TestRetryWarningRewriting :
292
314
@pytest .fixture (autouse = True )
@@ -321,14 +343,15 @@ def test_timeout(self, caplog: pytest.LogCaptureFixture) -> None:
321
343
"server didn't respond within 0.2 seconds, retrying 1 last time"
322
344
]
323
345
324
- def test_connection_aborted (self , caplog : pytest .LogCaptureFixture ) -> None :
325
- with HTTPServer (("localhost" , 0 ), InstantCloseHTTPHandler ) as server :
326
- with server_running (server ), PipSession (retries = 1 ) as session :
327
- with pytest .raises (DiagnosticPipError ):
328
- session .get (f"http://{ server .server_name } :{ server .server_port } /" )
329
- assert caplog .messages == [
330
- "the connection was closed unexpectedly, retrying 1 last time"
331
- ]
346
+ def test_connection_closed_by_peer (
347
+ self , caplog : pytest .LogCaptureFixture , instant_close_http_server : str
348
+ ) -> None :
349
+ with PipSession (retries = 1 ) as session :
350
+ with pytest .raises (DiagnosticPipError ):
351
+ session .get (instant_close_http_server )
352
+ assert caplog .messages == [
353
+ "the connection was closed unexpectedly, retrying 1 last time"
354
+ ]
332
355
333
356
def test_proxy (self , caplog : pytest .LogCaptureFixture ) -> None :
334
357
with PipSession (retries = 1 ) as session :
@@ -345,3 +368,69 @@ def test_verbose(self, caplog: pytest.LogCaptureFixture) -> None:
345
368
warnings = [r .message for r in caplog .records if r .levelno == logging .WARNING ]
346
369
assert len (warnings ) == 1
347
370
assert not warnings [0 ].endswith ("retrying 1 last time" )
371
+
372
+
373
+ @pytest .mark .network
374
+ class TestConnectionErrors :
375
+ @pytest .fixture
376
+ def session (self ) -> Iterator [PipSession ]:
377
+ with PipSession () as session :
378
+ yield session
379
+
380
+ def test_non_existent_domain (self , session : PipSession ) -> None :
381
+ url = "https://404.example.com/"
382
+ with pytest .raises (ConnectionFailedError ) as e :
383
+ session .get (url )
384
+ message , _ = render_diagnostic_error (e .value )
385
+ assert message == f"Failed to connect to 404.example.com while fetching { url } "
386
+
387
+ def test_connection_closed_by_peer (
388
+ self , session : PipSession , instant_close_http_server : str
389
+ ) -> None :
390
+ with pytest .raises (ConnectionFailedError ) as e :
391
+ session .get (instant_close_http_server )
392
+ message , context = render_diagnostic_error (e .value )
393
+ assert message == (
394
+ f"Failed to connect to localhost while fetching { instant_close_http_server } "
395
+ )
396
+ assert context == (
397
+ "Details: the connection was closed without a reply from the server."
398
+ )
399
+
400
+ def test_timeout (self , session : PipSession ) -> None :
401
+ url = "https://httpstat.us/200?sleep=400"
402
+ with pytest .raises (ConnectionTimeoutError ) as e :
403
+ session .get (url , timeout = 0.2 )
404
+ message , context = render_diagnostic_error (e .value )
405
+ assert message == f"Unable to fetch { url } "
406
+ assert context is not None
407
+ assert context .startswith ("httpstat.us didn't respond within 0.2 seconds" )
408
+
409
+ def test_expired_ssl (self , session : PipSession ) -> None :
410
+ url = "https://expired.badssl.com/"
411
+ with pytest .raises (SSLVerificationError ) as e :
412
+ session .get (url )
413
+ message , _ = render_diagnostic_error (e .value )
414
+ assert message == (
415
+ "Failed to establish a secure connection to expired.badssl.com "
416
+ f"while fetching { url } "
417
+ )
418
+
419
+ @patch ("pip._internal.network.utils.has_tls" , lambda : False )
420
+ def test_missing_python_ssl_support (self , session : PipSession ) -> None :
421
+ # So, this demonstrates a potentially invalid assumption: a SSL error
422
+ # when SSL support is missing is assumed to be caused by that. Not ideal
423
+ # but unlikely to cause issues in practice.
424
+ with pytest .raises (SSLVerificationError ) as e :
425
+ session .get ("https://expired.badssl.com/" )
426
+ _ , context = render_diagnostic_error (e .value )
427
+ assert context == "The built-in ssl module is not available."
428
+
429
+ def test_broken_proxy (self , session : PipSession ) -> None :
430
+ url = "https://pypi.org/"
431
+ proxy = "https://404.example.com"
432
+ session .proxies = {"https" : proxy }
433
+ with pytest .raises (ProxyConnectionError ) as e :
434
+ session .get (url )
435
+ message , _ = render_diagnostic_error (e .value )
436
+ assert message == f"Failed to connect to proxy { proxy } :443 while fetching { url } "
0 commit comments