From 48da0ddf6799019e008b9d94d8a168b440120028 Mon Sep 17 00:00:00 2001 From: abrehamteshale Date: Thu, 16 Jul 2026 07:38:04 -0700 Subject: [PATCH] fix(validators): allow GitLab URLs with nested subgroups The gitlabURLRegex only allowed two path segments (owner/repo), rejecting valid GitLab URLs with subgroups like: https://gitlab.com/myorg/team/subgroup/my-mcp-server This is a standard GitLab feature used in GitLab Enterprise. Changes: - Updated regex to allow any number of path segments - Added unit tests for subgroup URLs (1, 2, and 3 levels deep) - Added comprehensive test cases for IsValidRepositoryURL Fixes #1359 --- internal/validators/utils.go | 2 +- internal/validators/utils_test.go | 113 +++++++++++++++++++++++++ internal/validators/validators_test.go | 28 ++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 internal/validators/utils_test.go diff --git a/internal/validators/utils.go b/internal/validators/utils.go index 035132dbb..29e331054 100644 --- a/internal/validators/utils.go +++ b/internal/validators/utils.go @@ -11,7 +11,7 @@ var ( // These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository // For example: // - GitHub: https://github.com/user/repo githubURLRegex = regexp.MustCompile(`^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$`) - gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+/[\w.-]+/?$`) + gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+(/[\w.-]+)*/?$`) ) // IsValidRepositoryURL checks if the given URL is valid for the specified repository source diff --git a/internal/validators/utils_test.go b/internal/validators/utils_test.go new file mode 100644 index 000000000..2f01ada6e --- /dev/null +++ b/internal/validators/utils_test.go @@ -0,0 +1,113 @@ +package validators + +import ( + "testing" +) + +func TestIsValidRepositoryURL(t *testing.T) { + tests := []struct { + name string + source RepositorySource + url string + want bool + }{ + // GitHub valid URLs + { + name: "valid GitHub URL", + source: SourceGitHub, + url: "https://github.com/owner/repo", + want: true, + }, + { + name: "valid GitHub URL with www", + source: SourceGitHub, + url: "https://www.github.com/owner/repo", + want: true, + }, + { + name: "valid GitHub URL with trailing slash", + source: SourceGitHub, + url: "https://github.com/owner/repo/", + want: true, + }, + // GitHub invalid URLs + { + name: "GitHub URL missing repo", + source: SourceGitHub, + url: "https://github.com/owner", + want: false, + }, + { + name: "GitHub URL with subgroup", + source: SourceGitHub, + url: "https://github.com/org/team/repo", + want: false, + }, + // GitLab valid URLs + { + name: "valid GitLab URL", + source: SourceGitLab, + url: "https://gitlab.com/owner/repo", + want: true, + }, + { + name: "valid GitLab URL with www", + source: SourceGitLab, + url: "https://www.gitlab.com/owner/repo", + want: true, + }, + { + name: "valid GitLab URL with trailing slash", + source: SourceGitLab, + url: "https://gitlab.com/owner/repo/", + want: true, + }, + { + name: "valid GitLab URL with one subgroup", + source: SourceGitLab, + url: "https://gitlab.com/myorg/team/my-mcp-server", + want: true, + }, + { + name: "valid GitLab URL with nested subgroups", + source: SourceGitLab, + url: "https://gitlab.com/myorg/team/subgroup/my-mcp-server", + want: true, + }, + { + name: "valid GitLab URL with deeply nested subgroups", + source: SourceGitLab, + url: "https://gitlab.com/org/group/subgroup/project/repo", + want: true, + }, + // GitLab invalid URLs + { + name: "GitLab URL missing repo", + source: SourceGitLab, + url: "https://gitlab.com/owner", + want: false, + }, + { + name: "GitLab URL missing owner and repo", + source: SourceGitLab, + url: "https://gitlab.com", + want: false, + }, + // Unknown source + { + name: "unknown source", + source: "unknown", + url: "https://example.com/owner/repo", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsValidRepositoryURL(tt.source, tt.url) + if got != tt.want { + t.Errorf("IsValidRepositoryURL(%v, %q) = %v, want %v", tt.source, tt.url, got, tt.want) + } + }) + } +} diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index c3e5585bf..8fe91136d 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -323,6 +323,34 @@ func TestValidate(t *testing.T) { }, expectedError: validators.ErrInvalidRepositoryURL.Error(), }, + { + name: "server with valid GitLab URL with subgroup", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://gitlab.com/myorg/team/my-mcp-server", + Source: "gitlab", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "server with valid GitLab URL with nested subgroups", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://gitlab.com/myorg/team/subgroup/my-mcp-server", + Source: "gitlab", + }, + Version: "1.0.0", + }, + expectedError: "", + }, { name: "server with valid repository subfolder", serverDetail: apiv0.ServerJSON{