@@ -304,6 +304,7 @@ class RequestMatcherKwargs(TypedDict, total=False):
304304 query_string : QueryMatcher | str | bytes | Mapping [str , str ] | None
305305 header_value_matcher : HVMATCHER_T | None
306306 json : Any
307+ data_form : Mapping [str , str ] | None
307308
308309
309310class RequestMatcher :
@@ -333,6 +334,9 @@ class RequestMatcher:
333334 :param json: a python object (eg. a dict) whose value will be compared to the request body after it
334335 is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
335336 If that's desired, add it to the headers parameter.
337+ :param data_form: expected form fields, parsed by werkzeug using the request's *Content-Type*.
338+ A mapping matches the first value for each field; a ``MultiDict`` matches all values.
339+ Mutually exclusive with `data` and `json`.
336340 """
337341
338342 def __init__ (
@@ -345,15 +349,20 @@ def __init__(
345349 query_string : QueryMatcher | str | bytes | Mapping [str , str ] | None = None ,
346350 header_value_matcher : HVMATCHER_T | None = None ,
347351 json : Any = UNDEFINED ,
352+ * ,
353+ data_form : Mapping [str , str ] | None = None ,
348354 ) -> None :
349355 if json is not UNDEFINED and data is not None :
350356 raise ValueError ("data and json parameters are mutually exclusive" )
357+ if data_form is not None and (data is not None or json is not UNDEFINED ):
358+ raise ValueError ("data_form is mutually exclusive with data and json" )
351359
352360 self .uri = uri
353361 self .method = method
354362 self .query_string = query_string
355363 self .query_matcher = _create_query_matcher (self .query_string )
356364 self .json = json
365+ self .data_form = data_form
357366
358367 self .headers : Mapping [str , str ] = {}
359368 if headers is not None :
@@ -379,9 +388,11 @@ def __repr__(self) -> str:
379388 retval = "<{} " .format (class_name )
380389 retval += (
381390 "uri={uri!r} method={method!r} query_string={query_string!r} "
382- "headers={headers!r} data={data!r} json={json!r}> "
391+ "headers={headers!r} data={data!r} json={json!r}"
383392 ).format_map (self .__dict__ )
384- return retval
393+ if self .data_form is not None :
394+ retval += f" data_form={ self .data_form !r} "
395+ return retval + ">"
385396
386397 def match_data (self , request : Request ) -> bool :
387398 """
@@ -440,6 +451,17 @@ def match_json(self, request: Request) -> bool:
440451
441452 return bool (json_received == self .json )
442453
454+ def match_data_form (self , request : Request ) -> bool :
455+ """Match parsed form fields, or accept any form when no expectation is set."""
456+ if self .data_form is None :
457+ return True
458+
459+ # Cache the raw body before form parsing so response handlers can still read it.
460+ request .get_data ()
461+ if isinstance (self .data_form , MultiDict ):
462+ return bool (request .form == self .data_form )
463+ return request .form .to_dict () == dict (self .data_form )
464+
443465 def difference (self , request : Request ) -> list [tuple [str , str , str | URIPattern ]]:
444466 """
445467 Calculates the difference between the matcher and the request.
@@ -478,6 +500,8 @@ def difference(self, request: Request) -> list[tuple[str, str, str | URIPattern]
478500
479501 if not self .match_json (request ):
480502 retval .append (("json" , request .data , self .json ))
503+ if not self .match_data_form (request ):
504+ retval .append (("data_form" , request .form , self .data_form ))
481505 return retval
482506
483507 def match (self , request : Request ) -> bool :
@@ -603,9 +627,11 @@ def __repr__(self) -> str:
603627 retval = (
604628 f"<{ class_name } uri={ self .matcher .uri !r} method={ self .matcher .method !r} "
605629 f"query_string={ self .matcher .query_string !r} headers={ self .matcher .headers !r} data={ self .matcher .data !r} "
606- f"json={ self .matcher .json !r} > "
630+ f"json={ self .matcher .json !r} "
607631 )
608- return retval
632+ if self .matcher .data_form is not None :
633+ retval += f" data_form={ self .matcher .data_form !r} "
634+ return retval + ">"
609635
610636
611637class RequestHandlerList (list [RequestHandler ]):
@@ -1087,6 +1113,8 @@ def expect_request(
10871113 header_value_matcher : HVMATCHER_T | None = None ,
10881114 handler_type : HandlerType = HandlerType .PERMANENT ,
10891115 json : Any = UNDEFINED ,
1116+ * ,
1117+ data_form : Mapping [str , str ] | None = None ,
10901118 ) -> RequestHandler :
10911119 """
10921120 Create and register a request handler.
@@ -1134,10 +1162,13 @@ def expect_request(
11341162 :param json: a python object (eg. a dict) whose value will be compared to the request body after it
11351163 is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
11361164 If that's desired, add it to the headers parameter.
1165+ :param data_form: expected form fields, parsed by werkzeug using the request's *Content-Type*.
1166+ A mapping matches the first value for each field; a ``MultiDict`` matches all values.
1167+ Mutually exclusive with `data` and `json`.
11371168
11381169 :return: Created and register :py:class:`RequestHandler`.
11391170
1140- Parameters `json` and `data ` are mutually exclusive.
1171+ Parameters `json`, `data`, and `data_form ` are mutually exclusive.
11411172 """
11421173
11431174 matcher = self .create_matcher (
@@ -1149,6 +1180,7 @@ def expect_request(
11491180 query_string = query_string ,
11501181 header_value_matcher = header_value_matcher ,
11511182 json = json ,
1183+ ** ({"data_form" : data_form } if data_form is not None else {}),
11521184 )
11531185 request_handler = RequestHandler (matcher )
11541186 if handler_type == HandlerType .PERMANENT :
@@ -1169,6 +1201,8 @@ def expect_oneshot_request(
11691201 query_string : QueryMatcher | str | bytes | Mapping [str , str ] | None = None ,
11701202 header_value_matcher : HVMATCHER_T | None = None ,
11711203 json : Any = UNDEFINED ,
1204+ * ,
1205+ data_form : Mapping [str , str ] | None = None ,
11721206 ) -> RequestHandler :
11731207 """
11741208 Create and register a oneshot request handler.
@@ -1196,10 +1230,13 @@ def expect_oneshot_request(
11961230 :param json: a python object (eg. a dict) whose value will be compared to the request body after it
11971231 is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
11981232 If that's desired, add it to the headers parameter.
1233+ :param data_form: expected form fields, parsed by werkzeug using the request's *Content-Type*.
1234+ A mapping matches the first value for each field; a ``MultiDict`` matches all values.
1235+ Mutually exclusive with `data` and `json`.
11991236
12001237 :return: Created and register :py:class:`RequestHandler`.
12011238
1202- Parameters `json` and `data ` are mutually exclusive.
1239+ Parameters `json`, `data`, and `data_form ` are mutually exclusive.
12031240 """
12041241
12051242 return self .expect_request (
@@ -1212,6 +1249,7 @@ def expect_oneshot_request(
12121249 header_value_matcher = header_value_matcher ,
12131250 handler_type = HandlerType .ONESHOT ,
12141251 json = json ,
1252+ ** ({"data_form" : data_form } if data_form is not None else {}),
12151253 )
12161254
12171255 def expect_ordered_request (
@@ -1224,6 +1262,8 @@ def expect_ordered_request(
12241262 query_string : QueryMatcher | str | bytes | Mapping [str , str ] | None = None ,
12251263 header_value_matcher : HVMATCHER_T | None = None ,
12261264 json : Any = UNDEFINED ,
1265+ * ,
1266+ data_form : Mapping [str , str ] | None = None ,
12271267 ) -> RequestHandler :
12281268 """
12291269 Create and register a ordered request handler.
@@ -1251,10 +1291,13 @@ def expect_ordered_request(
12511291 :param json: a python object (eg. a dict) whose value will be compared to the request body after it
12521292 is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked.
12531293 If that's desired, add it to the headers parameter.
1294+ :param data_form: expected form fields, parsed by werkzeug using the request's *Content-Type*.
1295+ A mapping matches the first value for each field; a ``MultiDict`` matches all values.
1296+ Mutually exclusive with `data` and `json`.
12541297
12551298 :return: Created and register :py:class:`RequestHandler`.
12561299
1257- Parameters `json` and `data ` are mutually exclusive.
1300+ Parameters `json`, `data`, and `data_form ` are mutually exclusive.
12581301 """
12591302
12601303 return self .expect_request (
@@ -1267,6 +1310,7 @@ def expect_ordered_request(
12671310 header_value_matcher = header_value_matcher ,
12681311 handler_type = HandlerType .ORDERED ,
12691312 json = json ,
1313+ ** ({"data_form" : data_form } if data_form is not None else {}),
12701314 )
12711315
12721316 def format_matchers (self ) -> str :
0 commit comments