pytest-openapi 0.1.1.dev202601050151__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,834 @@
1
+ """OpenAPI contract testing - execute tests against live endpoints."""
2
+
3
+ import json
4
+
5
+ import requests
6
+
7
+ from .case_generator import generate_test_cases_for_schema
8
+
9
+ # Global list to store test reports
10
+ test_reports = []
11
+
12
+
13
+ def make_request(method, url, json=None, timeout=10):
14
+ """Wrapper for HTTP requests that logs all requests and responses
15
+ for reporting.
16
+
17
+ Args:
18
+ method: HTTP method (GET, POST, PUT, DELETE)
19
+ url: Full URL to request
20
+ json: Optional JSON body for request
21
+ timeout: Request timeout in seconds
22
+
23
+ Returns:
24
+ requests.Response object
25
+ """
26
+ # Make the actual request
27
+ if method.upper() == "GET":
28
+ response = requests.get(url, timeout=timeout)
29
+ elif method.upper() == "POST":
30
+ response = requests.post(url, json=json, timeout=timeout)
31
+ elif method.upper() == "PUT":
32
+ response = requests.put(url, json=json, timeout=timeout)
33
+ elif method.upper() == "DELETE":
34
+ response = requests.delete(url, timeout=timeout)
35
+ else:
36
+ raise ValueError(f"Unsupported HTTP method: {method}")
37
+
38
+ return response
39
+
40
+
41
+ def log_test_result(
42
+ method,
43
+ path,
44
+ request_body,
45
+ expected_status,
46
+ expected_body,
47
+ actual_status,
48
+ actual_body,
49
+ success,
50
+ error_message=None,
51
+ ):
52
+ """Log a test result for the final report.
53
+
54
+ Args:
55
+ method: HTTP method (GET, POST, PUT, DELETE)
56
+ path: URL path
57
+ request_body: Request body (if any)
58
+ expected_status: Expected HTTP status code
59
+ expected_body: Expected response body
60
+ actual_status: Actual HTTP status code
61
+ actual_body: Actual response body
62
+ success: Whether the test passed
63
+ error_message: Error message if test failed
64
+ """
65
+ report = {
66
+ "method": method,
67
+ "path": path,
68
+ "request_body": request_body,
69
+ "expected_status": expected_status,
70
+ "expected_body": expected_body,
71
+ "actual_status": actual_status,
72
+ "actual_body": actual_body,
73
+ "success": success,
74
+ "error_message": error_message,
75
+ }
76
+ test_reports.append(report)
77
+
78
+
79
+ def get_test_report():
80
+ """Generate a human-readable test report.
81
+
82
+ Returns:
83
+ str: Formatted report of all tests
84
+ """
85
+ if not test_reports:
86
+ return "No tests have been run yet."
87
+
88
+ report_lines = []
89
+ report_lines.append("=" * 80)
90
+ report_lines.append("OpenAPI Contract Test Report")
91
+ report_lines.append("=" * 80)
92
+ report_lines.append("")
93
+
94
+ for i, test in enumerate(test_reports, 1):
95
+ status_symbol = "✅" if test["success"] else "❌"
96
+ report_lines.append(f"Test #{i} {status_symbol}")
97
+ report_lines.append(f"{test['method']} {test['path']}")
98
+
99
+ if test["request_body"] is not None:
100
+ formatted_request = json.dumps(test["request_body"], indent=2)
101
+ report_lines.append("Requested:")
102
+ for line in formatted_request.split("\n"):
103
+ report_lines.append(f" {line}")
104
+
105
+ report_lines.append("")
106
+
107
+ # Format expected body
108
+ if test["expected_body"] == "" or test["expected_body"] is None:
109
+ expected_body_str = "(empty)"
110
+ else:
111
+ try:
112
+ expected_body_str = json.dumps(test["expected_body"], indent=2)
113
+ expected_body_str = "\n ".join(expected_body_str.split("\n"))
114
+ except (TypeError, ValueError):
115
+ expected_body_str = str(test["expected_body"])
116
+
117
+ # Format actual body
118
+ if test["actual_body"] == "" or test["actual_body"] is None:
119
+ actual_body_str = "(empty)"
120
+ else:
121
+ try:
122
+ actual_body_str = json.dumps(test["actual_body"], indent=2)
123
+ actual_body_str = "\n ".join(actual_body_str.split("\n"))
124
+ except (TypeError, ValueError):
125
+ actual_body_str = str(test["actual_body"])
126
+
127
+ report_lines.append(f"Expected {test['expected_status']}")
128
+ report_lines.append(f" {expected_body_str}")
129
+ report_lines.append("")
130
+ report_lines.append(f"Actual {test['actual_status']}")
131
+ report_lines.append(f" {actual_body_str}")
132
+
133
+ if not test["success"] and test["error_message"]:
134
+ report_lines.append("")
135
+ report_lines.append(f"Error: {test['error_message']}")
136
+
137
+ report_lines.append("")
138
+ report_lines.append("-" * 80)
139
+ report_lines.append("")
140
+
141
+ return "\n".join(report_lines)
142
+
143
+
144
+ def compare_responses(expected, actual):
145
+ """Compare expected and actual responses with detailed error
146
+ messages.
147
+
148
+ Args:
149
+ expected: Expected response (from OpenAPI example)
150
+ actual: Actual response from API
151
+
152
+ Returns:
153
+ tuple: (matches: bool, error_message: str or None)
154
+ """
155
+ if expected == actual:
156
+ return True, None
157
+
158
+ # Check for missing keys in actual
159
+ if isinstance(expected, dict) and isinstance(actual, dict):
160
+ for key in expected:
161
+ if key not in actual:
162
+ return (
163
+ False,
164
+ f"Missing key in actual response: '{key}'. Expected: {expected}, Actual: {actual}",
165
+ )
166
+
167
+ # Check for extra keys in actual
168
+ for key in actual:
169
+ if key not in expected:
170
+ return (
171
+ False,
172
+ f"Extra key in actual response: '{key}'. Expected: {expected}, Actual: {actual}",
173
+ )
174
+
175
+ # Check for type mismatches
176
+ for key in expected:
177
+ if key in actual:
178
+ expected_type = type(expected[key]).__name__
179
+ actual_type = type(actual[key]).__name__
180
+ if expected_type != actual_type:
181
+ return (
182
+ False,
183
+ f"Type mismatch for key '{key}': expected {expected_type}, got {actual_type}. "
184
+ f"Expected value: {expected[key]}, Actual value: {actual[key]}",
185
+ )
186
+
187
+ # Recursively check nested dicts/lists
188
+ if isinstance(expected[key], (dict, list)):
189
+ matches, error = compare_responses(
190
+ expected[key], actual[key]
191
+ )
192
+ if not matches:
193
+ return False, error
194
+
195
+ # If we've checked keys and types and any nested structures
196
+ # recursively without finding a mismatch, consider this a match
197
+ return True, None
198
+
199
+ # For lists, check element-wise types/structure
200
+ if isinstance(expected, list) and isinstance(actual, list):
201
+ if len(expected) != len(actual):
202
+ return (
203
+ False,
204
+ f"List length mismatch: expected {len(expected)}, got {len(actual)}. Expected: {expected}, Actual: {actual}",
205
+ )
206
+ for e_item, a_item in zip(expected, actual):
207
+ matches, error = compare_responses(e_item, a_item)
208
+ if not matches:
209
+ return False, error
210
+ return True, None
211
+
212
+ return False, f"Response mismatch.\nExpected: {expected}\nActual: {actual}"
213
+
214
+
215
+ def test_get_endpoint(base_url, path, operation):
216
+ """Test a GET endpoint using the example from the OpenAPI spec.
217
+
218
+ Args:
219
+ base_url: Base URL of the API server
220
+ path: API endpoint path
221
+ operation: OpenAPI operation object
222
+
223
+ Returns:
224
+ tuple: (success: bool, error_message: str or None)
225
+ """
226
+ url = f"{base_url}{path}"
227
+
228
+ # Get the expected response from examples or generate from schema
229
+ responses = operation.get("responses", {})
230
+ response_200 = responses.get("200", {})
231
+ content = response_200.get("content", {})
232
+
233
+ expected_response = None
234
+ for media_type, media_obj in content.items():
235
+ if "example" in media_obj:
236
+ expected_response = media_obj["example"]
237
+ break
238
+ elif "examples" in media_obj:
239
+ examples = media_obj["examples"]
240
+ if examples:
241
+ first_example = next(iter(examples.values()))
242
+ expected_response = first_example.get("value")
243
+ break
244
+ elif "schema" in media_obj:
245
+ # Generate test cases from schema
246
+ schema = media_obj["schema"]
247
+ response_test_cases, warnings = generate_test_cases_for_schema(
248
+ schema
249
+ )
250
+ if warnings:
251
+ print(f"\nWarning for GET {path} response schema:")
252
+ for warning in warnings:
253
+ print(f" - {warning}")
254
+ if response_test_cases:
255
+ expected_response = response_test_cases[0]
256
+ break
257
+
258
+ if expected_response is None:
259
+ return False, "No example or schema found for 200 response"
260
+
261
+ # Make the GET request
262
+ try:
263
+ response = make_request("GET", url)
264
+ except requests.exceptions.RequestException as e:
265
+ error_msg = f"Request failed: {e}"
266
+ log_test_result(
267
+ "GET",
268
+ path,
269
+ None,
270
+ 200,
271
+ expected_response,
272
+ None,
273
+ None,
274
+ False,
275
+ error_msg,
276
+ )
277
+ return False, error_msg
278
+
279
+ # Check status code
280
+ actual_response = (
281
+ response.json() if response.status_code == 200 else response.text
282
+ )
283
+
284
+ if response.status_code != 200:
285
+ error_msg = f"Expected status 200, got {response.status_code}. Response: {response.text}"
286
+ log_test_result(
287
+ "GET",
288
+ path,
289
+ None,
290
+ 200,
291
+ expected_response,
292
+ response.status_code,
293
+ actual_response,
294
+ False,
295
+ error_msg,
296
+ )
297
+ return False, error_msg
298
+
299
+ # Check response matches example
300
+ matches, error = compare_responses(expected_response, actual_response)
301
+ if not matches:
302
+ log_test_result(
303
+ "GET",
304
+ path,
305
+ None,
306
+ 200,
307
+ expected_response,
308
+ response.status_code,
309
+ actual_response,
310
+ False,
311
+ error,
312
+ )
313
+ return False, error
314
+
315
+ log_test_result(
316
+ "GET",
317
+ path,
318
+ None,
319
+ 200,
320
+ expected_response,
321
+ response.status_code,
322
+ actual_response,
323
+ True,
324
+ )
325
+ return True, None
326
+
327
+
328
+ def test_post_endpoint(base_url, path, operation):
329
+ """Test a POST endpoint using examples from OpenAPI spec AND
330
+ generated test cases from schema.
331
+
332
+ Args:
333
+ base_url: Base URL of the API server
334
+ path: API endpoint path
335
+ operation: OpenAPI operation object
336
+
337
+ Returns:
338
+ tuple: (success: bool, error_message: str or None)
339
+ """
340
+ url = f"{base_url}{path}"
341
+
342
+ # Collect ALL test cases: both from examples AND generated from schema
343
+ request_test_cases = []
344
+ warnings = []
345
+
346
+ request_body = operation.get("requestBody", {})
347
+ request_content = request_body.get("content", {})
348
+
349
+ for media_type, media_obj in request_content.items():
350
+ # Collect explicit examples
351
+ if "example" in media_obj:
352
+ request_test_cases.append(media_obj["example"])
353
+ if "examples" in media_obj:
354
+ examples_dict = media_obj["examples"]
355
+ for ex_name, ex_obj in examples_dict.items():
356
+ if "value" in ex_obj:
357
+ request_test_cases.append(ex_obj["value"])
358
+
359
+ # Also generate test cases from schema
360
+ if "schema" in media_obj:
361
+ schema = media_obj["schema"]
362
+ generated, warning = generate_test_cases_for_schema(
363
+ schema, "request_body"
364
+ )
365
+ if warning:
366
+ if isinstance(warning, list):
367
+ warnings.extend(warning)
368
+ else:
369
+ warnings.append(warning)
370
+ request_test_cases.extend(generated)
371
+ break
372
+
373
+ if not request_test_cases:
374
+ return False, "No request body examples or schema found"
375
+
376
+ # Print warnings if any
377
+ for warning in warnings:
378
+ print(f"\n{warning}")
379
+
380
+ # Get expected response - collect examples AND generated test cases
381
+ responses = operation.get("responses", {})
382
+ response_200 = responses.get("200", {}) or responses.get("201", {})
383
+ content = response_200.get("content", {})
384
+
385
+ expected_response = None
386
+ expected_status = 201 if "201" in responses else 200
387
+
388
+ for media_type, media_obj in content.items():
389
+ # Prefer explicit example
390
+ if "example" in media_obj:
391
+ expected_response = media_obj["example"]
392
+ break
393
+ elif "examples" in media_obj:
394
+ examples_dict = media_obj["examples"]
395
+ if examples_dict:
396
+ first_example = next(iter(examples_dict.values()))
397
+ expected_response = first_example.get("value")
398
+ break
399
+ elif "schema" in media_obj:
400
+ # Generate test case from response schema
401
+ schema = media_obj["schema"]
402
+ generated, warning = generate_test_cases_for_schema(
403
+ schema, "response_body"
404
+ )
405
+ if warning:
406
+ if isinstance(warning, list):
407
+ warnings.extend(warning)
408
+ else:
409
+ warnings.append(warning)
410
+ if generated:
411
+ expected_response = generated[0]
412
+ break
413
+
414
+ if expected_response is None:
415
+ return False, "No example or schema found for 200/201 response"
416
+
417
+ # Test with all collected test cases (examples + generated)
418
+ errors = []
419
+ for request_test_case in request_test_cases:
420
+ # Make the POST request
421
+ try:
422
+ response = make_request("POST", url, json=request_test_case)
423
+ except requests.exceptions.RequestException as e:
424
+ error_msg = f"Request failed: {e}"
425
+ log_test_result(
426
+ "POST",
427
+ path,
428
+ request_test_case,
429
+ expected_status,
430
+ expected_response,
431
+ None,
432
+ None,
433
+ False,
434
+ error_msg,
435
+ )
436
+ errors.append(error_msg)
437
+ continue
438
+
439
+ # Check status code (accept both 200 and 201 for POST)
440
+ actual_response = (
441
+ response.json()
442
+ if response.status_code in [200, 201]
443
+ else response.text
444
+ )
445
+
446
+ if response.status_code not in [200, 201]:
447
+ error_msg = f"Expected status 200/201, got {response.status_code}. Response: {response.text}"
448
+ log_test_result(
449
+ "POST",
450
+ path,
451
+ request_test_case,
452
+ expected_status,
453
+ expected_response,
454
+ response.status_code,
455
+ actual_response,
456
+ False,
457
+ error_msg,
458
+ )
459
+ errors.append(error_msg)
460
+ continue
461
+
462
+ # Check response matches example
463
+ matches, error = compare_responses(expected_response, actual_response)
464
+ if not matches:
465
+ log_test_result(
466
+ "POST",
467
+ path,
468
+ request_test_case,
469
+ expected_status,
470
+ expected_response,
471
+ response.status_code,
472
+ actual_response,
473
+ False,
474
+ error,
475
+ )
476
+ errors.append(error)
477
+ continue
478
+
479
+ log_test_result(
480
+ "POST",
481
+ path,
482
+ request_test_case,
483
+ expected_status,
484
+ expected_response,
485
+ response.status_code,
486
+ actual_response,
487
+ True,
488
+ )
489
+
490
+ if errors:
491
+ # Return the first few errors for brevity
492
+ combined = "; ".join(str(e) for e in errors[:3])
493
+ return False, combined
494
+
495
+ return True, None
496
+
497
+
498
+ def test_put_endpoint(base_url, path, operation):
499
+ """Test a PUT endpoint using the example from the OpenAPI spec.
500
+
501
+ Args:
502
+ base_url: Base URL of the API server
503
+ path: API endpoint path (may contain path parameters)
504
+ operation: OpenAPI operation object
505
+
506
+ Returns:
507
+ tuple: (success: bool, error_message: str or None)
508
+ """
509
+ # Collect ALL test cases: both from examples AND generated from schema
510
+ request_test_cases = []
511
+ warnings = []
512
+
513
+ request_body = operation.get("requestBody", {})
514
+ request_content = request_body.get("content", {})
515
+
516
+ for media_type, media_obj in request_content.items():
517
+ # Collect explicit examples
518
+ if "example" in media_obj:
519
+ request_test_cases.append(media_obj["example"])
520
+ if "examples" in media_obj:
521
+ examples_dict = media_obj["examples"]
522
+ for ex_name, ex_obj in examples_dict.items():
523
+ if "value" in ex_obj:
524
+ request_test_cases.append(ex_obj["value"])
525
+
526
+ # Also generate test cases from schema
527
+ if "schema" in media_obj:
528
+ schema = media_obj["schema"]
529
+ generated, warning = generate_test_cases_for_schema(schema)
530
+ if warning:
531
+ if isinstance(warning, list):
532
+ warnings.extend(warning)
533
+ else:
534
+ warnings.append(warning)
535
+ request_test_cases.extend(generated)
536
+ break
537
+
538
+ if not request_test_cases:
539
+ return False, "No request body examples or schema found"
540
+
541
+ # Print warnings if any
542
+ for warning in warnings:
543
+ print(f"\n{warning}")
544
+
545
+ # Get the expected response from examples or generate from schema
546
+ responses = operation.get("responses", {})
547
+ response_200 = responses.get("200", {})
548
+ content = response_200.get("content", {})
549
+
550
+ expected_response = None
551
+ for media_type, media_obj in content.items():
552
+ if "example" in media_obj:
553
+ expected_response = media_obj["example"]
554
+ break
555
+ elif "examples" in media_obj:
556
+ examples = media_obj["examples"]
557
+ if examples:
558
+ first_example = next(iter(examples.values()))
559
+ expected_response = first_example.get("value")
560
+ break
561
+ elif "schema" in media_obj:
562
+ # Generate test cases from schema
563
+ schema = media_obj["schema"]
564
+ response_test_cases, warning = generate_test_cases_for_schema(
565
+ schema
566
+ )
567
+ if warning:
568
+ if isinstance(warning, list):
569
+ for w in warning:
570
+ print(f"\n{w}")
571
+ else:
572
+ print(f"\n{warning}")
573
+ if response_test_cases:
574
+ expected_response = response_test_cases[0]
575
+ break
576
+
577
+ if expected_response is None:
578
+ return False, "No example or schema found for 200 response"
579
+
580
+ # Replace path parameters with values from the response example
581
+ url = f"{base_url}{path}"
582
+ resolved_path = path
583
+ if "{" in path:
584
+ import re
585
+
586
+ for match in re.finditer(r"\{(\w+)\}", path):
587
+ param_name = match.group(1)
588
+
589
+ # Try to find the value in the response example
590
+ # Common mappings: item_id -> id, user_id -> id, etc.
591
+ value = None
592
+
593
+ if param_name in expected_response:
594
+ value = expected_response[param_name]
595
+ elif param_name.endswith("_id") and "id" in expected_response:
596
+ # Map item_id -> id, user_id -> id, etc.
597
+ value = expected_response["id"]
598
+ elif "id" in expected_response:
599
+ # Default to using the id field
600
+ value = expected_response["id"]
601
+ else:
602
+ # Use a default test value
603
+ value = 1
604
+
605
+ url = url.replace(f"{{{param_name}}}", str(value))
606
+ resolved_path = resolved_path.replace(
607
+ f"{{{param_name}}}", str(value)
608
+ )
609
+
610
+ # Test all collected request cases
611
+ errors = []
612
+ for request_test_case in request_test_cases:
613
+ # Make the PUT request
614
+ try:
615
+ response = make_request("PUT", url, json=request_test_case)
616
+ except requests.exceptions.RequestException as e:
617
+ error_msg = f"Request failed: {e}"
618
+ log_test_result(
619
+ "PUT",
620
+ resolved_path,
621
+ request_test_case,
622
+ 200,
623
+ expected_response,
624
+ None,
625
+ None,
626
+ False,
627
+ error_msg,
628
+ )
629
+ errors.append(error_msg)
630
+ continue
631
+
632
+ # Check status code
633
+ actual_response = (
634
+ response.json() if response.status_code == 200 else response.text
635
+ )
636
+
637
+ if response.status_code != 200:
638
+ error_msg = f"Expected status 200, got {response.status_code}. Response: {response.text}"
639
+ log_test_result(
640
+ "PUT",
641
+ resolved_path,
642
+ request_test_case,
643
+ 200,
644
+ expected_response,
645
+ response.status_code,
646
+ actual_response,
647
+ False,
648
+ error_msg,
649
+ )
650
+ errors.append(error_msg)
651
+ continue
652
+
653
+ # Check response matches example
654
+ matches, error = compare_responses(expected_response, actual_response)
655
+ if not matches:
656
+ log_test_result(
657
+ "PUT",
658
+ resolved_path,
659
+ request_test_case,
660
+ 200,
661
+ expected_response,
662
+ response.status_code,
663
+ actual_response,
664
+ False,
665
+ error,
666
+ )
667
+ errors.append(error)
668
+ continue
669
+
670
+ log_test_result(
671
+ "PUT",
672
+ resolved_path,
673
+ request_test_case,
674
+ 200,
675
+ expected_response,
676
+ response.status_code,
677
+ actual_response,
678
+ True,
679
+ )
680
+
681
+ if errors:
682
+ combined = "; ".join(str(e) for e in errors[:3])
683
+ return False, combined
684
+
685
+ return True, None
686
+
687
+
688
+ def test_delete_endpoint(base_url, path, operation):
689
+ """Test a DELETE endpoint using the example from the OpenAPI spec.
690
+
691
+ Args:
692
+ base_url: Base URL of the API server
693
+ path: API endpoint path (may contain path parameters)
694
+ operation: OpenAPI operation object
695
+
696
+ Returns:
697
+ tuple: (success: bool, error_message: str or None)
698
+ """
699
+ # For DELETE, we need to get path parameters from the 200/204 response example or schema
700
+ responses = operation.get("responses", {})
701
+
702
+ # Try to find an example with path parameters
703
+ response_example = None
704
+ expected_status = None
705
+ expected_body = None
706
+
707
+ for status_code in ["200", "204", "404"]:
708
+ resp_obj = responses.get(status_code, {})
709
+ if status_code in ["200", "204"]:
710
+ expected_status = int(status_code)
711
+ content = resp_obj.get("content", {})
712
+ for media_type, media_obj in content.items():
713
+ if "example" in media_obj:
714
+ response_example = media_obj["example"]
715
+ expected_body = response_example
716
+ break
717
+ elif "examples" in media_obj:
718
+ examples = media_obj["examples"]
719
+ if examples:
720
+ first_example = next(iter(examples.values()))
721
+ response_example = first_example.get("value")
722
+ expected_body = response_example
723
+ break
724
+ elif "schema" in media_obj:
725
+ # Generate test cases from schema
726
+ schema = media_obj["schema"]
727
+ response_test_cases, warning = generate_test_cases_for_schema(
728
+ schema
729
+ )
730
+ if warning:
731
+ if isinstance(warning, list):
732
+ for w in warning:
733
+ print(f"\n{w}")
734
+ else:
735
+ print(f"\n{warning}")
736
+ if response_test_cases:
737
+ response_example = response_test_cases[0]
738
+ expected_body = response_example
739
+ break
740
+ if response_example:
741
+ break
742
+
743
+ # If no expected body found (common for 204 responses), use empty string
744
+ if expected_status is None:
745
+ expected_status = 204
746
+ if expected_body is None:
747
+ expected_body = ""
748
+
749
+ # Replace path parameters with values from the example
750
+ url = f"{base_url}{path}"
751
+ resolved_path = path
752
+ if "{" in path:
753
+ import re
754
+
755
+ # For DELETE, use a hardcoded ID if no example provides it
756
+ # This is a simplification - in reality we might need to create a resource first
757
+ for match in re.finditer(r"\{(\w+)\}", path):
758
+ param_name = match.group(1)
759
+
760
+ # Try to find the value in the response example
761
+ # Common mappings: item_id -> id, user_id -> id, etc.
762
+ param_value = None
763
+
764
+ if response_example and isinstance(response_example, dict):
765
+ if param_name in response_example:
766
+ param_value = response_example[param_name]
767
+ elif param_name.endswith("_id") and "id" in response_example:
768
+ # Map item_id -> id, user_id -> id, etc.
769
+ param_value = response_example["id"]
770
+ elif "id" in response_example:
771
+ # Default to using the id field
772
+ param_value = response_example["id"]
773
+
774
+ # Default test value if not found
775
+ if param_value is None:
776
+ param_value = 1
777
+
778
+ url = url.replace(f"{{{param_name}}}", str(param_value))
779
+ resolved_path = resolved_path.replace(
780
+ f"{{{param_name}}}", str(param_value)
781
+ )
782
+
783
+ # Make the DELETE request
784
+ try:
785
+ response = make_request("DELETE", url)
786
+ except requests.exceptions.RequestException as e:
787
+ error_msg = f"Request failed: {e}"
788
+ log_test_result(
789
+ "DELETE",
790
+ resolved_path,
791
+ None,
792
+ expected_status,
793
+ expected_body,
794
+ None,
795
+ None,
796
+ False,
797
+ error_msg,
798
+ )
799
+ return False, error_msg
800
+
801
+ # Check status code (accept 200 or 204 for successful DELETE)
802
+ actual_response = ""
803
+ if response.status_code == 200 and response.text:
804
+ try:
805
+ actual_response = response.json()
806
+ except Exception:
807
+ actual_response = response.text
808
+
809
+ if response.status_code not in [200, 204]:
810
+ error_msg = f"Expected status 200/204, got {response.status_code}. Response: {response.text}"
811
+ log_test_result(
812
+ "DELETE",
813
+ resolved_path,
814
+ None,
815
+ expected_status,
816
+ expected_body,
817
+ response.status_code,
818
+ actual_response,
819
+ False,
820
+ error_msg,
821
+ )
822
+ return False, error_msg
823
+
824
+ log_test_result(
825
+ "DELETE",
826
+ resolved_path,
827
+ None,
828
+ expected_status,
829
+ expected_body,
830
+ response.status_code,
831
+ actual_response,
832
+ True,
833
+ )
834
+ return True, None