@@ -2,6 +2,7 @@ package validators_test
22
33import (
44 "fmt"
5+ "strings"
56 "testing"
67
78 "github.com/modelcontextprotocol/registry/internal/validators"
@@ -648,6 +649,183 @@ func TestValidateArgument_ValidValueFields(t *testing.T) {
648649}
649650
650651// Helper function to create a valid server with a specific argument for testing
652+ func TestValidate_RegistryTypes (t * testing.T ) {
653+ testCases := []struct {
654+ name string
655+ registryType string
656+ baseURL string
657+ identifier string
658+ expectError bool
659+ }{
660+ // Valid registry types (should pass)
661+ {"valid_npm" , model .RegistryTypeNPM , model .RegistryURLNPM , "test-package" , false },
662+ {"valid_pypi" , model .RegistryTypePyPI , model .RegistryURLPyPI , "test-package" , false },
663+ {"valid_oci" , model .RegistryTypeOCI , model .RegistryURLDocker , "test-package" , false },
664+ {"valid_nuget" , model .RegistryTypeNuGet , model .RegistryURLNuGet , "test-package" , false },
665+ {"valid_mcpb_github" , model .RegistryTypeMCPB , model .RegistryURLGitHub , "https://github.com/owner/repo" , false },
666+ {"valid_mcpb_gitlab" , model .RegistryTypeMCPB , model .RegistryURLGitLab , "https://gitlab.com/owner/repo" , false },
667+
668+ // Invalid registry types (should fail)
669+ {"invalid_maven" , "maven" , "https://example.com/registry" , "test-package" , true },
670+ {"invalid_cargo" , "cargo" , "https://example.com/registry" , "test-package" , true },
671+ {"invalid_gem" , "gem" , "https://example.com/registry" , "test-package" , true },
672+ {"invalid_invalid" , "invalid" , "https://example.com/registry" , "test-package" , true },
673+ {"invalid_unknown" , "UNKNOWN" , "https://example.com/registry" , "test-package" , true },
674+ {"invalid_custom" , "custom-registry" , "https://example.com/registry" , "test-package" , true },
675+ {"invalid_github" , "github" , "https://example.com/registry" , "test-package" , true }, // This is a source, not a registry type
676+ {"invalid_docker" , "docker" , "https://example.com/registry" , "test-package" , true }, // Should be "oci"
677+ {"invalid_empty" , "" , "https://example.com/registry" , "test-package" , true }, // Empty registry type
678+ }
679+
680+ for _ , tc := range testCases {
681+ t .Run (tc .name , func (t * testing.T ) {
682+ serverDetail := apiv0.ServerJSON {
683+ Name : "com.example/test-server" ,
684+ Description : "A test server" ,
685+ Repository : model.Repository {
686+ URL : "https://github.com/owner/repo" ,
687+ Source : "github" ,
688+ ID : "owner/repo" ,
689+ },
690+ VersionDetail : model.VersionDetail {
691+ Version : "1.0.0" ,
692+ },
693+ Packages : []model.Package {
694+ {
695+ Identifier : tc .identifier ,
696+ RegistryType : tc .registryType ,
697+ RegistryBaseURL : tc .baseURL ,
698+ },
699+ },
700+ Remotes : []model.Remote {
701+ {
702+ URL : "https://example.com/remote" ,
703+ },
704+ },
705+ }
706+
707+ err := validators .ValidateServerJSON (& serverDetail )
708+ if tc .expectError {
709+ assert .Error (t , err )
710+ assert .Contains (t , err .Error (), validators .ErrUnsupportedRegistryType .Error ())
711+ } else {
712+ assert .NoError (t , err )
713+ }
714+ })
715+ }
716+ }
717+
718+ func TestValidate_RegistryBaseURLs (t * testing.T ) {
719+ testCases := []struct {
720+ name string
721+ registryType string
722+ baseURL string
723+ identifier string
724+ expectError bool
725+ }{
726+ // Invalid base URLs for specific registry types
727+ {"npm_wrong_url" , model .RegistryTypeNPM , "https://pypi.org" , "test-package" , true },
728+ {"pypi_wrong_url" , model .RegistryTypePyPI , "https://registry.npmjs.org" , "test-package" , true },
729+ {"oci_wrong_url" , model .RegistryTypeOCI , "https://registry.npmjs.org" , "test-package" , true },
730+ {"nuget_wrong_url" , model .RegistryTypeNuGet , "https://docker.io" , "test-package" , true },
731+ {"mcpb_wrong_url" , model .RegistryTypeMCPB , "https://evil.com" , "https://github.com/owner/repo" , true },
732+ {"empty_base_url" , model .RegistryTypeNPM , "" , "test-package" , true },
733+ {"empty_base_url" , model .RegistryTypeNPM , model .RegistryURLDocker , "test-package" , true },
734+ {"empty_base_url" , model .RegistryTypeOCI , model .RegistryTypeNuGet , "test-package" , true },
735+
736+ // Localhost URLs should be rejected - no development exceptions
737+ {"localhost_npm" , model .RegistryTypeNPM , "http://localhost:3000" , "test-package" , true },
738+ {"localhost_ip" , model .RegistryTypePyPI , "http://127.0.0.1:8080" , "test-package" , true },
739+
740+ // Valid combinations (should pass)
741+ {"valid_npm" , model .RegistryTypeNPM , model .RegistryURLNPM , "test-package" , false },
742+ {"valid_pypi" , model .RegistryTypePyPI , model .RegistryURLPyPI , "test-package" , false },
743+ {"valid_oci" , model .RegistryTypeOCI , model .RegistryURLDocker , "test-package" , false },
744+ {"valid_nuget" , model .RegistryTypeNuGet , model .RegistryURLNuGet , "test-package" , false },
745+ {"valid_mcpb_github" , model .RegistryTypeMCPB , model .RegistryURLGitHub , "https://github.com/owner/repo" , false },
746+ {"valid_mcpb_gitlab" , model .RegistryTypeMCPB , model .RegistryURLGitLab , "https://gitlab.com/owner/repo" , false },
747+
748+ // Trailing slash URLs should be rejected - strict exact match only
749+ {"npm_trailing_slash" , model .RegistryTypeNPM , "https://registry.npmjs.org/" , "test-package" , true },
750+ {"pypi_trailing_slash" , model .RegistryTypePyPI , "https://pypi.org/" , "test-package" , true },
751+ }
752+
753+ for _ , tc := range testCases {
754+ t .Run (tc .name , func (t * testing.T ) {
755+ serverDetail := apiv0.ServerJSON {
756+ Name : "com.example/test-server" ,
757+ Description : "A test server" ,
758+ Repository : model.Repository {
759+ URL : "https://github.com/owner/repo" ,
760+ Source : "github" ,
761+ ID : "owner/repo" ,
762+ },
763+ VersionDetail : model.VersionDetail {
764+ Version : "1.0.0" ,
765+ },
766+ Packages : []model.Package {
767+ {
768+ Identifier : tc .identifier ,
769+ RegistryType : tc .registryType ,
770+ RegistryBaseURL : tc .baseURL ,
771+ },
772+ },
773+ Remotes : []model.Remote {
774+ {
775+ URL : "https://example.com/remote" ,
776+ },
777+ },
778+ }
779+
780+ err := validators .ValidateServerJSON (& serverDetail )
781+ if tc .expectError {
782+ assert .Error (t , err )
783+ // Check that the error is related to registry validation
784+ errStr := err .Error ()
785+ assert .True (t ,
786+ strings .Contains (errStr , validators .ErrUnsupportedRegistryBaseURL .Error ()) ||
787+ strings .Contains (errStr , validators .ErrMismatchedRegistryTypeAndURL .Error ()),
788+ "Expected registry validation error, got: %s" , errStr )
789+ } else {
790+ assert .NoError (t , err )
791+ }
792+ })
793+ }
794+ }
795+
796+ func TestValidate_EmptyRegistryType (t * testing.T ) {
797+ // Test that empty registry type is rejected
798+ serverDetail := apiv0.ServerJSON {
799+ Name : "com.example/test-server" ,
800+ Description : "A test server" ,
801+ Repository : model.Repository {
802+ URL : "https://github.com/owner/repo" ,
803+ Source : "github" ,
804+ ID : "owner/repo" ,
805+ },
806+ VersionDetail : model.VersionDetail {
807+ Version : "1.0.0" ,
808+ },
809+ Packages : []model.Package {
810+ {
811+ Identifier : "test-package" ,
812+ RegistryType : "" , // Empty registry type
813+ RegistryBaseURL : "" ,
814+ },
815+ },
816+ Remotes : []model.Remote {
817+ {
818+ URL : "https://example.com/remote" ,
819+ },
820+ },
821+ }
822+
823+ err := validators .ValidateServerJSON (& serverDetail )
824+ assert .Error (t , err )
825+ assert .Contains (t , err .Error (), validators .ErrUnsupportedRegistryType .Error ())
826+ assert .Contains (t , err .Error (), "registry type is required" )
827+ }
828+
651829func createValidServerWithArgument (arg model.Argument ) apiv0.ServerJSON {
652830 return apiv0.ServerJSON {
653831 Name : "com.example/test-server" ,
0 commit comments