1
1
import logging
2
2
import os
3
+ from dataclasses import dataclass
3
4
from http .server import HTTPServer
4
5
from pathlib import Path
5
- from typing import Any , Iterator , List , Optional
6
+ from typing import Any , Iterator , List , Optional , Tuple
7
+ from unittest .mock import patch
6
8
from urllib .parse import urlparse
7
9
from urllib .request import getproxies
8
10
9
11
import pytest
10
12
from pip ._vendor import requests
11
13
12
14
from pip import __version__
13
- from pip ._internal .exceptions import DiagnosticPipError
15
+ from pip ._internal .exceptions import (
16
+ ConnectionFailedError ,
17
+ ConnectionTimeoutError ,
18
+ DiagnosticPipError ,
19
+ ProxyConnectionError ,
20
+ SSLVerificationError ,
21
+ )
14
22
from pip ._internal .models .link import Link
15
23
from pip ._internal .network .session import (
16
24
CI_ENVIRONMENT_VARIABLES ,
17
25
PipSession ,
18
26
user_agent ,
19
27
)
20
28
from pip ._internal .utils .logging import VERBOSE
29
+ from tests .lib .output import render_to_text
21
30
from tests .lib .server import InstantCloseHTTPHandler , server_running
22
31
23
32
33
+ def render_diagnostic_error (error : DiagnosticPipError ) -> Tuple [str , Optional [str ]]:
34
+ message = render_to_text (error .message ).rstrip ()
35
+ if error .context is None :
36
+ return (message , None )
37
+ return (message , render_to_text (error .context ).rstrip ())
38
+
39
+
40
+ @dataclass (frozen = True )
41
+ class Address :
42
+ host : str
43
+ port : int
44
+
45
+ @property
46
+ def url (self ) -> str :
47
+ return f"http://{ self .host } :{ self .port } /"
48
+
49
+
50
+ @pytest .fixture (scope = "module" )
51
+ def instant_close_server () -> Iterator [Address ]:
52
+ with HTTPServer (("127.0.0.1" , 0 ), InstantCloseHTTPHandler ) as server :
53
+ with server_running (server ):
54
+ yield Address (server .server_name , server .server_port )
55
+
56
+
24
57
def get_user_agent () -> str :
25
58
# These tests are testing the computation of the user agent, so we want to
26
59
# avoid reusing cached values.
@@ -321,14 +354,15 @@ def test_timeout(self, caplog: pytest.LogCaptureFixture) -> None:
321
354
"server didn't respond within 0.2 seconds, retrying 1 last time"
322
355
]
323
356
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
- ]
357
+ def test_connection_closed_by_peer (
358
+ self , caplog : pytest .LogCaptureFixture , instant_close_server : Address
359
+ ) -> None :
360
+ with PipSession (retries = 1 ) as session :
361
+ with pytest .raises (DiagnosticPipError ):
362
+ session .get (instant_close_server .url )
363
+ assert caplog .messages == [
364
+ "the connection was closed unexpectedly, retrying 1 last time"
365
+ ]
332
366
333
367
def test_proxy (self , caplog : pytest .LogCaptureFixture ) -> None :
334
368
with PipSession (retries = 1 ) as session :
@@ -345,3 +379,70 @@ def test_verbose(self, caplog: pytest.LogCaptureFixture) -> None:
345
379
warnings = [r .message for r in caplog .records if r .levelno == logging .WARNING ]
346
380
assert len (warnings ) == 1
347
381
assert not warnings [0 ].endswith ("retrying 1 last time" )
382
+
383
+
384
+ @pytest .mark .network
385
+ class TestConnectionErrors :
386
+ @pytest .fixture
387
+ def session (self ) -> Iterator [PipSession ]:
388
+ with PipSession () as session :
389
+ yield session
390
+
391
+ def test_non_existent_domain (self , session : PipSession ) -> None :
392
+ url = "https://404.example.com/"
393
+ with pytest .raises (ConnectionFailedError ) as e :
394
+ session .get (url )
395
+ message , _ = render_diagnostic_error (e .value )
396
+ assert message == f"Failed to connect to 404.example.com while fetching { url } "
397
+
398
+ def test_connection_closed_by_peer (
399
+ self , session : PipSession , instant_close_server : Address
400
+ ) -> None :
401
+ with pytest .raises (ConnectionFailedError ) as e :
402
+ session .get (instant_close_server .url )
403
+ message , context = render_diagnostic_error (e .value )
404
+ assert message == (
405
+ f"Failed to connect to { instant_close_server .host } "
406
+ f"while fetching { instant_close_server .url } "
407
+ )
408
+ assert context == (
409
+ "Details: the connection was closed without a reply from the server."
410
+ )
411
+
412
+ def test_timeout (self , session : PipSession ) -> None :
413
+ url = "https://httpstat.us/200?sleep=400"
414
+ with pytest .raises (ConnectionTimeoutError ) as e :
415
+ session .get (url , timeout = 0.2 )
416
+ message , context = render_diagnostic_error (e .value )
417
+ assert message == f"Unable to fetch { url } "
418
+ assert context is not None
419
+ assert context .startswith ("httpstat.us didn't respond within 0.2 seconds" )
420
+
421
+ def test_expired_ssl (self , session : PipSession ) -> None :
422
+ url = "https://expired.badssl.com/"
423
+ with pytest .raises (SSLVerificationError ) as e :
424
+ session .get (url )
425
+ message , _ = render_diagnostic_error (e .value )
426
+ assert message == (
427
+ "Failed to establish a secure connection to expired.badssl.com "
428
+ f"while fetching { url } "
429
+ )
430
+
431
+ @patch ("pip._internal.network.utils.has_tls" , lambda : False )
432
+ def test_missing_python_ssl_support (self , session : PipSession ) -> None :
433
+ # So, this demonstrates a potentially invalid assumption: a SSL error
434
+ # when SSL support is missing is assumed to be caused by that. Not ideal
435
+ # but unlikely to cause issues in practice.
436
+ with pytest .raises (SSLVerificationError ) as e :
437
+ session .get ("https://expired.badssl.com/" )
438
+ _ , context = render_diagnostic_error (e .value )
439
+ assert context == "The built-in ssl module is not available."
440
+
441
+ def test_broken_proxy (self , session : PipSession ) -> None :
442
+ url = "https://pypi.org/"
443
+ proxy = "https://404.example.com"
444
+ session .proxies = {"https" : proxy }
445
+ with pytest .raises (ProxyConnectionError ) as e :
446
+ session .get (url )
447
+ message , _ = render_diagnostic_error (e .value )
448
+ assert message == f"Failed to connect to proxy { proxy } :443 while fetching { url } "
0 commit comments