A Python port of the Invisible Internet Project (I2P)
at main 189 lines 6.0 kB view raw
1"""Tests for net.i2p.util Tier 3 — HTTP client & SSL. 2 3TDD — tests for I2PSSLSocketFactory, EepGet, SSLEepGet. 4Uses a local test HTTP server, no external network access. 5""" 6 7from __future__ import annotations 8 9import http.server 10import json 11import ssl 12import tempfile 13import threading 14import os 15 16import pytest 17 18from i2p_util.ssl_factory import create_ssl_context 19from i2p_util.eep_get import EepGet, EepGetResult 20 21 22# --------------------------------------------------------------------------- 23# SSL Context Factory 24# --------------------------------------------------------------------------- 25 26 27class TestSSLFactory: 28 """SSL context creation.""" 29 30 def test_create_default_context(self): 31 ctx = create_ssl_context() 32 assert isinstance(ctx, ssl.SSLContext) 33 34 def test_minimum_tls_version(self): 35 ctx = create_ssl_context() 36 assert ctx.minimum_version >= ssl.TLSVersion.TLSv1_2 37 38 def test_hostname_check_disabled_for_i2p(self): 39 ctx = create_ssl_context(verify_hostname=False) 40 assert ctx.check_hostname is False 41 42 43# --------------------------------------------------------------------------- 44# Test HTTP server fixture 45# --------------------------------------------------------------------------- 46 47 48class _TestHandler(http.server.BaseHTTPRequestHandler): 49 """Minimal test HTTP server.""" 50 51 def do_GET(self): 52 if self.path == "/hello": 53 body = b"Hello, World!" 54 self.send_response(200) 55 self.send_header("Content-Length", str(len(body))) 56 self.end_headers() 57 self.wfile.write(body) 58 elif self.path == "/redirect": 59 self.send_response(302) 60 self.send_header("Location", "/hello") 61 self.end_headers() 62 elif self.path == "/large": 63 body = b"X" * 10000 64 self.send_response(200) 65 self.send_header("Content-Length", str(len(body))) 66 self.end_headers() 67 self.wfile.write(body) 68 elif self.path == "/not-modified": 69 ims = self.headers.get("If-Modified-Since") 70 if ims: 71 self.send_response(304) 72 self.end_headers() 73 else: 74 body = b"fresh content" 75 self.send_response(200) 76 self.send_header("Content-Length", str(len(body))) 77 self.end_headers() 78 self.wfile.write(body) 79 else: 80 self.send_response(404) 81 self.end_headers() 82 83 def log_message(self, format, *args): 84 pass # suppress logs 85 86 87@pytest.fixture(scope="module") 88def test_server(): 89 """Start a local HTTP server for testing.""" 90 server = http.server.HTTPServer(("127.0.0.1", 0), _TestHandler) 91 port = server.server_address[1] 92 thread = threading.Thread(target=server.serve_forever, daemon=True) 93 thread.start() 94 yield f"http://127.0.0.1:{port}" 95 server.shutdown() 96 97 98# --------------------------------------------------------------------------- 99# EepGet 100# --------------------------------------------------------------------------- 101 102 103class TestEepGet: 104 """HTTP client for I2P.""" 105 106 def test_fetch_success(self, test_server): 107 eg = EepGet() 108 result = eg.fetch(f"{test_server}/hello") 109 assert result.success is True 110 assert result.data == b"Hello, World!" 111 assert result.status_code == 200 112 113 def test_fetch_404(self, test_server): 114 eg = EepGet() 115 result = eg.fetch(f"{test_server}/missing") 116 assert result.status_code == 404 117 118 def test_fetch_follow_redirect(self, test_server): 119 eg = EepGet() 120 result = eg.fetch(f"{test_server}/redirect") 121 assert result.success is True 122 assert result.data == b"Hello, World!" 123 124 def test_fetch_to_file(self, test_server): 125 eg = EepGet() 126 with tempfile.NamedTemporaryFile(delete=False) as f: 127 tmp_path = f.name 128 try: 129 result = eg.fetch(f"{test_server}/hello", output_file=tmp_path) 130 assert result.success is True 131 with open(tmp_path, "rb") as f: 132 assert f.read() == b"Hello, World!" 133 finally: 134 os.unlink(tmp_path) 135 136 def test_fetch_large(self, test_server): 137 eg = EepGet() 138 result = eg.fetch(f"{test_server}/large") 139 assert result.success is True 140 assert len(result.data) == 10000 141 142 def test_progress_callback(self, test_server): 143 progress_calls = [] 144 145 def on_progress(downloaded, total): 146 progress_calls.append((downloaded, total)) 147 148 eg = EepGet() 149 result = eg.fetch(f"{test_server}/large", progress_callback=on_progress) 150 assert result.success is True 151 assert len(progress_calls) > 0 152 153 def test_timeout(self): 154 """Connection to non-routable address should timeout.""" 155 eg = EepGet(connect_timeout=0.5) 156 result = eg.fetch("http://192.0.2.1:1/timeout") 157 assert result.success is False 158 159 def test_if_modified_since(self, test_server): 160 eg = EepGet() 161 result = eg.fetch( 162 f"{test_server}/not-modified", 163 if_modified_since="Thu, 01 Jan 2099 00:00:00 GMT", 164 ) 165 assert result.status_code == 304 166 167 def test_user_agent(self, test_server): 168 """Default User-Agent should be 'I2P'.""" 169 eg = EepGet() 170 assert eg.user_agent == "I2P" 171 172 173# --------------------------------------------------------------------------- 174# EepGetResult 175# --------------------------------------------------------------------------- 176 177 178class TestEepGetResult: 179 """Result object from EepGet.""" 180 181 def test_success_result(self): 182 r = EepGetResult(success=True, status_code=200, data=b"ok") 183 assert r.success 184 assert r.content_length == 2 185 186 def test_failure_result(self): 187 r = EepGetResult(success=False, status_code=0, data=b"", error="timeout") 188 assert not r.success 189 assert r.error == "timeout"