88from enum import Enum
99from contextlib import suppress , contextmanager
1010from copy import copy
11- from typing import Callable , Mapping , Optional , Union , Pattern
11+ from typing import Any , Callable , Mapping , Optional , Union , Pattern
1212from ssl import SSLContext
1313import abc
1414
1515from werkzeug .http import parse_authorization_header
1616from werkzeug .serving import make_server
17- from werkzeug .wrappers import Request , Response
17+ from werkzeug .wrappers import Request
18+ from werkzeug .wrappers import Response
19+
1820import werkzeug .urls
1921from werkzeug .datastructures import MultiDict
2022
2123URI_DEFAULT = ""
2224METHOD_ALL = "__ALL"
2325
2426
27+ class Undefined :
28+ def __repr__ (self ):
29+ return "<UNDEFINED>"
30+
31+
32+ UNDEFINED = Undefined ()
33+
34+
2535class Error (Exception ):
2636 """
2737 Base class for all exception defined in this package.
@@ -273,12 +283,17 @@ def __init__(
273283 data_encoding : str = "utf-8" ,
274284 headers : Optional [Mapping [str , str ]] = None ,
275285 query_string : Union [None , QueryMatcher , str , bytes , Mapping ] = None ,
276- header_value_matcher : Optional [HeaderValueMatcher ] = None ):
286+ header_value_matcher : Optional [HeaderValueMatcher ] = None ,
287+ json : Any = UNDEFINED ):
288+
289+ if json is not UNDEFINED and data is not None :
290+ raise ValueError ("data and json parameters are mutually exclusive" )
277291
278292 self .uri = uri
279293 self .method = method
280294 self .query_string = query_string
281295 self .query_matcher = _create_query_matcher (self .query_string )
296+ self .json = json
282297
283298 if headers is None :
284299 self .headers = {}
@@ -289,6 +304,7 @@ def __init__(
289304 data = data .encode (data_encoding )
290305
291306 self .data = data
307+ self .data_encoding = data_encoding
292308
293309 self .header_value_matcher = HeaderValueMatcher () if header_value_matcher is None else header_value_matcher
294310
@@ -299,7 +315,8 @@ def __repr__(self):
299315
300316 class_name = self .__class__ .__name__
301317 retval = "<{} " .format (class_name )
302- retval += "uri={uri!r} method={method!r} query_string={query_string!r} headers={headers!r} data={data!r}>" .format_map (self .__dict__ )
318+ retval += "uri={uri!r} method={method!r} query_string={query_string!r} headers={headers!r} data={data!r} json={json!r}>" .format_map (
319+ self .__dict__ )
303320 return retval
304321
305322 def match_data (self , request : Request ) -> bool :
@@ -335,6 +352,30 @@ def match_uri(self, request: Request) -> bool:
335352
336353 return self .uri == URI_DEFAULT or path == self .uri
337354
355+ def match_json (self , request : Request ) -> bool :
356+ """
357+ Matches the request data as json.
358+
359+ Load the request data as json and compare it to self.json which is a
360+ json-serializable data structure (eg. a dict or list).
361+
362+ :param request: the HTTP request
363+ :return: `True` when the data is matched or no matching is required. `False` otherwise.
364+ """
365+ if self .json is UNDEFINED :
366+ return True
367+
368+ try :
369+ # do the decoding here as python 3.5 requires string and does not
370+ # accept bytes
371+ json_received = json .loads (request .data .decode (self .data_encoding ))
372+ except json .JSONDecodeError :
373+ return False
374+ except UnicodeDecodeError :
375+ return False
376+
377+ return json_received == self .json
378+
338379 def difference (self , request : Request ) -> list :
339380 """
340381 Calculates the difference between the matcher and the request.
@@ -371,6 +412,8 @@ def difference(self, request: Request) -> list:
371412 if not self .match_data (request ):
372413 retval .append (("data" , request .data , self .data ))
373414
415+ if not self .match_json (request ):
416+ retval .append (("json" , request .data , self .json ))
374417 return retval
375418
376419 def match (self , request : Request ) -> bool :
@@ -614,7 +657,8 @@ def expect_request(
614657 headers : Optional [Mapping [str , str ]] = None ,
615658 query_string : Union [None , QueryMatcher , str , bytes , Mapping ] = None ,
616659 header_value_matcher : Optional [HeaderValueMatcher ] = None ,
617- handler_type : HandlerType = HandlerType .PERMANENT ) -> RequestHandler :
660+ handler_type : HandlerType = HandlerType .PERMANENT ,
661+ json : Any = UNDEFINED ) -> RequestHandler :
618662 """
619663 Create and register a request handler.
620664
@@ -647,8 +691,13 @@ def expect_request(
647691 object from werkzeug.
648692 :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers.
649693 :param handler_type: type of handler
694+ :param json: a python object (eg. a dict) whose value will be compared to the request body after it
695+ is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
696+ If that's desired, add it to the headers parameter.
650697
651698 :return: Created and register :py:class:`RequestHandler`.
699+
700+ Parameters `json` and `data` are mutually exclusive.
652701 """
653702
654703 matcher = self .create_matcher (
@@ -659,6 +708,7 @@ def expect_request(
659708 headers = headers ,
660709 query_string = query_string ,
661710 header_value_matcher = header_value_matcher ,
711+ json = json ,
662712 )
663713 request_handler = RequestHandler (matcher )
664714 if handler_type == HandlerType .PERMANENT :
@@ -677,7 +727,8 @@ def expect_oneshot_request(
677727 data_encoding : str = "utf-8" ,
678728 headers : Optional [Mapping [str , str ]] = None ,
679729 query_string : Union [None , QueryMatcher , str , bytes , Mapping ] = None ,
680- header_value_matcher : Optional [HeaderValueMatcher ] = None ) -> RequestHandler :
730+ header_value_matcher : Optional [HeaderValueMatcher ] = None ,
731+ json : Any = UNDEFINED ) -> RequestHandler :
681732 """
682733 Create and register a oneshot request handler.
683734
@@ -698,8 +749,13 @@ def expect_oneshot_request(
698749 value will be used. If multiple values needed to be handled, use ``MultiDict``
699750 object from werkzeug.
700751 :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers.
752+ :param json: a python object (eg. a dict) whose value will be compared to the request body after it
753+ is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
754+ If that's desired, add it to the headers parameter.
701755
702756 :return: Created and register :py:class:`RequestHandler`.
757+
758+ Parameters `json` and `data` are mutually exclusive.
703759 """
704760
705761 return self .expect_request (
@@ -711,6 +767,7 @@ def expect_oneshot_request(
711767 query_string = query_string ,
712768 header_value_matcher = header_value_matcher ,
713769 handler_type = HandlerType .ONESHOT ,
770+ json = json ,
714771 )
715772
716773 def expect_ordered_request (
@@ -721,7 +778,8 @@ def expect_ordered_request(
721778 data_encoding : str = "utf-8" ,
722779 headers : Optional [Mapping [str , str ]] = None ,
723780 query_string : Union [None , QueryMatcher , str , bytes , Mapping ] = None ,
724- header_value_matcher : Optional [HeaderValueMatcher ] = None ) -> RequestHandler :
781+ header_value_matcher : Optional [HeaderValueMatcher ] = None ,
782+ json : Any = UNDEFINED ) -> RequestHandler :
725783 """
726784 Create and register a ordered request handler.
727785
@@ -742,8 +800,13 @@ def expect_ordered_request(
742800 value will be used. If multiple values needed to be handled, use ``MultiDict``
743801 object from werkzeug.
744802 :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers.
803+ :param json: a python object (eg. a dict) whose value will be compared to the request body after it
804+ is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
805+ If that's desired, add it to the headers parameter.
745806
746807 :return: Created and register :py:class:`RequestHandler`.
808+
809+ Parameters `json` and `data` are mutually exclusive.
747810 """
748811
749812 return self .expect_request (
@@ -755,6 +818,7 @@ def expect_ordered_request(
755818 query_string = query_string ,
756819 header_value_matcher = header_value_matcher ,
757820 handler_type = HandlerType .ORDERED ,
821+ json = json ,
758822 )
759823
760824 def thread_target (self ):
0 commit comments