spoof 2.2.2__tar.gz → 2.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,25 @@
1
+ 2.3.1 (2026-05-01)
2
+ ==================
3
+
4
+ - Return ``self`` for ``.restart()`` method
5
+ - Update tests
6
+ - Update docs
7
+
8
+ 2.3.0 (2026-04-27)
9
+ ==================
10
+
11
+ - Add ``.restart()`` convenience method
12
+ - Add ``.serverAddress`` property to allow setting the server address
13
+ (requires restart after setting)
14
+ - Add ``.sslContext`` property to allow setting the sslContext
15
+ (requires restart after setting)
16
+ - Return ``None`` for server address, port, and URL if not started
17
+ - Return ``self`` for ``.start()`` method
18
+ - Apply least permission to GitHub Actions workflows
19
+ - Cleanup code
20
+ - Update tests
21
+ - Update docs
22
+
1
23
  2.2.2 (2026-04-21)
2
24
  ==================
3
25
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spoof
3
- Version: 2.2.2
4
- Summary: HTTP server for testing environments
3
+ Version: 2.3.1
4
+ Summary: A simple HTTP server for test environments
5
5
  Author-email: Lex Scarisbrick <lex@scarisbrick.org>
6
6
  License-Expression: MIT
7
7
  Project-URL: Documentation, https://spoof.readthedocs.io/en/latest/
@@ -19,6 +19,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Topic :: Software Development :: Quality Assurance
21
21
  Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Topic :: Software Development :: Testing :: Mocking
22
23
  Classifier: Topic :: Software Development :: Testing :: Traffic Generation
23
24
  Requires-Python: >=3.10
24
25
  Description-Content-Type: text/x-rst
@@ -56,10 +57,10 @@ Spoof 👻
56
57
 
57
58
  A test interface for HTTP
58
59
  =========================
59
- Spoof lets you easily create HTTP servers listening on real network
60
- sockets. Designed for test environments, what responses to send can be
61
- configured anytime, including while an HTTP server is running. Requests
62
- can be inspected live or after a response is sent.
60
+ Spoof lets you easily create HTTP servers on real network sockets.
61
+ Designed for test environments, what responses to send can be configured
62
+ anytime, including while an HTTP server is running. Requests can be
63
+ inspected live or after a response is sent.
63
64
 
64
65
  Unlike a conventional HTTP server, where specific methods and paths are
65
66
  configured in advance, Spoof accepts and records *all* requests, sending
@@ -72,7 +73,13 @@ HTTP client code. Have you ever felt icky patching a client library to
72
73
  write tests? Ever been burned by this? Ever wanted to refactor a client
73
74
  library, but had no way to verify behavior apart from doing live
74
75
  integration testing? Ever wanted mock for HTTP? If you answered yes to
75
- any of the above, Spoof might be for you.
76
+ any of the above, Spoof might be for you. Some key features:
77
+
78
+ * Decoupled requests and responses
79
+ * SSL/TLS with PQC
80
+ * HTTP/S proxy via CONNECT
81
+ * IPv6
82
+ * Live request debugging
76
83
 
77
84
  Installation and Compatibility
78
85
  ==============================
@@ -84,7 +91,8 @@ Spoof is available on PyPI:
84
91
 
85
92
  Spoof is tested on Python 3.10 to 3.14, leverages the ``http.server`` module
86
93
  included in the Python standard library, and has no external dependencies.
87
- It may work on older versions of Python, but this is not supported.
94
+ It may work on older versions of Python, but this is
95
+ `not supported <https://devguide.python.org/versions/>`__.
88
96
 
89
97
  Multiple Spoof HTTP servers can be run concurrently, and by default, the port
90
98
  number is the next available unused port. With OpenSSL installed, Spoof can
@@ -107,7 +115,7 @@ Spoof expects responses to have the following syntax:
107
115
  # bytes content
108
116
  [200, [("Content-Type", "application/json")], b'{"success": true }']
109
117
 
110
- # responses can also be a callback
118
+ # responses can also be a callback, with request as the only argument
111
119
  def callback(request):
112
120
  return [200, [], request.path]
113
121
 
@@ -175,8 +183,8 @@ Request history
175
183
  Spoof records each request and appends it to the ``.requests`` property,
176
184
  which is backed by a
177
185
  `deque <https://docs.python.org/3/library/collections.html#collections.deque>`__
178
- instance, the same as the ``.responses`` property. Think of it like a pre-parsed access log. Example
179
- using request history:
186
+ instance, the same as the ``.responses`` property. Think of it like a structured
187
+ access log. Example using request history:
180
188
 
181
189
  .. code-block:: python
182
190
 
@@ -29,10 +29,10 @@ Spoof 👻
29
29
 
30
30
  A test interface for HTTP
31
31
  =========================
32
- Spoof lets you easily create HTTP servers listening on real network
33
- sockets. Designed for test environments, what responses to send can be
34
- configured anytime, including while an HTTP server is running. Requests
35
- can be inspected live or after a response is sent.
32
+ Spoof lets you easily create HTTP servers on real network sockets.
33
+ Designed for test environments, what responses to send can be configured
34
+ anytime, including while an HTTP server is running. Requests can be
35
+ inspected live or after a response is sent.
36
36
 
37
37
  Unlike a conventional HTTP server, where specific methods and paths are
38
38
  configured in advance, Spoof accepts and records *all* requests, sending
@@ -45,7 +45,13 @@ HTTP client code. Have you ever felt icky patching a client library to
45
45
  write tests? Ever been burned by this? Ever wanted to refactor a client
46
46
  library, but had no way to verify behavior apart from doing live
47
47
  integration testing? Ever wanted mock for HTTP? If you answered yes to
48
- any of the above, Spoof might be for you.
48
+ any of the above, Spoof might be for you. Some key features:
49
+
50
+ * Decoupled requests and responses
51
+ * SSL/TLS with PQC
52
+ * HTTP/S proxy via CONNECT
53
+ * IPv6
54
+ * Live request debugging
49
55
 
50
56
  Installation and Compatibility
51
57
  ==============================
@@ -57,7 +63,8 @@ Spoof is available on PyPI:
57
63
 
58
64
  Spoof is tested on Python 3.10 to 3.14, leverages the ``http.server`` module
59
65
  included in the Python standard library, and has no external dependencies.
60
- It may work on older versions of Python, but this is not supported.
66
+ It may work on older versions of Python, but this is
67
+ `not supported <https://devguide.python.org/versions/>`__.
61
68
 
62
69
  Multiple Spoof HTTP servers can be run concurrently, and by default, the port
63
70
  number is the next available unused port. With OpenSSL installed, Spoof can
@@ -80,7 +87,7 @@ Spoof expects responses to have the following syntax:
80
87
  # bytes content
81
88
  [200, [("Content-Type", "application/json")], b'{"success": true }']
82
89
 
83
- # responses can also be a callback
90
+ # responses can also be a callback, with request as the only argument
84
91
  def callback(request):
85
92
  return [200, [], request.path]
86
93
 
@@ -148,8 +155,8 @@ Request history
148
155
  Spoof records each request and appends it to the ``.requests`` property,
149
156
  which is backed by a
150
157
  `deque <https://docs.python.org/3/library/collections.html#collections.deque>`__
151
- instance, the same as the ``.responses`` property. Think of it like a pre-parsed access log. Example
152
- using request history:
158
+ instance, the same as the ``.responses`` property. Think of it like a structured
159
+ access log. Example using request history:
153
160
 
154
161
  .. code-block:: python
155
162
 
@@ -7,7 +7,7 @@ name = "spoof"
7
7
  dynamic = ["version"]
8
8
  requires-python = ">=3.10"
9
9
  dependencies = []
10
- description = "HTTP server for testing environments"
10
+ description = "A simple HTTP server for test environments"
11
11
  readme = "README.rst"
12
12
  authors = [
13
13
  {"name" = "Lex Scarisbrick", "email" = "lex@scarisbrick.org"},
@@ -25,6 +25,7 @@ classifiers = [
25
25
  "Topic :: Software Development :: Libraries :: Python Modules",
26
26
  "Topic :: Software Development :: Quality Assurance",
27
27
  "Topic :: Software Development :: Testing",
28
+ "Topic :: Software Development :: Testing :: Mocking",
28
29
  "Topic :: Software Development :: Testing :: Traffic Generation",
29
30
  ]
30
31
 
@@ -44,7 +45,6 @@ py-modules = ["spoof"]
44
45
  [tool.setuptools.packages.find]
45
46
  where = ["src"]
46
47
  include = ["spoof*"]
47
- #exclude = [".github*", "tests*", "dist*", "build*", "*.egg-info*", "src/docs*"]
48
48
 
49
49
  [tool.setuptools_scm]
50
50
  # https://setuptools-scm.readthedocs.io/en/latest/extending/#setuptools_scmversion_scheme
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spoof
3
- Version: 2.2.2
4
- Summary: HTTP server for testing environments
3
+ Version: 2.3.1
4
+ Summary: A simple HTTP server for test environments
5
5
  Author-email: Lex Scarisbrick <lex@scarisbrick.org>
6
6
  License-Expression: MIT
7
7
  Project-URL: Documentation, https://spoof.readthedocs.io/en/latest/
@@ -19,6 +19,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Topic :: Software Development :: Quality Assurance
21
21
  Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Topic :: Software Development :: Testing :: Mocking
22
23
  Classifier: Topic :: Software Development :: Testing :: Traffic Generation
23
24
  Requires-Python: >=3.10
24
25
  Description-Content-Type: text/x-rst
@@ -56,10 +57,10 @@ Spoof 👻
56
57
 
57
58
  A test interface for HTTP
58
59
  =========================
59
- Spoof lets you easily create HTTP servers listening on real network
60
- sockets. Designed for test environments, what responses to send can be
61
- configured anytime, including while an HTTP server is running. Requests
62
- can be inspected live or after a response is sent.
60
+ Spoof lets you easily create HTTP servers on real network sockets.
61
+ Designed for test environments, what responses to send can be configured
62
+ anytime, including while an HTTP server is running. Requests can be
63
+ inspected live or after a response is sent.
63
64
 
64
65
  Unlike a conventional HTTP server, where specific methods and paths are
65
66
  configured in advance, Spoof accepts and records *all* requests, sending
@@ -72,7 +73,13 @@ HTTP client code. Have you ever felt icky patching a client library to
72
73
  write tests? Ever been burned by this? Ever wanted to refactor a client
73
74
  library, but had no way to verify behavior apart from doing live
74
75
  integration testing? Ever wanted mock for HTTP? If you answered yes to
75
- any of the above, Spoof might be for you.
76
+ any of the above, Spoof might be for you. Some key features:
77
+
78
+ * Decoupled requests and responses
79
+ * SSL/TLS with PQC
80
+ * HTTP/S proxy via CONNECT
81
+ * IPv6
82
+ * Live request debugging
76
83
 
77
84
  Installation and Compatibility
78
85
  ==============================
@@ -84,7 +91,8 @@ Spoof is available on PyPI:
84
91
 
85
92
  Spoof is tested on Python 3.10 to 3.14, leverages the ``http.server`` module
86
93
  included in the Python standard library, and has no external dependencies.
87
- It may work on older versions of Python, but this is not supported.
94
+ It may work on older versions of Python, but this is
95
+ `not supported <https://devguide.python.org/versions/>`__.
88
96
 
89
97
  Multiple Spoof HTTP servers can be run concurrently, and by default, the port
90
98
  number is the next available unused port. With OpenSSL installed, Spoof can
@@ -107,7 +115,7 @@ Spoof expects responses to have the following syntax:
107
115
  # bytes content
108
116
  [200, [("Content-Type", "application/json")], b'{"success": true }']
109
117
 
110
- # responses can also be a callback
118
+ # responses can also be a callback, with request as the only argument
111
119
  def callback(request):
112
120
  return [200, [], request.path]
113
121
 
@@ -175,8 +183,8 @@ Request history
175
183
  Spoof records each request and appends it to the ``.requests`` property,
176
184
  which is backed by a
177
185
  `deque <https://docs.python.org/3/library/collections.html#collections.deque>`__
178
- instance, the same as the ``.responses`` property. Think of it like a pre-parsed access log. Example
179
- using request history:
186
+ instance, the same as the ``.responses`` property. Think of it like a structured
187
+ access log. Example using request history:
180
188
 
181
189
  .. code-block:: python
182
190
 
@@ -21,11 +21,11 @@ URI_QUERY_SEPARATOR = "?"
21
21
  MEGABYTE = 2**20
22
22
 
23
23
 
24
- class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
25
- """Provides HTTP handler for use with `BaseHTTPServer.HTTPServer`
26
- compatible class server. Because the class is passed directly
27
- instead of an instance of the class, the `*Queue` class attributes
28
- must be set before passing to the HTTP server.
24
+ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
25
+ """Provides HTTP handler for use with ``http.server`` compatible class
26
+ server. Because the class is passed directly instead of an instance of
27
+ the class, the `*Queue` class attributes must be set before passing to
28
+ the HTTP server.
29
29
  """
30
30
 
31
31
  debug = False
@@ -33,7 +33,7 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
33
33
  errorResponse = [
34
34
  HTTP_SERVICE_UNAVAILABLE,
35
35
  [],
36
- "No responses queued and no default response set\n\n",
36
+ "The .responses queue is empty and .defaultResponse is None\n\n",
37
37
  ]
38
38
  maxRequestLength = 1 * MEGABYTE
39
39
  proxyRequestGen = collections.namedtuple("SpoofProxyRequest", "thread run")
@@ -85,18 +85,21 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
85
85
  response = self.errorResponse
86
86
  return response
87
87
 
88
- def sendResponse(self, response):
89
- """Sends response to HTTP client."""
90
- statusCode, headers, content = response
91
-
88
+ def encodeResponseContent(self, content):
92
89
  if content and isinstance(content, str):
93
90
  content = content.encode(RESPONSE_ENCODING)
94
- contentLength = len(content) if content else 0
91
+
92
+ return content
93
+
94
+ def sendResponse(self, response):
95
+ """Sends response to HTTP client."""
96
+ statusCode, headers, rawContent = response
97
+ content = self.encodeResponseContent(rawContent)
95
98
 
96
99
  self.send_response(statusCode)
97
100
 
98
101
  if content is not None:
99
- self.send_header("Content-Length", contentLength)
102
+ self.send_header("Content-Length", len(content))
100
103
  for header in headers:
101
104
  self.send_header(*header)
102
105
  self.end_headers()
@@ -151,7 +154,7 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
151
154
  def _proxyRequest(self, run):
152
155
  downstream = self.request
153
156
  upstream = socket.create_connection(
154
- self.server.upstream.serverAddress, self.server.upstream.timeout
157
+ (self.server.upstream.address, self.server.upstream.port), self.server.upstream.timeout
155
158
  )
156
159
  writer = {downstream: upstream, upstream: downstream}
157
160
 
@@ -176,7 +179,7 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
176
179
  super(HTTPRequestHandler, self).log_message(*args, **kwargs)
177
180
 
178
181
 
179
- class HTTPServer(object):
182
+ class HTTPServer:
180
183
  """Provides a single-threaded HTTP testing server.
181
184
 
182
185
  Class attributes:
@@ -199,53 +202,57 @@ class HTTPServer(object):
199
202
  """
200
203
  self._requests = collections.deque()
201
204
  self._responses = collections.deque()
202
- self._sslContext = sslContext
205
+ self._serverAddress = None
203
206
  self._upstream = None
204
- self.handlerClass = self.configureHandlerClass(self.handlerClass)
207
+ self.handlerClass = self.configureHandlerClass()
205
208
  self.proxyMode = proxy
206
209
  self.proxyThreads = []
207
210
  self.recvSize = 4096
208
211
  self.selectTimeout = 0.1
209
212
  self.server = None
210
213
  self.serverAddress = (host, port)
211
- if ":" in str(host):
212
- self.serverClass = HTTPServer6.configureServerClass(host)
213
- else:
214
- self.serverClass = self.configureServerClass(host)
215
- self.serverClass.timeout = timeout
216
- self.serverClass.sslContext = sslContext
214
+ self.sslContext = sslContext
217
215
  self.thread = None
216
+ self.timeout = timeout
218
217
 
219
218
  if self.proxyMode:
220
- self.upstream = type(self)(host="localhost", port=0, sslContext=sslContext, proxy=False)
221
- self.defaultResponse = [200, [], None]
219
+ self.setupDefaultUpstream()
220
+
221
+ @property
222
+ def serverAddress(self):
223
+ """Returns address to bind for HTTP server."""
224
+ return self._serverAddress
225
+
226
+ @serverAddress.setter
227
+ def serverAddress(self, address):
228
+ """Sets address to bind for HTTP server and setup server classes."""
229
+ self._serverAddress = address
230
+ self.serverClass = self.configureServerClass(address[0])
222
231
 
223
232
  @property
224
233
  def address(self):
225
- """Returns server IP/IPv6 address."""
226
- return self.serverAddress[0]
234
+ """Returns bound server IP/IPv6 address or ``None`` if unbound."""
235
+ return None if self.server is None else self.server.server_address[0]
227
236
 
228
237
  @property
229
238
  def port(self):
230
- """Returns server TCP port."""
231
- return self.serverAddress[1]
239
+ """Returns bound server TCP port or ``None`` if unbound."""
240
+ return None if self.server is None else self.server.server_address[1]
232
241
 
233
242
  @property
234
243
  def url(self):
235
- """Returns URL string to connect to this server instance."""
236
- protocol = "http" if self.sslContext is None else "https"
237
- address = "[{0}]".format(self.address) if ":" in self.address else self.address
238
- return "{0}://{1}:{2}".format(protocol, address, self.port)
239
-
240
- @property
241
- def sslContext(self):
242
- """Returns `ssl.SSLContext` instance."""
243
- return self._sslContext
244
+ """Returns URL string for server instance or ``None`` if unbound."""
245
+ url = None
246
+ if self.server is not None:
247
+ protocol = "http" if self.sslContext is None else "https"
248
+ address = "[{0}]".format(self.address) if ":" in self.address else self.address
249
+ url = "{0}://{1}:{2}".format(protocol, address, self.port)
250
+ return url
244
251
 
245
252
  @property
246
253
  def timeout(self):
247
254
  """Returns HTTP server timeout."""
248
- timeout = self.serverClass.timeout
255
+ timeout = self._timeout
249
256
  if self.server is not None:
250
257
  timeout = self.server.timeout
251
258
  return timeout
@@ -253,7 +260,7 @@ class HTTPServer(object):
253
260
  @timeout.setter
254
261
  def timeout(self, value):
255
262
  """Sets HTTP server timeout."""
256
- self.serverClass.timeout = value
263
+ self._timeout = value
257
264
  if self.server is not None:
258
265
  self.server.timeout = value
259
266
 
@@ -272,36 +279,47 @@ class HTTPServer(object):
272
279
  if self.server is not None:
273
280
  self.server.upstream = value
274
281
 
275
- @classmethod
276
- def configureServerClass(cls, host):
277
- """Reloads and configures server class. This is necessary, because of
282
+ def configureServerClass(self, host):
283
+ """Reloads and configures server class. This is necessary because of
278
284
  the use of class attributes. If more than one address family is used
279
285
  concurrently (e.g. IPv4 _and_ IPv6), one will overwrite the other, as
280
286
  the default behavior is for a class to be loaded once and only once,
281
287
  including attributes. Using `type()` effectively creates a new class
282
- with _discrete_ attributes. This is a class method, because it is
283
- called outside of the scope of an initialized class instance.
284
-
285
- :host: hostname string of server
288
+ with _discrete_ attributes.
286
289
  """
287
- sourceClass = cls.serverClass
290
+ sourceClass = type(self).serverClass
288
291
  serverClass = type(sourceClass.__name__, (sourceClass, object), dict())
289
- serverClass.address_family = cls.addressFamily
292
+ serverClass.address_family = socket.AF_INET6 if host.count(":") > 1 else self.addressFamily
290
293
  serverClass.serverName = host
291
294
  return serverClass
292
295
 
293
- def configureHandlerClass(self, sourceClass):
296
+ def configureHandlerClass(self):
294
297
  """Reloads and configures handler class. This is necessary because of
295
298
  the use of class attributes, which are necessary because the handler
296
299
  class is instantiated anew to handle each request by `BaseServer`.
297
300
  To keep the handler class unique to each `Spoof` instance, `type()` is
298
301
  used to effectively create a discrete handler class and attributes.
299
302
  """
303
+ sourceClass = type(self).handlerClass
300
304
  handlerClass = type(sourceClass.__name__, (sourceClass, object), dict())
301
305
  handlerClass.responseContentQueue = self._responses
302
306
  handlerClass.requestReportQueue = self._requests
303
307
  return handlerClass
304
308
 
309
+ def setupDefaultUpstream(self):
310
+ """Configures ready-to-use default upstream HTTP server."""
311
+ self.upstream = type(self)(
312
+ host="localhost", port=0, sslContext=self.sslContext, proxy=False
313
+ )
314
+ # RFC 7231, Section 4.3.6, "A server MUST NOT send any Transfer-Encoding or
315
+ # Content-Length header fields in a 2xx (Successful) response to CONNECT."
316
+ self.defaultResponse = [200, [], None]
317
+
318
+ def restart(self):
319
+ """Stops and starts HTTP server."""
320
+ self.stop()
321
+ return self.start()
322
+
305
323
  def start(self):
306
324
  """Starts HTTP server thread(s)."""
307
325
  if self.server is not None:
@@ -311,15 +329,15 @@ class HTTPServer(object):
311
329
  self.upstream.start()
312
330
 
313
331
  self.server = self.serverClass(self.serverAddress, self.handlerClass)
314
- self.serverAddress = self.server.server_address
315
- self.server.upstream = self.upstream
316
- if self.server.sslContext is not None:
317
- self.server.socket = self.server.sslContext.wrap_socket(
318
- self.server.socket, server_side=True
319
- )
332
+ self.server.timeout = self._timeout
333
+ self.server.upstream = self._upstream
334
+ if self.sslContext is not None:
335
+ self.server.socket = self.sslContext.wrap_socket(self.server.socket, server_side=True)
336
+
320
337
  name = getattr(type(self), "__name__")
321
338
  self.thread = threading.Thread(target=self.server.serve_forever, name=name)
322
339
  self.thread.start()
340
+ return self
323
341
 
324
342
  def stop(self):
325
343
  """Stops HTTP server thread(s) and closes socket(s)."""
@@ -340,19 +358,15 @@ class HTTPServer(object):
340
358
  self.thread = None
341
359
 
342
360
  def __enter__(self):
343
- """Starts HTTP server and returns `Spoof` instance when invoked as a
344
- context manager (with/as)."""
345
- self.start()
346
- return self
361
+ """Starts and returns HTTP server instance when invoked as a context manager (with/as)."""
362
+ return self.start()
347
363
 
348
364
  def __exit__(self, exceptionType, exceptionValue, traceback):
349
- """Destroys HTTP server instance when context manager block finishes.
350
- If context block ends normally, all arguments will be `None`.
351
- """
365
+ """Stops HTTP server instance when context manager block exits."""
352
366
  self.stop()
353
367
 
354
368
  def __del__(self):
355
- """Closes HTTP server socket when instance goes out of scope."""
369
+ """Stops HTTP server when instance goes out of scope."""
356
370
  if getattr(self, "server", None) is not None:
357
371
  self.stop()
358
372
 
@@ -468,7 +482,7 @@ class HTTPServer6(HTTPServer):
468
482
  addressFamily = socket.AF_INET6
469
483
 
470
484
 
471
- class SSLContext(object):
485
+ class SSLContext:
472
486
  """Provides methods to create SSL context for use with `HTTPServer`."""
473
487
 
474
488
  @staticmethod
@@ -1,6 +1,8 @@
1
+ import itertools
1
2
  import json
2
3
  import os
3
4
  import random
5
+ import socket
4
6
  import ssl
5
7
  import unittest
6
8
 
@@ -21,10 +23,8 @@ class TestRequest(BaseMixin):
21
23
  def setUpClass(cls):
22
24
  cls.selfSigned = spoof.SelfSignedSSLContext()
23
25
  cls.sslContext = cls.selfSigned.sslContext
24
- cls.httpd = spoof.HTTPServer(sslContext=cls.sslContext)
25
- cls.httpd6 = spoof.HTTPServer6("::1", sslContext=cls.sslContext)
26
- cls.httpd.start()
27
- cls.httpd6.start()
26
+ cls.httpd = spoof.HTTPServer(sslContext=cls.sslContext).start()
27
+ cls.httpd6 = spoof.HTTPServer6(sslContext=cls.sslContext).start()
28
28
 
29
29
  @classmethod
30
30
  def tearDownClass(cls):
@@ -146,10 +146,13 @@ class TestRequest(BaseMixin):
146
146
  self.assertEqual(request.status_code, errorResponse[0])
147
147
 
148
148
  def test_spoof_selfSigned_raises_exception_on_connect(self):
149
- sslContext = spoof.SSLContext.selfSigned()
150
- httpd = spoof.HTTPServer(sslContext=sslContext)
151
- with self.assertRaises(requests.ConnectionError):
152
- self.session.get(httpd.url + "/random")
149
+ # this kind of ssl http server setup has no way to access the underlying
150
+ # certificate and key files. the intent is to provide a real ssl context
151
+ # that is untrusted and kicked back as invalid, potentially to test out
152
+ # implementations that trust ssl connections that should not be trusted.
153
+ with spoof.HTTPServer(sslContext=spoof.SSLContext.selfSigned()) as httpd:
154
+ with self.assertRaises(requests.exceptions.SSLError):
155
+ self.session.get(httpd.url)
153
156
 
154
157
  def test_spoof_allows_callable_defaultResponse(self):
155
158
  status_code = random.randint(205, 299)
@@ -213,6 +216,75 @@ class TestRequest(BaseMixin):
213
216
  self.assertEqual(expected_path, response.text)
214
217
  self.assertEqual(expected_path, self.httpd.requests[-1].path)
215
218
 
219
+ def test_large_batch_queued_responses(self):
220
+ batchSize = 1_000
221
+ responses = [
222
+ [200, [("Content-Type", "application/json")], f'{{"seq": {seq}}}']
223
+ for seq in range(batchSize)
224
+ ]
225
+ with spoof.HTTPServer() as httpd:
226
+ httpd.responses.extend(responses)
227
+ for seq in range(batchSize):
228
+ self.assertEqual({"seq": seq}, requests.get(httpd.url).json())
229
+ self.assertEqual(batchSize, len(httpd.requests))
230
+
231
+ def test_large_batch_generated_responses(self):
232
+ def responseGenerator():
233
+ for seq in itertools.count():
234
+ yield [200, [("Content-Type", "application/json")], json.dumps({"seq": seq})]
235
+
236
+ with spoof.HTTPServer() as httpd:
237
+ batchSize = 1_000
238
+ response = responseGenerator()
239
+ httpd.defaultResponse = lambda request: next(response)
240
+ for seq in range(batchSize):
241
+ self.assertEqual({"seq": seq}, self.session.get(httpd.url).json())
242
+ self.assertEqual(batchSize, len(httpd.requests))
243
+
244
+ def test_attrs_return_None_if_server_is_unbound(self):
245
+ httpd = spoof.HTTPServer()
246
+ self.assertIsNone(httpd.server)
247
+ self.assertIsNone(httpd.address)
248
+ self.assertIsNone(httpd.port)
249
+ self.assertIsNone(httpd.url)
250
+
251
+ def test_changing_sslContext_and_restarting(self):
252
+ expected = "set-sslcontext-live"
253
+ httpd = spoof.HTTPServer(sslContext=spoof.SSLContext.selfSigned()).start()
254
+ httpd.defaultResponse = [200, [], expected]
255
+ with self.assertRaises(requests.exceptions.SSLError):
256
+ requests.get(httpd.url)
257
+
258
+ httpd.sslContext = self.selfSigned.sslContext
259
+ httpd.restart()
260
+ result = requests.get(httpd.url, verify=self.selfSigned.certFile).text
261
+ self.assertTrue(httpd.url.startswith("https"))
262
+ self.assertEqual(expected, result)
263
+
264
+ def test_changing_serverAddress_and_restarting(self):
265
+ httpd = spoof.HTTPServer(host="127.0.0.1").start()
266
+ httpd.defaultResponse = lambda request: [200, [], request.serverName]
267
+
268
+ httpd.serverAddress = ("::1", 0)
269
+ httpd.restart()
270
+ self.assertEqual(requests.get(httpd.url).text, "::1")
271
+ self.assertEqual(httpd.server.socket.family, socket.AF_INET6)
272
+
273
+ httpd.serverAddress = ("127.0.0.1", 0)
274
+ httpd.restart()
275
+ self.assertEqual(requests.get(httpd.url).text, "127.0.0.1")
276
+ self.assertEqual(httpd.server.socket.family, socket.AF_INET)
277
+
278
+ def test_restart_returns_spoof_instance(self):
279
+ expected = httpd = spoof.HTTPServer()
280
+ result = httpd.restart()
281
+ self.assertEqual(expected, result)
282
+
283
+ def test_start_returns_spoof_instance(self):
284
+ expected = httpd = spoof.HTTPServer()
285
+ result = httpd.start()
286
+ self.assertEqual(expected, result)
287
+
216
288
 
217
289
  class TestProxy(BaseMixin):
218
290
  @classmethod
@@ -224,8 +296,7 @@ class TestProxy(BaseMixin):
224
296
  cls.unlink(cls.cert, cls.key)
225
297
 
226
298
  def setUp(self):
227
- self.httpd = spoof.HTTPServer()
228
- self.httpd.start()
299
+ self.httpd = spoof.HTTPServer().start()
229
300
  self.session = requests.Session()
230
301
  self.session.verify = self.cert
231
302
  self.sslContext = spoof.SSLContext.fromCertChain(self.cert, self.key)
@@ -265,16 +336,14 @@ class TestProxy(BaseMixin):
265
336
  self.assertEqual(expected, result)
266
337
 
267
338
  def test_spoof_https_site_through_https_proxy(self):
268
- # https proxies for https supported in requests v2.25.0 / urllib3 v1.26
269
339
  expected = upstream_content = b"octet-comeback-squirmy"
270
- httpd = spoof.HTTPServer(sslContext=self.sslContext)
340
+ httpd = spoof.HTTPServer(sslContext=self.sslContext).start()
271
341
  httpd.defaultResponse = [200, [], None]
272
- httpd.start()
273
- httpd.upstream = spoof.HTTPServer(sslContext=self.sslContext)
342
+ httpd.upstream = spoof.HTTPServer(sslContext=self.sslContext).start()
274
343
  httpd.upstream.defaultResponse = [200, [], upstream_content]
275
- httpd.upstream.start()
276
- proxies = {"https": httpd.url}
277
- result = self.session.get(httpd.upstream.url, proxies=proxies).content
344
+
345
+ # https proxies for https supported in requests v2.25.0 / urllib3 v1.26
346
+ result = self.session.get(httpd.upstream.url, proxies={"https": httpd.url}).content
278
347
  self.assertTrue(httpd.upstream.url.startswith("https"))
279
348
  self.assertTrue(httpd.url.startswith("https"))
280
349
  self.assertEqual(expected, result)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes