Jeremiah Lowin Claude commited on
Commit
5057c7c
·
1 Parent(s): d7899a9

Add comprehensive OpenAPI 3.0 vs 3.1 compatibility tests

Browse files

Adds test coverage for specific differences between OpenAPI 3.0 and 3.1 that can cause validation issues, including the scenario reported in GitHub issue #1021.

Test coverage includes:
- OpenAPI 3.0 boolean exclusiveMaximum format vs 3.1 numeric format
- OpenAPI 3.0 nullable vs 3.1 type array format
- Complex schemas with $defs and multiple exclusive constraints
- Edge cases with both exclusiveMaximum and exclusiveMinimum

These tests serve as regression prevention and document expected behavior for both OpenAPI versions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

tests/server/openapi/test_openapi_compatibility.py CHANGED
@@ -369,3 +369,312 @@ class TestOpenAPI31Compatibility:
369
  assert result.data["id"] == "o3"
370
  assert result.data["customer"] == "Charlie"
371
  assert result.data["items"] == ["item4", "item5"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  assert result.data["id"] == "o3"
370
  assert result.data["customer"] == "Charlie"
371
  assert result.data["items"] == ["item4", "item5"]
372
+
373
+
374
+ class TestOpenAPIVersionDifferences:
375
+ """Test specific differences between OpenAPI 3.0 and 3.1 that can cause compatibility issues."""
376
+
377
+ def test_openapi_30_exclusive_maximum_boolean_format(self):
378
+ """Test OpenAPI 3.0 format with boolean exclusiveMaximum (reproduces GitHub issue #1021)."""
379
+ spec_with_exclusive_max = {
380
+ "openapi": "3.0.0",
381
+ "info": {"title": "Loan API", "version": "1.0.0"},
382
+ "paths": {
383
+ "/loans": {
384
+ "post": {
385
+ "operationId": "createLoan",
386
+ "summary": "Create a loan",
387
+ "requestBody": {
388
+ "required": True,
389
+ "content": {
390
+ "application/json": {
391
+ "schema": {
392
+ "$ref": "#/components/schemas/LoanDetails"
393
+ }
394
+ }
395
+ },
396
+ },
397
+ "responses": {"201": {"description": "Loan created"}},
398
+ }
399
+ }
400
+ },
401
+ "components": {
402
+ "schemas": {
403
+ "LoanDetails": {
404
+ "type": "object",
405
+ "properties": {
406
+ "amount": {"type": "number", "minimum": 0},
407
+ "interest_rate": {
408
+ "type": "number",
409
+ "minimum": 0,
410
+ "maximum": 100,
411
+ "exclusiveMaximum": True, # OpenAPI 3.0 boolean format
412
+ },
413
+ },
414
+ "required": ["amount", "interest_rate"],
415
+ }
416
+ }
417
+ },
418
+ }
419
+
420
+ # This should not raise a ValidationError
421
+ from fastmcp.utilities.openapi import parse_openapi_to_http_routes
422
+
423
+ routes = parse_openapi_to_http_routes(spec_with_exclusive_max)
424
+ assert len(routes) == 1
425
+ assert routes[0].operation_id == "createLoan"
426
+
427
+ def test_openapi_31_exclusive_maximum_numeric_format(self):
428
+ """Test OpenAPI 3.1 format with numeric exclusiveMaximum."""
429
+ spec_with_exclusive_max = {
430
+ "openapi": "3.1.0",
431
+ "info": {"title": "Loan API", "version": "1.0.0"},
432
+ "paths": {
433
+ "/loans": {
434
+ "post": {
435
+ "operationId": "createLoan",
436
+ "summary": "Create a loan",
437
+ "requestBody": {
438
+ "required": True,
439
+ "content": {
440
+ "application/json": {
441
+ "schema": {
442
+ "$ref": "#/components/schemas/LoanDetails"
443
+ }
444
+ }
445
+ },
446
+ },
447
+ "responses": {"201": {"description": "Loan created"}},
448
+ }
449
+ }
450
+ },
451
+ "components": {
452
+ "schemas": {
453
+ "LoanDetails": {
454
+ "type": "object",
455
+ "properties": {
456
+ "amount": {"type": "number", "minimum": 0},
457
+ "interest_rate": {
458
+ "type": "number",
459
+ "minimum": 0,
460
+ "exclusiveMaximum": 100, # OpenAPI 3.1 numeric format
461
+ },
462
+ },
463
+ "required": ["amount", "interest_rate"],
464
+ }
465
+ }
466
+ },
467
+ }
468
+
469
+ # This should not raise a ValidationError
470
+ from fastmcp.utilities.openapi import parse_openapi_to_http_routes
471
+
472
+ routes = parse_openapi_to_http_routes(spec_with_exclusive_max)
473
+ assert len(routes) == 1
474
+ assert routes[0].operation_id == "createLoan"
475
+
476
+ def test_openapi_30_nullable_format(self):
477
+ """Test OpenAPI 3.0 nullable format."""
478
+ spec_with_nullable = {
479
+ "openapi": "3.0.0",
480
+ "info": {"title": "User API", "version": "1.0.0"},
481
+ "paths": {
482
+ "/users": {
483
+ "post": {
484
+ "operationId": "createUser",
485
+ "summary": "Create a user",
486
+ "requestBody": {
487
+ "required": True,
488
+ "content": {
489
+ "application/json": {
490
+ "schema": {
491
+ "type": "object",
492
+ "properties": {
493
+ "name": {"type": "string"},
494
+ "email": {
495
+ "type": "string",
496
+ "nullable": True, # OpenAPI 3.0 nullable format
497
+ },
498
+ },
499
+ "required": ["name"],
500
+ }
501
+ }
502
+ },
503
+ },
504
+ "responses": {"201": {"description": "User created"}},
505
+ }
506
+ }
507
+ },
508
+ }
509
+
510
+ # This should not raise a ValidationError
511
+ from fastmcp.utilities.openapi import parse_openapi_to_http_routes
512
+
513
+ routes = parse_openapi_to_http_routes(spec_with_nullable)
514
+ assert len(routes) == 1
515
+ assert routes[0].operation_id == "createUser"
516
+
517
+ def test_openapi_31_type_array_format(self):
518
+ """Test OpenAPI 3.1 type array format for nullable values."""
519
+ spec_with_type_array = {
520
+ "openapi": "3.1.0",
521
+ "info": {"title": "User API", "version": "1.0.0"},
522
+ "paths": {
523
+ "/users": {
524
+ "post": {
525
+ "operationId": "createUser",
526
+ "summary": "Create a user",
527
+ "requestBody": {
528
+ "required": True,
529
+ "content": {
530
+ "application/json": {
531
+ "schema": {
532
+ "type": "object",
533
+ "properties": {
534
+ "name": {"type": "string"},
535
+ "email": {
536
+ "type": [
537
+ "string",
538
+ "null",
539
+ ], # OpenAPI 3.1 type array format
540
+ },
541
+ },
542
+ "required": ["name"],
543
+ }
544
+ }
545
+ },
546
+ },
547
+ "responses": {"201": {"description": "User created"}},
548
+ }
549
+ }
550
+ },
551
+ }
552
+
553
+ # This should not raise a ValidationError
554
+ from fastmcp.utilities.openapi import parse_openapi_to_http_routes
555
+
556
+ routes = parse_openapi_to_http_routes(spec_with_type_array)
557
+ assert len(routes) == 1
558
+ assert routes[0].operation_id == "createUser"
559
+
560
+ def test_openapi_30_with_defs_and_exclusive_maximum(self):
561
+ """Test OpenAPI 3.0 with $defs and exclusiveMaximum (complex case from GitHub issue #1021)."""
562
+ spec_with_defs = {
563
+ "openapi": "3.0.0",
564
+ "info": {"title": "Complex Loan API", "version": "1.0.0"},
565
+ "paths": {
566
+ "/loans": {
567
+ "post": {
568
+ "operationId": "createComplexLoan",
569
+ "summary": "Create a complex loan",
570
+ "requestBody": {
571
+ "required": True,
572
+ "content": {
573
+ "application/json": {
574
+ "schema": {
575
+ "type": "object",
576
+ "properties": {
577
+ "loanDetails": {
578
+ "$ref": "#/components/schemas/LoanDetails"
579
+ }
580
+ },
581
+ "required": ["loanDetails"],
582
+ "$defs": {
583
+ "LoanDetails": {
584
+ "type": "object",
585
+ "properties": {
586
+ "interest_rate": {
587
+ "type": "number",
588
+ "minimum": 0,
589
+ "maximum": 100,
590
+ "exclusiveMaximum": True, # This should trigger the issue
591
+ },
592
+ },
593
+ "required": ["interest_rate"],
594
+ }
595
+ },
596
+ }
597
+ }
598
+ },
599
+ },
600
+ "responses": {"201": {"description": "Complex loan created"}},
601
+ }
602
+ }
603
+ },
604
+ "components": {
605
+ "schemas": {
606
+ "LoanDetails": {
607
+ "type": "object",
608
+ "properties": {
609
+ "interest_rate": {
610
+ "type": "number",
611
+ "minimum": 0,
612
+ "maximum": 100,
613
+ "exclusiveMaximum": True,
614
+ },
615
+ },
616
+ "required": ["interest_rate"],
617
+ }
618
+ }
619
+ },
620
+ }
621
+
622
+ # This should reproduce the validation error from GitHub issue #1021
623
+ from fastmcp.utilities.openapi import parse_openapi_to_http_routes
624
+
625
+ try:
626
+ routes = parse_openapi_to_http_routes(spec_with_defs)
627
+ assert len(routes) == 1
628
+ assert routes[0].operation_id == "createComplexLoan"
629
+ except ValueError as e:
630
+ # If this fails, it's reproducing the issue
631
+ pytest.fail(f"OpenAPI 3.0 validation failed: {e}")
632
+
633
+ def test_openapi_30_edge_case_with_multiple_exclusive_constraints(self):
634
+ """Test edge case with multiple exclusive constraints that might trigger validation issues."""
635
+ spec_edge_case = {
636
+ "openapi": "3.0.0",
637
+ "info": {"title": "Edge Case API", "version": "1.0.0"},
638
+ "paths": {
639
+ "/validate": {
640
+ "post": {
641
+ "operationId": "validateData",
642
+ "summary": "Validate data with edge case constraints",
643
+ "requestBody": {
644
+ "required": True,
645
+ "content": {
646
+ "application/json": {
647
+ "schema": {
648
+ "type": "object",
649
+ "properties": {
650
+ "percentage": {
651
+ "type": "number",
652
+ "minimum": 0,
653
+ "maximum": 100,
654
+ "exclusiveMaximum": True,
655
+ "exclusiveMinimum": True, # Both exclusive constraints
656
+ },
657
+ "rating": {
658
+ "type": "integer",
659
+ "minimum": 1,
660
+ "maximum": 10,
661
+ "exclusiveMaximum": True,
662
+ },
663
+ },
664
+ "required": ["percentage", "rating"],
665
+ }
666
+ }
667
+ },
668
+ },
669
+ "responses": {"200": {"description": "Data validated"}},
670
+ }
671
+ }
672
+ },
673
+ }
674
+
675
+ # This might trigger validation issues with multiple exclusive constraints
676
+ from fastmcp.utilities.openapi import parse_openapi_to_http_routes
677
+
678
+ routes = parse_openapi_to_http_routes(spec_edge_case)
679
+ assert len(routes) == 1
680
+ assert routes[0].operation_id == "validateData"