@@ -543,3 +543,343 @@ async def test_reset_metrics(self, server_service, test_db):
543
543
await server_service .reset_metrics (test_db )
544
544
test_db .execute .assert_called_once ()
545
545
test_db .commit .assert_called_once ()
546
+
547
+ # --------------------------- UUID normalization -------------------- #
548
+ @pytest .mark .asyncio
549
+ async def test_register_server_uuid_normalization_standard_format (self , server_service , test_db ):
550
+ """Test server registration with standard UUID format (with dashes) normalizes to hex format."""
551
+ import uuid as uuid_module
552
+
553
+ # Standard UUID format (with dashes)
554
+ standard_uuid = "550e8400-e29b-41d4-a716-446655440000"
555
+ expected_hex_uuid = str (uuid_module .UUID (standard_uuid )).replace ('-' , '' )
556
+
557
+ # No existing server with the same name
558
+ mock_scalar = Mock ()
559
+ mock_scalar .scalar_one_or_none .return_value = None
560
+ test_db .execute = Mock (return_value = mock_scalar )
561
+
562
+ # Capture the server being added to verify UUID normalization
563
+ captured_server = None
564
+ def capture_add (server ):
565
+ nonlocal captured_server
566
+ captured_server = server
567
+
568
+ test_db .add = Mock (side_effect = capture_add )
569
+ test_db .commit = Mock ()
570
+ test_db .refresh = Mock ()
571
+ test_db .get = Mock (return_value = None ) # No associated items
572
+
573
+ # Mock service methods
574
+ server_service ._notify_server_added = AsyncMock ()
575
+ server_service ._convert_server_to_read = Mock (
576
+ return_value = ServerRead (
577
+ id = expected_hex_uuid ,
578
+ name = "UUID Normalization Test" ,
579
+ description = "Test UUID normalization" ,
580
+ icon = None ,
581
+ created_at = "2023-01-01T00:00:00" ,
582
+ updated_at = "2023-01-01T00:00:00" ,
583
+ is_active = True ,
584
+ associated_tools = [],
585
+ associated_resources = [],
586
+ associated_prompts = [],
587
+ metrics = {
588
+ "total_executions" : 0 ,
589
+ "successful_executions" : 0 ,
590
+ "failed_executions" : 0 ,
591
+ "failure_rate" : 0.0 ,
592
+ "min_response_time" : None ,
593
+ "max_response_time" : None ,
594
+ "avg_response_time" : None ,
595
+ "last_execution_time" : None ,
596
+ },
597
+ )
598
+ )
599
+
600
+ server_create = ServerCreate (
601
+ id = standard_uuid ,
602
+ name = "UUID Normalization Test" ,
603
+ description = "Test UUID normalization"
604
+ )
605
+
606
+ # Call the service method
607
+ result = await server_service .register_server (test_db , server_create )
608
+
609
+ # Verify UUID was normalized to hex format
610
+ assert captured_server is not None
611
+ assert captured_server .id == expected_hex_uuid
612
+ assert len (captured_server .id ) == 32
613
+ assert "-" not in captured_server .id
614
+ assert result .id == expected_hex_uuid
615
+
616
+ # Verify other operations were called
617
+ test_db .add .assert_called_once ()
618
+ test_db .commit .assert_called_once ()
619
+ test_db .refresh .assert_called_once ()
620
+
621
+ @pytest .mark .asyncio
622
+ async def test_register_server_uuid_normalization_hex_format (self , server_service , test_db ):
623
+ """Test server registration with hex UUID format works correctly."""
624
+ import uuid as uuid_module
625
+
626
+ # Standard UUID that will be normalized
627
+ standard_uuid = "123e4567-e89b-12d3-a456-426614174000"
628
+ expected_hex_uuid = str (uuid_module .UUID (standard_uuid )).replace ('-' , '' )
629
+
630
+ # No existing server with the same name
631
+ mock_scalar = Mock ()
632
+ mock_scalar .scalar_one_or_none .return_value = None
633
+ test_db .execute = Mock (return_value = mock_scalar )
634
+
635
+ # Capture the server being added to verify UUID normalization
636
+ captured_server = None
637
+ def capture_add (server ):
638
+ nonlocal captured_server
639
+ captured_server = server
640
+
641
+ test_db .add = Mock (side_effect = capture_add )
642
+ test_db .commit = Mock ()
643
+ test_db .refresh = Mock ()
644
+ test_db .get = Mock (return_value = None ) # No associated items
645
+
646
+ # Mock service methods
647
+ server_service ._notify_server_added = AsyncMock ()
648
+ server_service ._convert_server_to_read = Mock (
649
+ return_value = ServerRead (
650
+ id = expected_hex_uuid ,
651
+ name = "Hex UUID Test" ,
652
+ description = "Test hex UUID handling" ,
653
+ icon = None ,
654
+ created_at = "2023-01-01T00:00:00" ,
655
+ updated_at = "2023-01-01T00:00:00" ,
656
+ is_active = True ,
657
+ associated_tools = [],
658
+ associated_resources = [],
659
+ associated_prompts = [],
660
+ metrics = {
661
+ "total_executions" : 0 ,
662
+ "successful_executions" : 0 ,
663
+ "failed_executions" : 0 ,
664
+ "failure_rate" : 0.0 ,
665
+ "min_response_time" : None ,
666
+ "max_response_time" : None ,
667
+ "avg_response_time" : None ,
668
+ "last_execution_time" : None ,
669
+ },
670
+ )
671
+ )
672
+
673
+ server_create = ServerCreate (
674
+ id = standard_uuid , # Will be normalized by the service
675
+ name = "Hex UUID Test" ,
676
+ description = "Test hex UUID handling"
677
+ )
678
+
679
+ # Call the service method
680
+ result = await server_service .register_server (test_db , server_create )
681
+
682
+ # Verify UUID was normalized correctly
683
+ assert captured_server is not None
684
+ assert captured_server .id == expected_hex_uuid
685
+ assert len (captured_server .id ) == 32
686
+ assert "-" not in captured_server .id
687
+ assert captured_server .id .isalnum ()
688
+ assert result .id == expected_hex_uuid
689
+
690
+ @pytest .mark .asyncio
691
+ async def test_register_server_no_uuid_auto_generation (self , server_service , test_db ):
692
+ """Test server registration without UUID allows auto-generation."""
693
+ # No existing server with the same name
694
+ mock_scalar = Mock ()
695
+ mock_scalar .scalar_one_or_none .return_value = None
696
+ test_db .execute = Mock (return_value = mock_scalar )
697
+
698
+ # Capture the server being added
699
+ captured_server = None
700
+ def capture_add (server ):
701
+ nonlocal captured_server
702
+ captured_server = server
703
+
704
+ test_db .add = Mock (side_effect = capture_add )
705
+ test_db .commit = Mock ()
706
+ test_db .refresh = Mock ()
707
+ test_db .get = Mock (return_value = None ) # No associated items
708
+
709
+ # Mock service methods
710
+ server_service ._notify_server_added = AsyncMock ()
711
+ server_service ._convert_server_to_read = Mock (
712
+ return_value = ServerRead (
713
+ id = "auto_generated_uuid_32_chars_hex" ,
714
+ name = "Auto UUID Test" ,
715
+ description = "Test auto UUID generation" ,
716
+ icon = None ,
717
+ created_at = "2023-01-01T00:00:00" ,
718
+ updated_at = "2023-01-01T00:00:00" ,
719
+ is_active = True ,
720
+ associated_tools = [],
721
+ associated_resources = [],
722
+ associated_prompts = [],
723
+ metrics = {
724
+ "total_executions" : 0 ,
725
+ "successful_executions" : 0 ,
726
+ "failed_executions" : 0 ,
727
+ "failure_rate" : 0.0 ,
728
+ "min_response_time" : None ,
729
+ "max_response_time" : None ,
730
+ "avg_response_time" : None ,
731
+ "last_execution_time" : None ,
732
+ },
733
+ )
734
+ )
735
+
736
+ server_create = ServerCreate (
737
+ name = "Auto UUID Test" ,
738
+ description = "Test auto UUID generation"
739
+ )
740
+ # Verify no UUID is set
741
+ assert server_create .id is None
742
+
743
+ # Call the service method
744
+ result = await server_service .register_server (test_db , server_create )
745
+
746
+ # Verify no UUID was set on the server (letting DB handle auto-generation)
747
+ assert captured_server is not None
748
+ assert captured_server .id is None # Service doesn't set UUID when not provided
749
+ assert result .id == "auto_generated_uuid_32_chars_hex"
750
+
751
+ @pytest .mark .asyncio
752
+ async def test_register_server_uuid_normalization_error_handling (self , server_service , test_db ):
753
+ """Test that UUID normalization handles errors gracefully."""
754
+ # No existing server with the same name
755
+ mock_scalar = Mock ()
756
+ mock_scalar .scalar_one_or_none .return_value = None
757
+ test_db .execute = Mock (return_value = mock_scalar )
758
+
759
+ # Mock database rollback for error scenarios
760
+ test_db .rollback = Mock ()
761
+
762
+ server_create = ServerCreate (
763
+ id = "550e8400-e29b-41d4-a716-446655440000" ,
764
+ name = "Error Test" ,
765
+ description = "Test error handling"
766
+ )
767
+
768
+ # Simulate an error during database operations
769
+ test_db .add = Mock (side_effect = Exception ("Database error" ))
770
+
771
+ # The service should catch the exception and raise ServerError
772
+ with pytest .raises (ServerError ) as exc :
773
+ await server_service .register_server (test_db , server_create )
774
+
775
+ assert "Failed to register server" in str (exc .value )
776
+ test_db .rollback .assert_called_once ()
777
+
778
+ @pytest .mark .asyncio
779
+ async def test_update_server_uuid_normalization (self , server_service , test_db ):
780
+ """Test server update with UUID normalization."""
781
+ import uuid as uuid_module
782
+
783
+ # Mock existing server
784
+ existing_server = MagicMock (spec = DbServer )
785
+ existing_server .id = "oldserverid"
786
+ existing_server .name = "Old Name"
787
+ existing_server .is_active = True
788
+ existing_server .tools = []
789
+ existing_server .resources = []
790
+ existing_server .prompts = []
791
+
792
+ # New UUID to update to
793
+ new_standard_uuid = "550e8400-e29b-41d4-a716-446655440000"
794
+ expected_hex_uuid = str (uuid_module .UUID (new_standard_uuid )).replace ('-' , '' )
795
+
796
+ # Mock db.get to return existing server for the initial lookup, then None for the UUID check
797
+ test_db .get = Mock (side_effect = lambda cls , _id : existing_server if _id == "oldserverid" else None )
798
+
799
+ # Mock name conflict check
800
+ mock_scalar = Mock ()
801
+ mock_scalar .scalar_one_or_none .return_value = None
802
+ test_db .execute = Mock (return_value = mock_scalar )
803
+
804
+ test_db .commit = Mock ()
805
+ test_db .refresh = Mock ()
806
+
807
+ # Mock service methods
808
+ server_service ._notify_server_updated = AsyncMock ()
809
+ server_service ._convert_server_to_read = Mock (
810
+ return_value = ServerRead (
811
+ id = expected_hex_uuid ,
812
+ name = "Updated Server" ,
813
+ description = "Updated description" ,
814
+ icon = None ,
815
+ created_at = "2023-01-01T00:00:00" ,
816
+ updated_at = "2023-01-01T00:00:00" ,
817
+ is_active = True ,
818
+ associated_tools = [],
819
+ associated_resources = [],
820
+ associated_prompts = [],
821
+ metrics = {
822
+ "total_executions" : 0 ,
823
+ "successful_executions" : 0 ,
824
+ "failed_executions" : 0 ,
825
+ "failure_rate" : 0.0 ,
826
+ "min_response_time" : None ,
827
+ "max_response_time" : None ,
828
+ "avg_response_time" : None ,
829
+ "last_execution_time" : None ,
830
+ },
831
+ )
832
+ )
833
+
834
+ server_update = ServerUpdate (
835
+ id = new_standard_uuid ,
836
+ name = "Updated Server" ,
837
+ description = "Updated description"
838
+ )
839
+
840
+ # Call the service method
841
+ result = await server_service .update_server (test_db , "oldserverid" , server_update )
842
+
843
+ # Verify UUID was set correctly (note: actual normalization happens at create time)
844
+ # The update method currently just sets the ID directly
845
+ assert existing_server .id == new_standard_uuid # Update doesn't normalize currently
846
+ assert result .id == expected_hex_uuid
847
+ test_db .commit .assert_called_once ()
848
+ test_db .refresh .assert_called_once ()
849
+
850
+ def test_uuid_normalization_edge_cases (self , server_service ):
851
+ """Test edge cases in UUID normalization logic."""
852
+ import uuid as uuid_module
853
+
854
+ # Test various UUID formats that should all normalize correctly
855
+ test_cases = [
856
+ {
857
+ "input" : "550e8400-e29b-41d4-a716-446655440000" ,
858
+ "expected" : "550e8400e29b41d4a716446655440000" ,
859
+ "description" : "Standard lowercase UUID"
860
+ },
861
+ {
862
+ "input" : "550E8400-E29B-41D4-A716-446655440000" ,
863
+ "expected" : "550e8400e29b41d4a716446655440000" ,
864
+ "description" : "Uppercase UUID (should normalize to lowercase)"
865
+ },
866
+ {
867
+ "input" : "00000000-0000-0000-0000-000000000000" ,
868
+ "expected" : "00000000000000000000000000000000" ,
869
+ "description" : "Nil UUID"
870
+ },
871
+ {
872
+ "input" : "ffffffff-ffff-ffff-ffff-ffffffffffff" ,
873
+ "expected" : "ffffffffffffffffffffffffffffffff" ,
874
+ "description" : "Max UUID"
875
+ },
876
+ ]
877
+
878
+ for case in test_cases :
879
+ # Simulate the exact normalization logic from server_service.py
880
+ normalized = str (uuid_module .UUID (case ["input" ])).replace ('-' , '' )
881
+ assert normalized == case ["expected" ], f"Failed for { case ['description' ]} : expected { case ['expected' ]} , got { normalized } "
882
+ assert len (normalized ) == 32
883
+ # Check that any alphabetic characters are lowercase
884
+ assert normalized .islower () or not any (c .isalpha () for c in normalized )
885
+ assert normalized .isalnum ()
0 commit comments