spoof 2.2.0__tar.gz → 2.2.2__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,13 @@
1
+ 2.2.2 (2026-04-21)
2
+ ==================
3
+
4
+ - Update docs
5
+
6
+ 2.2.1 (2026-04-17)
7
+ ==================
8
+
9
+ - Update docs
10
+
1
11
  2.2.0 (2026-04-13)
2
12
  ==================
3
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spoof
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: HTTP server for testing environments
5
5
  Author-email: Lex Scarisbrick <lex@scarisbrick.org>
6
6
  License-Expression: MIT
@@ -57,12 +57,12 @@ Spoof 👻
57
57
  A test interface for HTTP
58
58
  =========================
59
59
  Spoof lets you easily create HTTP servers listening on real network
60
- sockets. Designed for test environments, what responses to return can be
61
- configured while an HTTP server is running. Requests can be inspected
62
- live or after a response is sent.
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.
63
63
 
64
- Unlike a traditional HTTP server, where specific methods and paths are
65
- configured in advance, Spoof accepts and captures *all* requests, sending
64
+ Unlike a conventional HTTP server, where specific methods and paths are
65
+ configured in advance, Spoof accepts and records *all* requests, sending
66
66
  whatever responses are queued, or a default response if the queue is empty.
67
67
 
68
68
  Why would I want this?
@@ -70,13 +70,12 @@ Why would I want this?
70
70
  Spoof is all about enabling test-driven development (and refactoring) of
71
71
  HTTP client code. Have you ever felt icky patching a client library to
72
72
  write tests? Ever been burned by this? Ever wanted to refactor a client
73
- library, but had no way to prove functionality apart from doing live
74
- integration testing? Ever wanted mock functionality for HTTP? If you
75
- answered yes to any of the above, Spoof might be for you.
73
+ library, but had no way to verify behavior apart from doing live
74
+ integration testing? Ever wanted mock for HTTP? If you answered yes to
75
+ any of the above, Spoof might be for you.
76
76
 
77
77
  Installation and Compatibility
78
78
  ==============================
79
-
80
79
  Spoof is available on PyPI:
81
80
 
82
81
  .. code-block:: console
@@ -91,74 +90,8 @@ Multiple Spoof HTTP servers can be run concurrently, and by default, the port
91
90
  number is the next available unused port. With OpenSSL installed, Spoof can
92
91
  also provide an SSL/TLS HTTP server. HTTP proxying and IPv6 are also supported.
93
92
 
94
- Request instances
95
- =================
96
- Spoof captures each request as a ``SpoofRequestEnv`` instance with the following
97
- properties:
98
-
99
- +-------------------------+----------------------------------------------+
100
- | Property | Description |
101
- +=========================+==============================================+
102
- | content | ``bytes`` object of request content |
103
- +-------------------------+----------------------------------------------+
104
- | contentEncoding | Value of Content-Encoding header, if present |
105
- +-------------------------+----------------------------------------------+
106
- | contentLength | Value of Content-Length header, if present |
107
- +-------------------------+----------------------------------------------+
108
- | contentType | Value of Content-Type header, if present |
109
- +-------------------------+----------------------------------------------+
110
- | headers | ``http.client.HTTPMessage`` object of headers|
111
- +-------------------------+----------------------------------------------+
112
- | json() | Convenience to call ``json.loads`` on content|
113
- +-------------------------+----------------------------------------------+
114
- | method | Request method (e.g. GET, POST, HEAD) |
115
- +-------------------------+----------------------------------------------+
116
- | path | Decoded URI path, without query string |
117
- +-------------------------+----------------------------------------------+
118
- | protocol | Protocol version (e.g. HTTP/1.0) |
119
- +-------------------------+----------------------------------------------+
120
- | queryString | Anything in URI after ``?`` |
121
- +-------------------------+----------------------------------------------+
122
- | serverName | Host name of HTTP server |
123
- +-------------------------+----------------------------------------------+
124
- | serverPort | Port number of HTTP server |
125
- +-------------------------+----------------------------------------------+
126
- | uri | Raw URI path and query string, if present |
127
- +-------------------------+----------------------------------------------+
128
-
129
- Example with request properties:
130
-
131
- .. code-block:: python
132
-
133
- >>> import requests
134
- ... import spoof
135
- ...
136
- ... with spoof.HTTPServer() as httpd:
137
- ... httpd.defaultResponse = [200, [], None]
138
- ...
139
- ... [requests.get(httpd.url + path) for path in ["/a", "/b", "/c"]]
140
- ... [f"{r.method} {r.path} {r.protocol}" for r in httpd.requests]
141
- ...
142
- [<Response [200]>, <Response [200]>, <Response [200]>]
143
- ['GET /a HTTP/1.1', 'GET /b HTTP/1.1', 'GET /c HTTP/1.1']
144
-
145
- Response precedence
146
- ===================
147
-
148
- Spoof determines what response to send to incoming requests based on
149
- the following precedence, highest to lowest:
150
-
151
- #. Oldest response queued in ``.responses`` using first-in, first-out (FIFO) order
152
- #. Response stored in ``.defaultResponse`` if no responses are queued
153
- #. Response stored in ``.errorResponse`` if ``.defaultResponse`` is ``None``
154
-
155
- By default, an HTTP error response will be sent to all requests, because
156
- newly created Spoof instances have no responses queued, and no default
157
- response set. This requires non-error responses to be explicitly specified.
158
-
159
93
  Response syntax
160
94
  ===============
161
-
162
95
  Spoof expects responses to have the following syntax:
163
96
 
164
97
  .. code-block:: python
@@ -178,13 +111,31 @@ Spoof expects responses to have the following syntax:
178
111
  def callback(request):
179
112
  return [200, [], request.path]
180
113
 
181
- Queued responses
182
- ================
114
+ Response precedence
115
+ ===================
116
+ Spoof determines what response to send to incoming requests based on
117
+ the following precedence, highest to lowest:
118
+
119
+ #. Oldest response queued in ``.responses`` using first-in, first-out (FIFO) order
120
+ #. Response stored in ``.defaultResponse`` if no responses are queued
121
+ #. Response stored in ``.errorResponse`` if ``.defaultResponse`` is ``None``
122
+
123
+ By default, Spoof will respond with an **HTTP 503 Service Unavailable** error,
124
+ because newly created Spoof instances have no responses queued and no default
125
+ response set. This requires non-error HTTP responses to be explicitly specified.
183
126
 
184
- Spoof HTTP servers run in a single background thread, so request and
185
- response order should be predictable. Tests using Spoof should be able
186
- to use the same fixtures, in the same order, and get the same results. Example
187
- queueing multiple responses, verifying content, and request paths:
127
+ Response queue
128
+ ==============
129
+ Spoof will always try to send a response from ``.responses`` first, before falling
130
+ back to ``.defaultResponse`` if the queue is empty. Backed by a
131
+ `deque <https://docs.python.org/3/library/collections.html#collections.deque>`__
132
+ instance, the ``.responses`` queue supports adding items via ``.responses.append()``
133
+ and ``.responses.extend()``, similar to a regular list.
134
+
135
+ Spoof HTTP servers run in a single background thread, so response order should
136
+ be predictably serial. Tests using Spoof should be able to use the same fixtures,
137
+ in the same order, and get the same results. Example queueing multiple responses,
138
+ verifying content, and request paths:
188
139
 
189
140
  .. code-block:: python
190
141
 
@@ -203,9 +154,11 @@ queueing multiple responses, verifying content, and request paths:
203
154
  assert requests.get(httpd.url + "/oops").status_code == 404
204
155
  assert [r.path for r in httpd.requests] == ["/path", "/alt/path", "/oops"]
205
156
 
206
- Callback response
207
- =================
208
- Set a callback as the default response (callbacks can also be queued):
157
+ Response default
158
+ ================
159
+ Spoof will always try to send a response from ``.responses`` first, before falling
160
+ back to ``.defaultResponse`` if the queue is empty. Example setting a callback as
161
+ a default response:
209
162
 
210
163
  .. code-block:: python
211
164
 
@@ -217,9 +170,69 @@ Set a callback as the default response (callbacks can also be queued):
217
170
 
218
171
  assert requests.get(httpd.url + "/alt").text == "/alt"
219
172
 
173
+ Request history
174
+ ===============
175
+ Spoof records each request and appends it to the ``.requests`` property,
176
+ which is backed by a
177
+ `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:
180
+
181
+ .. code-block:: python
182
+
183
+ >>> import requests
184
+ ... import spoof
185
+ ...
186
+ ... with spoof.HTTPServer() as httpd:
187
+ ... httpd.defaultResponse = [200, [], None]
188
+ ...
189
+ ... [requests.get(httpd.url + path) for path in ["/a", "/b", "/c"]]
190
+ ... [f"{r.method} {r.path} {r.protocol}" for r in httpd.requests]
191
+ ...
192
+ [<Response [200]>, <Response [200]>, <Response [200]>]
193
+ ['GET /a HTTP/1.1', 'GET /b HTTP/1.1', 'GET /c HTTP/1.1']
194
+
195
+ Request properties
196
+ ==================
197
+ ``SpoofRequestEnv`` instances have the following properties:
198
+
199
+ +-------------------------+----------------------------------------------+
200
+ | Property | Description |
201
+ +=========================+==============================================+
202
+ | content | ``bytes`` object of request content |
203
+ +-------------------------+----------------------------------------------+
204
+ | contentEncoding | Value of Content-Encoding header, if present |
205
+ +-------------------------+----------------------------------------------+
206
+ | contentLength | Value of Content-Length header, if present |
207
+ +-------------------------+----------------------------------------------+
208
+ | contentType | Value of Content-Type header, if present |
209
+ +-------------------------+----------------------------------------------+
210
+ | headers | ``http.client.HTTPMessage`` object of headers|
211
+ +-------------------------+----------------------------------------------+
212
+ | json() | Convenience to call ``json.loads`` on content|
213
+ +-------------------------+----------------------------------------------+
214
+ | method | Request method (e.g. GET, POST, HEAD) |
215
+ +-------------------------+----------------------------------------------+
216
+ | path | Decoded URI path, without query string |
217
+ +-------------------------+----------------------------------------------+
218
+ | protocol | Protocol version (e.g. HTTP/1.0) |
219
+ +-------------------------+----------------------------------------------+
220
+ | queryString | Anything in URI after ``?`` |
221
+ +-------------------------+----------------------------------------------+
222
+ | serverName | Host name of HTTP server |
223
+ +-------------------------+----------------------------------------------+
224
+ | serverPort | Port number of HTTP server |
225
+ +-------------------------+----------------------------------------------+
226
+ | uri | Raw URI path and query string, if present |
227
+ +-------------------------+----------------------------------------------+
228
+
220
229
  SSL/TLS Mode
221
230
  ============
222
- Test queued response with a self-signed SSL/TLS certificate:
231
+ Spoof supports SSL/TLS connectivity by passing an
232
+ `SSLContext <https://docs.python.org/3/library/ssl.html#ssl-contexts>`__,
233
+ or if OpenSSL command line tools are available, creating an ``SSLContext``
234
+ with a self-signed certificate. Configured correctly, this should not raise
235
+ any warnings or errors:
223
236
 
224
237
  .. code-block:: python
225
238
 
@@ -251,8 +264,8 @@ set to the path of the self-signed certificate to silence SSL/TLS errors:
251
264
  response = requests.get(httpd.url)
252
265
  assert response.text == "No self-signed cert warning!"
253
266
 
254
- If OpenSSL 3.5.0 or later is installed, Post-Quantum Cryptography (PQC)
255
- key algorithms can be used:
267
+ If `OpenSSL 3.5.0 <https://openssl-library.org/post/2025-04-08-openssl-35-final-release/>`__
268
+ or later is installed, Post-Quantum Cryptography (PQC) key algorithms can be used:
256
269
 
257
270
  .. code-block:: python
258
271
 
@@ -293,8 +306,31 @@ external services. Example usage:
293
306
  assert proxy.upstream.requests[0].path == "/ayt"
294
307
  assert response.text == "I'm here!"
295
308
 
296
- HTTP on IPv6
297
- ============
309
+ If setting the ``proxies`` option in ``requests`` isn't workable, the
310
+ ``https_proxy`` environment variable can be set to the URL of the proxy:
311
+
312
+ .. code-block:: python
313
+
314
+ import os
315
+ import requests
316
+ import spoof
317
+
318
+ with spoof.SelfSignedSSLContext(commonName="example.spoof") as selfSigned:
319
+ with spoof.HTTPServer(sslContext=selfSigned.sslContext, proxy=True) as proxy:
320
+ proxy.upstream.defaultResponse = [200, [], "I'm here!"]
321
+
322
+ os.environ["https_proxy"] = proxy.url
323
+ os.environ["REQUESTS_CA_BUNDLE"] = selfSigned.certFile
324
+
325
+ response = requests.get("https://example.spoof/ayt")
326
+ assert proxy.requests[0].method == "CONNECT"
327
+ assert proxy.requests[0].path == "example.spoof:443"
328
+ assert proxy.upstream.requests[0].method == "GET"
329
+ assert proxy.upstream.requests[0].path == "/ayt"
330
+ assert response.text == "I'm here!"
331
+
332
+ IPv6 Mode
333
+ =========
298
334
  Setting the ``host`` attribute to an IPv6 address will work as expected. There
299
335
  is also an IPv6-only ``spoof.HTTPServer6`` class that can be used if needed to
300
336
  only listen on IPv6 sockets.
@@ -325,8 +361,8 @@ only listen on IPv6 sockets.
325
361
  'This is also Spoof on IPv6 👀'
326
362
  'http://[::1]:54296'
327
363
 
328
- Using a debugger
329
- ================
364
+ Debug mode
365
+ ==========
330
366
  Setting a callback with a ``breakpoint()`` can allow for live HTTP request
331
367
  debugging, including setting custom responses and inspecting requests. Note
332
368
  that callbacks can also be queued.
@@ -351,4 +387,3 @@ that callbacks can also be queued.
351
387
  (Pdb) response[2] = "content set from pdb"
352
388
  (Pdb) c
353
389
  'content set from pdb'
354
-
@@ -30,12 +30,12 @@ Spoof 👻
30
30
  A test interface for HTTP
31
31
  =========================
32
32
  Spoof lets you easily create HTTP servers listening on real network
33
- sockets. Designed for test environments, what responses to return can be
34
- configured while an HTTP server is running. Requests can be inspected
35
- live or after a response is sent.
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.
36
36
 
37
- Unlike a traditional HTTP server, where specific methods and paths are
38
- configured in advance, Spoof accepts and captures *all* requests, sending
37
+ Unlike a conventional HTTP server, where specific methods and paths are
38
+ configured in advance, Spoof accepts and records *all* requests, sending
39
39
  whatever responses are queued, or a default response if the queue is empty.
40
40
 
41
41
  Why would I want this?
@@ -43,13 +43,12 @@ Why would I want this?
43
43
  Spoof is all about enabling test-driven development (and refactoring) of
44
44
  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
- library, but had no way to prove functionality apart from doing live
47
- integration testing? Ever wanted mock functionality for HTTP? If you
48
- answered yes to any of the above, Spoof might be for you.
46
+ library, but had no way to verify behavior apart from doing live
47
+ integration testing? Ever wanted mock for HTTP? If you answered yes to
48
+ any of the above, Spoof might be for you.
49
49
 
50
50
  Installation and Compatibility
51
51
  ==============================
52
-
53
52
  Spoof is available on PyPI:
54
53
 
55
54
  .. code-block:: console
@@ -64,74 +63,8 @@ Multiple Spoof HTTP servers can be run concurrently, and by default, the port
64
63
  number is the next available unused port. With OpenSSL installed, Spoof can
65
64
  also provide an SSL/TLS HTTP server. HTTP proxying and IPv6 are also supported.
66
65
 
67
- Request instances
68
- =================
69
- Spoof captures each request as a ``SpoofRequestEnv`` instance with the following
70
- properties:
71
-
72
- +-------------------------+----------------------------------------------+
73
- | Property | Description |
74
- +=========================+==============================================+
75
- | content | ``bytes`` object of request content |
76
- +-------------------------+----------------------------------------------+
77
- | contentEncoding | Value of Content-Encoding header, if present |
78
- +-------------------------+----------------------------------------------+
79
- | contentLength | Value of Content-Length header, if present |
80
- +-------------------------+----------------------------------------------+
81
- | contentType | Value of Content-Type header, if present |
82
- +-------------------------+----------------------------------------------+
83
- | headers | ``http.client.HTTPMessage`` object of headers|
84
- +-------------------------+----------------------------------------------+
85
- | json() | Convenience to call ``json.loads`` on content|
86
- +-------------------------+----------------------------------------------+
87
- | method | Request method (e.g. GET, POST, HEAD) |
88
- +-------------------------+----------------------------------------------+
89
- | path | Decoded URI path, without query string |
90
- +-------------------------+----------------------------------------------+
91
- | protocol | Protocol version (e.g. HTTP/1.0) |
92
- +-------------------------+----------------------------------------------+
93
- | queryString | Anything in URI after ``?`` |
94
- +-------------------------+----------------------------------------------+
95
- | serverName | Host name of HTTP server |
96
- +-------------------------+----------------------------------------------+
97
- | serverPort | Port number of HTTP server |
98
- +-------------------------+----------------------------------------------+
99
- | uri | Raw URI path and query string, if present |
100
- +-------------------------+----------------------------------------------+
101
-
102
- Example with request properties:
103
-
104
- .. code-block:: python
105
-
106
- >>> import requests
107
- ... import spoof
108
- ...
109
- ... with spoof.HTTPServer() as httpd:
110
- ... httpd.defaultResponse = [200, [], None]
111
- ...
112
- ... [requests.get(httpd.url + path) for path in ["/a", "/b", "/c"]]
113
- ... [f"{r.method} {r.path} {r.protocol}" for r in httpd.requests]
114
- ...
115
- [<Response [200]>, <Response [200]>, <Response [200]>]
116
- ['GET /a HTTP/1.1', 'GET /b HTTP/1.1', 'GET /c HTTP/1.1']
117
-
118
- Response precedence
119
- ===================
120
-
121
- Spoof determines what response to send to incoming requests based on
122
- the following precedence, highest to lowest:
123
-
124
- #. Oldest response queued in ``.responses`` using first-in, first-out (FIFO) order
125
- #. Response stored in ``.defaultResponse`` if no responses are queued
126
- #. Response stored in ``.errorResponse`` if ``.defaultResponse`` is ``None``
127
-
128
- By default, an HTTP error response will be sent to all requests, because
129
- newly created Spoof instances have no responses queued, and no default
130
- response set. This requires non-error responses to be explicitly specified.
131
-
132
66
  Response syntax
133
67
  ===============
134
-
135
68
  Spoof expects responses to have the following syntax:
136
69
 
137
70
  .. code-block:: python
@@ -151,13 +84,31 @@ Spoof expects responses to have the following syntax:
151
84
  def callback(request):
152
85
  return [200, [], request.path]
153
86
 
154
- Queued responses
155
- ================
87
+ Response precedence
88
+ ===================
89
+ Spoof determines what response to send to incoming requests based on
90
+ the following precedence, highest to lowest:
91
+
92
+ #. Oldest response queued in ``.responses`` using first-in, first-out (FIFO) order
93
+ #. Response stored in ``.defaultResponse`` if no responses are queued
94
+ #. Response stored in ``.errorResponse`` if ``.defaultResponse`` is ``None``
95
+
96
+ By default, Spoof will respond with an **HTTP 503 Service Unavailable** error,
97
+ because newly created Spoof instances have no responses queued and no default
98
+ response set. This requires non-error HTTP responses to be explicitly specified.
156
99
 
157
- Spoof HTTP servers run in a single background thread, so request and
158
- response order should be predictable. Tests using Spoof should be able
159
- to use the same fixtures, in the same order, and get the same results. Example
160
- queueing multiple responses, verifying content, and request paths:
100
+ Response queue
101
+ ==============
102
+ Spoof will always try to send a response from ``.responses`` first, before falling
103
+ back to ``.defaultResponse`` if the queue is empty. Backed by a
104
+ `deque <https://docs.python.org/3/library/collections.html#collections.deque>`__
105
+ instance, the ``.responses`` queue supports adding items via ``.responses.append()``
106
+ and ``.responses.extend()``, similar to a regular list.
107
+
108
+ Spoof HTTP servers run in a single background thread, so response order should
109
+ be predictably serial. Tests using Spoof should be able to use the same fixtures,
110
+ in the same order, and get the same results. Example queueing multiple responses,
111
+ verifying content, and request paths:
161
112
 
162
113
  .. code-block:: python
163
114
 
@@ -176,9 +127,11 @@ queueing multiple responses, verifying content, and request paths:
176
127
  assert requests.get(httpd.url + "/oops").status_code == 404
177
128
  assert [r.path for r in httpd.requests] == ["/path", "/alt/path", "/oops"]
178
129
 
179
- Callback response
180
- =================
181
- Set a callback as the default response (callbacks can also be queued):
130
+ Response default
131
+ ================
132
+ Spoof will always try to send a response from ``.responses`` first, before falling
133
+ back to ``.defaultResponse`` if the queue is empty. Example setting a callback as
134
+ a default response:
182
135
 
183
136
  .. code-block:: python
184
137
 
@@ -190,9 +143,69 @@ Set a callback as the default response (callbacks can also be queued):
190
143
 
191
144
  assert requests.get(httpd.url + "/alt").text == "/alt"
192
145
 
146
+ Request history
147
+ ===============
148
+ Spoof records each request and appends it to the ``.requests`` property,
149
+ which is backed by a
150
+ `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:
153
+
154
+ .. code-block:: python
155
+
156
+ >>> import requests
157
+ ... import spoof
158
+ ...
159
+ ... with spoof.HTTPServer() as httpd:
160
+ ... httpd.defaultResponse = [200, [], None]
161
+ ...
162
+ ... [requests.get(httpd.url + path) for path in ["/a", "/b", "/c"]]
163
+ ... [f"{r.method} {r.path} {r.protocol}" for r in httpd.requests]
164
+ ...
165
+ [<Response [200]>, <Response [200]>, <Response [200]>]
166
+ ['GET /a HTTP/1.1', 'GET /b HTTP/1.1', 'GET /c HTTP/1.1']
167
+
168
+ Request properties
169
+ ==================
170
+ ``SpoofRequestEnv`` instances have the following properties:
171
+
172
+ +-------------------------+----------------------------------------------+
173
+ | Property | Description |
174
+ +=========================+==============================================+
175
+ | content | ``bytes`` object of request content |
176
+ +-------------------------+----------------------------------------------+
177
+ | contentEncoding | Value of Content-Encoding header, if present |
178
+ +-------------------------+----------------------------------------------+
179
+ | contentLength | Value of Content-Length header, if present |
180
+ +-------------------------+----------------------------------------------+
181
+ | contentType | Value of Content-Type header, if present |
182
+ +-------------------------+----------------------------------------------+
183
+ | headers | ``http.client.HTTPMessage`` object of headers|
184
+ +-------------------------+----------------------------------------------+
185
+ | json() | Convenience to call ``json.loads`` on content|
186
+ +-------------------------+----------------------------------------------+
187
+ | method | Request method (e.g. GET, POST, HEAD) |
188
+ +-------------------------+----------------------------------------------+
189
+ | path | Decoded URI path, without query string |
190
+ +-------------------------+----------------------------------------------+
191
+ | protocol | Protocol version (e.g. HTTP/1.0) |
192
+ +-------------------------+----------------------------------------------+
193
+ | queryString | Anything in URI after ``?`` |
194
+ +-------------------------+----------------------------------------------+
195
+ | serverName | Host name of HTTP server |
196
+ +-------------------------+----------------------------------------------+
197
+ | serverPort | Port number of HTTP server |
198
+ +-------------------------+----------------------------------------------+
199
+ | uri | Raw URI path and query string, if present |
200
+ +-------------------------+----------------------------------------------+
201
+
193
202
  SSL/TLS Mode
194
203
  ============
195
- Test queued response with a self-signed SSL/TLS certificate:
204
+ Spoof supports SSL/TLS connectivity by passing an
205
+ `SSLContext <https://docs.python.org/3/library/ssl.html#ssl-contexts>`__,
206
+ or if OpenSSL command line tools are available, creating an ``SSLContext``
207
+ with a self-signed certificate. Configured correctly, this should not raise
208
+ any warnings or errors:
196
209
 
197
210
  .. code-block:: python
198
211
 
@@ -224,8 +237,8 @@ set to the path of the self-signed certificate to silence SSL/TLS errors:
224
237
  response = requests.get(httpd.url)
225
238
  assert response.text == "No self-signed cert warning!"
226
239
 
227
- If OpenSSL 3.5.0 or later is installed, Post-Quantum Cryptography (PQC)
228
- key algorithms can be used:
240
+ If `OpenSSL 3.5.0 <https://openssl-library.org/post/2025-04-08-openssl-35-final-release/>`__
241
+ or later is installed, Post-Quantum Cryptography (PQC) key algorithms can be used:
229
242
 
230
243
  .. code-block:: python
231
244
 
@@ -266,8 +279,31 @@ external services. Example usage:
266
279
  assert proxy.upstream.requests[0].path == "/ayt"
267
280
  assert response.text == "I'm here!"
268
281
 
269
- HTTP on IPv6
270
- ============
282
+ If setting the ``proxies`` option in ``requests`` isn't workable, the
283
+ ``https_proxy`` environment variable can be set to the URL of the proxy:
284
+
285
+ .. code-block:: python
286
+
287
+ import os
288
+ import requests
289
+ import spoof
290
+
291
+ with spoof.SelfSignedSSLContext(commonName="example.spoof") as selfSigned:
292
+ with spoof.HTTPServer(sslContext=selfSigned.sslContext, proxy=True) as proxy:
293
+ proxy.upstream.defaultResponse = [200, [], "I'm here!"]
294
+
295
+ os.environ["https_proxy"] = proxy.url
296
+ os.environ["REQUESTS_CA_BUNDLE"] = selfSigned.certFile
297
+
298
+ response = requests.get("https://example.spoof/ayt")
299
+ assert proxy.requests[0].method == "CONNECT"
300
+ assert proxy.requests[0].path == "example.spoof:443"
301
+ assert proxy.upstream.requests[0].method == "GET"
302
+ assert proxy.upstream.requests[0].path == "/ayt"
303
+ assert response.text == "I'm here!"
304
+
305
+ IPv6 Mode
306
+ =========
271
307
  Setting the ``host`` attribute to an IPv6 address will work as expected. There
272
308
  is also an IPv6-only ``spoof.HTTPServer6`` class that can be used if needed to
273
309
  only listen on IPv6 sockets.
@@ -298,8 +334,8 @@ only listen on IPv6 sockets.
298
334
  'This is also Spoof on IPv6 👀'
299
335
  'http://[::1]:54296'
300
336
 
301
- Using a debugger
302
- ================
337
+ Debug mode
338
+ ==========
303
339
  Setting a callback with a ``breakpoint()`` can allow for live HTTP request
304
340
  debugging, including setting custom responses and inspecting requests. Note
305
341
  that callbacks can also be queued.
@@ -324,4 +360,3 @@ that callbacks can also be queued.
324
360
  (Pdb) response[2] = "content set from pdb"
325
361
  (Pdb) c
326
362
  'content set from pdb'
327
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spoof
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: HTTP server for testing environments
5
5
  Author-email: Lex Scarisbrick <lex@scarisbrick.org>
6
6
  License-Expression: MIT
@@ -57,12 +57,12 @@ Spoof 👻
57
57
  A test interface for HTTP
58
58
  =========================
59
59
  Spoof lets you easily create HTTP servers listening on real network
60
- sockets. Designed for test environments, what responses to return can be
61
- configured while an HTTP server is running. Requests can be inspected
62
- live or after a response is sent.
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.
63
63
 
64
- Unlike a traditional HTTP server, where specific methods and paths are
65
- configured in advance, Spoof accepts and captures *all* requests, sending
64
+ Unlike a conventional HTTP server, where specific methods and paths are
65
+ configured in advance, Spoof accepts and records *all* requests, sending
66
66
  whatever responses are queued, or a default response if the queue is empty.
67
67
 
68
68
  Why would I want this?
@@ -70,13 +70,12 @@ Why would I want this?
70
70
  Spoof is all about enabling test-driven development (and refactoring) of
71
71
  HTTP client code. Have you ever felt icky patching a client library to
72
72
  write tests? Ever been burned by this? Ever wanted to refactor a client
73
- library, but had no way to prove functionality apart from doing live
74
- integration testing? Ever wanted mock functionality for HTTP? If you
75
- answered yes to any of the above, Spoof might be for you.
73
+ library, but had no way to verify behavior apart from doing live
74
+ integration testing? Ever wanted mock for HTTP? If you answered yes to
75
+ any of the above, Spoof might be for you.
76
76
 
77
77
  Installation and Compatibility
78
78
  ==============================
79
-
80
79
  Spoof is available on PyPI:
81
80
 
82
81
  .. code-block:: console
@@ -91,74 +90,8 @@ Multiple Spoof HTTP servers can be run concurrently, and by default, the port
91
90
  number is the next available unused port. With OpenSSL installed, Spoof can
92
91
  also provide an SSL/TLS HTTP server. HTTP proxying and IPv6 are also supported.
93
92
 
94
- Request instances
95
- =================
96
- Spoof captures each request as a ``SpoofRequestEnv`` instance with the following
97
- properties:
98
-
99
- +-------------------------+----------------------------------------------+
100
- | Property | Description |
101
- +=========================+==============================================+
102
- | content | ``bytes`` object of request content |
103
- +-------------------------+----------------------------------------------+
104
- | contentEncoding | Value of Content-Encoding header, if present |
105
- +-------------------------+----------------------------------------------+
106
- | contentLength | Value of Content-Length header, if present |
107
- +-------------------------+----------------------------------------------+
108
- | contentType | Value of Content-Type header, if present |
109
- +-------------------------+----------------------------------------------+
110
- | headers | ``http.client.HTTPMessage`` object of headers|
111
- +-------------------------+----------------------------------------------+
112
- | json() | Convenience to call ``json.loads`` on content|
113
- +-------------------------+----------------------------------------------+
114
- | method | Request method (e.g. GET, POST, HEAD) |
115
- +-------------------------+----------------------------------------------+
116
- | path | Decoded URI path, without query string |
117
- +-------------------------+----------------------------------------------+
118
- | protocol | Protocol version (e.g. HTTP/1.0) |
119
- +-------------------------+----------------------------------------------+
120
- | queryString | Anything in URI after ``?`` |
121
- +-------------------------+----------------------------------------------+
122
- | serverName | Host name of HTTP server |
123
- +-------------------------+----------------------------------------------+
124
- | serverPort | Port number of HTTP server |
125
- +-------------------------+----------------------------------------------+
126
- | uri | Raw URI path and query string, if present |
127
- +-------------------------+----------------------------------------------+
128
-
129
- Example with request properties:
130
-
131
- .. code-block:: python
132
-
133
- >>> import requests
134
- ... import spoof
135
- ...
136
- ... with spoof.HTTPServer() as httpd:
137
- ... httpd.defaultResponse = [200, [], None]
138
- ...
139
- ... [requests.get(httpd.url + path) for path in ["/a", "/b", "/c"]]
140
- ... [f"{r.method} {r.path} {r.protocol}" for r in httpd.requests]
141
- ...
142
- [<Response [200]>, <Response [200]>, <Response [200]>]
143
- ['GET /a HTTP/1.1', 'GET /b HTTP/1.1', 'GET /c HTTP/1.1']
144
-
145
- Response precedence
146
- ===================
147
-
148
- Spoof determines what response to send to incoming requests based on
149
- the following precedence, highest to lowest:
150
-
151
- #. Oldest response queued in ``.responses`` using first-in, first-out (FIFO) order
152
- #. Response stored in ``.defaultResponse`` if no responses are queued
153
- #. Response stored in ``.errorResponse`` if ``.defaultResponse`` is ``None``
154
-
155
- By default, an HTTP error response will be sent to all requests, because
156
- newly created Spoof instances have no responses queued, and no default
157
- response set. This requires non-error responses to be explicitly specified.
158
-
159
93
  Response syntax
160
94
  ===============
161
-
162
95
  Spoof expects responses to have the following syntax:
163
96
 
164
97
  .. code-block:: python
@@ -178,13 +111,31 @@ Spoof expects responses to have the following syntax:
178
111
  def callback(request):
179
112
  return [200, [], request.path]
180
113
 
181
- Queued responses
182
- ================
114
+ Response precedence
115
+ ===================
116
+ Spoof determines what response to send to incoming requests based on
117
+ the following precedence, highest to lowest:
118
+
119
+ #. Oldest response queued in ``.responses`` using first-in, first-out (FIFO) order
120
+ #. Response stored in ``.defaultResponse`` if no responses are queued
121
+ #. Response stored in ``.errorResponse`` if ``.defaultResponse`` is ``None``
122
+
123
+ By default, Spoof will respond with an **HTTP 503 Service Unavailable** error,
124
+ because newly created Spoof instances have no responses queued and no default
125
+ response set. This requires non-error HTTP responses to be explicitly specified.
183
126
 
184
- Spoof HTTP servers run in a single background thread, so request and
185
- response order should be predictable. Tests using Spoof should be able
186
- to use the same fixtures, in the same order, and get the same results. Example
187
- queueing multiple responses, verifying content, and request paths:
127
+ Response queue
128
+ ==============
129
+ Spoof will always try to send a response from ``.responses`` first, before falling
130
+ back to ``.defaultResponse`` if the queue is empty. Backed by a
131
+ `deque <https://docs.python.org/3/library/collections.html#collections.deque>`__
132
+ instance, the ``.responses`` queue supports adding items via ``.responses.append()``
133
+ and ``.responses.extend()``, similar to a regular list.
134
+
135
+ Spoof HTTP servers run in a single background thread, so response order should
136
+ be predictably serial. Tests using Spoof should be able to use the same fixtures,
137
+ in the same order, and get the same results. Example queueing multiple responses,
138
+ verifying content, and request paths:
188
139
 
189
140
  .. code-block:: python
190
141
 
@@ -203,9 +154,11 @@ queueing multiple responses, verifying content, and request paths:
203
154
  assert requests.get(httpd.url + "/oops").status_code == 404
204
155
  assert [r.path for r in httpd.requests] == ["/path", "/alt/path", "/oops"]
205
156
 
206
- Callback response
207
- =================
208
- Set a callback as the default response (callbacks can also be queued):
157
+ Response default
158
+ ================
159
+ Spoof will always try to send a response from ``.responses`` first, before falling
160
+ back to ``.defaultResponse`` if the queue is empty. Example setting a callback as
161
+ a default response:
209
162
 
210
163
  .. code-block:: python
211
164
 
@@ -217,9 +170,69 @@ Set a callback as the default response (callbacks can also be queued):
217
170
 
218
171
  assert requests.get(httpd.url + "/alt").text == "/alt"
219
172
 
173
+ Request history
174
+ ===============
175
+ Spoof records each request and appends it to the ``.requests`` property,
176
+ which is backed by a
177
+ `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:
180
+
181
+ .. code-block:: python
182
+
183
+ >>> import requests
184
+ ... import spoof
185
+ ...
186
+ ... with spoof.HTTPServer() as httpd:
187
+ ... httpd.defaultResponse = [200, [], None]
188
+ ...
189
+ ... [requests.get(httpd.url + path) for path in ["/a", "/b", "/c"]]
190
+ ... [f"{r.method} {r.path} {r.protocol}" for r in httpd.requests]
191
+ ...
192
+ [<Response [200]>, <Response [200]>, <Response [200]>]
193
+ ['GET /a HTTP/1.1', 'GET /b HTTP/1.1', 'GET /c HTTP/1.1']
194
+
195
+ Request properties
196
+ ==================
197
+ ``SpoofRequestEnv`` instances have the following properties:
198
+
199
+ +-------------------------+----------------------------------------------+
200
+ | Property | Description |
201
+ +=========================+==============================================+
202
+ | content | ``bytes`` object of request content |
203
+ +-------------------------+----------------------------------------------+
204
+ | contentEncoding | Value of Content-Encoding header, if present |
205
+ +-------------------------+----------------------------------------------+
206
+ | contentLength | Value of Content-Length header, if present |
207
+ +-------------------------+----------------------------------------------+
208
+ | contentType | Value of Content-Type header, if present |
209
+ +-------------------------+----------------------------------------------+
210
+ | headers | ``http.client.HTTPMessage`` object of headers|
211
+ +-------------------------+----------------------------------------------+
212
+ | json() | Convenience to call ``json.loads`` on content|
213
+ +-------------------------+----------------------------------------------+
214
+ | method | Request method (e.g. GET, POST, HEAD) |
215
+ +-------------------------+----------------------------------------------+
216
+ | path | Decoded URI path, without query string |
217
+ +-------------------------+----------------------------------------------+
218
+ | protocol | Protocol version (e.g. HTTP/1.0) |
219
+ +-------------------------+----------------------------------------------+
220
+ | queryString | Anything in URI after ``?`` |
221
+ +-------------------------+----------------------------------------------+
222
+ | serverName | Host name of HTTP server |
223
+ +-------------------------+----------------------------------------------+
224
+ | serverPort | Port number of HTTP server |
225
+ +-------------------------+----------------------------------------------+
226
+ | uri | Raw URI path and query string, if present |
227
+ +-------------------------+----------------------------------------------+
228
+
220
229
  SSL/TLS Mode
221
230
  ============
222
- Test queued response with a self-signed SSL/TLS certificate:
231
+ Spoof supports SSL/TLS connectivity by passing an
232
+ `SSLContext <https://docs.python.org/3/library/ssl.html#ssl-contexts>`__,
233
+ or if OpenSSL command line tools are available, creating an ``SSLContext``
234
+ with a self-signed certificate. Configured correctly, this should not raise
235
+ any warnings or errors:
223
236
 
224
237
  .. code-block:: python
225
238
 
@@ -251,8 +264,8 @@ set to the path of the self-signed certificate to silence SSL/TLS errors:
251
264
  response = requests.get(httpd.url)
252
265
  assert response.text == "No self-signed cert warning!"
253
266
 
254
- If OpenSSL 3.5.0 or later is installed, Post-Quantum Cryptography (PQC)
255
- key algorithms can be used:
267
+ If `OpenSSL 3.5.0 <https://openssl-library.org/post/2025-04-08-openssl-35-final-release/>`__
268
+ or later is installed, Post-Quantum Cryptography (PQC) key algorithms can be used:
256
269
 
257
270
  .. code-block:: python
258
271
 
@@ -293,8 +306,31 @@ external services. Example usage:
293
306
  assert proxy.upstream.requests[0].path == "/ayt"
294
307
  assert response.text == "I'm here!"
295
308
 
296
- HTTP on IPv6
297
- ============
309
+ If setting the ``proxies`` option in ``requests`` isn't workable, the
310
+ ``https_proxy`` environment variable can be set to the URL of the proxy:
311
+
312
+ .. code-block:: python
313
+
314
+ import os
315
+ import requests
316
+ import spoof
317
+
318
+ with spoof.SelfSignedSSLContext(commonName="example.spoof") as selfSigned:
319
+ with spoof.HTTPServer(sslContext=selfSigned.sslContext, proxy=True) as proxy:
320
+ proxy.upstream.defaultResponse = [200, [], "I'm here!"]
321
+
322
+ os.environ["https_proxy"] = proxy.url
323
+ os.environ["REQUESTS_CA_BUNDLE"] = selfSigned.certFile
324
+
325
+ response = requests.get("https://example.spoof/ayt")
326
+ assert proxy.requests[0].method == "CONNECT"
327
+ assert proxy.requests[0].path == "example.spoof:443"
328
+ assert proxy.upstream.requests[0].method == "GET"
329
+ assert proxy.upstream.requests[0].path == "/ayt"
330
+ assert response.text == "I'm here!"
331
+
332
+ IPv6 Mode
333
+ =========
298
334
  Setting the ``host`` attribute to an IPv6 address will work as expected. There
299
335
  is also an IPv6-only ``spoof.HTTPServer6`` class that can be used if needed to
300
336
  only listen on IPv6 sockets.
@@ -325,8 +361,8 @@ only listen on IPv6 sockets.
325
361
  'This is also Spoof on IPv6 👀'
326
362
  'http://[::1]:54296'
327
363
 
328
- Using a debugger
329
- ================
364
+ Debug mode
365
+ ==========
330
366
  Setting a callback with a ``breakpoint()`` can allow for live HTTP request
331
367
  debugging, including setting custom responses and inspecting requests. Note
332
368
  that callbacks can also be queued.
@@ -351,4 +387,3 @@ that callbacks can also be queued.
351
387
  (Pdb) response[2] = "content set from pdb"
352
388
  (Pdb) c
353
389
  'content set from pdb'
354
-
@@ -303,7 +303,7 @@ class HTTPServer(object):
303
303
  return handlerClass
304
304
 
305
305
  def start(self):
306
- """Starts HTTP server thread."""
306
+ """Starts HTTP server thread(s)."""
307
307
  if self.server is not None:
308
308
  message = "server at {0} already started".format(self.url)
309
309
  raise RuntimeError(message)
@@ -505,10 +505,10 @@ class SSLContext(object):
505
505
  """Creates and returns file paths to self-signed certificate and key
506
506
  via OpenSSL command line tool.
507
507
 
508
- :commonName: string of hostname for X509 certificate
509
- :bits: RSA public key length in bits
510
- :days: length in days certificate is valid
511
- :openssl: name/path string of openssl command
508
+ :commonName: string of hostname for X509 certificate
509
+ :bits: RSA public key length in bits
510
+ :days: length in days certificate is valid
511
+ :openssl: name/path string of openssl command
512
512
  :keyAlgorithm: key algorithm to use (e.g. mldsa65); ignores ``bits`` arg
513
513
  """
514
514
  if keyAlgorithm is None:
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
File without changes