New upstream snapshot.
Debian Janitor
2 years ago
0 | 0.16.0 | |
1 | ------ | |
2 | ||
3 | * Fixed regression with `stream` parameter deprecation, requests.session() and cookie handling. | |
4 | * Replaced adhoc URL parsing with `urllib.parse`. | |
5 | * Added ``match`` parameter to ``add_callback`` method | |
6 | * Added `responses.matchers.fragment_identifier_matcher`. This matcher allows you | |
7 | to match request URL fragment identifier. | |
8 | * Improved test coverage. | |
9 | * Fixed failing test in python 2.7 when `python-future` is also installed. | |
10 | ||
11 | 0.15.0 | |
12 | ------ | |
13 | ||
14 | * Added `responses.PassthroughResponse` and | |
15 | `reponses.BaseResponse.passthrough`. These features make building passthrough | |
16 | responses more compatible with dynamcially generated response objects. | |
17 | * Removed the unused ``_is_redirect()`` function from responses internals. | |
18 | * Added `responses.matchers.request_kwargs_matcher`. This matcher allows you | |
19 | to match additional request arguments like `stream`. | |
20 | * Added `responses.matchers.multipart_matcher`. This matcher allows you | |
21 | to match request body and headers for ``multipart/form-data`` data | |
22 | * Added `responses.matchers.query_string_matcher`. This matcher allows you | |
23 | to match request query string, similar to `responses.matchers.query_param_matcher`. | |
24 | * Added `responses.matchers.header_matcher()`. This matcher allows you to match | |
25 | request headers. By default only headers supplied to `header_matcher()` are checked. | |
26 | You can make header matching exhaustive by passing `strict_match=True` to `header_matcher()`. | |
27 | * Changed all matchers output message in case of mismatch. Now message is aligned | |
28 | between Python2 and Python3 versions | |
29 | * Deprecate ``stream`` argument in ``Response`` and ``CallbackResponse`` | |
30 | * Added Python 3.10 support | |
31 | ||
32 | 0.14.0 | |
33 | ------ | |
34 | ||
35 | * Added `responses.matchers`. | |
36 | * Moved `responses.json_params_matcher` to `responses.matchers.json_params_matcher` | |
37 | * Moved `responses.urlencoded_params_matcher` to | |
38 | `responses.matchers.urlencoded_params_matcher` | |
39 | * Added `responses.matchers.query_param_matcher`. This matcher allows you | |
40 | to match query strings with a dictionary. | |
41 | * Added `auto_calculate_content_length` option to `responses.add()`. When | |
42 | enabled, this option will generate a `Content-Length` header | |
43 | based on the number of bytes in the response body. | |
44 | ||
0 | 45 | 0.13.4 |
1 | 46 | ------ |
2 | 47 |
0 | 0 | include README.rst CHANGES LICENSE |
1 | include test_responses.py tox.ini | |
1 | include test_responses.py test_matchers.py | |
2 | include **/*.pyi | |
3 | include tox.ini | |
2 | 4 | global-exclude *~ |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: responses |
2 | Version: 0.13.4 | |
2 | Version: 0.14.0 | |
3 | 3 | Summary: A utility library for mocking out the `requests` Python library. |
4 | 4 | Home-page: https://github.com/getsentry/responses |
5 | 5 | Author: David Cramer |
6 | 6 | License: Apache 2.0 |
7 | Description: Responses | |
8 | ========= | |
9 | ||
10 | .. image:: https://img.shields.io/pypi/v/responses.svg | |
11 | :target: https://pypi.python.org/pypi/responses/ | |
12 | ||
13 | .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master | |
14 | :target: https://travis-ci.org/getsentry/responses | |
15 | ||
16 | .. image:: https://img.shields.io/pypi/pyversions/responses.svg | |
17 | :target: https://pypi.org/project/responses/ | |
18 | ||
19 | A utility library for mocking out the ``requests`` Python library. | |
20 | ||
21 | .. note:: | |
22 | ||
23 | Responses requires Python 2.7 or newer, and requests >= 2.0 | |
24 | ||
25 | ||
26 | Installing | |
27 | ---------- | |
28 | ||
29 | ``pip install responses`` | |
30 | ||
31 | ||
32 | Basics | |
33 | ------ | |
34 | ||
35 | The core of ``responses`` comes from registering mock responses: | |
36 | ||
37 | .. code-block:: python | |
38 | ||
39 | import responses | |
40 | import requests | |
41 | ||
42 | @responses.activate | |
43 | def test_simple(): | |
44 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
45 | json={'error': 'not found'}, status=404) | |
46 | ||
47 | resp = requests.get('http://twitter.com/api/1/foobar') | |
48 | ||
49 | assert resp.json() == {"error": "not found"} | |
50 | ||
51 | assert len(responses.calls) == 1 | |
52 | assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' | |
53 | assert responses.calls[0].response.text == '{"error": "not found"}' | |
54 | ||
55 | If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise | |
56 | a ``ConnectionError``: | |
57 | ||
58 | .. code-block:: python | |
59 | ||
60 | import responses | |
61 | import requests | |
62 | ||
63 | from requests.exceptions import ConnectionError | |
64 | ||
65 | @responses.activate | |
66 | def test_simple(): | |
67 | with pytest.raises(ConnectionError): | |
68 | requests.get('http://twitter.com/api/1/foobar') | |
69 | ||
70 | Lastly, you can pass an ``Exception`` as the body to trigger an error on the request: | |
71 | ||
72 | .. code-block:: python | |
73 | ||
74 | import responses | |
75 | import requests | |
76 | ||
77 | @responses.activate | |
78 | def test_simple(): | |
79 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
80 | body=Exception('...')) | |
81 | with pytest.raises(Exception): | |
82 | requests.get('http://twitter.com/api/1/foobar') | |
83 | ||
84 | ||
85 | Response Parameters | |
86 | ------------------- | |
87 | ||
88 | Responses are automatically registered via params on ``add``, but can also be | |
89 | passed directly: | |
90 | ||
91 | .. code-block:: python | |
92 | ||
93 | import responses | |
94 | ||
95 | responses.add( | |
96 | responses.Response( | |
97 | method='GET', | |
98 | url='http://example.com', | |
99 | ) | |
100 | ) | |
101 | ||
102 | The following attributes can be passed to a Response mock: | |
103 | ||
104 | method (``str``) | |
105 | The HTTP method (GET, POST, etc). | |
106 | ||
107 | url (``str`` or compiled regular expression) | |
108 | The full resource URL. | |
109 | ||
110 | match_querystring (``bool``) | |
111 | Include the query string when matching requests. | |
112 | Enabled by default if the response URL contains a query string, | |
113 | disabled if it doesn't or the URL is a regular expression. | |
114 | ||
115 | body (``str`` or ``BufferedReader``) | |
116 | The response body. | |
117 | ||
118 | json | |
119 | A Python object representing the JSON response body. Automatically configures | |
120 | the appropriate Content-Type. | |
121 | ||
122 | status (``int``) | |
123 | The HTTP status code. | |
124 | ||
125 | content_type (``content_type``) | |
126 | Defaults to ``text/plain``. | |
127 | ||
128 | headers (``dict``) | |
129 | Response headers. | |
130 | ||
131 | stream (``bool``) | |
132 | Disabled by default. Indicates the response should use the streaming API. | |
133 | ||
134 | match (``list``) | |
135 | A list of callbacks to match requests based on request body contents. | |
136 | ||
137 | ||
138 | Matching Request Parameters | |
139 | --------------------------- | |
140 | ||
141 | When adding responses for endpoints that are sent request data you can add | |
142 | matchers to ensure your code is sending the right parameters and provide | |
143 | different responses based on the request body contents. Responses provides | |
144 | matchers for JSON and URLencoded request bodies and you can supply your own for | |
145 | other formats. | |
146 | ||
147 | .. code-block:: python | |
148 | ||
149 | import responses | |
150 | import requests | |
151 | ||
152 | @responses.activate | |
153 | def test_calc_api(): | |
154 | responses.add( | |
155 | responses.POST, | |
156 | url='http://calc.com/sum', | |
157 | body="4", | |
158 | match=[ | |
159 | responses.urlencoded_params_matcher({"left": "1", "right": "3"}) | |
160 | ] | |
161 | ) | |
162 | requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) | |
163 | ||
164 | Matching JSON encoded data can be done with ``responses.json_params_matcher()``. | |
165 | If your application uses other encodings you can build your own matcher that | |
166 | returns ``True`` or ``False`` if the request parameters match. Your matcher can | |
167 | expect a ``request_body`` parameter to be provided by responses. | |
168 | ||
169 | Dynamic Responses | |
170 | ----------------- | |
171 | ||
172 | You can utilize callbacks to provide dynamic responses. The callback must return | |
173 | a tuple of (``status``, ``headers``, ``body``). | |
174 | ||
175 | .. code-block:: python | |
176 | ||
177 | import json | |
178 | ||
179 | import responses | |
180 | import requests | |
181 | ||
182 | @responses.activate | |
183 | def test_calc_api(): | |
184 | ||
185 | def request_callback(request): | |
186 | payload = json.loads(request.body) | |
187 | resp_body = {'value': sum(payload['numbers'])} | |
188 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
189 | return (200, headers, json.dumps(resp_body)) | |
190 | ||
191 | responses.add_callback( | |
192 | responses.POST, 'http://calc.com/sum', | |
193 | callback=request_callback, | |
194 | content_type='application/json', | |
195 | ) | |
196 | ||
197 | resp = requests.post( | |
198 | 'http://calc.com/sum', | |
199 | json.dumps({'numbers': [1, 2, 3]}), | |
200 | headers={'content-type': 'application/json'}, | |
201 | ) | |
202 | ||
203 | assert resp.json() == {'value': 6} | |
204 | ||
205 | assert len(responses.calls) == 1 | |
206 | assert responses.calls[0].request.url == 'http://calc.com/sum' | |
207 | assert responses.calls[0].response.text == '{"value": 6}' | |
208 | assert ( | |
209 | responses.calls[0].response.headers['request-id'] == | |
210 | '728d329e-0e86-11e4-a748-0c84dc037c13' | |
211 | ) | |
212 | ||
213 | You can also pass a compiled regex to ``add_callback`` to match multiple urls: | |
214 | ||
215 | .. code-block:: python | |
216 | ||
217 | import re, json | |
218 | ||
219 | from functools import reduce | |
220 | ||
221 | import responses | |
222 | import requests | |
223 | ||
224 | operators = { | |
225 | 'sum': lambda x, y: x+y, | |
226 | 'prod': lambda x, y: x*y, | |
227 | 'pow': lambda x, y: x**y | |
228 | } | |
229 | ||
230 | @responses.activate | |
231 | def test_regex_url(): | |
232 | ||
233 | def request_callback(request): | |
234 | payload = json.loads(request.body) | |
235 | operator_name = request.path_url[1:] | |
236 | ||
237 | operator = operators[operator_name] | |
238 | ||
239 | resp_body = {'value': reduce(operator, payload['numbers'])} | |
240 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
241 | return (200, headers, json.dumps(resp_body)) | |
242 | ||
243 | responses.add_callback( | |
244 | responses.POST, | |
245 | re.compile('http://calc.com/(sum|prod|pow|unsupported)'), | |
246 | callback=request_callback, | |
247 | content_type='application/json', | |
248 | ) | |
249 | ||
250 | resp = requests.post( | |
251 | 'http://calc.com/prod', | |
252 | json.dumps({'numbers': [2, 3, 4]}), | |
253 | headers={'content-type': 'application/json'}, | |
254 | ) | |
255 | assert resp.json() == {'value': 24} | |
256 | ||
257 | test_regex_url() | |
258 | ||
259 | ||
260 | If you want to pass extra keyword arguments to the callback function, for example when reusing | |
261 | a callback function to give a slightly different result, you can use ``functools.partial``: | |
262 | ||
263 | .. code-block:: python | |
264 | ||
265 | from functools import partial | |
266 | ||
267 | ... | |
268 | ||
269 | def request_callback(request, id=None): | |
270 | payload = json.loads(request.body) | |
271 | resp_body = {'value': sum(payload['numbers'])} | |
272 | headers = {'request-id': id} | |
273 | return (200, headers, json.dumps(resp_body)) | |
274 | ||
275 | responses.add_callback( | |
276 | responses.POST, 'http://calc.com/sum', | |
277 | callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'), | |
278 | content_type='application/json', | |
279 | ) | |
280 | ||
281 | ||
282 | You can see params passed in the original ``request`` in ``responses.calls[].request.params``: | |
283 | ||
284 | .. code-block:: python | |
285 | ||
286 | import responses | |
287 | import requests | |
288 | ||
289 | @responses.activate | |
290 | def test_request_params(): | |
291 | responses.add( | |
292 | method=responses.GET, | |
293 | url="http://example.com?hello=world", | |
294 | body="test", | |
295 | match_querystring=False, | |
296 | ) | |
297 | ||
298 | resp = requests.get('http://example.com', params={"hello": "world"}) | |
299 | assert responses.calls[0].request.params == {"hello": "world"} | |
300 | ||
301 | Responses as a context manager | |
302 | ------------------------------ | |
303 | ||
304 | .. code-block:: python | |
305 | ||
306 | import responses | |
307 | import requests | |
308 | ||
309 | def test_my_api(): | |
310 | with responses.RequestsMock() as rsps: | |
311 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
312 | body='{}', status=200, | |
313 | content_type='application/json') | |
314 | resp = requests.get('http://twitter.com/api/1/foobar') | |
315 | ||
316 | assert resp.status_code == 200 | |
317 | ||
318 | # outside the context manager requests will hit the remote server | |
319 | resp = requests.get('http://twitter.com/api/1/foobar') | |
320 | resp.status_code == 404 | |
321 | ||
322 | Responses as a pytest fixture | |
323 | ----------------------------- | |
324 | ||
325 | .. code-block:: python | |
326 | ||
327 | @pytest.fixture | |
328 | def mocked_responses(): | |
329 | with responses.RequestsMock() as rsps: | |
330 | yield rsps | |
331 | ||
332 | def test_api(mocked_responses): | |
333 | mocked_responses.add( | |
334 | responses.GET, 'http://twitter.com/api/1/foobar', | |
335 | body='{}', status=200, | |
336 | content_type='application/json') | |
337 | resp = requests.get('http://twitter.com/api/1/foobar') | |
338 | assert resp.status_code == 200 | |
339 | ||
340 | Responses inside a unittest setUp() | |
341 | ----------------------------------- | |
342 | ||
343 | When run with unittest tests, this can be used to set up some | |
344 | generic class-level responses, that may be complemented by each test | |
345 | ||
346 | .. code-block:: python | |
347 | ||
348 | def setUp(): | |
349 | self.responses = responses.RequestsMock() | |
350 | self.responses.start() | |
351 | ||
352 | # self.responses.add(...) | |
353 | ||
354 | self.addCleanup(self.responses.stop) | |
355 | self.addCleanup(self.responses.reset) | |
356 | ||
357 | def test_api(self): | |
358 | self.responses.add( | |
359 | responses.GET, 'http://twitter.com/api/1/foobar', | |
360 | body='{}', status=200, | |
361 | content_type='application/json') | |
362 | resp = requests.get('http://twitter.com/api/1/foobar') | |
363 | assert resp.status_code == 200 | |
364 | ||
365 | Assertions on declared responses | |
366 | -------------------------------- | |
367 | ||
368 | When used as a context manager, Responses will, by default, raise an assertion | |
369 | error if a url was registered but not accessed. This can be disabled by passing | |
370 | the ``assert_all_requests_are_fired`` value: | |
371 | ||
372 | .. code-block:: python | |
373 | ||
374 | import responses | |
375 | import requests | |
376 | ||
377 | def test_my_api(): | |
378 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
379 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
380 | body='{}', status=200, | |
381 | content_type='application/json') | |
382 | ||
383 | assert_call_count | |
384 | ----------------- | |
385 | ||
386 | Assert that the request was called exactly n times. | |
387 | ||
388 | .. code-block:: python | |
389 | ||
390 | import responses | |
391 | import requests | |
392 | ||
393 | @responses.activate | |
394 | def test_assert_call_count(): | |
395 | responses.add(responses.GET, "http://example.com") | |
396 | ||
397 | requests.get("http://example.com") | |
398 | assert responses.assert_call_count("http://example.com", 1) is True | |
399 | ||
400 | requests.get("http://example.com") | |
401 | with pytest.raises(AssertionError) as excinfo: | |
402 | responses.assert_call_count("http://example.com", 1) | |
403 | assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) | |
404 | ||
405 | ||
406 | Multiple Responses | |
407 | ------------------ | |
408 | ||
409 | You can also add multiple responses for the same url: | |
410 | ||
411 | .. code-block:: python | |
412 | ||
413 | import responses | |
414 | import requests | |
415 | ||
416 | @responses.activate | |
417 | def test_my_api(): | |
418 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) | |
419 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
420 | body='{}', status=200, | |
421 | content_type='application/json') | |
422 | ||
423 | resp = requests.get('http://twitter.com/api/1/foobar') | |
424 | assert resp.status_code == 500 | |
425 | resp = requests.get('http://twitter.com/api/1/foobar') | |
426 | assert resp.status_code == 200 | |
427 | ||
428 | ||
429 | Using a callback to modify the response | |
430 | --------------------------------------- | |
431 | ||
432 | If you use customized processing in `requests` via subclassing/mixins, or if you | |
433 | have library tools that interact with `requests` at a low level, you may need | |
434 | to add extended processing to the mocked Response object to fully simulate the | |
435 | environment for your tests. A `response_callback` can be used, which will be | |
436 | wrapped by the library before being returned to the caller. The callback | |
437 | accepts a `response` as it's single argument, and is expected to return a | |
438 | single `response` object. | |
439 | ||
440 | .. code-block:: python | |
441 | ||
442 | import responses | |
443 | import requests | |
444 | ||
445 | def response_callback(resp): | |
446 | resp.callback_processed = True | |
447 | return resp | |
448 | ||
449 | with responses.RequestsMock(response_callback=response_callback) as m: | |
450 | m.add(responses.GET, 'http://example.com', body=b'test') | |
451 | resp = requests.get('http://example.com') | |
452 | assert resp.text == "test" | |
453 | assert hasattr(resp, 'callback_processed') | |
454 | assert resp.callback_processed is True | |
455 | ||
456 | ||
457 | Passing through real requests | |
458 | ----------------------------- | |
459 | ||
460 | In some cases you may wish to allow for certain requests to pass through responses | |
461 | and hit a real server. This can be done with the ``add_passthru`` methods: | |
462 | ||
463 | .. code-block:: python | |
464 | ||
465 | import responses | |
466 | ||
467 | @responses.activate | |
468 | def test_my_api(): | |
469 | responses.add_passthru('https://percy.io') | |
470 | ||
471 | This will allow any requests matching that prefix, that is otherwise not registered | |
472 | as a mock response, to passthru using the standard behavior. | |
473 | ||
474 | Regex can be used like: | |
475 | ||
476 | .. code-block:: python | |
477 | ||
478 | responses.add_passthru(re.compile('https://percy.io/\\w+')) | |
479 | ||
480 | ||
481 | Viewing/Modifying registered responses | |
482 | -------------------------------------- | |
483 | ||
484 | Registered responses are available as a public method of the RequestMock | |
485 | instance. It is sometimes useful for debugging purposes to view the stack of | |
486 | registered responses which can be accessed via ``responses.registered()``. | |
487 | ||
488 | The ``replace`` function allows a previously registered ``response`` to be | |
489 | changed. The method signature is identical to ``add``. ``response`` s are | |
490 | identified using ``method`` and ``url``. Only the first matched ``response`` is | |
491 | replaced. | |
492 | ||
493 | .. code-block:: python | |
494 | ||
495 | import responses | |
496 | import requests | |
497 | ||
498 | @responses.activate | |
499 | def test_replace(): | |
500 | ||
501 | responses.add(responses.GET, 'http://example.org', json={'data': 1}) | |
502 | responses.replace(responses.GET, 'http://example.org', json={'data': 2}) | |
503 | ||
504 | resp = requests.get('http://example.org') | |
505 | ||
506 | assert resp.json() == {'data': 2} | |
507 | ||
508 | ||
509 | The ``upsert`` function allows a previously registered ``response`` to be | |
510 | changed like ``replace``. If the response is registered, the ``upsert`` function | |
511 | will registered it like ``add``. | |
512 | ||
513 | ``remove`` takes a ``method`` and ``url`` argument and will remove **all** | |
514 | matched responses from the registered list. | |
515 | ||
516 | Finally, ``reset`` will reset all registered responses. | |
517 | ||
518 | Contributing | |
519 | ------------ | |
520 | ||
521 | Responses uses several linting and autoformatting utilities, so it's important that when | |
522 | submitting patches you use the appropriate toolchain: | |
523 | ||
524 | Clone the repository: | |
525 | ||
526 | .. code-block:: shell | |
527 | ||
528 | git clone https://github.com/getsentry/responses.git | |
529 | ||
530 | Create an environment (e.g. with ``virtualenv``): | |
531 | ||
532 | .. code-block:: shell | |
533 | ||
534 | virtualenv .env && source .env/bin/activate | |
535 | ||
536 | Configure development requirements: | |
537 | ||
538 | .. code-block:: shell | |
539 | ||
540 | make develop | |
541 | ||
542 | Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for | |
543 | testing. You can run all tests by: | |
544 | ||
545 | .. code-block:: shell | |
546 | ||
547 | pytest | |
548 | ||
549 | And run a single test by: | |
550 | ||
551 | .. code-block:: shell | |
552 | ||
553 | pytest -k '<test_function_name>' | |
554 | ||
555 | 7 | Platform: UNKNOWN |
556 | 8 | Classifier: Intended Audience :: Developers |
557 | 9 | Classifier: Intended Audience :: System Administrators |
565 | 17 | Classifier: Programming Language :: Python :: 3.7 |
566 | 18 | Classifier: Programming Language :: Python :: 3.8 |
567 | 19 | Classifier: Programming Language :: Python :: 3.9 |
20 | Classifier: Programming Language :: Python :: 3.10 | |
568 | 21 | Classifier: Topic :: Software Development |
569 | 22 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* |
570 | 23 | Description-Content-Type: text/x-rst |
571 | 24 | Provides-Extra: tests |
25 | License-File: LICENSE | |
26 | ||
27 | Responses | |
28 | ========= | |
29 | ||
30 | .. image:: https://img.shields.io/pypi/v/responses.svg | |
31 | :target: https://pypi.python.org/pypi/responses/ | |
32 | ||
33 | .. image:: https://img.shields.io/pypi/pyversions/responses.svg | |
34 | :target: https://pypi.org/project/responses/ | |
35 | ||
36 | .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg | |
37 | :target: https://codecov.io/gh/getsentry/responses/ | |
38 | ||
39 | A utility library for mocking out the ``requests`` Python library. | |
40 | ||
41 | .. note:: | |
42 | ||
43 | Responses requires Python 2.7 or newer, and requests >= 2.0 | |
44 | ||
45 | ||
46 | Installing | |
47 | ---------- | |
48 | ||
49 | ``pip install responses`` | |
50 | ||
51 | ||
52 | Basics | |
53 | ------ | |
54 | ||
55 | The core of ``responses`` comes from registering mock responses: | |
56 | ||
57 | .. code-block:: python | |
58 | ||
59 | import responses | |
60 | import requests | |
61 | ||
62 | @responses.activate | |
63 | def test_simple(): | |
64 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
65 | json={'error': 'not found'}, status=404) | |
66 | ||
67 | resp = requests.get('http://twitter.com/api/1/foobar') | |
68 | ||
69 | assert resp.json() == {"error": "not found"} | |
70 | ||
71 | assert len(responses.calls) == 1 | |
72 | assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' | |
73 | assert responses.calls[0].response.text == '{"error": "not found"}' | |
74 | ||
75 | If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise | |
76 | a ``ConnectionError``: | |
77 | ||
78 | .. code-block:: python | |
79 | ||
80 | import responses | |
81 | import requests | |
82 | ||
83 | from requests.exceptions import ConnectionError | |
84 | ||
85 | @responses.activate | |
86 | def test_simple(): | |
87 | with pytest.raises(ConnectionError): | |
88 | requests.get('http://twitter.com/api/1/foobar') | |
89 | ||
90 | Lastly, you can pass an ``Exception`` as the body to trigger an error on the request: | |
91 | ||
92 | .. code-block:: python | |
93 | ||
94 | import responses | |
95 | import requests | |
96 | ||
97 | @responses.activate | |
98 | def test_simple(): | |
99 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
100 | body=Exception('...')) | |
101 | with pytest.raises(Exception): | |
102 | requests.get('http://twitter.com/api/1/foobar') | |
103 | ||
104 | ||
105 | Response Parameters | |
106 | ------------------- | |
107 | ||
108 | Responses are automatically registered via params on ``add``, but can also be | |
109 | passed directly: | |
110 | ||
111 | .. code-block:: python | |
112 | ||
113 | import responses | |
114 | ||
115 | responses.add( | |
116 | responses.Response( | |
117 | method='GET', | |
118 | url='http://example.com', | |
119 | ) | |
120 | ) | |
121 | ||
122 | The following attributes can be passed to a Response mock: | |
123 | ||
124 | method (``str``) | |
125 | The HTTP method (GET, POST, etc). | |
126 | ||
127 | url (``str`` or compiled regular expression) | |
128 | The full resource URL. | |
129 | ||
130 | match_querystring (``bool``) | |
131 | Include the query string when matching requests. | |
132 | Enabled by default if the response URL contains a query string, | |
133 | disabled if it doesn't or the URL is a regular expression. | |
134 | ||
135 | body (``str`` or ``BufferedReader``) | |
136 | The response body. | |
137 | ||
138 | json | |
139 | A Python object representing the JSON response body. Automatically configures | |
140 | the appropriate Content-Type. | |
141 | ||
142 | status (``int``) | |
143 | The HTTP status code. | |
144 | ||
145 | content_type (``content_type``) | |
146 | Defaults to ``text/plain``. | |
147 | ||
148 | headers (``dict``) | |
149 | Response headers. | |
150 | ||
151 | stream (``bool``) | |
152 | DEPRECATED: use ``stream`` argument in request directly | |
153 | ||
154 | auto_calculate_content_length (``bool``) | |
155 | Disabled by default. Automatically calculates the length of a supplied string or JSON body. | |
156 | ||
157 | match (``list``) | |
158 | A list of callbacks to match requests based on request attributes. | |
159 | Current module provides multiple matchers that you can use to match: | |
160 | ||
161 | * body contents in JSON format | |
162 | * body contents in URL encoded data format | |
163 | * request query parameters | |
164 | * request query string (similar to query parameters but takes string as input) | |
165 | * kwargs provided to request e.g. ``stream``, ``verify`` | |
166 | * 'multipart/form-data' content and headers in request | |
167 | * request headers | |
168 | * request fragment identifier | |
169 | ||
170 | Alternatively user can create custom matcher. | |
171 | Read more `Matching Requests`_ | |
172 | ||
173 | ||
174 | Matching Requests | |
175 | ----------------- | |
176 | ||
177 | When adding responses for endpoints that are sent request data you can add | |
178 | matchers to ensure your code is sending the right parameters and provide | |
179 | different responses based on the request body contents. Responses provides | |
180 | matchers for JSON and URL-encoded request bodies and you can supply your own for | |
181 | other formats. | |
182 | ||
183 | .. code-block:: python | |
184 | ||
185 | import responses | |
186 | import requests | |
187 | from responses import matchers | |
188 | ||
189 | @responses.activate | |
190 | def test_calc_api(): | |
191 | responses.add( | |
192 | responses.POST, | |
193 | url='http://calc.com/sum', | |
194 | body="4", | |
195 | match=[ | |
196 | matchers.urlencoded_params_matcher({"left": "1", "right": "3"}) | |
197 | ] | |
198 | ) | |
199 | requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) | |
200 | ||
201 | Matching JSON encoded data can be done with ``matchers.json_params_matcher()``. | |
202 | If your application uses other encodings you can build your own matcher that | |
203 | returns ``True`` or ``False`` if the request parameters match. Your matcher can | |
204 | expect a ``request`` parameter to be provided by responses. | |
205 | ||
206 | Similarly, you can use the ``matchers.query_param_matcher`` function to match | |
207 | against the ``params`` request parameter. | |
208 | Note, you must set ``match_querystring=False`` | |
209 | ||
210 | .. code-block:: python | |
211 | ||
212 | import responses | |
213 | import requests | |
214 | from responses import matchers | |
215 | ||
216 | @responses.activate | |
217 | def test_calc_api(): | |
218 | url = "http://example.com/test" | |
219 | params = {"hello": "world", "I am": "a big test"} | |
220 | responses.add( | |
221 | method=responses.GET, | |
222 | url=url, | |
223 | body="test", | |
224 | match=[matchers.query_param_matcher(params)], | |
225 | match_querystring=False, | |
226 | ) | |
227 | ||
228 | resp = requests.get(url, params=params) | |
229 | ||
230 | constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" | |
231 | assert resp.url == constructed_url | |
232 | assert resp.request.url == constructed_url | |
233 | assert resp.request.params == params | |
234 | ||
235 | ||
236 | As alternative, you can use query string value in ``matchers.query_string_matcher`` | |
237 | ||
238 | .. code-block:: python | |
239 | ||
240 | import requests | |
241 | import responses | |
242 | from responses import matchers | |
243 | ||
244 | @responses.activate | |
245 | def my_func(): | |
246 | responses.add( | |
247 | responses.GET, | |
248 | "https://httpbin.org/get", | |
249 | match=[matchers.query_string_matcher("didi=pro&test=1")], | |
250 | ) | |
251 | resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"}) | |
252 | ||
253 | my_func() | |
254 | ||
255 | To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match | |
256 | against the request kwargs. | |
257 | Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated | |
258 | ||
259 | .. code-block:: python | |
260 | ||
261 | import responses | |
262 | import requests | |
263 | from responses import matchers | |
264 | ||
265 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
266 | req_kwargs = { | |
267 | "stream": True, | |
268 | "verify": False, | |
269 | } | |
270 | rsps.add( | |
271 | "GET", | |
272 | "http://111.com", | |
273 | match=[matchers.request_kwargs_matcher(req_kwargs)], | |
274 | ) | |
275 | ||
276 | requests.get("http://111.com", stream=True) | |
277 | ||
278 | # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False} | |
279 | ||
280 | To validate request body and headers for ``multipart/form-data`` data you can use | |
281 | ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared | |
282 | to the request: | |
283 | ||
284 | .. code-block:: python | |
285 | ||
286 | import requests | |
287 | import responses | |
288 | from responses.matchers import multipart_matcher | |
289 | ||
290 | @responses.activate | |
291 | def my_func(): | |
292 | req_data = {"some": "other", "data": "fields"} | |
293 | req_files = {"file_name": b"Old World!"} | |
294 | responses.add( | |
295 | responses.POST, url="http://httpbin.org/post", | |
296 | match=[multipart_matcher(req_data, req_files)] | |
297 | ) | |
298 | resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"}) | |
299 | ||
300 | my_func() | |
301 | # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs. | |
302 | ||
303 | ||
304 | To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``. | |
305 | The matcher takes fragment string (everything after ``#`` sign) as input for comparison: | |
306 | ||
307 | .. code-block:: python | |
308 | ||
309 | import requests | |
310 | import responses | |
311 | from responses.matchers import fragment_identifier_matcher | |
312 | ||
313 | @responses.activate | |
314 | def run(): | |
315 | url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" | |
316 | responses.add( | |
317 | responses.GET, | |
318 | url, | |
319 | match_querystring=True, | |
320 | match=[fragment_identifier_matcher("test=1&foo=bar")], | |
321 | body=b"test", | |
322 | ) | |
323 | ||
324 | # two requests to check reversed order of fragment identifier | |
325 | resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") | |
326 | resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") | |
327 | ||
328 | run() | |
329 | ||
330 | Matching Request Headers | |
331 | ------------------------ | |
332 | ||
333 | When adding responses you can specify matchers to ensure that your code is | |
334 | sending the right headers and provide different responses based on the request | |
335 | headers. | |
336 | ||
337 | .. code-block:: python | |
338 | ||
339 | import responses | |
340 | import requests | |
341 | from responses import matchers | |
342 | ||
343 | ||
344 | @responses.activate | |
345 | def test_content_type(): | |
346 | responses.add( | |
347 | responses.GET, | |
348 | url="http://example.com/", | |
349 | body="hello world", | |
350 | match=[ | |
351 | matchers.header_matcher({"Accept": "text/plain"}) | |
352 | ] | |
353 | ) | |
354 | ||
355 | responses.add( | |
356 | responses.GET, | |
357 | url="http://example.com/", | |
358 | json={"content": "hello world"}, | |
359 | match=[ | |
360 | matchers.header_matcher({"Accept": "application/json"}) | |
361 | ] | |
362 | ) | |
363 | ||
364 | # request in reverse order to how they were added! | |
365 | resp = requests.get("http://example.com/", headers={"Accept": "application/json"}) | |
366 | assert resp.json() == {"content": "hello world"} | |
367 | ||
368 | resp = requests.get("http://example.com/", headers={"Accept": "text/plain"}) | |
369 | assert resp.text == "hello world" | |
370 | ||
371 | Because ``requests`` will send several standard headers in addition to what was | |
372 | specified by your code, request headers that are additional to the ones | |
373 | passed to the matcher are ignored by default. You can change this behaviour by | |
374 | passing ``strict_match=True`` to the matcher to ensure that only the headers | |
375 | that you're expecting are sent and no others. Note that you will probably have | |
376 | to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't | |
377 | include any additional headers. | |
378 | ||
379 | .. code-block:: python | |
380 | ||
381 | import responses | |
382 | import requests | |
383 | from responses import matchers | |
384 | ||
385 | @responses.activate | |
386 | def test_content_type(): | |
387 | responses.add( | |
388 | responses.GET, | |
389 | url="http://example.com/", | |
390 | body="hello world", | |
391 | match=[ | |
392 | matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) | |
393 | ] | |
394 | ) | |
395 | ||
396 | # this will fail because requests adds its own headers | |
397 | with pytest.raises(ConnectionError): | |
398 | requests.get("http://example.com/", headers={"Accept": "text/plain"}) | |
399 | ||
400 | # a prepared request where you overwrite the headers before sending will work | |
401 | session = requests.Session() | |
402 | prepped = session.prepare_request( | |
403 | requests.Request( | |
404 | method="GET", | |
405 | url="http://example.com/", | |
406 | ) | |
407 | ) | |
408 | prepped.headers = {"Accept": "text/plain"} | |
409 | ||
410 | resp = session.send(prepped) | |
411 | assert resp.text == "hello world" | |
412 | ||
413 | Dynamic Responses | |
414 | ----------------- | |
415 | ||
416 | You can utilize callbacks to provide dynamic responses. The callback must return | |
417 | a tuple of (``status``, ``headers``, ``body``). | |
418 | ||
419 | .. code-block:: python | |
420 | ||
421 | import json | |
422 | ||
423 | import responses | |
424 | import requests | |
425 | ||
426 | @responses.activate | |
427 | def test_calc_api(): | |
428 | ||
429 | def request_callback(request): | |
430 | payload = json.loads(request.body) | |
431 | resp_body = {'value': sum(payload['numbers'])} | |
432 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
433 | return (200, headers, json.dumps(resp_body)) | |
434 | ||
435 | responses.add_callback( | |
436 | responses.POST, 'http://calc.com/sum', | |
437 | callback=request_callback, | |
438 | content_type='application/json', | |
439 | ) | |
440 | ||
441 | resp = requests.post( | |
442 | 'http://calc.com/sum', | |
443 | json.dumps({'numbers': [1, 2, 3]}), | |
444 | headers={'content-type': 'application/json'}, | |
445 | ) | |
446 | ||
447 | assert resp.json() == {'value': 6} | |
448 | ||
449 | assert len(responses.calls) == 1 | |
450 | assert responses.calls[0].request.url == 'http://calc.com/sum' | |
451 | assert responses.calls[0].response.text == '{"value": 6}' | |
452 | assert ( | |
453 | responses.calls[0].response.headers['request-id'] == | |
454 | '728d329e-0e86-11e4-a748-0c84dc037c13' | |
455 | ) | |
456 | ||
457 | You can also pass a compiled regex to ``add_callback`` to match multiple urls: | |
458 | ||
459 | .. code-block:: python | |
460 | ||
461 | import re, json | |
462 | ||
463 | from functools import reduce | |
464 | ||
465 | import responses | |
466 | import requests | |
467 | ||
468 | operators = { | |
469 | 'sum': lambda x, y: x+y, | |
470 | 'prod': lambda x, y: x*y, | |
471 | 'pow': lambda x, y: x**y | |
472 | } | |
473 | ||
474 | @responses.activate | |
475 | def test_regex_url(): | |
476 | ||
477 | def request_callback(request): | |
478 | payload = json.loads(request.body) | |
479 | operator_name = request.path_url[1:] | |
480 | ||
481 | operator = operators[operator_name] | |
482 | ||
483 | resp_body = {'value': reduce(operator, payload['numbers'])} | |
484 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
485 | return (200, headers, json.dumps(resp_body)) | |
486 | ||
487 | responses.add_callback( | |
488 | responses.POST, | |
489 | re.compile('http://calc.com/(sum|prod|pow|unsupported)'), | |
490 | callback=request_callback, | |
491 | content_type='application/json', | |
492 | ) | |
493 | ||
494 | resp = requests.post( | |
495 | 'http://calc.com/prod', | |
496 | json.dumps({'numbers': [2, 3, 4]}), | |
497 | headers={'content-type': 'application/json'}, | |
498 | ) | |
499 | assert resp.json() == {'value': 24} | |
500 | ||
501 | test_regex_url() | |
502 | ||
503 | ||
504 | If you want to pass extra keyword arguments to the callback function, for example when reusing | |
505 | a callback function to give a slightly different result, you can use ``functools.partial``: | |
506 | ||
507 | .. code-block:: python | |
508 | ||
509 | from functools import partial | |
510 | ||
511 | ... | |
512 | ||
513 | def request_callback(request, id=None): | |
514 | payload = json.loads(request.body) | |
515 | resp_body = {'value': sum(payload['numbers'])} | |
516 | headers = {'request-id': id} | |
517 | return (200, headers, json.dumps(resp_body)) | |
518 | ||
519 | responses.add_callback( | |
520 | responses.POST, 'http://calc.com/sum', | |
521 | callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'), | |
522 | content_type='application/json', | |
523 | ) | |
524 | ||
525 | ||
526 | You can see params passed in the original ``request`` in ``responses.calls[].request.params``: | |
527 | ||
528 | .. code-block:: python | |
529 | ||
530 | import responses | |
531 | import requests | |
532 | ||
533 | @responses.activate | |
534 | def test_request_params(): | |
535 | responses.add( | |
536 | method=responses.GET, | |
537 | url="http://example.com?hello=world", | |
538 | body="test", | |
539 | match_querystring=False, | |
540 | ) | |
541 | ||
542 | resp = requests.get('http://example.com', params={"hello": "world"}) | |
543 | assert responses.calls[0].request.params == {"hello": "world"} | |
544 | ||
545 | Responses as a context manager | |
546 | ------------------------------ | |
547 | ||
548 | .. code-block:: python | |
549 | ||
550 | import responses | |
551 | import requests | |
552 | ||
553 | def test_my_api(): | |
554 | with responses.RequestsMock() as rsps: | |
555 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
556 | body='{}', status=200, | |
557 | content_type='application/json') | |
558 | resp = requests.get('http://twitter.com/api/1/foobar') | |
559 | ||
560 | assert resp.status_code == 200 | |
561 | ||
562 | # outside the context manager requests will hit the remote server | |
563 | resp = requests.get('http://twitter.com/api/1/foobar') | |
564 | resp.status_code == 404 | |
565 | ||
566 | Responses as a pytest fixture | |
567 | ----------------------------- | |
568 | ||
569 | .. code-block:: python | |
570 | ||
571 | @pytest.fixture | |
572 | def mocked_responses(): | |
573 | with responses.RequestsMock() as rsps: | |
574 | yield rsps | |
575 | ||
576 | def test_api(mocked_responses): | |
577 | mocked_responses.add( | |
578 | responses.GET, 'http://twitter.com/api/1/foobar', | |
579 | body='{}', status=200, | |
580 | content_type='application/json') | |
581 | resp = requests.get('http://twitter.com/api/1/foobar') | |
582 | assert resp.status_code == 200 | |
583 | ||
584 | Responses inside a unittest setUp() | |
585 | ----------------------------------- | |
586 | ||
587 | When run with unittest tests, this can be used to set up some | |
588 | generic class-level responses, that may be complemented by each test | |
589 | ||
590 | .. code-block:: python | |
591 | ||
592 | def setUp(): | |
593 | self.responses = responses.RequestsMock() | |
594 | self.responses.start() | |
595 | ||
596 | # self.responses.add(...) | |
597 | ||
598 | self.addCleanup(self.responses.stop) | |
599 | self.addCleanup(self.responses.reset) | |
600 | ||
601 | def test_api(self): | |
602 | self.responses.add( | |
603 | responses.GET, 'http://twitter.com/api/1/foobar', | |
604 | body='{}', status=200, | |
605 | content_type='application/json') | |
606 | resp = requests.get('http://twitter.com/api/1/foobar') | |
607 | assert resp.status_code == 200 | |
608 | ||
609 | Assertions on declared responses | |
610 | -------------------------------- | |
611 | ||
612 | When used as a context manager, Responses will, by default, raise an assertion | |
613 | error if a url was registered but not accessed. This can be disabled by passing | |
614 | the ``assert_all_requests_are_fired`` value: | |
615 | ||
616 | .. code-block:: python | |
617 | ||
618 | import responses | |
619 | import requests | |
620 | ||
621 | def test_my_api(): | |
622 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
623 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
624 | body='{}', status=200, | |
625 | content_type='application/json') | |
626 | ||
627 | assert_call_count | |
628 | ----------------- | |
629 | ||
630 | Assert that the request was called exactly n times. | |
631 | ||
632 | .. code-block:: python | |
633 | ||
634 | import responses | |
635 | import requests | |
636 | ||
637 | @responses.activate | |
638 | def test_assert_call_count(): | |
639 | responses.add(responses.GET, "http://example.com") | |
640 | ||
641 | requests.get("http://example.com") | |
642 | assert responses.assert_call_count("http://example.com", 1) is True | |
643 | ||
644 | requests.get("http://example.com") | |
645 | with pytest.raises(AssertionError) as excinfo: | |
646 | responses.assert_call_count("http://example.com", 1) | |
647 | assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) | |
648 | ||
649 | ||
650 | Multiple Responses | |
651 | ------------------ | |
652 | ||
653 | You can also add multiple responses for the same url: | |
654 | ||
655 | .. code-block:: python | |
656 | ||
657 | import responses | |
658 | import requests | |
659 | ||
660 | @responses.activate | |
661 | def test_my_api(): | |
662 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) | |
663 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
664 | body='{}', status=200, | |
665 | content_type='application/json') | |
666 | ||
667 | resp = requests.get('http://twitter.com/api/1/foobar') | |
668 | assert resp.status_code == 500 | |
669 | resp = requests.get('http://twitter.com/api/1/foobar') | |
670 | assert resp.status_code == 200 | |
671 | ||
672 | ||
673 | Using a callback to modify the response | |
674 | --------------------------------------- | |
675 | ||
676 | If you use customized processing in `requests` via subclassing/mixins, or if you | |
677 | have library tools that interact with `requests` at a low level, you may need | |
678 | to add extended processing to the mocked Response object to fully simulate the | |
679 | environment for your tests. A `response_callback` can be used, which will be | |
680 | wrapped by the library before being returned to the caller. The callback | |
681 | accepts a `response` as it's single argument, and is expected to return a | |
682 | single `response` object. | |
683 | ||
684 | .. code-block:: python | |
685 | ||
686 | import responses | |
687 | import requests | |
688 | ||
689 | def response_callback(resp): | |
690 | resp.callback_processed = True | |
691 | return resp | |
692 | ||
693 | with responses.RequestsMock(response_callback=response_callback) as m: | |
694 | m.add(responses.GET, 'http://example.com', body=b'test') | |
695 | resp = requests.get('http://example.com') | |
696 | assert resp.text == "test" | |
697 | assert hasattr(resp, 'callback_processed') | |
698 | assert resp.callback_processed is True | |
699 | ||
700 | ||
701 | Passing through real requests | |
702 | ----------------------------- | |
703 | ||
704 | In some cases you may wish to allow for certain requests to pass through responses | |
705 | and hit a real server. This can be done with the ``add_passthru`` methods: | |
706 | ||
707 | .. code-block:: python | |
708 | ||
709 | import responses | |
710 | ||
711 | @responses.activate | |
712 | def test_my_api(): | |
713 | responses.add_passthru('https://percy.io') | |
714 | ||
715 | This will allow any requests matching that prefix, that is otherwise not | |
716 | registered as a mock response, to passthru using the standard behavior. | |
717 | ||
718 | Pass through endpoints can be configured with regex patterns if you | |
719 | need to allow an entire domain or path subtree to send requests: | |
720 | ||
721 | .. code-block:: python | |
722 | ||
723 | responses.add_passthru(re.compile('https://percy.io/\\w+')) | |
724 | ||
725 | ||
726 | Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or | |
727 | use ``PassthroughResponse`` to enable a response to behave as a pass through. | |
728 | ||
729 | .. code-block:: python | |
730 | ||
731 | # Enable passthrough for a single response | |
732 | response = Response(responses.GET, 'http://example.com', body='not used') | |
733 | response.passthrough = True | |
734 | responses.add(response) | |
735 | ||
736 | # Use PassthroughResponse | |
737 | response = PassthroughResponse(responses.GET, 'http://example.com') | |
738 | responses.add(response) | |
739 | ||
740 | Viewing/Modifying registered responses | |
741 | -------------------------------------- | |
742 | ||
743 | Registered responses are available as a public method of the RequestMock | |
744 | instance. It is sometimes useful for debugging purposes to view the stack of | |
745 | registered responses which can be accessed via ``responses.registered()``. | |
746 | ||
747 | The ``replace`` function allows a previously registered ``response`` to be | |
748 | changed. The method signature is identical to ``add``. ``response`` s are | |
749 | identified using ``method`` and ``url``. Only the first matched ``response`` is | |
750 | replaced. | |
751 | ||
752 | .. code-block:: python | |
753 | ||
754 | import responses | |
755 | import requests | |
756 | ||
757 | @responses.activate | |
758 | def test_replace(): | |
759 | ||
760 | responses.add(responses.GET, 'http://example.org', json={'data': 1}) | |
761 | responses.replace(responses.GET, 'http://example.org', json={'data': 2}) | |
762 | ||
763 | resp = requests.get('http://example.org') | |
764 | ||
765 | assert resp.json() == {'data': 2} | |
766 | ||
767 | ||
768 | The ``upsert`` function allows a previously registered ``response`` to be | |
769 | changed like ``replace``. If the response is registered, the ``upsert`` function | |
770 | will registered it like ``add``. | |
771 | ||
772 | ``remove`` takes a ``method`` and ``url`` argument and will remove **all** | |
773 | matched responses from the registered list. | |
774 | ||
775 | Finally, ``reset`` will reset all registered responses. | |
776 | ||
777 | Contributing | |
778 | ------------ | |
779 | ||
780 | Responses uses several linting and autoformatting utilities, so it's important that when | |
781 | submitting patches you use the appropriate toolchain: | |
782 | ||
783 | Clone the repository: | |
784 | ||
785 | .. code-block:: shell | |
786 | ||
787 | git clone https://github.com/getsentry/responses.git | |
788 | ||
789 | Create an environment (e.g. with ``virtualenv``): | |
790 | ||
791 | .. code-block:: shell | |
792 | ||
793 | virtualenv .env && source .env/bin/activate | |
794 | ||
795 | Configure development requirements: | |
796 | ||
797 | .. code-block:: shell | |
798 | ||
799 | make develop | |
800 | ||
801 | Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for | |
802 | testing. You can run all tests by: | |
803 | ||
804 | .. code-block:: shell | |
805 | ||
806 | pytest | |
807 | ||
808 | And run a single test by: | |
809 | ||
810 | .. code-block:: shell | |
811 | ||
812 | pytest -k '<test_function_name>' | |
813 | ||
814 | To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter: | |
815 | ||
816 | .. code-block:: shell | |
817 | ||
818 | mypy --config-file=./mypy.ini -p responses | |
819 | ||
820 | To check code style and reformat it run: | |
821 | ||
822 | .. code-block:: shell | |
823 | ||
824 | pre-commit run --all-files | |
825 | ||
826 | Note: on some OS, you have to use ``pre_commit`` | |
827 | ||
828 |
3 | 3 | .. image:: https://img.shields.io/pypi/v/responses.svg |
4 | 4 | :target: https://pypi.python.org/pypi/responses/ |
5 | 5 | |
6 | .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master | |
7 | :target: https://travis-ci.org/getsentry/responses | |
8 | ||
9 | 6 | .. image:: https://img.shields.io/pypi/pyversions/responses.svg |
10 | 7 | :target: https://pypi.org/project/responses/ |
8 | ||
9 | .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg | |
10 | :target: https://codecov.io/gh/getsentry/responses/ | |
11 | 11 | |
12 | 12 | A utility library for mocking out the ``requests`` Python library. |
13 | 13 | |
122 | 122 | Response headers. |
123 | 123 | |
124 | 124 | stream (``bool``) |
125 | Disabled by default. Indicates the response should use the streaming API. | |
125 | DEPRECATED: use ``stream`` argument in request directly | |
126 | ||
127 | auto_calculate_content_length (``bool``) | |
128 | Disabled by default. Automatically calculates the length of a supplied string or JSON body. | |
126 | 129 | |
127 | 130 | match (``list``) |
128 | A list of callbacks to match requests based on request body contents. | |
129 | ||
130 | ||
131 | Matching Request Parameters | |
132 | --------------------------- | |
131 | A list of callbacks to match requests based on request attributes. | |
132 | Current module provides multiple matchers that you can use to match: | |
133 | ||
134 | * body contents in JSON format | |
135 | * body contents in URL encoded data format | |
136 | * request query parameters | |
137 | * request query string (similar to query parameters but takes string as input) | |
138 | * kwargs provided to request e.g. ``stream``, ``verify`` | |
139 | * 'multipart/form-data' content and headers in request | |
140 | * request headers | |
141 | * request fragment identifier | |
142 | ||
143 | Alternatively user can create custom matcher. | |
144 | Read more `Matching Requests`_ | |
145 | ||
146 | ||
147 | Matching Requests | |
148 | ----------------- | |
133 | 149 | |
134 | 150 | When adding responses for endpoints that are sent request data you can add |
135 | 151 | matchers to ensure your code is sending the right parameters and provide |
136 | 152 | different responses based on the request body contents. Responses provides |
137 | matchers for JSON and URLencoded request bodies and you can supply your own for | |
153 | matchers for JSON and URL-encoded request bodies and you can supply your own for | |
138 | 154 | other formats. |
139 | 155 | |
140 | 156 | .. code-block:: python |
141 | 157 | |
142 | 158 | import responses |
143 | 159 | import requests |
160 | from responses import matchers | |
144 | 161 | |
145 | 162 | @responses.activate |
146 | 163 | def test_calc_api(): |
149 | 166 | url='http://calc.com/sum', |
150 | 167 | body="4", |
151 | 168 | match=[ |
152 | responses.urlencoded_params_matcher({"left": "1", "right": "3"}) | |
169 | matchers.urlencoded_params_matcher({"left": "1", "right": "3"}) | |
153 | 170 | ] |
154 | 171 | ) |
155 | 172 | requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) |
156 | 173 | |
157 | Matching JSON encoded data can be done with ``responses.json_params_matcher()``. | |
174 | Matching JSON encoded data can be done with ``matchers.json_params_matcher()``. | |
158 | 175 | If your application uses other encodings you can build your own matcher that |
159 | 176 | returns ``True`` or ``False`` if the request parameters match. Your matcher can |
160 | expect a ``request_body`` parameter to be provided by responses. | |
177 | expect a ``request`` parameter to be provided by responses. | |
178 | ||
179 | Similarly, you can use the ``matchers.query_param_matcher`` function to match | |
180 | against the ``params`` request parameter. | |
181 | Note, you must set ``match_querystring=False`` | |
182 | ||
183 | .. code-block:: python | |
184 | ||
185 | import responses | |
186 | import requests | |
187 | from responses import matchers | |
188 | ||
189 | @responses.activate | |
190 | def test_calc_api(): | |
191 | url = "http://example.com/test" | |
192 | params = {"hello": "world", "I am": "a big test"} | |
193 | responses.add( | |
194 | method=responses.GET, | |
195 | url=url, | |
196 | body="test", | |
197 | match=[matchers.query_param_matcher(params)], | |
198 | match_querystring=False, | |
199 | ) | |
200 | ||
201 | resp = requests.get(url, params=params) | |
202 | ||
203 | constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" | |
204 | assert resp.url == constructed_url | |
205 | assert resp.request.url == constructed_url | |
206 | assert resp.request.params == params | |
207 | ||
208 | ||
209 | As alternative, you can use query string value in ``matchers.query_string_matcher`` | |
210 | ||
211 | .. code-block:: python | |
212 | ||
213 | import requests | |
214 | import responses | |
215 | from responses import matchers | |
216 | ||
217 | @responses.activate | |
218 | def my_func(): | |
219 | responses.add( | |
220 | responses.GET, | |
221 | "https://httpbin.org/get", | |
222 | match=[matchers.query_string_matcher("didi=pro&test=1")], | |
223 | ) | |
224 | resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"}) | |
225 | ||
226 | my_func() | |
227 | ||
228 | To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match | |
229 | against the request kwargs. | |
230 | Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated | |
231 | ||
232 | .. code-block:: python | |
233 | ||
234 | import responses | |
235 | import requests | |
236 | from responses import matchers | |
237 | ||
238 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
239 | req_kwargs = { | |
240 | "stream": True, | |
241 | "verify": False, | |
242 | } | |
243 | rsps.add( | |
244 | "GET", | |
245 | "http://111.com", | |
246 | match=[matchers.request_kwargs_matcher(req_kwargs)], | |
247 | ) | |
248 | ||
249 | requests.get("http://111.com", stream=True) | |
250 | ||
251 | # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False} | |
252 | ||
253 | To validate request body and headers for ``multipart/form-data`` data you can use | |
254 | ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared | |
255 | to the request: | |
256 | ||
257 | .. code-block:: python | |
258 | ||
259 | import requests | |
260 | import responses | |
261 | from responses.matchers import multipart_matcher | |
262 | ||
263 | @responses.activate | |
264 | def my_func(): | |
265 | req_data = {"some": "other", "data": "fields"} | |
266 | req_files = {"file_name": b"Old World!"} | |
267 | responses.add( | |
268 | responses.POST, url="http://httpbin.org/post", | |
269 | match=[multipart_matcher(req_data, req_files)] | |
270 | ) | |
271 | resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"}) | |
272 | ||
273 | my_func() | |
274 | # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs. | |
275 | ||
276 | ||
277 | To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``. | |
278 | The matcher takes fragment string (everything after ``#`` sign) as input for comparison: | |
279 | ||
280 | .. code-block:: python | |
281 | ||
282 | import requests | |
283 | import responses | |
284 | from responses.matchers import fragment_identifier_matcher | |
285 | ||
286 | @responses.activate | |
287 | def run(): | |
288 | url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" | |
289 | responses.add( | |
290 | responses.GET, | |
291 | url, | |
292 | match_querystring=True, | |
293 | match=[fragment_identifier_matcher("test=1&foo=bar")], | |
294 | body=b"test", | |
295 | ) | |
296 | ||
297 | # two requests to check reversed order of fragment identifier | |
298 | resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") | |
299 | resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") | |
300 | ||
301 | run() | |
302 | ||
303 | Matching Request Headers | |
304 | ------------------------ | |
305 | ||
306 | When adding responses you can specify matchers to ensure that your code is | |
307 | sending the right headers and provide different responses based on the request | |
308 | headers. | |
309 | ||
310 | .. code-block:: python | |
311 | ||
312 | import responses | |
313 | import requests | |
314 | from responses import matchers | |
315 | ||
316 | ||
317 | @responses.activate | |
318 | def test_content_type(): | |
319 | responses.add( | |
320 | responses.GET, | |
321 | url="http://example.com/", | |
322 | body="hello world", | |
323 | match=[ | |
324 | matchers.header_matcher({"Accept": "text/plain"}) | |
325 | ] | |
326 | ) | |
327 | ||
328 | responses.add( | |
329 | responses.GET, | |
330 | url="http://example.com/", | |
331 | json={"content": "hello world"}, | |
332 | match=[ | |
333 | matchers.header_matcher({"Accept": "application/json"}) | |
334 | ] | |
335 | ) | |
336 | ||
337 | # request in reverse order to how they were added! | |
338 | resp = requests.get("http://example.com/", headers={"Accept": "application/json"}) | |
339 | assert resp.json() == {"content": "hello world"} | |
340 | ||
341 | resp = requests.get("http://example.com/", headers={"Accept": "text/plain"}) | |
342 | assert resp.text == "hello world" | |
343 | ||
344 | Because ``requests`` will send several standard headers in addition to what was | |
345 | specified by your code, request headers that are additional to the ones | |
346 | passed to the matcher are ignored by default. You can change this behaviour by | |
347 | passing ``strict_match=True`` to the matcher to ensure that only the headers | |
348 | that you're expecting are sent and no others. Note that you will probably have | |
349 | to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't | |
350 | include any additional headers. | |
351 | ||
352 | .. code-block:: python | |
353 | ||
354 | import responses | |
355 | import requests | |
356 | from responses import matchers | |
357 | ||
358 | @responses.activate | |
359 | def test_content_type(): | |
360 | responses.add( | |
361 | responses.GET, | |
362 | url="http://example.com/", | |
363 | body="hello world", | |
364 | match=[ | |
365 | matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) | |
366 | ] | |
367 | ) | |
368 | ||
369 | # this will fail because requests adds its own headers | |
370 | with pytest.raises(ConnectionError): | |
371 | requests.get("http://example.com/", headers={"Accept": "text/plain"}) | |
372 | ||
373 | # a prepared request where you overwrite the headers before sending will work | |
374 | session = requests.Session() | |
375 | prepped = session.prepare_request( | |
376 | requests.Request( | |
377 | method="GET", | |
378 | url="http://example.com/", | |
379 | ) | |
380 | ) | |
381 | prepped.headers = {"Accept": "text/plain"} | |
382 | ||
383 | resp = session.send(prepped) | |
384 | assert resp.text == "hello world" | |
161 | 385 | |
162 | 386 | Dynamic Responses |
163 | 387 | ----------------- |
461 | 685 | def test_my_api(): |
462 | 686 | responses.add_passthru('https://percy.io') |
463 | 687 | |
464 | This will allow any requests matching that prefix, that is otherwise not registered | |
465 | as a mock response, to passthru using the standard behavior. | |
466 | ||
467 | Regex can be used like: | |
688 | This will allow any requests matching that prefix, that is otherwise not | |
689 | registered as a mock response, to passthru using the standard behavior. | |
690 | ||
691 | Pass through endpoints can be configured with regex patterns if you | |
692 | need to allow an entire domain or path subtree to send requests: | |
468 | 693 | |
469 | 694 | .. code-block:: python |
470 | 695 | |
471 | 696 | responses.add_passthru(re.compile('https://percy.io/\\w+')) |
472 | 697 | |
698 | ||
699 | Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or | |
700 | use ``PassthroughResponse`` to enable a response to behave as a pass through. | |
701 | ||
702 | .. code-block:: python | |
703 | ||
704 | # Enable passthrough for a single response | |
705 | response = Response(responses.GET, 'http://example.com', body='not used') | |
706 | response.passthrough = True | |
707 | responses.add(response) | |
708 | ||
709 | # Use PassthroughResponse | |
710 | response = PassthroughResponse(responses.GET, 'http://example.com') | |
711 | responses.add(response) | |
473 | 712 | |
474 | 713 | Viewing/Modifying registered responses |
475 | 714 | -------------------------------------- |
544 | 783 | .. code-block:: shell |
545 | 784 | |
546 | 785 | pytest -k '<test_function_name>' |
786 | ||
787 | To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter: | |
788 | ||
789 | .. code-block:: shell | |
790 | ||
791 | mypy --config-file=./mypy.ini -p responses | |
792 | ||
793 | To check code style and reformat it run: | |
794 | ||
795 | .. code-block:: shell | |
796 | ||
797 | pre-commit run --all-files | |
798 | ||
799 | Note: on some OS, you have to use ``pre_commit`` |
0 | responses (0.13.4+git20211111.1.8a5a4b6-1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream snapshot. | |
3 | ||
4 | -- Debian Janitor <janitor@jelmer.uk> Mon, 15 Nov 2021 21:32:27 -0000 | |
5 | ||
0 | 6 | responses (0.13.4-1) unstable; urgency=medium |
1 | 7 | |
2 | 8 | * Team upload. |
12 | 12 | from functools import update_wrapper |
13 | 13 | from requests.adapters import HTTPAdapter |
14 | 14 | from requests.exceptions import ConnectionError |
15 | from requests.sessions import REDIRECT_STATI | |
16 | 15 | from requests.utils import cookiejar_from_dict |
16 | from responses.matchers import json_params_matcher as _json_params_matcher | |
17 | from responses.matchers import urlencoded_params_matcher as _urlencoded_params_matcher | |
18 | from warnings import warn | |
17 | 19 | |
18 | 20 | try: |
19 | 21 | from collections.abc import Sequence, Sized |
22 | 24 | |
23 | 25 | try: |
24 | 26 | from requests.packages.urllib3.response import HTTPResponse |
25 | except ImportError: | |
26 | from urllib3.response import HTTPResponse | |
27 | except ImportError: # pragma: no cover | |
28 | from urllib3.response import HTTPResponse # pragma: no cover | |
27 | 29 | try: |
28 | 30 | from requests.packages.urllib3.connection import HTTPHeaderDict |
29 | except ImportError: | |
30 | from urllib3.response import HTTPHeaderDict | |
31 | except ImportError: # pragma: no cover | |
32 | from urllib3.response import HTTPHeaderDict # pragma: no cover | |
31 | 33 | try: |
32 | 34 | from requests.packages.urllib3.util.url import parse_url |
33 | except ImportError: | |
34 | from urllib3.util.url import parse_url | |
35 | except ImportError: # pragma: no cover | |
36 | from urllib3.util.url import parse_url # pragma: no cover | |
35 | 37 | |
36 | 38 | if six.PY2: |
37 | from urlparse import urlparse, parse_qsl, urlsplit, urlunsplit | |
39 | from urlparse import urlparse, urlunparse, parse_qsl, urlsplit, urlunsplit | |
38 | 40 | from urllib import quote |
39 | 41 | else: |
40 | from urllib.parse import urlparse, parse_qsl, urlsplit, urlunsplit, quote | |
42 | from urllib.parse import ( | |
43 | urlparse, | |
44 | urlunparse, | |
45 | parse_qsl, | |
46 | urlsplit, | |
47 | urlunsplit, | |
48 | quote, | |
49 | ) | |
41 | 50 | |
42 | 51 | if six.PY2: |
43 | 52 | try: |
58 | 67 | # Python 3.7 |
59 | 68 | Pattern = re.Pattern |
60 | 69 | |
61 | try: | |
62 | from json.decoder import JSONDecodeError | |
63 | except ImportError: | |
64 | JSONDecodeError = ValueError | |
65 | ||
66 | 70 | UNSET = object() |
67 | 71 | |
68 | 72 | Call = namedtuple("Call", ["request", "response"]) |
70 | 74 | _real_send = HTTPAdapter.send |
71 | 75 | |
72 | 76 | logger = logging.getLogger("responses") |
77 | ||
78 | ||
79 | def urlencoded_params_matcher(params): | |
80 | warn( | |
81 | "Function is deprecated. Use 'from responses.matchers import urlencoded_params_matcher'", | |
82 | DeprecationWarning, | |
83 | ) | |
84 | return _urlencoded_params_matcher(params) | |
85 | ||
86 | ||
87 | def json_params_matcher(params): | |
88 | warn( | |
89 | "Function is deprecated. Use 'from responses.matchers import json_params_matcher'", | |
90 | DeprecationWarning, | |
91 | ) | |
92 | return _json_params_matcher(params) | |
73 | 93 | |
74 | 94 | |
75 | 95 | def _is_string(s): |
104 | 124 | return "".join(chars) |
105 | 125 | |
106 | 126 | |
107 | def _is_redirect(response): | |
108 | try: | |
109 | # 2.0.0 <= requests <= 2.2 | |
110 | return response.is_redirect | |
111 | ||
112 | except AttributeError: | |
113 | # requests > 2.2 | |
114 | return ( | |
115 | # use request.sessions conditional | |
116 | response.status_code in REDIRECT_STATI | |
117 | and "location" in response.headers | |
118 | ) | |
119 | ||
120 | ||
121 | 127 | def _ensure_str(s): |
122 | 128 | if six.PY2: |
123 | 129 | s = s.encode("utf-8") if isinstance(s, six.text_type) else s |
126 | 132 | |
127 | 133 | def _cookies_from_headers(headers): |
128 | 134 | try: |
129 | import http.cookies as cookies | |
130 | ||
131 | resp_cookie = cookies.SimpleCookie() | |
135 | import http.cookies as _cookies | |
136 | ||
137 | resp_cookie = _cookies.SimpleCookie() | |
132 | 138 | resp_cookie.load(headers["set-cookie"]) |
133 | 139 | |
134 | 140 | cookies_dict = {name: v.value for name, v in resp_cookie.items()} |
135 | except ImportError: | |
141 | except (ImportError, AttributeError): | |
136 | 142 | from cookies import Cookies |
137 | 143 | |
138 | 144 | resp_cookies = Cookies.from_request(_ensure_str(headers["set-cookie"])) |
156 | 162 | |
157 | 163 | # Preserve the argspec for the wrapped function so that testing |
158 | 164 | # tools such as pytest can continue to use their fixture injection. |
159 | if hasattr(func, "__self__"): | |
160 | args = args[1:] # Omit 'self' | |
161 | 165 | func_args = inspect.formatargspec(args, a, kw, None) |
162 | 166 | else: |
163 | 167 | signature = inspect.signature(func) |
227 | 231 | return url |
228 | 232 | |
229 | 233 | |
234 | def _get_url_and_path(url): | |
235 | url_parsed = urlparse(url) | |
236 | url_and_path = urlunparse( | |
237 | [url_parsed.scheme, url_parsed.netloc, url_parsed.path, None, None, None] | |
238 | ) | |
239 | return parse_url(url_and_path).url | |
240 | ||
241 | ||
230 | 242 | def _handle_body(body): |
231 | 243 | if isinstance(body, six.text_type): |
232 | 244 | body = body.encode("utf-8") |
239 | 251 | _unspecified = object() |
240 | 252 | |
241 | 253 | |
242 | def urlencoded_params_matcher(params): | |
243 | def match(request_body): | |
244 | return ( | |
245 | params is None | |
246 | if request_body is None | |
247 | else sorted(params.items()) == sorted(parse_qsl(request_body)) | |
248 | ) | |
249 | ||
250 | return match | |
251 | ||
252 | ||
253 | def json_params_matcher(params): | |
254 | def match(request_body): | |
255 | try: | |
256 | if isinstance(request_body, bytes): | |
257 | request_body = request_body.decode("utf-8") | |
258 | return ( | |
259 | params is None | |
260 | if request_body is None | |
261 | else params == json_module.loads(request_body) | |
262 | ) | |
263 | except JSONDecodeError: | |
264 | return False | |
265 | ||
266 | return match | |
267 | ||
268 | ||
269 | 254 | class BaseResponse(object): |
255 | passthrough = False | |
270 | 256 | content_type = None |
271 | 257 | headers = None |
272 | 258 | |
273 | 259 | stream = False |
274 | 260 | |
275 | def __init__(self, method, url, match_querystring=_unspecified, match=[]): | |
261 | def __init__(self, method, url, match_querystring=_unspecified, match=()): | |
276 | 262 | self.method = method |
277 | 263 | # ensure the url has a default path set if the url is a string |
278 | 264 | self.url = _ensure_url_default_path(url) |
330 | 316 | if match_querystring: |
331 | 317 | normalize_url = parse_url(url).url |
332 | 318 | return self._url_matches_strict(normalize_url, other) |
333 | ||
334 | 319 | else: |
335 | url_without_qs = url.split("?", 1)[0] | |
336 | other_without_qs = other.split("?", 1)[0] | |
337 | normalized_url_without_qs = parse_url(url_without_qs).url | |
338 | ||
339 | return normalized_url_without_qs == other_without_qs | |
320 | return _get_url_and_path(url) == _get_url_and_path(other) | |
340 | 321 | |
341 | 322 | elif isinstance(url, Pattern) and url.match(other): |
342 | 323 | return True |
344 | 325 | else: |
345 | 326 | return False |
346 | 327 | |
347 | def _body_matches(self, match, request_body): | |
328 | @staticmethod | |
329 | def _req_attr_matches(match, request): | |
348 | 330 | for matcher in match: |
349 | if not matcher(request_body): | |
350 | return False | |
351 | ||
352 | return True | |
331 | valid, reason = matcher(request) | |
332 | if not valid: | |
333 | return False, reason | |
334 | ||
335 | return True, "" | |
353 | 336 | |
354 | 337 | def get_headers(self): |
355 | 338 | headers = HTTPHeaderDict() # Duplicate headers are legal |
369 | 352 | if not self._url_matches(self.url, request.url, self.match_querystring): |
370 | 353 | return False, "URL does not match" |
371 | 354 | |
372 | if not self._body_matches(self.match, request.body): | |
373 | return False, "Parameters do not match" | |
355 | valid, reason = self._req_attr_matches(self.match, request) | |
356 | if not valid: | |
357 | return False, reason | |
374 | 358 | |
375 | 359 | return True, "" |
376 | 360 | |
384 | 368 | json=None, |
385 | 369 | status=200, |
386 | 370 | headers=None, |
387 | stream=False, | |
371 | stream=None, | |
388 | 372 | content_type=UNSET, |
373 | auto_calculate_content_length=False, | |
389 | 374 | **kwargs |
390 | 375 | ): |
391 | 376 | # if we were passed a `json` argument, |
405 | 390 | self.body = body |
406 | 391 | self.status = status |
407 | 392 | self.headers = headers |
393 | ||
394 | if stream is not None: | |
395 | warn( | |
396 | "stream argument is deprecated. Use stream parameter in request directly", | |
397 | DeprecationWarning, | |
398 | ) | |
399 | ||
408 | 400 | self.stream = stream |
409 | 401 | self.content_type = content_type |
402 | self.auto_calculate_content_length = auto_calculate_content_length | |
410 | 403 | super(Response, self).__init__(method, url, **kwargs) |
411 | 404 | |
412 | 405 | def get_response(self, request): |
416 | 409 | headers = self.get_headers() |
417 | 410 | status = self.status |
418 | 411 | body = _handle_body(self.body) |
412 | ||
413 | if ( | |
414 | self.auto_calculate_content_length | |
415 | and isinstance(body, BufferIO) | |
416 | and "Content-Length" not in headers | |
417 | ): | |
418 | content_length = len(body.getvalue()) | |
419 | headers["Content-Length"] = str(content_length) | |
420 | ||
419 | 421 | return HTTPResponse( |
420 | 422 | status=status, |
421 | 423 | reason=six.moves.http_client.responses.get(status), |
439 | 441 | |
440 | 442 | class CallbackResponse(BaseResponse): |
441 | 443 | def __init__( |
442 | self, method, url, callback, stream=False, content_type="text/plain", **kwargs | |
444 | self, method, url, callback, stream=None, content_type="text/plain", **kwargs | |
443 | 445 | ): |
444 | 446 | self.callback = callback |
447 | ||
448 | if stream is not None: | |
449 | warn( | |
450 | "stream argument is deprecated. Use stream parameter in request directly", | |
451 | DeprecationWarning, | |
452 | ) | |
445 | 453 | self.stream = stream |
446 | 454 | self.content_type = content_type |
447 | 455 | super(CallbackResponse, self).__init__(method, url, **kwargs) |
483 | 491 | ) |
484 | 492 | |
485 | 493 | |
494 | class PassthroughResponse(BaseResponse): | |
495 | passthrough = True | |
496 | ||
497 | ||
486 | 498 | class OriginalResponseShim(object): |
487 | 499 | """ |
488 | 500 | Shim for compatibility with older versions of urllib3 |
501 | 513 | |
502 | 514 | def isclosed(self): |
503 | 515 | return True |
516 | ||
517 | def close(self): | |
518 | return | |
504 | 519 | |
505 | 520 | |
506 | 521 | class RequestsMock(object): |
654 | 669 | self.add(method_or_response, url, body, *args, **kwargs) |
655 | 670 | |
656 | 671 | def add_callback( |
657 | self, method, url, callback, match_querystring=False, content_type="text/plain" | |
672 | self, | |
673 | method, | |
674 | url, | |
675 | callback, | |
676 | match_querystring=False, | |
677 | content_type="text/plain", | |
678 | match=(), | |
658 | 679 | ): |
659 | 680 | # ensure the url has a default path set if the url is a string |
660 | 681 | # url = _ensure_url_default_path(url, match_querystring) |
666 | 687 | callback=callback, |
667 | 688 | content_type=content_type, |
668 | 689 | match_querystring=match_querystring, |
690 | match=match, | |
669 | 691 | ) |
670 | 692 | ) |
671 | 693 | |
690 | 712 | return get_wrapped(func, self) |
691 | 713 | |
692 | 714 | def _find_match(self, request): |
715 | """ | |
716 | Iterates through all available matches and validates if any of them matches the request | |
717 | ||
718 | :param request: (PreparedRequest), request object | |
719 | :return: | |
720 | (Response) found match. If multiple found, then remove & return the first match. | |
721 | (list) list with reasons why other matches don't match | |
722 | """ | |
693 | 723 | found = None |
694 | 724 | found_match = None |
695 | 725 | match_failed_reasons = [] |
716 | 746 | return params |
717 | 747 | |
718 | 748 | def _on_request(self, adapter, request, **kwargs): |
749 | # add attributes params and req_kwargs to 'request' object for further match comparison | |
750 | # original request object does not have these attributes | |
751 | request.params = self._parse_request_params(request.path_url) | |
752 | request.req_kwargs = kwargs | |
753 | ||
719 | 754 | match, match_failed_reasons = self._find_match(request) |
720 | 755 | resp_callback = self.response_callback |
721 | request.params = self._parse_request_params(request.path_url) | |
722 | 756 | |
723 | 757 | if match is None: |
724 | 758 | if any( |
751 | 785 | response = resp_callback(response) if resp_callback else response |
752 | 786 | raise response |
753 | 787 | |
754 | try: | |
755 | response = adapter.build_response(request, match.get_response(request)) | |
756 | except BaseException as response: | |
757 | match.call_count += 1 | |
758 | self._calls.add(request, response) | |
759 | response = resp_callback(response) if resp_callback else response | |
760 | raise | |
761 | ||
762 | if not match.stream: | |
763 | response.content # NOQA | |
788 | if match.passthrough: | |
789 | logger.info("request.passthrough-response", extra={"url": request.url}) | |
790 | response = _real_send(adapter, request, **kwargs) | |
791 | else: | |
792 | try: | |
793 | response = adapter.build_response(request, match.get_response(request)) | |
794 | except BaseException as response: | |
795 | match.call_count += 1 | |
796 | self._calls.add(request, response) | |
797 | response = resp_callback(response) if resp_callback else response | |
798 | raise | |
799 | ||
800 | stream = kwargs.get("stream") | |
801 | if not stream: | |
802 | response.content # NOQA required to ensure that response body is read. | |
803 | response.close() | |
764 | 804 | |
765 | 805 | response = resp_callback(response) if resp_callback else response |
766 | 806 | match.call_count += 1 |
0 | from collections import Sequence, Sized | |
1 | from typing import ( | |
2 | Any, | |
3 | Callable, | |
4 | Iterator, | |
5 | Mapping, | |
6 | Optional, | |
7 | NamedTuple, | |
8 | Protocol, | |
9 | TypeVar, | |
10 | Dict, | |
11 | List, | |
12 | Tuple, | |
13 | Union, | |
14 | Iterable | |
15 | ) | |
16 | from io import BufferedReader, BytesIO | |
17 | from re import Pattern | |
18 | from requests.adapters import HTTPResponse, PreparedRequest | |
19 | from requests.cookies import RequestsCookieJar | |
20 | from typing_extensions import Literal | |
21 | from unittest import mock as std_mock | |
22 | from urllib.parse import quote as quote | |
23 | from urllib3.response import HTTPHeaderDict | |
24 | from .matchers import urlencoded_params_matcher, json_params_matcher | |
25 | ||
26 | ||
27 | def _clean_unicode(url: str) -> str: ... | |
28 | def _cookies_from_headers(headers: Dict[str, str]) -> RequestsCookieJar: ... | |
29 | def _ensure_str(s: str) -> str: ... | |
30 | def _ensure_url_default_path( | |
31 | url: Union[Pattern[str], str] | |
32 | ) -> Union[Pattern[str], str]: ... | |
33 | def _get_url_and_path(url: str) -> str: ... | |
34 | def _handle_body( | |
35 | body: Optional[Union[bytes, BufferedReader, str]] | |
36 | ) -> Union[BufferedReader, BytesIO]: ... | |
37 | def _has_unicode(s: str) -> bool: ... | |
38 | def _is_string(s: Union[Pattern[str], str]) -> bool: ... | |
39 | def get_wrapped( | |
40 | func: Callable[..., Any], responses: RequestsMock | |
41 | ) -> Callable[..., Any]: ... | |
42 | ||
43 | ||
44 | class Call(NamedTuple): | |
45 | request: PreparedRequest | |
46 | response: Any | |
47 | ||
48 | _Body = Union[str, BaseException, "Response", BufferedReader, bytes] | |
49 | ||
50 | MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]] | |
51 | ||
52 | class CallList(Sequence[Call], Sized): | |
53 | def __init__(self) -> None: ... | |
54 | def __iter__(self) -> Iterator[Call]: ... | |
55 | def __len__(self) -> int: ... | |
56 | def __getitem__(self, idx: int) -> Call: ... # type: ignore [override] | |
57 | def add(self, request: PreparedRequest, response: _Body) -> None: ... | |
58 | def reset(self) -> None: ... | |
59 | ||
60 | class BaseResponse: | |
61 | passthrough: bool = ... | |
62 | content_type: Optional[str] = ... | |
63 | headers: Optional[Mapping[str, str]] = ... | |
64 | stream: bool = ... | |
65 | method: Any = ... | |
66 | url: Any = ... | |
67 | match_querystring: Any = ... | |
68 | match: MatcherIterable = ... | |
69 | call_count: int = ... | |
70 | def __init__( | |
71 | self, | |
72 | method: str, | |
73 | url: Union[Pattern[str], str], | |
74 | match_querystring: Union[bool, object] = ..., | |
75 | match: MatcherIterable = ..., | |
76 | ) -> None: ... | |
77 | def __eq__(self, other: Any) -> bool: ... | |
78 | def __ne__(self, other: Any) -> bool: ... | |
79 | def _req_attr_matches( | |
80 | self, match: MatcherIterable, request: Optional[Union[bytes, str]] | |
81 | ) -> Tuple[bool, str]: ... | |
82 | def _should_match_querystring( | |
83 | self, match_querystring_argument: Union[bool, object] | |
84 | ) -> bool: ... | |
85 | def _url_matches( | |
86 | self, url: Union[Pattern[str], str], other: str, match_querystring: bool = ... | |
87 | ) -> bool: ... | |
88 | def _url_matches_strict(self, url: str, other: str) -> bool: ... | |
89 | def get_headers(self) -> HTTPHeaderDict: ... # type: ignore [no-any-unimported] | |
90 | def get_response(self, request: PreparedRequest) -> None: ... | |
91 | def matches(self, request: PreparedRequest) -> Tuple[bool, str]: ... | |
92 | ||
93 | class Response(BaseResponse): | |
94 | body: _Body = ... | |
95 | status: int = ... | |
96 | headers: Optional[Mapping[str, str]] = ... | |
97 | stream: bool = ... | |
98 | content_type: Optional[str] = ... | |
99 | auto_calculate_content_length: bool = ... | |
100 | def __init__( | |
101 | self, | |
102 | method: str, | |
103 | url: Union[Pattern[str], str], | |
104 | body: _Body = ..., | |
105 | json: Optional[Any] = ..., | |
106 | status: int = ..., | |
107 | headers: Optional[Mapping[str, str]] = ..., | |
108 | stream: bool = ..., | |
109 | content_type: Optional[str] = ..., | |
110 | auto_calculate_content_length: bool = ..., | |
111 | match_querystring: bool = ..., | |
112 | match: MatcherIterable = ..., | |
113 | ) -> None: ... | |
114 | def get_response( # type: ignore [override] | |
115 | self, request: PreparedRequest | |
116 | ) -> HTTPResponse: ... | |
117 | ||
118 | class CallbackResponse(BaseResponse): | |
119 | callback: Callable[[Any], Any] = ... | |
120 | stream: bool = ... | |
121 | content_type: Optional[str] = ... | |
122 | def __init__( | |
123 | self, | |
124 | method: str, | |
125 | url: Union[Pattern[str], str], | |
126 | callback: Callable[[Any], Any], | |
127 | stream: bool = ..., | |
128 | content_type: Optional[str] = ..., | |
129 | match_querystring: bool = ..., | |
130 | match: MatcherIterable = ..., | |
131 | ) -> None: ... | |
132 | def get_response( # type: ignore [override] | |
133 | self, request: PreparedRequest | |
134 | ) -> HTTPResponse: ... | |
135 | ||
136 | class PassthroughResponse(BaseResponse): | |
137 | passthrough: bool = ... | |
138 | ||
139 | class OriginalResponseShim: | |
140 | msg: Any = ... | |
141 | def __init__( # type: ignore [no-any-unimported] | |
142 | self, headers: HTTPHeaderDict | |
143 | ) -> None: ... | |
144 | def isclosed(self) -> bool: ... | |
145 | ||
146 | class RequestsMock: | |
147 | DELETE: Literal["DELETE"] | |
148 | GET: Literal["GET"] | |
149 | HEAD: Literal["HEAD"] | |
150 | OPTIONS: Literal["OPTIONS"] | |
151 | PATCH: Literal["PATCH"] | |
152 | POST: Literal["POST"] | |
153 | PUT: Literal["PUT"] | |
154 | response_callback: Optional[Callable[[Any], Any]] = ... | |
155 | assert_all_requests_are_fired: Any = ... | |
156 | passthru_prefixes: Tuple[str, ...] = ... | |
157 | target: Any = ... | |
158 | _matches: List[Any] | |
159 | def __init__( | |
160 | self, | |
161 | assert_all_requests_are_fired: bool = ..., | |
162 | response_callback: Optional[Callable[[Any], Any]] = ..., | |
163 | passthru_prefixes: Tuple[str, ...] = ..., | |
164 | target: str = ..., | |
165 | ) -> None: ... | |
166 | def reset(self) -> None: ... | |
167 | add: _Add | |
168 | add_passthru: _AddPassthru | |
169 | def remove( | |
170 | self, | |
171 | method_or_response: Optional[Union[str, Response]] = ..., | |
172 | url: Optional[Union[Pattern[str], str]] = ..., | |
173 | ) -> None: ... | |
174 | replace: _Replace | |
175 | add_callback: _AddCallback | |
176 | @property | |
177 | def calls(self) -> CallList: ... | |
178 | def __enter__(self) -> RequestsMock: ... | |
179 | def __exit__(self, type: Any, value: Any, traceback: Any) -> bool: ... | |
180 | activate: _Activate | |
181 | def start(self) -> None: ... | |
182 | def stop(self, allow_assert: bool = ...) -> None: ... | |
183 | def assert_call_count(self, url: str, count: int) -> bool: ... | |
184 | def registered(self) -> List[Any]: ... | |
185 | ||
186 | _F = TypeVar("_F", bound=Callable[..., Any]) | |
187 | ||
188 | HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]] | |
189 | ||
190 | class _Activate(Protocol): | |
191 | def __call__(self, func: _F) -> _F: ... | |
192 | ||
193 | class _Add(Protocol): | |
194 | def __call__( | |
195 | self, | |
196 | method: Optional[Union[str, BaseResponse]] = ..., | |
197 | url: Optional[Union[Pattern[str], str]] = ..., | |
198 | body: _Body = ..., | |
199 | json: Optional[Any] = ..., | |
200 | status: int = ..., | |
201 | headers: HeaderSet = ..., | |
202 | stream: bool = ..., | |
203 | content_type: Optional[str] = ..., | |
204 | auto_calculate_content_length: bool = ..., | |
205 | adding_headers: HeaderSet = ..., | |
206 | match_querystring: bool = ..., | |
207 | match: MatcherIterable = ..., | |
208 | ) -> None: ... | |
209 | ||
210 | class _AddCallback(Protocol): | |
211 | def __call__( | |
212 | self, | |
213 | method: str, | |
214 | url: Union[Pattern[str], str], | |
215 | callback: Callable[[PreparedRequest], Union[Exception, Tuple[int, Mapping[str, str], _Body]]], | |
216 | match_querystring: bool = ..., | |
217 | content_type: Optional[str] = ..., | |
218 | match: MatcherIterable = ..., | |
219 | ) -> None: ... | |
220 | ||
221 | class _AddPassthru(Protocol): | |
222 | def __call__( | |
223 | self, prefix: Union[Pattern[str], str] | |
224 | ) -> None: ... | |
225 | ||
226 | class _Remove(Protocol): | |
227 | def __call__( | |
228 | self, | |
229 | method_or_response: Optional[Union[str, BaseResponse]] = ..., | |
230 | url: Optional[Union[Pattern[str], str]] = ..., | |
231 | ) -> None: ... | |
232 | ||
233 | class _Replace(Protocol): | |
234 | def __call__( | |
235 | self, | |
236 | method_or_response: Optional[Union[str, BaseResponse]] = ..., | |
237 | url: Optional[Union[Pattern[str], str]] = ..., | |
238 | body: _Body = ..., | |
239 | json: Optional[Any] = ..., | |
240 | status: int = ..., | |
241 | headers: HeaderSet = ..., | |
242 | stream: bool = ..., | |
243 | content_type: Optional[str] = ..., | |
244 | adding_headers: HeaderSet = ..., | |
245 | match_querystring: bool = ..., | |
246 | match: MatcherIterable = ..., | |
247 | ) -> None: ... | |
248 | ||
249 | class _Upsert(Protocol): | |
250 | def __call__( | |
251 | self, | |
252 | method: Optional[Union[str, BaseResponse]] = ..., | |
253 | url: Optional[Union[Pattern[str], str]] = ..., | |
254 | body: _Body = ..., | |
255 | json: Optional[Any] = ..., | |
256 | status: int = ..., | |
257 | headers: HeaderSet = ..., | |
258 | stream: bool = ..., | |
259 | content_type: Optional[str] = ..., | |
260 | adding_headers: HeaderSet = ..., | |
261 | match_querystring: bool = ..., | |
262 | match: MatcherIterable = ..., | |
263 | ) -> None: ... | |
264 | ||
265 | class _Registered(Protocol): | |
266 | def __call__(self) -> List[Response]: ... | |
267 | ||
268 | ||
269 | activate: _Activate | |
270 | add: _Add | |
271 | add_callback: _AddCallback | |
272 | add_passthru: _AddPassthru | |
273 | assert_all_requests_are_fired: bool | |
274 | assert_call_count: Callable[[str, int], bool] | |
275 | calls: CallList | |
276 | DELETE: Literal["DELETE"] | |
277 | GET: Literal["GET"] | |
278 | HEAD: Literal["HEAD"] | |
279 | mock: RequestsMock | |
280 | _default_mock: RequestsMock | |
281 | OPTIONS: Literal["OPTIONS"] | |
282 | passthru_prefixes: Tuple[str, ...] | |
283 | PATCH: Literal["PATCH"] | |
284 | POST: Literal["POST"] | |
285 | PUT: Literal["PUT"] | |
286 | registered: _Registered | |
287 | remove: _Remove | |
288 | replace: _Replace | |
289 | reset: Callable[[], None] | |
290 | response_callback: Callable[[Any], Any] | |
291 | start: Callable[[], None] | |
292 | stop: Callable[..., None] | |
293 | target: Any | |
294 | upsert: _Upsert | |
295 | ||
296 | __all__ = [ | |
297 | "CallbackResponse", | |
298 | "Response", | |
299 | "RequestsMock", | |
300 | # Exposed by the RequestsMock class: | |
301 | "activate", | |
302 | "add", | |
303 | "add_callback", | |
304 | "add_passthru", | |
305 | "assert_all_requests_are_fired", | |
306 | "assert_call_count", | |
307 | "calls", | |
308 | "DELETE", | |
309 | "GET", | |
310 | "HEAD", | |
311 | "OPTIONS", | |
312 | "passthru_prefixes", | |
313 | "PATCH", | |
314 | "POST", | |
315 | "PUT", | |
316 | "registered", | |
317 | "remove", | |
318 | "replace", | |
319 | "reset", | |
320 | "response_callback", | |
321 | "start", | |
322 | "stop", | |
323 | "target", | |
324 | "upsert" | |
325 | ] |
0 | import six | |
1 | import json as json_module | |
2 | ||
3 | from requests import PreparedRequest | |
4 | ||
5 | if six.PY2: | |
6 | from urlparse import parse_qsl, urlparse | |
7 | else: | |
8 | from urllib.parse import parse_qsl, urlparse | |
9 | ||
10 | ||
11 | try: | |
12 | from requests.packages.urllib3.util.url import parse_url | |
13 | except ImportError: # pragma: no cover | |
14 | from urllib3.util.url import parse_url # pragma: no cover | |
15 | ||
16 | try: | |
17 | from json.decoder import JSONDecodeError | |
18 | except ImportError: | |
19 | JSONDecodeError = ValueError | |
20 | ||
21 | ||
22 | def _create_key_val_str(input_dict): | |
23 | """ | |
24 | Returns string of format {'key': val, 'key2': val2} | |
25 | Function is called recursively for nested dictionaries | |
26 | ||
27 | :param input_dict: dictionary to transform | |
28 | :return: (str) reformatted string | |
29 | """ | |
30 | ||
31 | def list_to_str(input_list): | |
32 | """ | |
33 | Convert all list items to string. | |
34 | Function is called recursively for nested lists | |
35 | """ | |
36 | converted_list = [] | |
37 | for item in sorted(input_list, key=lambda x: str(x)): | |
38 | if isinstance(item, dict): | |
39 | item = _create_key_val_str(item) | |
40 | elif isinstance(item, list): | |
41 | item = list_to_str(item) | |
42 | ||
43 | converted_list.append(str(item)) | |
44 | list_str = ", ".join(converted_list) | |
45 | return "[" + list_str + "]" | |
46 | ||
47 | items_list = [] | |
48 | for key in sorted(input_dict.keys(), key=lambda x: str(x)): | |
49 | val = input_dict[key] | |
50 | if isinstance(val, dict): | |
51 | val = _create_key_val_str(val) | |
52 | elif isinstance(val, list): | |
53 | val = list_to_str(input_list=val) | |
54 | ||
55 | items_list.append("{}: {}".format(key, val)) | |
56 | ||
57 | key_val_str = "{{{}}}".format(", ".join(items_list)) | |
58 | return key_val_str | |
59 | ||
60 | ||
61 | def urlencoded_params_matcher(params): | |
62 | """ | |
63 | Matches URL encoded data | |
64 | ||
65 | :param params: (dict) data provided to 'data' arg of request | |
66 | :return: (func) matcher | |
67 | """ | |
68 | ||
69 | def match(request): | |
70 | reason = "" | |
71 | request_body = request.body | |
72 | qsl_body = dict(parse_qsl(request_body)) if request_body else {} | |
73 | params_dict = params or {} | |
74 | valid = params is None if request_body is None else params_dict == qsl_body | |
75 | if not valid: | |
76 | reason = "request.body doesn't match: {} doesn't match {}".format( | |
77 | _create_key_val_str(qsl_body), _create_key_val_str(params_dict) | |
78 | ) | |
79 | ||
80 | return valid, reason | |
81 | ||
82 | return match | |
83 | ||
84 | ||
85 | def json_params_matcher(params): | |
86 | """ | |
87 | Matches JSON encoded data | |
88 | ||
89 | :param params: (dict) JSON data provided to 'json' arg of request | |
90 | :return: (func) matcher | |
91 | """ | |
92 | ||
93 | def match(request): | |
94 | reason = "" | |
95 | request_body = request.body | |
96 | params_dict = params or {} | |
97 | try: | |
98 | if isinstance(request_body, bytes): | |
99 | request_body = request_body.decode("utf-8") | |
100 | json_body = json_module.loads(request_body) if request_body else {} | |
101 | ||
102 | valid = params is None if request_body is None else params_dict == json_body | |
103 | ||
104 | if not valid: | |
105 | reason = "request.body doesn't match: {} doesn't match {}".format( | |
106 | _create_key_val_str(json_body), _create_key_val_str(params_dict) | |
107 | ) | |
108 | ||
109 | except JSONDecodeError: | |
110 | valid = False | |
111 | reason = ( | |
112 | "request.body doesn't match: JSONDecodeError: Cannot parse request.body" | |
113 | ) | |
114 | ||
115 | return valid, reason | |
116 | ||
117 | return match | |
118 | ||
119 | ||
120 | def fragment_identifier_matcher(identifier): | |
121 | def match(request): | |
122 | reason = "" | |
123 | url_fragment = urlparse(request.url).fragment | |
124 | if identifier: | |
125 | url_fragment_qsl = sorted(parse_qsl(url_fragment)) | |
126 | identifier_qsl = sorted(parse_qsl(identifier)) | |
127 | valid = identifier_qsl == url_fragment_qsl | |
128 | else: | |
129 | valid = not url_fragment | |
130 | ||
131 | if not valid: | |
132 | reason = "URL fragment identifier is different: {} doesn't match {}".format( | |
133 | identifier, url_fragment | |
134 | ) | |
135 | return valid, reason | |
136 | ||
137 | return match | |
138 | ||
139 | ||
140 | def query_param_matcher(params): | |
141 | """ | |
142 | Matcher to match 'params' argument in request | |
143 | ||
144 | :param params: (dict), same as provided to request | |
145 | :return: (func) matcher | |
146 | """ | |
147 | ||
148 | def match(request): | |
149 | reason = "" | |
150 | request_params = request.params | |
151 | request_params_dict = request_params or {} | |
152 | params_dict = params or {} | |
153 | valid = ( | |
154 | params is None | |
155 | if request_params is None | |
156 | else params_dict == request_params_dict | |
157 | ) | |
158 | ||
159 | if not valid: | |
160 | reason = "Parameters do not match. {} doesn't match {}".format( | |
161 | _create_key_val_str(request_params_dict), | |
162 | _create_key_val_str(params_dict), | |
163 | ) | |
164 | ||
165 | return valid, reason | |
166 | ||
167 | return match | |
168 | ||
169 | ||
170 | def query_string_matcher(query): | |
171 | """ | |
172 | Matcher to match query string part of request | |
173 | ||
174 | :param query: (str), same as constructed by request | |
175 | :return: (func) matcher | |
176 | """ | |
177 | ||
178 | def match(request): | |
179 | reason = "" | |
180 | data = parse_url(request.url) | |
181 | request_query = data.query | |
182 | ||
183 | request_qsl = sorted(parse_qsl(request_query)) | |
184 | matcher_qsl = sorted(parse_qsl(query)) | |
185 | ||
186 | valid = not query if request_query is None else request_qsl == matcher_qsl | |
187 | ||
188 | if not valid: | |
189 | reason = "Query string doesn't match. {} doesn't match {}".format( | |
190 | _create_key_val_str(dict(request_qsl)), | |
191 | _create_key_val_str(dict(matcher_qsl)), | |
192 | ) | |
193 | ||
194 | return valid, reason | |
195 | ||
196 | return match | |
197 | ||
198 | ||
199 | def request_kwargs_matcher(kwargs): | |
200 | """ | |
201 | Matcher to match keyword arguments provided to request | |
202 | ||
203 | :param kwargs: (dict), keyword arguments, same as provided to request | |
204 | :return: (func) matcher | |
205 | """ | |
206 | ||
207 | def match(request): | |
208 | reason = "" | |
209 | kwargs_dict = kwargs or {} | |
210 | # validate only kwargs that were requested for comparison, skip defaults | |
211 | request_kwargs = { | |
212 | k: v for k, v in request.req_kwargs.items() if k in kwargs_dict | |
213 | } | |
214 | ||
215 | valid = ( | |
216 | not kwargs_dict | |
217 | if not request_kwargs | |
218 | else sorted(kwargs.items()) == sorted(request_kwargs.items()) | |
219 | ) | |
220 | ||
221 | if not valid: | |
222 | reason = "Arguments don't match: {} doesn't match {}".format( | |
223 | _create_key_val_str(request_kwargs), _create_key_val_str(kwargs_dict) | |
224 | ) | |
225 | ||
226 | return valid, reason | |
227 | ||
228 | return match | |
229 | ||
230 | ||
231 | def multipart_matcher(files, data=None): | |
232 | """ | |
233 | Matcher to match 'multipart/form-data' content-type. | |
234 | This function constructs request body and headers from provided 'data' and 'files' | |
235 | arguments and compares to actual request | |
236 | ||
237 | :param files: (dict), same as provided to request | |
238 | :param data: (dict), same as provided to request | |
239 | :return: (func) matcher | |
240 | """ | |
241 | if not files: | |
242 | raise TypeError("files argument cannot be empty") | |
243 | ||
244 | prepared = PreparedRequest() | |
245 | prepared.headers = {"Content-Type": ""} | |
246 | prepared.prepare_body(data=data, files=files) | |
247 | ||
248 | def get_boundary(content_type): | |
249 | """ | |
250 | Parse 'boundary' value from header. | |
251 | ||
252 | :param content_type: (str) headers["Content-Type"] value | |
253 | :return: (str) boundary value | |
254 | """ | |
255 | if "boundary=" not in content_type: | |
256 | return "" | |
257 | ||
258 | return content_type.split("boundary=")[1] | |
259 | ||
260 | def match(request): | |
261 | reason = "multipart/form-data doesn't match. " | |
262 | if "Content-Type" not in request.headers: | |
263 | return False, reason + "Request is missing the 'Content-Type' header" | |
264 | ||
265 | request_boundary = get_boundary(request.headers["Content-Type"]) | |
266 | prepared_boundary = get_boundary(prepared.headers["Content-Type"]) | |
267 | ||
268 | # replace boundary value in header and in body, since by default | |
269 | # urllib3.filepost.encode_multipart_formdata dynamically calculates | |
270 | # random boundary alphanumeric value | |
271 | request_content_type = request.headers["Content-Type"] | |
272 | prepared_content_type = prepared.headers["Content-Type"].replace( | |
273 | prepared_boundary, request_boundary | |
274 | ) | |
275 | ||
276 | request_body = request.body | |
277 | if isinstance(request_body, bytes): | |
278 | request_body = request_body.decode("utf-8") | |
279 | ||
280 | prepared_body = prepared.body | |
281 | if isinstance(prepared_body, bytes): | |
282 | prepared_body = prepared_body.decode("utf-8") | |
283 | ||
284 | prepared_body = prepared_body.replace(prepared_boundary, request_boundary) | |
285 | ||
286 | headers_valid = prepared_content_type == request_content_type | |
287 | if not headers_valid: | |
288 | return ( | |
289 | False, | |
290 | reason | |
291 | + "Request headers['Content-Type'] is different. {} isn't equal to {}".format( | |
292 | request_content_type, prepared_content_type | |
293 | ), | |
294 | ) | |
295 | ||
296 | body_valid = prepared_body == request_body | |
297 | if not body_valid: | |
298 | return False, reason + "Request body differs. {} aren't equal {}".format( | |
299 | request_body, prepared_body | |
300 | ) | |
301 | ||
302 | return True, "" | |
303 | ||
304 | return match | |
305 | ||
306 | ||
307 | def header_matcher(headers, strict_match=False): | |
308 | """ | |
309 | Matcher to match 'headers' argument in request using the responses library. | |
310 | ||
311 | Because ``requests`` will send several standard headers in addition to what | |
312 | was specified by your code, request headers that are additional to the ones | |
313 | passed to the matcher are ignored by default. You can change this behaviour | |
314 | by passing ``strict_match=True``. | |
315 | ||
316 | :param headers: (dict), same as provided to request | |
317 | :param strict_match: (bool), whether headers in addition to those specified | |
318 | in the matcher should cause the match to fail. | |
319 | :return: (func) matcher | |
320 | """ | |
321 | ||
322 | def match(request): | |
323 | request_headers = request.headers or {} | |
324 | ||
325 | if not strict_match: | |
326 | # filter down to just the headers specified in the matcher | |
327 | request_headers = {k: v for k, v in request_headers.items() if k in headers} | |
328 | ||
329 | valid = sorted(headers.items()) == sorted(request_headers.items()) | |
330 | ||
331 | if not valid: | |
332 | return False, "Headers do not match: {} doesn't match {}".format( | |
333 | _create_key_val_str(request_headers), _create_key_val_str(headers) | |
334 | ) | |
335 | ||
336 | return valid, "" | |
337 | ||
338 | return match |
0 | from typing import ( | |
1 | Any, | |
2 | Callable, | |
3 | Optional, | |
4 | Dict, | |
5 | ) | |
6 | ||
7 | JSONDecodeError = ValueError | |
8 | ||
9 | ||
10 | def _create_key_val_str(input_dict: Dict[Any, Any]) -> str: ... | |
11 | ||
12 | def json_params_matcher( | |
13 | params: Optional[Dict[str, Any]] | |
14 | ) -> Callable[..., Any]: ... | |
15 | ||
16 | def urlencoded_params_matcher( | |
17 | params: Optional[Dict[str, str]] | |
18 | ) -> Callable[..., Any]: ... | |
19 | ||
20 | def query_param_matcher( | |
21 | params: Optional[Dict[str, str]] | |
22 | ) -> Callable[..., Any]: ... | |
23 | ||
24 | def query_string_matcher( | |
25 | query: Optional[str] | |
26 | ) -> Callable[..., Any]: ... | |
27 | ||
28 | def request_kwargs_matcher( | |
29 | kwargs: Optional[Dict[str, Any]] | |
30 | ) -> Callable[..., Any]: ... | |
31 | ||
32 | def multipart_matcher( | |
33 | files: Dict[str, Any], data: Optional[Dict[str, str]] = ... | |
34 | ) -> Callable[..., Any]: ... | |
35 | ||
36 | def header_matcher( | |
37 | headers: Dict[str, str], | |
38 | strict_match: bool = ... | |
39 | ) -> Callable[..., Any]: ... | |
40 | ||
41 | def fragment_identifier_matcher( | |
42 | identifier: Optional[str] | |
43 | ) -> Callable[..., Any]: ... |
0 | from __future__ import absolute_import, print_function, division, unicode_literals | |
1 | ||
2 | import six | |
3 | from sys import version_info | |
4 | ||
5 | import pytest | |
6 | import requests | |
7 | import responses | |
8 | from requests.exceptions import ConnectionError | |
9 | from responses import matchers | |
10 | ||
11 | ||
12 | def assert_response(resp, body=None, content_type="text/plain"): | |
13 | assert resp.status_code == 200 | |
14 | assert resp.reason == "OK" | |
15 | assert resp.headers["Content-Type"] == content_type | |
16 | assert resp.text == body | |
17 | ||
18 | ||
19 | def assert_reset(): | |
20 | assert len(responses._default_mock._matches) == 0 | |
21 | assert len(responses.calls) == 0 | |
22 | ||
23 | ||
24 | def test_query_string_matcher(): | |
25 | @responses.activate | |
26 | def run(): | |
27 | url = "http://example.com?test=1&foo=bar" | |
28 | responses.add( | |
29 | responses.GET, | |
30 | url, | |
31 | body=b"test", | |
32 | match=[matchers.query_string_matcher("test=1&foo=bar")], | |
33 | ) | |
34 | resp = requests.get("http://example.com?test=1&foo=bar") | |
35 | assert_response(resp, "test") | |
36 | resp = requests.get("http://example.com?foo=bar&test=1") | |
37 | assert_response(resp, "test") | |
38 | resp = requests.get("http://example.com/?foo=bar&test=1") | |
39 | assert_response(resp, "test") | |
40 | ||
41 | run() | |
42 | assert_reset() | |
43 | ||
44 | ||
45 | def test_request_matches_post_params(): | |
46 | @responses.activate | |
47 | def run(deprecated): | |
48 | if deprecated: | |
49 | json_params_matcher = getattr(responses, "json_params_matcher") | |
50 | urlencoded_params_matcher = getattr(responses, "urlencoded_params_matcher") | |
51 | else: | |
52 | json_params_matcher = matchers.json_params_matcher | |
53 | urlencoded_params_matcher = matchers.urlencoded_params_matcher | |
54 | ||
55 | responses.add( | |
56 | method=responses.POST, | |
57 | url="http://example.com/", | |
58 | body="one", | |
59 | match=[json_params_matcher({"page": {"name": "first", "type": "json"}})], | |
60 | ) | |
61 | responses.add( | |
62 | method=responses.POST, | |
63 | url="http://example.com/", | |
64 | body="two", | |
65 | match=[urlencoded_params_matcher({"page": "second", "type": "urlencoded"})], | |
66 | ) | |
67 | ||
68 | resp = requests.request( | |
69 | "POST", | |
70 | "http://example.com/", | |
71 | headers={"Content-Type": "x-www-form-urlencoded"}, | |
72 | data={"page": "second", "type": "urlencoded"}, | |
73 | ) | |
74 | assert_response(resp, "two") | |
75 | ||
76 | resp = requests.request( | |
77 | "POST", | |
78 | "http://example.com/", | |
79 | headers={"Content-Type": "application/json"}, | |
80 | json={"page": {"name": "first", "type": "json"}}, | |
81 | ) | |
82 | assert_response(resp, "one") | |
83 | ||
84 | with pytest.deprecated_call(): | |
85 | run(deprecated=True) | |
86 | assert_reset() | |
87 | ||
88 | run(deprecated=False) | |
89 | assert_reset() | |
90 | ||
91 | ||
92 | def test_request_matches_empty_body(): | |
93 | def run(): | |
94 | with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: | |
95 | # test that both json and urlencoded body are empty in matcher and in request | |
96 | rsps.add( | |
97 | method=responses.POST, | |
98 | url="http://example.com/", | |
99 | body="one", | |
100 | match=[matchers.json_params_matcher(None)], | |
101 | ) | |
102 | ||
103 | rsps.add( | |
104 | method=responses.POST, | |
105 | url="http://example.com/", | |
106 | body="two", | |
107 | match=[matchers.urlencoded_params_matcher(None)], | |
108 | ) | |
109 | ||
110 | resp = requests.request("POST", "http://example.com/") | |
111 | assert_response(resp, "one") | |
112 | ||
113 | resp = requests.request( | |
114 | "POST", | |
115 | "http://example.com/", | |
116 | headers={"Content-Type": "x-www-form-urlencoded"}, | |
117 | ) | |
118 | assert_response(resp, "two") | |
119 | ||
120 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
121 | # test exception raise if matcher body is None but request data is not None | |
122 | rsps.add( | |
123 | method=responses.POST, | |
124 | url="http://example.com/", | |
125 | body="one", | |
126 | match=[matchers.json_params_matcher(None)], | |
127 | ) | |
128 | ||
129 | with pytest.raises(ConnectionError) as excinfo: | |
130 | resp = requests.request( | |
131 | "POST", | |
132 | "http://example.com/", | |
133 | json={"my": "data"}, | |
134 | headers={"Content-Type": "application/json"}, | |
135 | ) | |
136 | ||
137 | msg = str(excinfo.value) | |
138 | assert "request.body doesn't match: {my: data} doesn't match {}" in msg | |
139 | ||
140 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
141 | rsps.add( | |
142 | method=responses.POST, | |
143 | url="http://example.com/", | |
144 | body="two", | |
145 | match=[matchers.urlencoded_params_matcher(None)], | |
146 | ) | |
147 | with pytest.raises(ConnectionError) as excinfo: | |
148 | resp = requests.request( | |
149 | "POST", | |
150 | "http://example.com/", | |
151 | headers={"Content-Type": "x-www-form-urlencoded"}, | |
152 | data={"page": "second", "type": "urlencoded"}, | |
153 | ) | |
154 | msg = str(excinfo.value) | |
155 | assert ( | |
156 | "request.body doesn't match: {page: second, type: urlencoded} doesn't match {}" | |
157 | in msg | |
158 | ) | |
159 | ||
160 | run() | |
161 | assert_reset() | |
162 | ||
163 | ||
164 | def test_request_matches_params(): | |
165 | @responses.activate | |
166 | def run(): | |
167 | url = "http://example.com/test" | |
168 | params = {"hello": "world", "I am": "a big test"} | |
169 | responses.add( | |
170 | method=responses.GET, | |
171 | url=url, | |
172 | body="test", | |
173 | match=[matchers.query_param_matcher(params)], | |
174 | match_querystring=False, | |
175 | ) | |
176 | ||
177 | # exchange parameter places for the test | |
178 | params = { | |
179 | "I am": "a big test", | |
180 | "hello": "world", | |
181 | } | |
182 | resp = requests.get(url, params=params) | |
183 | ||
184 | if six.PY3 and version_info[1] >= 7: | |
185 | # only after py 3.7 dictionaries are ordered, so we can check URL | |
186 | constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" | |
187 | assert resp.url == constructed_url | |
188 | assert resp.request.url == constructed_url | |
189 | ||
190 | resp_params = getattr(resp.request, "params") | |
191 | assert resp_params == params | |
192 | ||
193 | run() | |
194 | assert_reset() | |
195 | ||
196 | ||
197 | def test_fail_matchers_error(): | |
198 | """ | |
199 | Validate that Exception is raised if request does not match responses.matchers | |
200 | validate matchers.urlencoded_params_matcher | |
201 | validate matchers.json_params_matcher | |
202 | validate matchers.query_param_matcher | |
203 | validate matchers.request_kwargs_matcher | |
204 | :return: None | |
205 | """ | |
206 | ||
207 | def run(): | |
208 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
209 | rsps.add( | |
210 | "POST", | |
211 | "http://example.com", | |
212 | match=[matchers.urlencoded_params_matcher({"foo": "bar"})], | |
213 | ) | |
214 | rsps.add( | |
215 | "POST", | |
216 | "http://example.com", | |
217 | match=[matchers.json_params_matcher({"fail": "json"})], | |
218 | ) | |
219 | ||
220 | with pytest.raises(ConnectionError) as excinfo: | |
221 | requests.post("http://example.com", data={"id": "bad"}) | |
222 | ||
223 | msg = str(excinfo.value) | |
224 | assert ( | |
225 | "request.body doesn't match: {id: bad} doesn't match {foo: bar}" in msg | |
226 | ) | |
227 | ||
228 | assert ( | |
229 | "request.body doesn't match: JSONDecodeError: Cannot parse request.body" | |
230 | in msg | |
231 | ) | |
232 | ||
233 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
234 | rsps.add( | |
235 | "GET", | |
236 | "http://111.com", | |
237 | match=[matchers.query_param_matcher({"my": "params"})], | |
238 | ) | |
239 | ||
240 | rsps.add( | |
241 | method=responses.GET, | |
242 | url="http://111.com/", | |
243 | body="two", | |
244 | match=[matchers.json_params_matcher({"page": "one"})], | |
245 | ) | |
246 | ||
247 | with pytest.raises(ConnectionError) as excinfo: | |
248 | requests.get( | |
249 | "http://111.com", params={"id": "bad"}, json={"page": "two"} | |
250 | ) | |
251 | ||
252 | msg = str(excinfo.value) | |
253 | assert ( | |
254 | "Parameters do not match. {id: bad} doesn't match {my: params}" in msg | |
255 | ) | |
256 | assert ( | |
257 | "request.body doesn't match: {page: two} doesn't match {page: one}" | |
258 | in msg | |
259 | ) | |
260 | ||
261 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
262 | req_kwargs = { | |
263 | "stream": True, | |
264 | "verify": False, | |
265 | } | |
266 | rsps.add( | |
267 | "GET", | |
268 | "http://111.com", | |
269 | match=[matchers.request_kwargs_matcher(req_kwargs)], | |
270 | ) | |
271 | ||
272 | with pytest.raises(ConnectionError) as excinfo: | |
273 | requests.get("http://111.com", stream=True) | |
274 | ||
275 | msg = str(excinfo.value) | |
276 | assert ( | |
277 | "Arguments don't match: " | |
278 | "{stream: True, verify: True} doesn't match {stream: True, verify: False}" | |
279 | ) in msg | |
280 | ||
281 | run() | |
282 | assert_reset() | |
283 | ||
284 | ||
285 | def test_multipart_matcher(): | |
286 | @responses.activate | |
287 | def run(): | |
288 | req_data = {"some": "other", "data": "fields"} | |
289 | req_files = {"file_name": b"Old World!"} | |
290 | responses.add( | |
291 | responses.POST, | |
292 | url="http://httpbin.org/post", | |
293 | match=[matchers.multipart_matcher(req_files, data=req_data)], | |
294 | ) | |
295 | resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files) | |
296 | assert resp.status_code == 200 | |
297 | ||
298 | with pytest.raises(TypeError): | |
299 | responses.add( | |
300 | responses.POST, | |
301 | url="http://httpbin.org/post", | |
302 | match=[matchers.multipart_matcher(files={})], | |
303 | ) | |
304 | ||
305 | run() | |
306 | assert_reset() | |
307 | ||
308 | ||
309 | def test_multipart_matcher_fail(): | |
310 | """ | |
311 | Validate that Exception is raised if request does not match responses.matchers | |
312 | validate matchers.multipart_matcher | |
313 | :return: None | |
314 | """ | |
315 | ||
316 | def run(): | |
317 | # different file contents | |
318 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
319 | req_data = {"some": "other", "data": "fields"} | |
320 | req_files = {"file_name": b"Old World!"} | |
321 | rsps.add( | |
322 | responses.POST, | |
323 | url="http://httpbin.org/post", | |
324 | match=[matchers.multipart_matcher(req_files, data=req_data)], | |
325 | ) | |
326 | ||
327 | with pytest.raises(ConnectionError) as excinfo: | |
328 | requests.post( | |
329 | "http://httpbin.org/post", | |
330 | data=req_data, | |
331 | files={"file_name": b"New World!"}, | |
332 | ) | |
333 | ||
334 | msg = str(excinfo.value) | |
335 | assert "multipart/form-data doesn't match. Request body differs." in msg | |
336 | assert ( | |
337 | '\r\nContent-Disposition: form-data; name="file_name"; ' | |
338 | 'filename="file_name"\r\n\r\nOld World!\r\n' | |
339 | ) in msg | |
340 | assert ( | |
341 | '\r\nContent-Disposition: form-data; name="file_name"; ' | |
342 | 'filename="file_name"\r\n\r\nNew World!\r\n' | |
343 | ) in msg | |
344 | ||
345 | # x-www-form-urlencoded request | |
346 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
347 | req_data = {"some": "other", "data": "fields"} | |
348 | req_files = {"file_name": b"Old World!"} | |
349 | rsps.add( | |
350 | responses.POST, | |
351 | url="http://httpbin.org/post", | |
352 | match=[matchers.multipart_matcher(req_files, data=req_data)], | |
353 | ) | |
354 | ||
355 | with pytest.raises(ConnectionError) as excinfo: | |
356 | requests.post("http://httpbin.org/post", data=req_data) | |
357 | ||
358 | msg = str(excinfo.value) | |
359 | assert ( | |
360 | "multipart/form-data doesn't match. Request headers['Content-Type'] is different." | |
361 | in msg | |
362 | ) | |
363 | assert ( | |
364 | "application/x-www-form-urlencoded isn't equal to multipart/form-data; boundary=" | |
365 | in msg | |
366 | ) | |
367 | ||
368 | # empty body request | |
369 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
370 | req_files = {"file_name": b"Old World!"} | |
371 | rsps.add( | |
372 | responses.POST, | |
373 | url="http://httpbin.org/post", | |
374 | match=[matchers.multipart_matcher(req_files)], | |
375 | ) | |
376 | ||
377 | with pytest.raises(ConnectionError) as excinfo: | |
378 | requests.post("http://httpbin.org/post") | |
379 | ||
380 | msg = str(excinfo.value) | |
381 | assert "Request is missing the 'Content-Type' header" in msg | |
382 | ||
383 | run() | |
384 | assert_reset() | |
385 | ||
386 | ||
387 | def test_query_string_matcher_raises(): | |
388 | """ | |
389 | Validate that Exception is raised if request does not match responses.matchers | |
390 | validate matchers.query_string_matcher | |
391 | :return: None | |
392 | """ | |
393 | ||
394 | def run(): | |
395 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
396 | rsps.add( | |
397 | "GET", | |
398 | "http://111.com", | |
399 | match=[matchers.query_string_matcher("didi=pro")], | |
400 | ) | |
401 | ||
402 | with pytest.raises(ConnectionError) as excinfo: | |
403 | requests.get("http://111.com", params={"test": "1", "didi": "pro"}) | |
404 | ||
405 | msg = str(excinfo.value) | |
406 | assert ( | |
407 | "Query string doesn't match. {didi: pro, test: 1} doesn't match {didi: pro}" | |
408 | in msg | |
409 | ) | |
410 | ||
411 | run() | |
412 | assert_reset() | |
413 | ||
414 | ||
415 | def test_request_matches_headers(): | |
416 | @responses.activate | |
417 | def run(): | |
418 | url = "http://example.com/" | |
419 | responses.add( | |
420 | method=responses.GET, | |
421 | url=url, | |
422 | json={"success": True}, | |
423 | match=[matchers.header_matcher({"Accept": "application/json"})], | |
424 | ) | |
425 | ||
426 | responses.add( | |
427 | method=responses.GET, | |
428 | url=url, | |
429 | body="success", | |
430 | match=[matchers.header_matcher({"Accept": "text/plain"})], | |
431 | ) | |
432 | ||
433 | # the actual request can contain extra headers (requests always adds some itself anyway) | |
434 | resp = requests.get( | |
435 | url, headers={"Accept": "application/json", "Accept-Charset": "utf-8"} | |
436 | ) | |
437 | assert_response(resp, body='{"success": true}', content_type="application/json") | |
438 | ||
439 | resp = requests.get(url, headers={"Accept": "text/plain"}) | |
440 | assert_response(resp, body="success", content_type="text/plain") | |
441 | ||
442 | run() | |
443 | assert_reset() | |
444 | ||
445 | ||
446 | def test_request_matches_headers_no_match(): | |
447 | @responses.activate | |
448 | def run(): | |
449 | url = "http://example.com/" | |
450 | responses.add( | |
451 | method=responses.GET, | |
452 | url=url, | |
453 | json={"success": True}, | |
454 | match=[matchers.header_matcher({"Accept": "application/json"})], | |
455 | ) | |
456 | ||
457 | with pytest.raises(ConnectionError) as excinfo: | |
458 | requests.get(url, headers={"Accept": "application/xml"}) | |
459 | ||
460 | msg = str(excinfo.value) | |
461 | assert ( | |
462 | "Headers do not match: {Accept: application/xml} doesn't match " | |
463 | "{Accept: application/json}" | |
464 | ) in msg | |
465 | ||
466 | run() | |
467 | assert_reset() | |
468 | ||
469 | ||
470 | def test_request_matches_headers_strict_match(): | |
471 | @responses.activate | |
472 | def run(): | |
473 | url = "http://example.com/" | |
474 | responses.add( | |
475 | method=responses.GET, | |
476 | url=url, | |
477 | body="success", | |
478 | match=[ | |
479 | matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) | |
480 | ], | |
481 | ) | |
482 | ||
483 | # requests will add some extra headers of its own, so we have to use prepared requests | |
484 | session = requests.Session() | |
485 | ||
486 | # make sure we send *just* the header we're expectin | |
487 | prepped = session.prepare_request( | |
488 | requests.Request( | |
489 | method="GET", | |
490 | url=url, | |
491 | ) | |
492 | ) | |
493 | prepped.headers.clear() | |
494 | prepped.headers["Accept"] = "text/plain" | |
495 | ||
496 | resp = session.send(prepped) | |
497 | assert_response(resp, body="success", content_type="text/plain") | |
498 | ||
499 | # include the "Accept-Charset" header, which will fail to match | |
500 | prepped = session.prepare_request( | |
501 | requests.Request( | |
502 | method="GET", | |
503 | url=url, | |
504 | ) | |
505 | ) | |
506 | prepped.headers.clear() | |
507 | prepped.headers["Accept"] = "text/plain" | |
508 | prepped.headers["Accept-Charset"] = "utf-8" | |
509 | ||
510 | with pytest.raises(ConnectionError) as excinfo: | |
511 | session.send(prepped) | |
512 | ||
513 | msg = str(excinfo.value) | |
514 | assert ( | |
515 | "Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} " | |
516 | "doesn't match {Accept: text/plain}" | |
517 | ) in msg | |
518 | ||
519 | run() | |
520 | assert_reset() | |
521 | ||
522 | ||
523 | def test_fragment_identifier_matcher(): | |
524 | @responses.activate | |
525 | def run(): | |
526 | responses.add( | |
527 | responses.GET, | |
528 | "http://example.com", | |
529 | match=[matchers.fragment_identifier_matcher("test=1&foo=bar")], | |
530 | body=b"test", | |
531 | ) | |
532 | ||
533 | resp = requests.get("http://example.com#test=1&foo=bar") | |
534 | assert_response(resp, "test") | |
535 | ||
536 | run() | |
537 | assert_reset() | |
538 | ||
539 | ||
540 | def test_fragment_identifier_matcher_error(): | |
541 | @responses.activate | |
542 | def run(): | |
543 | responses.add( | |
544 | responses.GET, | |
545 | "http://example.com/", | |
546 | match=[matchers.fragment_identifier_matcher("test=1")], | |
547 | ) | |
548 | responses.add( | |
549 | responses.GET, | |
550 | "http://example.com/", | |
551 | match=[matchers.fragment_identifier_matcher(None)], | |
552 | ) | |
553 | ||
554 | with pytest.raises(ConnectionError) as excinfo: | |
555 | requests.get("http://example.com/#test=2") | |
556 | ||
557 | msg = str(excinfo.value) | |
558 | assert ( | |
559 | "URL fragment identifier is different: test=1 doesn't match test=2" | |
560 | ) in msg | |
561 | assert ( | |
562 | "URL fragment identifier is different: None doesn't match test=2" | |
563 | ) in msg | |
564 | ||
565 | run() | |
566 | assert_reset() | |
567 | ||
568 | ||
569 | def test_fragment_identifier_matcher_and_match_querystring(): | |
570 | @responses.activate | |
571 | def run(): | |
572 | url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" | |
573 | responses.add( | |
574 | responses.GET, | |
575 | url, | |
576 | match_querystring=True, | |
577 | match=[matchers.fragment_identifier_matcher("test=1&foo=bar")], | |
578 | body=b"test", | |
579 | ) | |
580 | ||
581 | # two requests to check reversed order of fragment identifier | |
582 | resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") | |
583 | assert_response(resp, "test") | |
584 | resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") | |
585 | assert_response(resp, "test") | |
586 | ||
587 | run() | |
588 | assert_reset() | |
589 | ||
590 | ||
591 | def test_matchers_create_key_val_str(): | |
592 | """ | |
593 | Test that matchers._create_key_val_str does recursive conversion | |
594 | """ | |
595 | data = { | |
596 | "my_list": [ | |
597 | 1, | |
598 | 2, | |
599 | "a", | |
600 | {"key1": "val1", "key2": 2, 3: "test"}, | |
601 | "!", | |
602 | [["list", "nested"], {"nested": "dict"}], | |
603 | ], | |
604 | 1: 4, | |
605 | "test": "val", | |
606 | "high": {"nested": "nested_dict"}, | |
607 | } | |
608 | conv_str = matchers._create_key_val_str(data) | |
609 | reference = ( | |
610 | "{1: 4, high: {nested: nested_dict}, my_list: [!, 1, 2, [[list, nested], {nested: dict}], " | |
611 | "a, {3: test, key1: val1, key2: 2}], test: val}" | |
612 | ) | |
613 | assert conv_str == reference |
2 | 2 | from __future__ import absolute_import, print_function, division, unicode_literals |
3 | 3 | |
4 | 4 | import inspect |
5 | import os | |
5 | 6 | import re |
6 | 7 | import six |
7 | 8 | from io import BufferedReader, BytesIO |
9 | 10 | import pytest |
10 | 11 | import requests |
11 | 12 | import responses |
12 | from requests.exceptions import ConnectionError, HTTPError | |
13 | from responses import BaseResponse, Response | |
13 | from requests.exceptions import ConnectionError, HTTPError, ChunkedEncodingError | |
14 | from responses import ( | |
15 | BaseResponse, | |
16 | Response, | |
17 | PassthroughResponse, | |
18 | matchers, | |
19 | CallbackResponse, | |
20 | ) | |
21 | ||
14 | 22 | |
15 | 23 | try: |
16 | 24 | from mock import patch, Mock |
89 | 97 | assert_response(resp, "") |
90 | 98 | assert len(responses.calls) == 2 |
91 | 99 | assert responses.calls[1].request.url == "http://example.com/?foo=bar" |
100 | ||
101 | run() | |
102 | assert_reset() | |
92 | 103 | |
93 | 104 | |
94 | 105 | @pytest.mark.parametrize( |
330 | 341 | assert_reset() |
331 | 342 | |
332 | 343 | |
333 | def test_match_empty_querystring(): | |
344 | def test_match_querystring_empty(): | |
334 | 345 | @responses.activate |
335 | 346 | def run(): |
336 | 347 | responses.add( |
529 | 540 | } |
530 | 541 | url = "http://example.com/" |
531 | 542 | |
532 | def request_callback(request): | |
533 | return (status, headers, body) | |
543 | def request_callback(_request): | |
544 | return status, headers, body | |
534 | 545 | |
535 | 546 | @responses.activate |
536 | 547 | def run(): |
547 | 558 | assert_reset() |
548 | 559 | |
549 | 560 | |
561 | def test_callback_deprecated_argument(): | |
562 | with pytest.deprecated_call(): | |
563 | CallbackResponse(responses.GET, "url", lambda x: x, stream=False) | |
564 | ||
565 | ||
550 | 566 | def test_callback_exception_result(): |
551 | 567 | result = Exception() |
552 | 568 | url = "http://example.com/" |
572 | 588 | url = "http://example.com/" |
573 | 589 | |
574 | 590 | def request_callback(request): |
575 | return (200, {}, body) | |
591 | return 200, {}, body | |
576 | 592 | |
577 | 593 | @responses.activate |
578 | 594 | def run(): |
594 | 610 | headers = {"foo": "bar"} |
595 | 611 | url = "http://example.com/" |
596 | 612 | |
597 | def request_callback(request): | |
598 | return (status, headers, body) | |
613 | def request_callback(_request): | |
614 | return status, headers, body | |
599 | 615 | |
600 | 616 | @responses.activate |
601 | 617 | def run(): |
630 | 646 | assert_reset() |
631 | 647 | |
632 | 648 | |
649 | def test_callback_matchers(): | |
650 | def request_callback(request): | |
651 | return ( | |
652 | 200, | |
653 | {"Content-Type": "application/json"}, | |
654 | b"foo", | |
655 | ) | |
656 | ||
657 | @responses.activate | |
658 | def run(): | |
659 | req_data = {"some": "other", "data": "fields"} | |
660 | req_files = {"file_name": b"Old World!"} | |
661 | ||
662 | responses.add_callback( | |
663 | responses.POST, | |
664 | url="http://httpbin.org/post", | |
665 | match=[matchers.multipart_matcher(req_files, data=req_data)], | |
666 | callback=request_callback, | |
667 | ) | |
668 | resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files) | |
669 | assert resp.text == "foo" | |
670 | assert resp.headers["content-type"] == "application/json" | |
671 | ||
672 | run() | |
673 | assert_reset() | |
674 | ||
675 | ||
676 | def test_callback_matchers_fail(): | |
677 | @responses.activate | |
678 | def run(): | |
679 | req_data = {"some": "other", "data": "fields"} | |
680 | req_files = {"file_name": b"Old World!"} | |
681 | ||
682 | responses.add_callback( | |
683 | responses.POST, | |
684 | url="http://httpbin.org/post", | |
685 | match=[matchers.multipart_matcher(req_files, data=req_data)], | |
686 | callback=lambda x: ( | |
687 | 0, | |
688 | {"a": ""}, | |
689 | "", | |
690 | ), | |
691 | ) | |
692 | with pytest.raises(ConnectionError) as exc: | |
693 | requests.post( | |
694 | "http://httpbin.org/post", | |
695 | data={"some": "other", "data": "wrong"}, | |
696 | files=req_files, | |
697 | ) | |
698 | ||
699 | assert "multipart/form-data doesn't match." in str(exc.value) | |
700 | ||
701 | run() | |
702 | assert_reset() | |
703 | ||
704 | ||
633 | 705 | def test_callback_content_type_tuple(): |
634 | 706 | def request_callback(request): |
635 | 707 | return ( |
669 | 741 | |
670 | 742 | run() |
671 | 743 | assert_reset() |
744 | ||
745 | ||
746 | def test_base_response_get_response(): | |
747 | resp = BaseResponse("GET", ".com") | |
748 | with pytest.raises(NotImplementedError): | |
749 | resp.get_response(requests.PreparedRequest()) | |
672 | 750 | |
673 | 751 | |
674 | 752 | def test_custom_adapter(): |
738 | 816 | assert decorated_test_function(3) == test_function(3) |
739 | 817 | |
740 | 818 | |
819 | @pytest.fixture | |
820 | def my_fruit(): | |
821 | return "apple" | |
822 | ||
823 | ||
824 | @pytest.fixture | |
825 | def fruit_basket(my_fruit): | |
826 | return ["banana", my_fruit] | |
827 | ||
828 | ||
829 | @pytest.mark.usefixtures("my_fruit", "fruit_basket") | |
830 | class TestFixtures(object): | |
831 | """ | |
832 | Test that pytest fixtures work well with 'activate' decorator | |
833 | """ | |
834 | ||
835 | def test_function(self, my_fruit, fruit_basket): | |
836 | assert my_fruit in fruit_basket | |
837 | assert my_fruit == "apple" | |
838 | ||
839 | test_function_decorated = responses.activate(test_function) | |
840 | ||
841 | ||
741 | 842 | def test_activate_mock_interaction(): |
742 | 843 | @patch("sys.stdout") |
743 | 844 | def test_function(mock_stdout): |
763 | 864 | @pytest.mark.skipif(six.PY2, reason="Cannot run in python2") |
764 | 865 | def test_activate_doesnt_change_signature_with_return_type(): |
765 | 866 | def test_function(a, b=None): |
766 | return (a, b) | |
867 | return a, b | |
767 | 868 | |
768 | 869 | # Add type annotations as they are syntax errors in py2. |
769 | 870 | # Use a class to test for import errors in evaled code. |
771 | 872 | test_function.__annotations__["a"] = Mock |
772 | 873 | |
773 | 874 | decorated_test_function = responses.activate(test_function) |
774 | if hasattr(inspect, "signature"): | |
775 | assert inspect.signature(test_function) == inspect.signature( | |
776 | decorated_test_function | |
777 | ) | |
778 | else: | |
779 | assert inspect.getargspec(test_function) == inspect.getargspec( | |
780 | decorated_test_function | |
781 | ) | |
875 | assert inspect.signature(test_function) == inspect.signature( | |
876 | decorated_test_function | |
877 | ) | |
878 | ||
782 | 879 | assert decorated_test_function(1, 2) == test_function(1, 2) |
783 | 880 | assert decorated_test_function(3) == test_function(3) |
784 | 881 | |
818 | 915 | assert_reset() |
819 | 916 | |
820 | 917 | |
821 | def test_response_secure_cookies(): | |
918 | def test_response_cookies_secure(): | |
822 | 919 | body = b"test callback" |
823 | 920 | status = 200 |
824 | 921 | headers = {"set-cookie": "session_id=12345; a=b; c=d; secure"} |
867 | 964 | assert_reset() |
868 | 965 | |
869 | 966 | |
967 | @pytest.mark.parametrize("request_stream", (True, False, None)) | |
968 | @pytest.mark.parametrize("responses_stream", (True, False, None)) | |
969 | def test_response_cookies_session(request_stream, responses_stream): | |
970 | @responses.activate | |
971 | def run(): | |
972 | url = "https://example.com/path" | |
973 | responses.add( | |
974 | responses.GET, | |
975 | url, | |
976 | headers=[ | |
977 | ("Set-cookie", "mycookie=cookieval; path=/; secure"), | |
978 | ], | |
979 | body="ok", | |
980 | stream=responses_stream, | |
981 | ) | |
982 | session = requests.session() | |
983 | resp = session.get(url, stream=request_stream) | |
984 | assert resp.text == "ok" | |
985 | assert resp.status_code == 200 | |
986 | ||
987 | assert "mycookie" in resp.cookies | |
988 | assert resp.cookies["mycookie"] == "cookieval" | |
989 | assert set(resp.cookies.keys()) == set(["mycookie"]) | |
990 | ||
991 | assert "mycookie" in session.cookies | |
992 | assert session.cookies["mycookie"] == "cookieval" | |
993 | assert set(session.cookies.keys()) == set(["mycookie"]) | |
994 | ||
995 | run() | |
996 | assert_reset() | |
997 | ||
998 | ||
870 | 999 | def test_response_callback(): |
871 | 1000 | """adds a callback to decorate the response, then checks it""" |
872 | 1001 | |
886 | 1015 | assert_reset() |
887 | 1016 | |
888 | 1017 | |
1018 | @pytest.mark.skipif(six.PY2, reason="re.compile works differntly in PY2") | |
889 | 1019 | def test_response_filebody(): |
890 | 1020 | """ Adds the possibility to use actual (binary) files as responses """ |
891 | 1021 | |
892 | 1022 | def run(): |
1023 | current_file = os.path.abspath(__file__) | |
893 | 1024 | with responses.RequestsMock() as m: |
894 | with open("README.rst", "r") as out: | |
1025 | with open(current_file, "r") as out: | |
895 | 1026 | m.add(responses.GET, "http://example.com", body=out.read(), stream=True) |
896 | resp = requests.get("http://example.com") | |
897 | with open("README.rst", "r") as out: | |
1027 | resp = requests.get("http://example.com", stream=True) | |
1028 | with open(current_file, "r") as out: | |
898 | 1029 | assert resp.text == out.read() |
1030 | ||
1031 | run() | |
1032 | assert_reset() | |
1033 | ||
1034 | ||
1035 | def test_use_stream_twice_to_double_raw_io(): | |
1036 | @responses.activate | |
1037 | def run(): | |
1038 | url = "http://example.com" | |
1039 | responses.add(responses.GET, url, body=b"42", stream=True) | |
1040 | resp = requests.get(url, stream=True) | |
1041 | assert resp.raw.read() == b"42" | |
1042 | ||
1043 | run() | |
1044 | assert_reset() | |
899 | 1045 | |
900 | 1046 | |
901 | 1047 | def test_assert_all_requests_are_fired(): |
1064 | 1210 | assert_reset() |
1065 | 1211 | |
1066 | 1212 | |
1213 | def test_content_length_error(monkeypatch): | |
1214 | """ | |
1215 | Currently 'requests' does not enforce content length validation, | |
1216 | (validation that body length matches header). However, this could | |
1217 | be expected in next major version, see | |
1218 | https://github.com/psf/requests/pull/3563 | |
1219 | ||
1220 | Now user can manually patch URL3 lib to achieve the same | |
1221 | """ | |
1222 | ||
1223 | @responses.activate | |
1224 | def run(): | |
1225 | responses.add( | |
1226 | responses.GET, | |
1227 | "http://example.com/api/123", | |
1228 | json={"message": "this body is too large"}, | |
1229 | adding_headers={"content-length": "2"}, | |
1230 | ) | |
1231 | with pytest.raises(ChunkedEncodingError) as exc: | |
1232 | requests.get("http://example.com/api/123") | |
1233 | ||
1234 | assert "IncompleteRead" in str(exc.value) | |
1235 | ||
1236 | original_init = getattr(requests.packages.urllib3.HTTPResponse, "__init__") | |
1237 | ||
1238 | def patched_init(self, *args, **kwargs): | |
1239 | kwargs["enforce_content_length"] = True | |
1240 | original_init(self, *args, **kwargs) | |
1241 | ||
1242 | monkeypatch.setattr( | |
1243 | requests.packages.urllib3.HTTPResponse, "__init__", patched_init | |
1244 | ) | |
1245 | ||
1246 | run() | |
1247 | assert_reset() | |
1248 | ||
1249 | ||
1067 | 1250 | def test_legacy_adding_headers(): |
1068 | 1251 | @responses.activate |
1069 | 1252 | def run(): |
1075 | 1258 | ) |
1076 | 1259 | resp = requests.get("http://example.com") |
1077 | 1260 | assert resp.headers["X-Test"] == "foo" |
1261 | ||
1262 | run() | |
1263 | assert_reset() | |
1264 | ||
1265 | ||
1266 | def test_auto_calculate_content_length_string_body(): | |
1267 | @responses.activate | |
1268 | def run(): | |
1269 | url = "http://example.com/" | |
1270 | responses.add( | |
1271 | responses.GET, url, body="test", auto_calculate_content_length=True | |
1272 | ) | |
1273 | resp = requests.get(url) | |
1274 | assert_response(resp, "test") | |
1275 | assert resp.headers["Content-Length"] == "4" | |
1276 | ||
1277 | run() | |
1278 | assert_reset() | |
1279 | ||
1280 | ||
1281 | def test_auto_calculate_content_length_bytes_body(): | |
1282 | @responses.activate | |
1283 | def run(): | |
1284 | url = "http://example.com/" | |
1285 | responses.add( | |
1286 | responses.GET, url, body=b"test bytes", auto_calculate_content_length=True | |
1287 | ) | |
1288 | resp = requests.get(url) | |
1289 | assert_response(resp, "test bytes") | |
1290 | assert resp.headers["Content-Length"] == "10" | |
1291 | ||
1292 | run() | |
1293 | assert_reset() | |
1294 | ||
1295 | ||
1296 | def test_auto_calculate_content_length_json_body(): | |
1297 | @responses.activate | |
1298 | def run(): | |
1299 | content_type = "application/json" | |
1300 | ||
1301 | url = "http://example.com/" | |
1302 | responses.add( | |
1303 | responses.GET, | |
1304 | url, | |
1305 | json={"message": "success"}, | |
1306 | auto_calculate_content_length=True, | |
1307 | ) | |
1308 | resp = requests.get(url) | |
1309 | assert_response(resp, '{"message": "success"}', content_type) | |
1310 | assert resp.headers["Content-Length"] == "22" | |
1311 | ||
1312 | url = "http://example.com/1/" | |
1313 | responses.add(responses.GET, url, json=[], auto_calculate_content_length=True) | |
1314 | resp = requests.get(url) | |
1315 | assert_response(resp, "[]", content_type) | |
1316 | assert resp.headers["Content-Length"] == "2" | |
1317 | ||
1318 | run() | |
1319 | assert_reset() | |
1320 | ||
1321 | ||
1322 | def test_auto_calculate_content_length_unicode_body(): | |
1323 | @responses.activate | |
1324 | def run(): | |
1325 | url = "http://example.com/test" | |
1326 | responses.add( | |
1327 | responses.GET, url, body="михољско лето", auto_calculate_content_length=True | |
1328 | ) | |
1329 | resp = requests.get(url) | |
1330 | assert_response(resp, "михољско лето", content_type="text/plain; charset=utf-8") | |
1331 | assert resp.headers["Content-Length"] == "25" | |
1332 | ||
1333 | run() | |
1334 | assert_reset() | |
1335 | ||
1336 | ||
1337 | def test_auto_calculate_content_length_doesnt_work_for_buffered_reader_body(): | |
1338 | @responses.activate | |
1339 | def run(): | |
1340 | url = "http://example.com/test" | |
1341 | responses.add( | |
1342 | responses.GET, | |
1343 | url, | |
1344 | body=BufferedReader(BytesIO(b"testing")), # type: ignore | |
1345 | auto_calculate_content_length=True, | |
1346 | ) | |
1347 | resp = requests.get(url) | |
1348 | assert_response(resp, "testing") | |
1349 | assert "Content-Length" not in resp.headers | |
1350 | ||
1351 | run() | |
1352 | assert_reset() | |
1353 | ||
1354 | ||
1355 | def test_auto_calculate_content_length_doesnt_override_existing_value(): | |
1356 | @responses.activate | |
1357 | def run(): | |
1358 | url = "http://example.com/" | |
1359 | responses.add( | |
1360 | responses.GET, | |
1361 | url, | |
1362 | body="test", | |
1363 | headers={"Content-Length": "2"}, | |
1364 | auto_calculate_content_length=True, | |
1365 | ) | |
1366 | resp = requests.get(url) | |
1367 | assert_response(resp, "test") | |
1368 | assert resp.headers["Content-Length"] == "2" | |
1078 | 1369 | |
1079 | 1370 | run() |
1080 | 1371 | assert_reset() |
1125 | 1416 | assert_response(resp, "posted") |
1126 | 1417 | |
1127 | 1418 | run() |
1419 | assert_reset() | |
1420 | ||
1421 | ||
1422 | def test_passthrough_flag(httpserver): | |
1423 | httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) | |
1424 | response = Response(responses.GET, httpserver.url, body="MOCK") | |
1425 | ||
1426 | @responses.activate | |
1427 | def run_passthrough(): | |
1428 | responses.add(response) | |
1429 | resp = requests.get(httpserver.url) | |
1430 | assert_response(resp, "OK") | |
1431 | ||
1432 | @responses.activate | |
1433 | def run_mocked(): | |
1434 | responses.add(response) | |
1435 | resp = requests.get(httpserver.url) | |
1436 | assert_response(resp, "MOCK") | |
1437 | ||
1438 | run_mocked() | |
1439 | assert_reset() | |
1440 | ||
1441 | response.passthrough = True | |
1442 | run_passthrough() | |
1443 | assert_reset() | |
1444 | ||
1445 | ||
1446 | def test_passthrough_response(httpserver): | |
1447 | httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) | |
1448 | ||
1449 | @responses.activate | |
1450 | def run(): | |
1451 | responses.add(PassthroughResponse(responses.GET, httpserver.url)) | |
1452 | responses.add(responses.GET, "{}/one".format(httpserver.url), body="one") | |
1453 | responses.add(responses.GET, "http://example.com/two", body="two") | |
1454 | ||
1455 | resp = requests.get("http://example.com/two") | |
1456 | assert_response(resp, "two") | |
1457 | resp = requests.get("{}/one".format(httpserver.url)) | |
1458 | assert_response(resp, "one") | |
1459 | resp = requests.get(httpserver.url) | |
1460 | assert_response(resp, "OK") | |
1461 | ||
1462 | assert len(responses.calls) == 3 | |
1463 | responses.assert_call_count(httpserver.url, 1) | |
1464 | ||
1465 | run() | |
1466 | assert_reset() | |
1467 | ||
1468 | ||
1469 | def test_passthrough_response_stream(httpserver): | |
1470 | httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) | |
1471 | ||
1472 | @responses.activate | |
1473 | def run(): | |
1474 | responses.add(PassthroughResponse(responses.GET, httpserver.url)) | |
1475 | content_1 = requests.get(httpserver.url).content | |
1476 | with requests.get(httpserver.url, stream=True) as resp: | |
1477 | content_2 = resp.raw.read() | |
1478 | assert content_1 == content_2 | |
1479 | ||
1480 | run() | |
1481 | assert_reset() | |
1482 | ||
1483 | ||
1484 | def test_passthru_prefixes(httpserver): | |
1485 | httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) | |
1486 | ||
1487 | @responses.activate | |
1488 | def run_constructor_argument(): | |
1489 | with responses.RequestsMock(passthru_prefixes=(httpserver.url,)): | |
1490 | resp = requests.get(httpserver.url) | |
1491 | assert_response(resp, "OK") | |
1492 | ||
1493 | @responses.activate | |
1494 | def run_property_setter(): | |
1495 | with responses.RequestsMock() as m: | |
1496 | m.passthru_prefixes = tuple([httpserver.url]) | |
1497 | resp = requests.get(httpserver.url) | |
1498 | assert_response(resp, "OK") | |
1499 | ||
1500 | run_constructor_argument() | |
1501 | assert_reset() | |
1502 | run_property_setter() | |
1128 | 1503 | assert_reset() |
1129 | 1504 | |
1130 | 1505 | |
1301 | 1676 | assert_reset() |
1302 | 1677 | |
1303 | 1678 | |
1304 | def test_request_matches_post_params(): | |
1305 | @responses.activate | |
1306 | def run(): | |
1307 | responses.add( | |
1308 | method=responses.POST, | |
1309 | url="http://example.com/", | |
1310 | body="one", | |
1311 | match=[ | |
1312 | responses.json_params_matcher( | |
1313 | {"page": {"name": "first", "type": "json"}} | |
1314 | ) | |
1315 | ], | |
1316 | ) | |
1317 | responses.add( | |
1318 | method=responses.POST, | |
1319 | url="http://example.com/", | |
1320 | body="two", | |
1321 | match=[ | |
1322 | responses.urlencoded_params_matcher( | |
1323 | {"page": "second", "type": "urlencoded"} | |
1324 | ) | |
1325 | ], | |
1326 | ) | |
1327 | ||
1328 | resp = requests.request( | |
1329 | "POST", | |
1330 | "http://example.com/", | |
1331 | headers={"Content-Type": "x-www-form-urlencoded"}, | |
1332 | data={"page": "second", "type": "urlencoded"}, | |
1333 | ) | |
1334 | assert_response(resp, "two") | |
1335 | ||
1336 | resp = requests.request( | |
1337 | "POST", | |
1338 | "http://example.com/", | |
1339 | headers={"Content-Type": "application/json"}, | |
1340 | json={"page": {"name": "first", "type": "json"}}, | |
1341 | ) | |
1342 | assert_response(resp, "one") | |
1343 | ||
1344 | run() | |
1345 | assert_reset() | |
1346 | ||
1347 | ||
1348 | def test_request_matches_empty_body(): | |
1349 | @responses.activate | |
1350 | def run(): | |
1351 | responses.add( | |
1352 | method=responses.POST, | |
1353 | url="http://example.com/", | |
1354 | body="one", | |
1355 | match=[responses.json_params_matcher(None)], | |
1356 | ) | |
1357 | ||
1358 | responses.add( | |
1359 | method=responses.POST, | |
1360 | url="http://example.com/", | |
1361 | body="two", | |
1362 | match=[responses.urlencoded_params_matcher(None)], | |
1363 | ) | |
1364 | ||
1365 | resp = requests.request("POST", "http://example.com/") | |
1366 | assert_response(resp, "one") | |
1367 | ||
1368 | resp = requests.request( | |
1369 | "POST", | |
1370 | "http://example.com/", | |
1371 | headers={"Content-Type": "x-www-form-urlencoded"}, | |
1372 | ) | |
1373 | assert_response(resp, "two") | |
1374 | ||
1375 | run() | |
1376 | assert_reset() | |
1377 | ||
1378 | ||
1379 | 1679 | def test_fail_request_error(): |
1380 | @responses.activate | |
1381 | def run(): | |
1382 | responses.add("POST", "http://example1.com") | |
1383 | responses.add("GET", "http://example.com") | |
1384 | responses.add( | |
1385 | "POST", | |
1386 | "http://example.com", | |
1387 | match=[responses.urlencoded_params_matcher({"foo": "bar"})], | |
1388 | ) | |
1389 | ||
1390 | with pytest.raises(ConnectionError) as excinfo: | |
1391 | requests.post("http://example.com", data={"id": "bad"}) | |
1392 | msg = str(excinfo.value) | |
1393 | assert "- POST http://example1.com/ URL does not match" in msg | |
1394 | assert "- GET http://example.com/ Method does not match" in msg | |
1395 | assert "- POST http://example.com/ Parameters do not match" in msg | |
1680 | """ | |
1681 | Validate that exception is raised if request URL/Method/kwargs don't match | |
1682 | :return: | |
1683 | """ | |
1684 | ||
1685 | def run(): | |
1686 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
1687 | rsps.add("POST", "http://example1.com") | |
1688 | rsps.add("GET", "http://example.com") | |
1689 | ||
1690 | with pytest.raises(ConnectionError) as excinfo: | |
1691 | requests.post("http://example.com", data={"id": "bad"}) | |
1692 | ||
1693 | msg = str(excinfo.value) | |
1694 | assert "- POST http://example1.com/ URL does not match" in msg | |
1695 | assert "- GET http://example.com/ Method does not match" in msg | |
1396 | 1696 | |
1397 | 1697 | run() |
1398 | 1698 | assert_reset() |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: responses |
2 | Version: 0.13.4 | |
2 | Version: 0.14.0 | |
3 | 3 | Summary: A utility library for mocking out the `requests` Python library. |
4 | 4 | Home-page: https://github.com/getsentry/responses |
5 | 5 | Author: David Cramer |
6 | 6 | License: Apache 2.0 |
7 | Description: Responses | |
8 | ========= | |
9 | ||
10 | .. image:: https://img.shields.io/pypi/v/responses.svg | |
11 | :target: https://pypi.python.org/pypi/responses/ | |
12 | ||
13 | .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master | |
14 | :target: https://travis-ci.org/getsentry/responses | |
15 | ||
16 | .. image:: https://img.shields.io/pypi/pyversions/responses.svg | |
17 | :target: https://pypi.org/project/responses/ | |
18 | ||
19 | A utility library for mocking out the ``requests`` Python library. | |
20 | ||
21 | .. note:: | |
22 | ||
23 | Responses requires Python 2.7 or newer, and requests >= 2.0 | |
24 | ||
25 | ||
26 | Installing | |
27 | ---------- | |
28 | ||
29 | ``pip install responses`` | |
30 | ||
31 | ||
32 | Basics | |
33 | ------ | |
34 | ||
35 | The core of ``responses`` comes from registering mock responses: | |
36 | ||
37 | .. code-block:: python | |
38 | ||
39 | import responses | |
40 | import requests | |
41 | ||
42 | @responses.activate | |
43 | def test_simple(): | |
44 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
45 | json={'error': 'not found'}, status=404) | |
46 | ||
47 | resp = requests.get('http://twitter.com/api/1/foobar') | |
48 | ||
49 | assert resp.json() == {"error": "not found"} | |
50 | ||
51 | assert len(responses.calls) == 1 | |
52 | assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' | |
53 | assert responses.calls[0].response.text == '{"error": "not found"}' | |
54 | ||
55 | If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise | |
56 | a ``ConnectionError``: | |
57 | ||
58 | .. code-block:: python | |
59 | ||
60 | import responses | |
61 | import requests | |
62 | ||
63 | from requests.exceptions import ConnectionError | |
64 | ||
65 | @responses.activate | |
66 | def test_simple(): | |
67 | with pytest.raises(ConnectionError): | |
68 | requests.get('http://twitter.com/api/1/foobar') | |
69 | ||
70 | Lastly, you can pass an ``Exception`` as the body to trigger an error on the request: | |
71 | ||
72 | .. code-block:: python | |
73 | ||
74 | import responses | |
75 | import requests | |
76 | ||
77 | @responses.activate | |
78 | def test_simple(): | |
79 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
80 | body=Exception('...')) | |
81 | with pytest.raises(Exception): | |
82 | requests.get('http://twitter.com/api/1/foobar') | |
83 | ||
84 | ||
85 | Response Parameters | |
86 | ------------------- | |
87 | ||
88 | Responses are automatically registered via params on ``add``, but can also be | |
89 | passed directly: | |
90 | ||
91 | .. code-block:: python | |
92 | ||
93 | import responses | |
94 | ||
95 | responses.add( | |
96 | responses.Response( | |
97 | method='GET', | |
98 | url='http://example.com', | |
99 | ) | |
100 | ) | |
101 | ||
102 | The following attributes can be passed to a Response mock: | |
103 | ||
104 | method (``str``) | |
105 | The HTTP method (GET, POST, etc). | |
106 | ||
107 | url (``str`` or compiled regular expression) | |
108 | The full resource URL. | |
109 | ||
110 | match_querystring (``bool``) | |
111 | Include the query string when matching requests. | |
112 | Enabled by default if the response URL contains a query string, | |
113 | disabled if it doesn't or the URL is a regular expression. | |
114 | ||
115 | body (``str`` or ``BufferedReader``) | |
116 | The response body. | |
117 | ||
118 | json | |
119 | A Python object representing the JSON response body. Automatically configures | |
120 | the appropriate Content-Type. | |
121 | ||
122 | status (``int``) | |
123 | The HTTP status code. | |
124 | ||
125 | content_type (``content_type``) | |
126 | Defaults to ``text/plain``. | |
127 | ||
128 | headers (``dict``) | |
129 | Response headers. | |
130 | ||
131 | stream (``bool``) | |
132 | Disabled by default. Indicates the response should use the streaming API. | |
133 | ||
134 | match (``list``) | |
135 | A list of callbacks to match requests based on request body contents. | |
136 | ||
137 | ||
138 | Matching Request Parameters | |
139 | --------------------------- | |
140 | ||
141 | When adding responses for endpoints that are sent request data you can add | |
142 | matchers to ensure your code is sending the right parameters and provide | |
143 | different responses based on the request body contents. Responses provides | |
144 | matchers for JSON and URLencoded request bodies and you can supply your own for | |
145 | other formats. | |
146 | ||
147 | .. code-block:: python | |
148 | ||
149 | import responses | |
150 | import requests | |
151 | ||
152 | @responses.activate | |
153 | def test_calc_api(): | |
154 | responses.add( | |
155 | responses.POST, | |
156 | url='http://calc.com/sum', | |
157 | body="4", | |
158 | match=[ | |
159 | responses.urlencoded_params_matcher({"left": "1", "right": "3"}) | |
160 | ] | |
161 | ) | |
162 | requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) | |
163 | ||
164 | Matching JSON encoded data can be done with ``responses.json_params_matcher()``. | |
165 | If your application uses other encodings you can build your own matcher that | |
166 | returns ``True`` or ``False`` if the request parameters match. Your matcher can | |
167 | expect a ``request_body`` parameter to be provided by responses. | |
168 | ||
169 | Dynamic Responses | |
170 | ----------------- | |
171 | ||
172 | You can utilize callbacks to provide dynamic responses. The callback must return | |
173 | a tuple of (``status``, ``headers``, ``body``). | |
174 | ||
175 | .. code-block:: python | |
176 | ||
177 | import json | |
178 | ||
179 | import responses | |
180 | import requests | |
181 | ||
182 | @responses.activate | |
183 | def test_calc_api(): | |
184 | ||
185 | def request_callback(request): | |
186 | payload = json.loads(request.body) | |
187 | resp_body = {'value': sum(payload['numbers'])} | |
188 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
189 | return (200, headers, json.dumps(resp_body)) | |
190 | ||
191 | responses.add_callback( | |
192 | responses.POST, 'http://calc.com/sum', | |
193 | callback=request_callback, | |
194 | content_type='application/json', | |
195 | ) | |
196 | ||
197 | resp = requests.post( | |
198 | 'http://calc.com/sum', | |
199 | json.dumps({'numbers': [1, 2, 3]}), | |
200 | headers={'content-type': 'application/json'}, | |
201 | ) | |
202 | ||
203 | assert resp.json() == {'value': 6} | |
204 | ||
205 | assert len(responses.calls) == 1 | |
206 | assert responses.calls[0].request.url == 'http://calc.com/sum' | |
207 | assert responses.calls[0].response.text == '{"value": 6}' | |
208 | assert ( | |
209 | responses.calls[0].response.headers['request-id'] == | |
210 | '728d329e-0e86-11e4-a748-0c84dc037c13' | |
211 | ) | |
212 | ||
213 | You can also pass a compiled regex to ``add_callback`` to match multiple urls: | |
214 | ||
215 | .. code-block:: python | |
216 | ||
217 | import re, json | |
218 | ||
219 | from functools import reduce | |
220 | ||
221 | import responses | |
222 | import requests | |
223 | ||
224 | operators = { | |
225 | 'sum': lambda x, y: x+y, | |
226 | 'prod': lambda x, y: x*y, | |
227 | 'pow': lambda x, y: x**y | |
228 | } | |
229 | ||
230 | @responses.activate | |
231 | def test_regex_url(): | |
232 | ||
233 | def request_callback(request): | |
234 | payload = json.loads(request.body) | |
235 | operator_name = request.path_url[1:] | |
236 | ||
237 | operator = operators[operator_name] | |
238 | ||
239 | resp_body = {'value': reduce(operator, payload['numbers'])} | |
240 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
241 | return (200, headers, json.dumps(resp_body)) | |
242 | ||
243 | responses.add_callback( | |
244 | responses.POST, | |
245 | re.compile('http://calc.com/(sum|prod|pow|unsupported)'), | |
246 | callback=request_callback, | |
247 | content_type='application/json', | |
248 | ) | |
249 | ||
250 | resp = requests.post( | |
251 | 'http://calc.com/prod', | |
252 | json.dumps({'numbers': [2, 3, 4]}), | |
253 | headers={'content-type': 'application/json'}, | |
254 | ) | |
255 | assert resp.json() == {'value': 24} | |
256 | ||
257 | test_regex_url() | |
258 | ||
259 | ||
260 | If you want to pass extra keyword arguments to the callback function, for example when reusing | |
261 | a callback function to give a slightly different result, you can use ``functools.partial``: | |
262 | ||
263 | .. code-block:: python | |
264 | ||
265 | from functools import partial | |
266 | ||
267 | ... | |
268 | ||
269 | def request_callback(request, id=None): | |
270 | payload = json.loads(request.body) | |
271 | resp_body = {'value': sum(payload['numbers'])} | |
272 | headers = {'request-id': id} | |
273 | return (200, headers, json.dumps(resp_body)) | |
274 | ||
275 | responses.add_callback( | |
276 | responses.POST, 'http://calc.com/sum', | |
277 | callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'), | |
278 | content_type='application/json', | |
279 | ) | |
280 | ||
281 | ||
282 | You can see params passed in the original ``request`` in ``responses.calls[].request.params``: | |
283 | ||
284 | .. code-block:: python | |
285 | ||
286 | import responses | |
287 | import requests | |
288 | ||
289 | @responses.activate | |
290 | def test_request_params(): | |
291 | responses.add( | |
292 | method=responses.GET, | |
293 | url="http://example.com?hello=world", | |
294 | body="test", | |
295 | match_querystring=False, | |
296 | ) | |
297 | ||
298 | resp = requests.get('http://example.com', params={"hello": "world"}) | |
299 | assert responses.calls[0].request.params == {"hello": "world"} | |
300 | ||
301 | Responses as a context manager | |
302 | ------------------------------ | |
303 | ||
304 | .. code-block:: python | |
305 | ||
306 | import responses | |
307 | import requests | |
308 | ||
309 | def test_my_api(): | |
310 | with responses.RequestsMock() as rsps: | |
311 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
312 | body='{}', status=200, | |
313 | content_type='application/json') | |
314 | resp = requests.get('http://twitter.com/api/1/foobar') | |
315 | ||
316 | assert resp.status_code == 200 | |
317 | ||
318 | # outside the context manager requests will hit the remote server | |
319 | resp = requests.get('http://twitter.com/api/1/foobar') | |
320 | resp.status_code == 404 | |
321 | ||
322 | Responses as a pytest fixture | |
323 | ----------------------------- | |
324 | ||
325 | .. code-block:: python | |
326 | ||
327 | @pytest.fixture | |
328 | def mocked_responses(): | |
329 | with responses.RequestsMock() as rsps: | |
330 | yield rsps | |
331 | ||
332 | def test_api(mocked_responses): | |
333 | mocked_responses.add( | |
334 | responses.GET, 'http://twitter.com/api/1/foobar', | |
335 | body='{}', status=200, | |
336 | content_type='application/json') | |
337 | resp = requests.get('http://twitter.com/api/1/foobar') | |
338 | assert resp.status_code == 200 | |
339 | ||
340 | Responses inside a unittest setUp() | |
341 | ----------------------------------- | |
342 | ||
343 | When run with unittest tests, this can be used to set up some | |
344 | generic class-level responses, that may be complemented by each test | |
345 | ||
346 | .. code-block:: python | |
347 | ||
348 | def setUp(): | |
349 | self.responses = responses.RequestsMock() | |
350 | self.responses.start() | |
351 | ||
352 | # self.responses.add(...) | |
353 | ||
354 | self.addCleanup(self.responses.stop) | |
355 | self.addCleanup(self.responses.reset) | |
356 | ||
357 | def test_api(self): | |
358 | self.responses.add( | |
359 | responses.GET, 'http://twitter.com/api/1/foobar', | |
360 | body='{}', status=200, | |
361 | content_type='application/json') | |
362 | resp = requests.get('http://twitter.com/api/1/foobar') | |
363 | assert resp.status_code == 200 | |
364 | ||
365 | Assertions on declared responses | |
366 | -------------------------------- | |
367 | ||
368 | When used as a context manager, Responses will, by default, raise an assertion | |
369 | error if a url was registered but not accessed. This can be disabled by passing | |
370 | the ``assert_all_requests_are_fired`` value: | |
371 | ||
372 | .. code-block:: python | |
373 | ||
374 | import responses | |
375 | import requests | |
376 | ||
377 | def test_my_api(): | |
378 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
379 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
380 | body='{}', status=200, | |
381 | content_type='application/json') | |
382 | ||
383 | assert_call_count | |
384 | ----------------- | |
385 | ||
386 | Assert that the request was called exactly n times. | |
387 | ||
388 | .. code-block:: python | |
389 | ||
390 | import responses | |
391 | import requests | |
392 | ||
393 | @responses.activate | |
394 | def test_assert_call_count(): | |
395 | responses.add(responses.GET, "http://example.com") | |
396 | ||
397 | requests.get("http://example.com") | |
398 | assert responses.assert_call_count("http://example.com", 1) is True | |
399 | ||
400 | requests.get("http://example.com") | |
401 | with pytest.raises(AssertionError) as excinfo: | |
402 | responses.assert_call_count("http://example.com", 1) | |
403 | assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) | |
404 | ||
405 | ||
406 | Multiple Responses | |
407 | ------------------ | |
408 | ||
409 | You can also add multiple responses for the same url: | |
410 | ||
411 | .. code-block:: python | |
412 | ||
413 | import responses | |
414 | import requests | |
415 | ||
416 | @responses.activate | |
417 | def test_my_api(): | |
418 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) | |
419 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
420 | body='{}', status=200, | |
421 | content_type='application/json') | |
422 | ||
423 | resp = requests.get('http://twitter.com/api/1/foobar') | |
424 | assert resp.status_code == 500 | |
425 | resp = requests.get('http://twitter.com/api/1/foobar') | |
426 | assert resp.status_code == 200 | |
427 | ||
428 | ||
429 | Using a callback to modify the response | |
430 | --------------------------------------- | |
431 | ||
432 | If you use customized processing in `requests` via subclassing/mixins, or if you | |
433 | have library tools that interact with `requests` at a low level, you may need | |
434 | to add extended processing to the mocked Response object to fully simulate the | |
435 | environment for your tests. A `response_callback` can be used, which will be | |
436 | wrapped by the library before being returned to the caller. The callback | |
437 | accepts a `response` as it's single argument, and is expected to return a | |
438 | single `response` object. | |
439 | ||
440 | .. code-block:: python | |
441 | ||
442 | import responses | |
443 | import requests | |
444 | ||
445 | def response_callback(resp): | |
446 | resp.callback_processed = True | |
447 | return resp | |
448 | ||
449 | with responses.RequestsMock(response_callback=response_callback) as m: | |
450 | m.add(responses.GET, 'http://example.com', body=b'test') | |
451 | resp = requests.get('http://example.com') | |
452 | assert resp.text == "test" | |
453 | assert hasattr(resp, 'callback_processed') | |
454 | assert resp.callback_processed is True | |
455 | ||
456 | ||
457 | Passing through real requests | |
458 | ----------------------------- | |
459 | ||
460 | In some cases you may wish to allow for certain requests to pass through responses | |
461 | and hit a real server. This can be done with the ``add_passthru`` methods: | |
462 | ||
463 | .. code-block:: python | |
464 | ||
465 | import responses | |
466 | ||
467 | @responses.activate | |
468 | def test_my_api(): | |
469 | responses.add_passthru('https://percy.io') | |
470 | ||
471 | This will allow any requests matching that prefix, that is otherwise not registered | |
472 | as a mock response, to passthru using the standard behavior. | |
473 | ||
474 | Regex can be used like: | |
475 | ||
476 | .. code-block:: python | |
477 | ||
478 | responses.add_passthru(re.compile('https://percy.io/\\w+')) | |
479 | ||
480 | ||
481 | Viewing/Modifying registered responses | |
482 | -------------------------------------- | |
483 | ||
484 | Registered responses are available as a public method of the RequestMock | |
485 | instance. It is sometimes useful for debugging purposes to view the stack of | |
486 | registered responses which can be accessed via ``responses.registered()``. | |
487 | ||
488 | The ``replace`` function allows a previously registered ``response`` to be | |
489 | changed. The method signature is identical to ``add``. ``response`` s are | |
490 | identified using ``method`` and ``url``. Only the first matched ``response`` is | |
491 | replaced. | |
492 | ||
493 | .. code-block:: python | |
494 | ||
495 | import responses | |
496 | import requests | |
497 | ||
498 | @responses.activate | |
499 | def test_replace(): | |
500 | ||
501 | responses.add(responses.GET, 'http://example.org', json={'data': 1}) | |
502 | responses.replace(responses.GET, 'http://example.org', json={'data': 2}) | |
503 | ||
504 | resp = requests.get('http://example.org') | |
505 | ||
506 | assert resp.json() == {'data': 2} | |
507 | ||
508 | ||
509 | The ``upsert`` function allows a previously registered ``response`` to be | |
510 | changed like ``replace``. If the response is registered, the ``upsert`` function | |
511 | will registered it like ``add``. | |
512 | ||
513 | ``remove`` takes a ``method`` and ``url`` argument and will remove **all** | |
514 | matched responses from the registered list. | |
515 | ||
516 | Finally, ``reset`` will reset all registered responses. | |
517 | ||
518 | Contributing | |
519 | ------------ | |
520 | ||
521 | Responses uses several linting and autoformatting utilities, so it's important that when | |
522 | submitting patches you use the appropriate toolchain: | |
523 | ||
524 | Clone the repository: | |
525 | ||
526 | .. code-block:: shell | |
527 | ||
528 | git clone https://github.com/getsentry/responses.git | |
529 | ||
530 | Create an environment (e.g. with ``virtualenv``): | |
531 | ||
532 | .. code-block:: shell | |
533 | ||
534 | virtualenv .env && source .env/bin/activate | |
535 | ||
536 | Configure development requirements: | |
537 | ||
538 | .. code-block:: shell | |
539 | ||
540 | make develop | |
541 | ||
542 | Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for | |
543 | testing. You can run all tests by: | |
544 | ||
545 | .. code-block:: shell | |
546 | ||
547 | pytest | |
548 | ||
549 | And run a single test by: | |
550 | ||
551 | .. code-block:: shell | |
552 | ||
553 | pytest -k '<test_function_name>' | |
554 | ||
555 | 7 | Platform: UNKNOWN |
556 | 8 | Classifier: Intended Audience :: Developers |
557 | 9 | Classifier: Intended Audience :: System Administrators |
565 | 17 | Classifier: Programming Language :: Python :: 3.7 |
566 | 18 | Classifier: Programming Language :: Python :: 3.8 |
567 | 19 | Classifier: Programming Language :: Python :: 3.9 |
20 | Classifier: Programming Language :: Python :: 3.10 | |
568 | 21 | Classifier: Topic :: Software Development |
569 | 22 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* |
570 | 23 | Description-Content-Type: text/x-rst |
571 | 24 | Provides-Extra: tests |
25 | License-File: LICENSE | |
26 | ||
27 | Responses | |
28 | ========= | |
29 | ||
30 | .. image:: https://img.shields.io/pypi/v/responses.svg | |
31 | :target: https://pypi.python.org/pypi/responses/ | |
32 | ||
33 | .. image:: https://img.shields.io/pypi/pyversions/responses.svg | |
34 | :target: https://pypi.org/project/responses/ | |
35 | ||
36 | .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg | |
37 | :target: https://codecov.io/gh/getsentry/responses/ | |
38 | ||
39 | A utility library for mocking out the ``requests`` Python library. | |
40 | ||
41 | .. note:: | |
42 | ||
43 | Responses requires Python 2.7 or newer, and requests >= 2.0 | |
44 | ||
45 | ||
46 | Installing | |
47 | ---------- | |
48 | ||
49 | ``pip install responses`` | |
50 | ||
51 | ||
52 | Basics | |
53 | ------ | |
54 | ||
55 | The core of ``responses`` comes from registering mock responses: | |
56 | ||
57 | .. code-block:: python | |
58 | ||
59 | import responses | |
60 | import requests | |
61 | ||
62 | @responses.activate | |
63 | def test_simple(): | |
64 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
65 | json={'error': 'not found'}, status=404) | |
66 | ||
67 | resp = requests.get('http://twitter.com/api/1/foobar') | |
68 | ||
69 | assert resp.json() == {"error": "not found"} | |
70 | ||
71 | assert len(responses.calls) == 1 | |
72 | assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' | |
73 | assert responses.calls[0].response.text == '{"error": "not found"}' | |
74 | ||
75 | If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise | |
76 | a ``ConnectionError``: | |
77 | ||
78 | .. code-block:: python | |
79 | ||
80 | import responses | |
81 | import requests | |
82 | ||
83 | from requests.exceptions import ConnectionError | |
84 | ||
85 | @responses.activate | |
86 | def test_simple(): | |
87 | with pytest.raises(ConnectionError): | |
88 | requests.get('http://twitter.com/api/1/foobar') | |
89 | ||
90 | Lastly, you can pass an ``Exception`` as the body to trigger an error on the request: | |
91 | ||
92 | .. code-block:: python | |
93 | ||
94 | import responses | |
95 | import requests | |
96 | ||
97 | @responses.activate | |
98 | def test_simple(): | |
99 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
100 | body=Exception('...')) | |
101 | with pytest.raises(Exception): | |
102 | requests.get('http://twitter.com/api/1/foobar') | |
103 | ||
104 | ||
105 | Response Parameters | |
106 | ------------------- | |
107 | ||
108 | Responses are automatically registered via params on ``add``, but can also be | |
109 | passed directly: | |
110 | ||
111 | .. code-block:: python | |
112 | ||
113 | import responses | |
114 | ||
115 | responses.add( | |
116 | responses.Response( | |
117 | method='GET', | |
118 | url='http://example.com', | |
119 | ) | |
120 | ) | |
121 | ||
122 | The following attributes can be passed to a Response mock: | |
123 | ||
124 | method (``str``) | |
125 | The HTTP method (GET, POST, etc). | |
126 | ||
127 | url (``str`` or compiled regular expression) | |
128 | The full resource URL. | |
129 | ||
130 | match_querystring (``bool``) | |
131 | Include the query string when matching requests. | |
132 | Enabled by default if the response URL contains a query string, | |
133 | disabled if it doesn't or the URL is a regular expression. | |
134 | ||
135 | body (``str`` or ``BufferedReader``) | |
136 | The response body. | |
137 | ||
138 | json | |
139 | A Python object representing the JSON response body. Automatically configures | |
140 | the appropriate Content-Type. | |
141 | ||
142 | status (``int``) | |
143 | The HTTP status code. | |
144 | ||
145 | content_type (``content_type``) | |
146 | Defaults to ``text/plain``. | |
147 | ||
148 | headers (``dict``) | |
149 | Response headers. | |
150 | ||
151 | stream (``bool``) | |
152 | DEPRECATED: use ``stream`` argument in request directly | |
153 | ||
154 | auto_calculate_content_length (``bool``) | |
155 | Disabled by default. Automatically calculates the length of a supplied string or JSON body. | |
156 | ||
157 | match (``list``) | |
158 | A list of callbacks to match requests based on request attributes. | |
159 | Current module provides multiple matchers that you can use to match: | |
160 | ||
161 | * body contents in JSON format | |
162 | * body contents in URL encoded data format | |
163 | * request query parameters | |
164 | * request query string (similar to query parameters but takes string as input) | |
165 | * kwargs provided to request e.g. ``stream``, ``verify`` | |
166 | * 'multipart/form-data' content and headers in request | |
167 | * request headers | |
168 | * request fragment identifier | |
169 | ||
170 | Alternatively user can create custom matcher. | |
171 | Read more `Matching Requests`_ | |
172 | ||
173 | ||
174 | Matching Requests | |
175 | ----------------- | |
176 | ||
177 | When adding responses for endpoints that are sent request data you can add | |
178 | matchers to ensure your code is sending the right parameters and provide | |
179 | different responses based on the request body contents. Responses provides | |
180 | matchers for JSON and URL-encoded request bodies and you can supply your own for | |
181 | other formats. | |
182 | ||
183 | .. code-block:: python | |
184 | ||
185 | import responses | |
186 | import requests | |
187 | from responses import matchers | |
188 | ||
189 | @responses.activate | |
190 | def test_calc_api(): | |
191 | responses.add( | |
192 | responses.POST, | |
193 | url='http://calc.com/sum', | |
194 | body="4", | |
195 | match=[ | |
196 | matchers.urlencoded_params_matcher({"left": "1", "right": "3"}) | |
197 | ] | |
198 | ) | |
199 | requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) | |
200 | ||
201 | Matching JSON encoded data can be done with ``matchers.json_params_matcher()``. | |
202 | If your application uses other encodings you can build your own matcher that | |
203 | returns ``True`` or ``False`` if the request parameters match. Your matcher can | |
204 | expect a ``request`` parameter to be provided by responses. | |
205 | ||
206 | Similarly, you can use the ``matchers.query_param_matcher`` function to match | |
207 | against the ``params`` request parameter. | |
208 | Note, you must set ``match_querystring=False`` | |
209 | ||
210 | .. code-block:: python | |
211 | ||
212 | import responses | |
213 | import requests | |
214 | from responses import matchers | |
215 | ||
216 | @responses.activate | |
217 | def test_calc_api(): | |
218 | url = "http://example.com/test" | |
219 | params = {"hello": "world", "I am": "a big test"} | |
220 | responses.add( | |
221 | method=responses.GET, | |
222 | url=url, | |
223 | body="test", | |
224 | match=[matchers.query_param_matcher(params)], | |
225 | match_querystring=False, | |
226 | ) | |
227 | ||
228 | resp = requests.get(url, params=params) | |
229 | ||
230 | constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" | |
231 | assert resp.url == constructed_url | |
232 | assert resp.request.url == constructed_url | |
233 | assert resp.request.params == params | |
234 | ||
235 | ||
236 | As alternative, you can use query string value in ``matchers.query_string_matcher`` | |
237 | ||
238 | .. code-block:: python | |
239 | ||
240 | import requests | |
241 | import responses | |
242 | from responses import matchers | |
243 | ||
244 | @responses.activate | |
245 | def my_func(): | |
246 | responses.add( | |
247 | responses.GET, | |
248 | "https://httpbin.org/get", | |
249 | match=[matchers.query_string_matcher("didi=pro&test=1")], | |
250 | ) | |
251 | resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"}) | |
252 | ||
253 | my_func() | |
254 | ||
255 | To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match | |
256 | against the request kwargs. | |
257 | Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated | |
258 | ||
259 | .. code-block:: python | |
260 | ||
261 | import responses | |
262 | import requests | |
263 | from responses import matchers | |
264 | ||
265 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
266 | req_kwargs = { | |
267 | "stream": True, | |
268 | "verify": False, | |
269 | } | |
270 | rsps.add( | |
271 | "GET", | |
272 | "http://111.com", | |
273 | match=[matchers.request_kwargs_matcher(req_kwargs)], | |
274 | ) | |
275 | ||
276 | requests.get("http://111.com", stream=True) | |
277 | ||
278 | # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False} | |
279 | ||
280 | To validate request body and headers for ``multipart/form-data`` data you can use | |
281 | ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared | |
282 | to the request: | |
283 | ||
284 | .. code-block:: python | |
285 | ||
286 | import requests | |
287 | import responses | |
288 | from responses.matchers import multipart_matcher | |
289 | ||
290 | @responses.activate | |
291 | def my_func(): | |
292 | req_data = {"some": "other", "data": "fields"} | |
293 | req_files = {"file_name": b"Old World!"} | |
294 | responses.add( | |
295 | responses.POST, url="http://httpbin.org/post", | |
296 | match=[multipart_matcher(req_data, req_files)] | |
297 | ) | |
298 | resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"}) | |
299 | ||
300 | my_func() | |
301 | # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs. | |
302 | ||
303 | ||
304 | To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``. | |
305 | The matcher takes fragment string (everything after ``#`` sign) as input for comparison: | |
306 | ||
307 | .. code-block:: python | |
308 | ||
309 | import requests | |
310 | import responses | |
311 | from responses.matchers import fragment_identifier_matcher | |
312 | ||
313 | @responses.activate | |
314 | def run(): | |
315 | url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" | |
316 | responses.add( | |
317 | responses.GET, | |
318 | url, | |
319 | match_querystring=True, | |
320 | match=[fragment_identifier_matcher("test=1&foo=bar")], | |
321 | body=b"test", | |
322 | ) | |
323 | ||
324 | # two requests to check reversed order of fragment identifier | |
325 | resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") | |
326 | resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") | |
327 | ||
328 | run() | |
329 | ||
330 | Matching Request Headers | |
331 | ------------------------ | |
332 | ||
333 | When adding responses you can specify matchers to ensure that your code is | |
334 | sending the right headers and provide different responses based on the request | |
335 | headers. | |
336 | ||
337 | .. code-block:: python | |
338 | ||
339 | import responses | |
340 | import requests | |
341 | from responses import matchers | |
342 | ||
343 | ||
344 | @responses.activate | |
345 | def test_content_type(): | |
346 | responses.add( | |
347 | responses.GET, | |
348 | url="http://example.com/", | |
349 | body="hello world", | |
350 | match=[ | |
351 | matchers.header_matcher({"Accept": "text/plain"}) | |
352 | ] | |
353 | ) | |
354 | ||
355 | responses.add( | |
356 | responses.GET, | |
357 | url="http://example.com/", | |
358 | json={"content": "hello world"}, | |
359 | match=[ | |
360 | matchers.header_matcher({"Accept": "application/json"}) | |
361 | ] | |
362 | ) | |
363 | ||
364 | # request in reverse order to how they were added! | |
365 | resp = requests.get("http://example.com/", headers={"Accept": "application/json"}) | |
366 | assert resp.json() == {"content": "hello world"} | |
367 | ||
368 | resp = requests.get("http://example.com/", headers={"Accept": "text/plain"}) | |
369 | assert resp.text == "hello world" | |
370 | ||
371 | Because ``requests`` will send several standard headers in addition to what was | |
372 | specified by your code, request headers that are additional to the ones | |
373 | passed to the matcher are ignored by default. You can change this behaviour by | |
374 | passing ``strict_match=True`` to the matcher to ensure that only the headers | |
375 | that you're expecting are sent and no others. Note that you will probably have | |
376 | to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't | |
377 | include any additional headers. | |
378 | ||
379 | .. code-block:: python | |
380 | ||
381 | import responses | |
382 | import requests | |
383 | from responses import matchers | |
384 | ||
385 | @responses.activate | |
386 | def test_content_type(): | |
387 | responses.add( | |
388 | responses.GET, | |
389 | url="http://example.com/", | |
390 | body="hello world", | |
391 | match=[ | |
392 | matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) | |
393 | ] | |
394 | ) | |
395 | ||
396 | # this will fail because requests adds its own headers | |
397 | with pytest.raises(ConnectionError): | |
398 | requests.get("http://example.com/", headers={"Accept": "text/plain"}) | |
399 | ||
400 | # a prepared request where you overwrite the headers before sending will work | |
401 | session = requests.Session() | |
402 | prepped = session.prepare_request( | |
403 | requests.Request( | |
404 | method="GET", | |
405 | url="http://example.com/", | |
406 | ) | |
407 | ) | |
408 | prepped.headers = {"Accept": "text/plain"} | |
409 | ||
410 | resp = session.send(prepped) | |
411 | assert resp.text == "hello world" | |
412 | ||
413 | Dynamic Responses | |
414 | ----------------- | |
415 | ||
416 | You can utilize callbacks to provide dynamic responses. The callback must return | |
417 | a tuple of (``status``, ``headers``, ``body``). | |
418 | ||
419 | .. code-block:: python | |
420 | ||
421 | import json | |
422 | ||
423 | import responses | |
424 | import requests | |
425 | ||
426 | @responses.activate | |
427 | def test_calc_api(): | |
428 | ||
429 | def request_callback(request): | |
430 | payload = json.loads(request.body) | |
431 | resp_body = {'value': sum(payload['numbers'])} | |
432 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
433 | return (200, headers, json.dumps(resp_body)) | |
434 | ||
435 | responses.add_callback( | |
436 | responses.POST, 'http://calc.com/sum', | |
437 | callback=request_callback, | |
438 | content_type='application/json', | |
439 | ) | |
440 | ||
441 | resp = requests.post( | |
442 | 'http://calc.com/sum', | |
443 | json.dumps({'numbers': [1, 2, 3]}), | |
444 | headers={'content-type': 'application/json'}, | |
445 | ) | |
446 | ||
447 | assert resp.json() == {'value': 6} | |
448 | ||
449 | assert len(responses.calls) == 1 | |
450 | assert responses.calls[0].request.url == 'http://calc.com/sum' | |
451 | assert responses.calls[0].response.text == '{"value": 6}' | |
452 | assert ( | |
453 | responses.calls[0].response.headers['request-id'] == | |
454 | '728d329e-0e86-11e4-a748-0c84dc037c13' | |
455 | ) | |
456 | ||
457 | You can also pass a compiled regex to ``add_callback`` to match multiple urls: | |
458 | ||
459 | .. code-block:: python | |
460 | ||
461 | import re, json | |
462 | ||
463 | from functools import reduce | |
464 | ||
465 | import responses | |
466 | import requests | |
467 | ||
468 | operators = { | |
469 | 'sum': lambda x, y: x+y, | |
470 | 'prod': lambda x, y: x*y, | |
471 | 'pow': lambda x, y: x**y | |
472 | } | |
473 | ||
474 | @responses.activate | |
475 | def test_regex_url(): | |
476 | ||
477 | def request_callback(request): | |
478 | payload = json.loads(request.body) | |
479 | operator_name = request.path_url[1:] | |
480 | ||
481 | operator = operators[operator_name] | |
482 | ||
483 | resp_body = {'value': reduce(operator, payload['numbers'])} | |
484 | headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} | |
485 | return (200, headers, json.dumps(resp_body)) | |
486 | ||
487 | responses.add_callback( | |
488 | responses.POST, | |
489 | re.compile('http://calc.com/(sum|prod|pow|unsupported)'), | |
490 | callback=request_callback, | |
491 | content_type='application/json', | |
492 | ) | |
493 | ||
494 | resp = requests.post( | |
495 | 'http://calc.com/prod', | |
496 | json.dumps({'numbers': [2, 3, 4]}), | |
497 | headers={'content-type': 'application/json'}, | |
498 | ) | |
499 | assert resp.json() == {'value': 24} | |
500 | ||
501 | test_regex_url() | |
502 | ||
503 | ||
504 | If you want to pass extra keyword arguments to the callback function, for example when reusing | |
505 | a callback function to give a slightly different result, you can use ``functools.partial``: | |
506 | ||
507 | .. code-block:: python | |
508 | ||
509 | from functools import partial | |
510 | ||
511 | ... | |
512 | ||
513 | def request_callback(request, id=None): | |
514 | payload = json.loads(request.body) | |
515 | resp_body = {'value': sum(payload['numbers'])} | |
516 | headers = {'request-id': id} | |
517 | return (200, headers, json.dumps(resp_body)) | |
518 | ||
519 | responses.add_callback( | |
520 | responses.POST, 'http://calc.com/sum', | |
521 | callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'), | |
522 | content_type='application/json', | |
523 | ) | |
524 | ||
525 | ||
526 | You can see params passed in the original ``request`` in ``responses.calls[].request.params``: | |
527 | ||
528 | .. code-block:: python | |
529 | ||
530 | import responses | |
531 | import requests | |
532 | ||
533 | @responses.activate | |
534 | def test_request_params(): | |
535 | responses.add( | |
536 | method=responses.GET, | |
537 | url="http://example.com?hello=world", | |
538 | body="test", | |
539 | match_querystring=False, | |
540 | ) | |
541 | ||
542 | resp = requests.get('http://example.com', params={"hello": "world"}) | |
543 | assert responses.calls[0].request.params == {"hello": "world"} | |
544 | ||
545 | Responses as a context manager | |
546 | ------------------------------ | |
547 | ||
548 | .. code-block:: python | |
549 | ||
550 | import responses | |
551 | import requests | |
552 | ||
553 | def test_my_api(): | |
554 | with responses.RequestsMock() as rsps: | |
555 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
556 | body='{}', status=200, | |
557 | content_type='application/json') | |
558 | resp = requests.get('http://twitter.com/api/1/foobar') | |
559 | ||
560 | assert resp.status_code == 200 | |
561 | ||
562 | # outside the context manager requests will hit the remote server | |
563 | resp = requests.get('http://twitter.com/api/1/foobar') | |
564 | resp.status_code == 404 | |
565 | ||
566 | Responses as a pytest fixture | |
567 | ----------------------------- | |
568 | ||
569 | .. code-block:: python | |
570 | ||
571 | @pytest.fixture | |
572 | def mocked_responses(): | |
573 | with responses.RequestsMock() as rsps: | |
574 | yield rsps | |
575 | ||
576 | def test_api(mocked_responses): | |
577 | mocked_responses.add( | |
578 | responses.GET, 'http://twitter.com/api/1/foobar', | |
579 | body='{}', status=200, | |
580 | content_type='application/json') | |
581 | resp = requests.get('http://twitter.com/api/1/foobar') | |
582 | assert resp.status_code == 200 | |
583 | ||
584 | Responses inside a unittest setUp() | |
585 | ----------------------------------- | |
586 | ||
587 | When run with unittest tests, this can be used to set up some | |
588 | generic class-level responses, that may be complemented by each test | |
589 | ||
590 | .. code-block:: python | |
591 | ||
592 | def setUp(): | |
593 | self.responses = responses.RequestsMock() | |
594 | self.responses.start() | |
595 | ||
596 | # self.responses.add(...) | |
597 | ||
598 | self.addCleanup(self.responses.stop) | |
599 | self.addCleanup(self.responses.reset) | |
600 | ||
601 | def test_api(self): | |
602 | self.responses.add( | |
603 | responses.GET, 'http://twitter.com/api/1/foobar', | |
604 | body='{}', status=200, | |
605 | content_type='application/json') | |
606 | resp = requests.get('http://twitter.com/api/1/foobar') | |
607 | assert resp.status_code == 200 | |
608 | ||
609 | Assertions on declared responses | |
610 | -------------------------------- | |
611 | ||
612 | When used as a context manager, Responses will, by default, raise an assertion | |
613 | error if a url was registered but not accessed. This can be disabled by passing | |
614 | the ``assert_all_requests_are_fired`` value: | |
615 | ||
616 | .. code-block:: python | |
617 | ||
618 | import responses | |
619 | import requests | |
620 | ||
621 | def test_my_api(): | |
622 | with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: | |
623 | rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
624 | body='{}', status=200, | |
625 | content_type='application/json') | |
626 | ||
627 | assert_call_count | |
628 | ----------------- | |
629 | ||
630 | Assert that the request was called exactly n times. | |
631 | ||
632 | .. code-block:: python | |
633 | ||
634 | import responses | |
635 | import requests | |
636 | ||
637 | @responses.activate | |
638 | def test_assert_call_count(): | |
639 | responses.add(responses.GET, "http://example.com") | |
640 | ||
641 | requests.get("http://example.com") | |
642 | assert responses.assert_call_count("http://example.com", 1) is True | |
643 | ||
644 | requests.get("http://example.com") | |
645 | with pytest.raises(AssertionError) as excinfo: | |
646 | responses.assert_call_count("http://example.com", 1) | |
647 | assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) | |
648 | ||
649 | ||
650 | Multiple Responses | |
651 | ------------------ | |
652 | ||
653 | You can also add multiple responses for the same url: | |
654 | ||
655 | .. code-block:: python | |
656 | ||
657 | import responses | |
658 | import requests | |
659 | ||
660 | @responses.activate | |
661 | def test_my_api(): | |
662 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) | |
663 | responses.add(responses.GET, 'http://twitter.com/api/1/foobar', | |
664 | body='{}', status=200, | |
665 | content_type='application/json') | |
666 | ||
667 | resp = requests.get('http://twitter.com/api/1/foobar') | |
668 | assert resp.status_code == 500 | |
669 | resp = requests.get('http://twitter.com/api/1/foobar') | |
670 | assert resp.status_code == 200 | |
671 | ||
672 | ||
673 | Using a callback to modify the response | |
674 | --------------------------------------- | |
675 | ||
676 | If you use customized processing in `requests` via subclassing/mixins, or if you | |
677 | have library tools that interact with `requests` at a low level, you may need | |
678 | to add extended processing to the mocked Response object to fully simulate the | |
679 | environment for your tests. A `response_callback` can be used, which will be | |
680 | wrapped by the library before being returned to the caller. The callback | |
681 | accepts a `response` as it's single argument, and is expected to return a | |
682 | single `response` object. | |
683 | ||
684 | .. code-block:: python | |
685 | ||
686 | import responses | |
687 | import requests | |
688 | ||
689 | def response_callback(resp): | |
690 | resp.callback_processed = True | |
691 | return resp | |
692 | ||
693 | with responses.RequestsMock(response_callback=response_callback) as m: | |
694 | m.add(responses.GET, 'http://example.com', body=b'test') | |
695 | resp = requests.get('http://example.com') | |
696 | assert resp.text == "test" | |
697 | assert hasattr(resp, 'callback_processed') | |
698 | assert resp.callback_processed is True | |
699 | ||
700 | ||
701 | Passing through real requests | |
702 | ----------------------------- | |
703 | ||
704 | In some cases you may wish to allow for certain requests to pass through responses | |
705 | and hit a real server. This can be done with the ``add_passthru`` methods: | |
706 | ||
707 | .. code-block:: python | |
708 | ||
709 | import responses | |
710 | ||
711 | @responses.activate | |
712 | def test_my_api(): | |
713 | responses.add_passthru('https://percy.io') | |
714 | ||
715 | This will allow any requests matching that prefix, that is otherwise not | |
716 | registered as a mock response, to passthru using the standard behavior. | |
717 | ||
718 | Pass through endpoints can be configured with regex patterns if you | |
719 | need to allow an entire domain or path subtree to send requests: | |
720 | ||
721 | .. code-block:: python | |
722 | ||
723 | responses.add_passthru(re.compile('https://percy.io/\\w+')) | |
724 | ||
725 | ||
726 | Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or | |
727 | use ``PassthroughResponse`` to enable a response to behave as a pass through. | |
728 | ||
729 | .. code-block:: python | |
730 | ||
731 | # Enable passthrough for a single response | |
732 | response = Response(responses.GET, 'http://example.com', body='not used') | |
733 | response.passthrough = True | |
734 | responses.add(response) | |
735 | ||
736 | # Use PassthroughResponse | |
737 | response = PassthroughResponse(responses.GET, 'http://example.com') | |
738 | responses.add(response) | |
739 | ||
740 | Viewing/Modifying registered responses | |
741 | -------------------------------------- | |
742 | ||
743 | Registered responses are available as a public method of the RequestMock | |
744 | instance. It is sometimes useful for debugging purposes to view the stack of | |
745 | registered responses which can be accessed via ``responses.registered()``. | |
746 | ||
747 | The ``replace`` function allows a previously registered ``response`` to be | |
748 | changed. The method signature is identical to ``add``. ``response`` s are | |
749 | identified using ``method`` and ``url``. Only the first matched ``response`` is | |
750 | replaced. | |
751 | ||
752 | .. code-block:: python | |
753 | ||
754 | import responses | |
755 | import requests | |
756 | ||
757 | @responses.activate | |
758 | def test_replace(): | |
759 | ||
760 | responses.add(responses.GET, 'http://example.org', json={'data': 1}) | |
761 | responses.replace(responses.GET, 'http://example.org', json={'data': 2}) | |
762 | ||
763 | resp = requests.get('http://example.org') | |
764 | ||
765 | assert resp.json() == {'data': 2} | |
766 | ||
767 | ||
768 | The ``upsert`` function allows a previously registered ``response`` to be | |
769 | changed like ``replace``. If the response is registered, the ``upsert`` function | |
770 | will registered it like ``add``. | |
771 | ||
772 | ``remove`` takes a ``method`` and ``url`` argument and will remove **all** | |
773 | matched responses from the registered list. | |
774 | ||
775 | Finally, ``reset`` will reset all registered responses. | |
776 | ||
777 | Contributing | |
778 | ------------ | |
779 | ||
780 | Responses uses several linting and autoformatting utilities, so it's important that when | |
781 | submitting patches you use the appropriate toolchain: | |
782 | ||
783 | Clone the repository: | |
784 | ||
785 | .. code-block:: shell | |
786 | ||
787 | git clone https://github.com/getsentry/responses.git | |
788 | ||
789 | Create an environment (e.g. with ``virtualenv``): | |
790 | ||
791 | .. code-block:: shell | |
792 | ||
793 | virtualenv .env && source .env/bin/activate | |
794 | ||
795 | Configure development requirements: | |
796 | ||
797 | .. code-block:: shell | |
798 | ||
799 | make develop | |
800 | ||
801 | Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for | |
802 | testing. You can run all tests by: | |
803 | ||
804 | .. code-block:: shell | |
805 | ||
806 | pytest | |
807 | ||
808 | And run a single test by: | |
809 | ||
810 | .. code-block:: shell | |
811 | ||
812 | pytest -k '<test_function_name>' | |
813 | ||
814 | To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter: | |
815 | ||
816 | .. code-block:: shell | |
817 | ||
818 | mypy --config-file=./mypy.ini -p responses | |
819 | ||
820 | To check code style and reformat it run: | |
821 | ||
822 | .. code-block:: shell | |
823 | ||
824 | pre-commit run --all-files | |
825 | ||
826 | Note: on some OS, you have to use ``pre_commit`` | |
827 | ||
828 |
5 | 5 | setup.py |
6 | 6 | tox.ini |
7 | 7 | responses/__init__.py |
8 | responses/__init__.pyi | |
9 | responses/matchers.py | |
10 | responses/matchers.pyi | |
11 | responses/test_matchers.py | |
8 | 12 | responses/test_responses.py |
9 | 13 | responses.egg-info/PKG-INFO |
10 | 14 | responses.egg-info/SOURCES.txt |
0 | 0 | requests>=2.0 |
1 | six | |
1 | 2 | urllib3>=1.25.10 |
2 | six | |
3 | 3 | |
4 | 4 | [:python_version < "3.3"] |
5 | 5 | mock |
9 | 9 | |
10 | 10 | [tests] |
11 | 11 | coverage<6.0.0,>=3.7.1 |
12 | flake8 | |
12 | 13 | pytest-cov |
13 | 14 | pytest-localserver |
14 | flake8 | |
15 | 15 | types-mock |
16 | 16 | types-requests |
17 | 17 | types-six |
20 | 20 | pytest<5.0,>=4.6 |
21 | 21 | |
22 | 22 | [tests:python_version >= "3.5"] |
23 | mypy | |
23 | 24 | pytest>=4.6 |
24 | mypy |
58 | 58 | |
59 | 59 | setup( |
60 | 60 | name="responses", |
61 | version="0.13.4", | |
61 | version="0.14.0", | |
62 | 62 | author="David Cramer", |
63 | 63 | description=("A utility library for mocking out the `requests` Python library."), |
64 | 64 | url="https://github.com/getsentry/responses", |
88 | 88 | "Programming Language :: Python :: 3.7", |
89 | 89 | "Programming Language :: Python :: 3.8", |
90 | 90 | "Programming Language :: Python :: 3.9", |
91 | "Programming Language :: Python :: 3.10", | |
91 | 92 | "Topic :: Software Development", |
92 | 93 | ], |
93 | 94 | ) |
0 | 0 | [tox] |
1 | envlist = py27,py35,py36,py37,py38,py39 | |
1 | envlist = py27,py35,py36,py37,py38,py39,py310,mypy | |
2 | 2 | |
3 | 3 | [testenv] |
4 | 4 | extras = tests |
5 | 5 | commands = |
6 | 6 | pytest . --cov responses --cov-report term-missing |
7 | ||
8 | ||
9 | [testenv:mypy] | |
10 | description = Check types using 'mypy' | |
11 | basepython = python3.7 | |
12 | commands = | |
13 | python -m mypy --config-file=mypy.ini -p responses |