From 1fe142fde56abb1ff99ce168538b61ffadf9ba80 Mon Sep 17 00:00:00 2001 From: AakashSuresh2003 Date: Wed, 3 Dec 2025 17:16:19 +0530 Subject: [PATCH 1/5] feat: Add URL validation for org/repo/enterprise arguments with comprehensive tests (#1180) --- RELEASENOTES.md | 2 + .../CreateTeam/CreateTeamCommandArgs.cs | 14 +++- .../DownloadLogs/DownloadLogsCommandArgs.cs | 18 ++++- .../GenerateMannequinCsvCommandArgs.cs | 11 +++ .../GrantMigratorRoleCommandArgs.cs | 5 ++ .../ReclaimMannequinCommandArgs.cs | 9 ++- .../RevokeMigratorRoleCommandArgs.cs | 5 ++ src/Octoshift/Extensions/StringExtensions.cs | 29 ++++++++ .../CreateTeam/CreateTeamCommandArgsTests.cs | 44 ++++++++++++ .../DownloadLogsCommandArgsTests.cs | 60 ++++++++++++++++ .../GenerateMannequinCsvCommandArgsTests.cs | 41 +++++++++++ .../GrantMigratorRoleCommandArgsTests.cs | 16 +++++ .../ReclaimMannequinCommandArgsTests.cs | 15 ++++ .../RevokeMigratorRoleCommandArgsTests.cs | 16 +++++ .../StringExtensionsTests.cs | 23 ++++++ .../IntegrateBoardsCommandArgsTests.cs | 68 ++++++++++++++++++ .../MigrateRepoCommandArgsTests.cs | 72 +++++++++++++++++++ .../GenerateScriptCommandArgsTests.cs | 32 +++++++++ ...grateCodeScanningAlertsCommandArgsTests.cs | 65 +++++++++++++++++ .../MigrateOrg/MigrateOrgCommandArgsTests.cs | 52 ++++++++++++++ .../MigrateRepoCommandArgsTests.cs | 69 ++++++++++++++++++ .../MigrateSecretAlertsCommandArgsTests.cs | 65 +++++++++++++++++ .../IntegrateBoardsCommandArgs.cs | 15 ++++ .../MigrateRepo/MigrateRepoCommandArgs.cs | 15 ++++ .../GenerateScriptCommandArgs.cs | 10 +++ .../MigrateCodeScanningAlertsCommandArgs.cs | 20 ++++++ .../MigrateOrg/MigrateOrgCommandArgs.cs | 15 ++++ .../MigrateRepo/MigrateRepoCommandArgs.cs | 20 ++++++ .../MigrateSecretAlertsCommandArgs.cs | 20 ++++++ 29 files changed, 843 insertions(+), 3 deletions(-) create mode 100644 src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs create mode 100644 src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs create mode 100644 src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs create mode 100644 src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs create mode 100644 src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e88df6a06..990e756aa 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1 +1,3 @@ - Added `--migration-id` option to `download-logs` command to allow downloading logs directly by migration ID without requiring org/repo lookup +- Added validation to detect and return clear error messages when a URL is provided instead of a name for organization, repository, or enterprise arguments (e.g., `--github-org`, `--github-target-org`, `--source-repo`, `--github-target-enterprise`) + diff --git a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs index bf2d6697f..208d5d1c2 100644 --- a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs +++ b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs @@ -1,4 +1,8 @@ -namespace OctoshiftCLI.Commands.CreateTeam; +using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.Commands.CreateTeam; public class CreateTeamCommandArgs : CommandArgs { @@ -8,4 +12,12 @@ public class CreateTeamCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + } } diff --git a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs index 8795ca52d..a69533ec4 100644 --- a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs +++ b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs @@ -1,4 +1,7 @@ - +using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + namespace OctoshiftCLI.Commands.DownloadLogs; public class DownloadLogsCommandArgs : CommandArgs @@ -11,4 +14,17 @@ public class DownloadLogsCommandArgs : CommandArgs public string GithubPat { get; set; } public string MigrationLogFile { get; set; } public bool Overwrite { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } diff --git a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs index 7b92a6f0c..f1b43f46e 100644 --- a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs +++ b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs @@ -1,4 +1,7 @@ using System.IO; +using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.GenerateMannequinCsv; @@ -10,4 +13,12 @@ public class GenerateMannequinCsvCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + } } diff --git a/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs b/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs index c85a52004..cb71c94df 100644 --- a/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs +++ b/src/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgs.cs @@ -15,6 +15,11 @@ public class GrantMigratorRoleCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + ActorType = ActorType?.ToUpper(); if (ActorType is "TEAM" or "USER") diff --git a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs index 71db554cb..5218c0781 100644 --- a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs +++ b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs @@ -1,4 +1,6 @@ -using OctoshiftCLI.Services; +using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.ReclaimMannequin; @@ -17,6 +19,11 @@ public class ReclaimMannequinCommandArgs : CommandArgs public string TargetApiUrl { get; set; } public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + if (string.IsNullOrEmpty(Csv) && (string.IsNullOrEmpty(MannequinUser) || string.IsNullOrEmpty(TargetUser))) { throw new OctoshiftCliException($"Either --csv or --mannequin-user and --target-user must be specified"); diff --git a/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs b/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs index 1d9db9ef7..64bf2899c 100644 --- a/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs +++ b/src/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgs.cs @@ -15,6 +15,11 @@ public class RevokeMigratorRoleCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + ActorType = ActorType?.ToUpper(); if (ActorType is "TEAM" or "USER") diff --git a/src/Octoshift/Extensions/StringExtensions.cs b/src/Octoshift/Extensions/StringExtensions.cs index 8d323ff92..5272c12e3 100644 --- a/src/Octoshift/Extensions/StringExtensions.cs +++ b/src/Octoshift/Extensions/StringExtensions.cs @@ -26,5 +26,34 @@ public static class StringExtensions public static string EscapeDataString(this string value) => Uri.EscapeDataString(value); public static byte[] ToBytes(this string s) => Encoding.UTF8.GetBytes(s); + + public static bool IsUrl(this string s) + { + if (s.IsNullOrWhiteSpace()) + { + return false; + } + + // Check if string starts with http:// or https:// + if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if string contains common URL patterns like domain.com/path or www. + if (s.Contains("://") || s.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if it looks like a URL path (contains / and .) + if (s.Contains('/') && s.Contains('.')) + { + return true; + } + + return false; + } } } diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs new file mode 100644 index 000000000..19fcfc8c9 --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/CreateTeam/CreateTeamCommandArgsTests.cs @@ -0,0 +1,44 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.Commands.CreateTeam; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Commands.CreateTeam; + +public class CreateTeamCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string GITHUB_ORG = "foo-org"; + private const string TEAM_NAME = "my-team"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new CreateTeamCommandArgs + { + GithubOrg = "http://github.com/my-org", + TeamName = TEAM_NAME + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Name() + { + var args = new CreateTeamCommandArgs + { + GithubOrg = GITHUB_ORG, + TeamName = TEAM_NAME + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + } +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs new file mode 100644 index 000000000..f23079966 --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgsTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.Commands.DownloadLogs; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Commands.DownloadLogs; + +public class DownloadLogsCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string GITHUB_ORG = "foo-org"; + private const string GITHUB_REPO = "foo-repo"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new DownloadLogsCommandArgs + { + GithubOrg = "https://github.com/my-org", + GithubRepo = GITHUB_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubRepo_Is_Url() + { + var args = new DownloadLogsCommandArgs + { + GithubOrg = GITHUB_ORG, + GithubRepo = "github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Names() + { + var args = new DownloadLogsCommandArgs + { + GithubOrg = GITHUB_ORG, + GithubRepo = GITHUB_REPO + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + args.GithubRepo.Should().Be(GITHUB_REPO); + } +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs new file mode 100644 index 000000000..e92370e3e --- /dev/null +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgsTests.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.Commands.GenerateMannequinCsv; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.Octoshift.Commands.GenerateMannequinCsv; + +public class GenerateMannequinCsvCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string GITHUB_ORG = "foo-org"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new GenerateMannequinCsvCommandArgs + { + GithubOrg = "www.github.com" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Name() + { + var args = new GenerateMannequinCsvCommandArgs + { + GithubOrg = GITHUB_ORG + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + } +} diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs index f429438c4..3183a41ce 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/GrantMigratorRole/GrantMigratorRoleCommandArgsTests.cs @@ -42,4 +42,20 @@ public void It_Validates_GhesApiUrl_And_TargetApiUrl() FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) .Should().Throw(); } + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new GrantMigratorRoleCommandArgs + { + GithubOrg = "https://github.com/my-org", + Actor = ACTOR, + ActorType = "USER" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs index 5592afc86..5640c6d25 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgsTests.cs @@ -21,4 +21,19 @@ public void No_Parameters_Provided_Throws_OctoshiftCliException() .Invoking(() => args.Validate(_mockOctoLogger.Object)) .Should().Throw(); } + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new ReclaimMannequinCommandArgs + { + GithubOrg = "www.github.com/my-org", + Csv = "mannequins.csv" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } diff --git a/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs index 03d2ee051..ea4fc30f2 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Commands/RevokeMigratorRole/RevokeMigratorRoleCommandArgsTests.cs @@ -45,4 +45,20 @@ public void It_Validates_GhesApiUrl_And_TargetApiUrl() .Should() .ThrowExactly(); } + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new RevokeMigratorRoleCommandArgs + { + GithubOrg = "github.com/my-org", + Actor = ACTOR, + ActorType = "USER" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } diff --git a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs index 077de2cf8..6237d7aee 100644 --- a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs +++ b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs @@ -17,5 +17,28 @@ public void ReplaceInvalidCharactersWithDash_Returns_Valid_String(string value, normalizedValue.Should().Be(expectedValue); } + + [Theory] + [InlineData("https://github.com/my-org", true)] + [InlineData("http://github.com/my-org", true)] + [InlineData("https://github.com/my-org/my-repo", true)] + [InlineData("http://example.com", true)] + [InlineData("www.github.com", true)] + [InlineData("github.com/my-org", true)] + [InlineData("my-org", false)] + [InlineData("my-repo", false)] + [InlineData("my-org-123", false)] + [InlineData("my_repo", false)] + [InlineData("MyOrganization", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData(" ", false)] + public void IsUrl_Detects_URLs_Correctly(string value, bool expectedResult) + { + var result = value.IsUrl(); + + result.Should().Be(expectedResult); + } } } + diff --git a/src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs new file mode 100644 index 000000000..fbe52c145 --- /dev/null +++ b/src/OctoshiftCLI.Tests/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgsTests.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.AdoToGithub.Commands.IntegrateBoards; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.AdoToGithub.Commands.IntegrateBoards; + +public class IntegrateBoardsCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string ADO_ORG = "ado-org"; + private const string ADO_TEAM_PROJECT = "ado-project"; + private const string GITHUB_ORG = "github-org"; + private const string GITHUB_REPO = "github-repo"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new IntegrateBoardsCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + GithubOrg = "github.com/my-org", + GithubRepo = GITHUB_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubRepo_Is_Url() + { + var args = new IntegrateBoardsCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + GithubOrg = GITHUB_ORG, + GithubRepo = "http://github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Names() + { + var args = new IntegrateBoardsCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + GithubOrg = GITHUB_ORG, + GithubRepo = GITHUB_REPO + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + args.GithubRepo.Should().Be(GITHUB_REPO); + } +} diff --git a/src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs b/src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs new file mode 100644 index 000000000..161617093 --- /dev/null +++ b/src/OctoshiftCLI.Tests/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs @@ -0,0 +1,72 @@ +using FluentAssertions; +using Moq; +using OctoshiftCLI.AdoToGithub.Commands.MigrateRepo; +using OctoshiftCLI.Services; +using Xunit; + +namespace OctoshiftCLI.Tests.AdoToGithub.Commands.MigrateRepo; + +public class MigrateRepoCommandArgsTests +{ + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + + private const string ADO_ORG = "ado-org"; + private const string ADO_TEAM_PROJECT = "ado-project"; + private const string ADO_REPO = "ado-repo"; + private const string GITHUB_ORG = "github-org"; + private const string GITHUB_REPO = "github-repo"; + + [Fact] + public void Validate_Throws_When_GithubOrg_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + AdoRepo = ADO_REPO, + GithubOrg = "https://github.com/my-org", + GithubRepo = GITHUB_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubRepo_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + AdoRepo = ADO_REPO, + GithubOrg = GITHUB_ORG, + GithubRepo = "www.github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Succeeds_With_Valid_Names() + { + var args = new MigrateRepoCommandArgs + { + AdoOrg = ADO_ORG, + AdoTeamProject = ADO_TEAM_PROJECT, + AdoRepo = ADO_REPO, + GithubOrg = GITHUB_ORG, + GithubRepo = GITHUB_REPO + }; + + args.Validate(_mockOctoLogger.Object); + + args.GithubOrg.Should().Be(GITHUB_ORG); + args.GithubRepo.Should().Be(GITHUB_REPO); + } +} diff --git a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs index 981b0ce5c..3ad43a6ab 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandArgsTests.cs @@ -107,5 +107,37 @@ public void UseGithubStorage_And_Aws_Bucket_Name_Throws() .ThrowExactly() .WithMessage("*--use-github-storage flag was provided with an AWS S3 Bucket name*"); } + + [Fact] + public void Validate_Throws_When_GithubSourceOrg_Is_Url() + { + var args = new GenerateScriptCommandArgs + { + GithubSourceOrg = "https://github.com/my-org", + GithubTargetOrg = TARGET_ORG, + Output = new FileInfo("unit-test-output") + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetOrg_Is_Url() + { + var args = new GenerateScriptCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = "https://github.com/my-org", + Output = new FileInfo("unit-test-output") + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs index a90226961..2d1c24a5f 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgsTests.cs @@ -28,4 +28,69 @@ public void Target_Repo_Defaults_To_Source_Repo() args.TargetRepo.Should().Be(SOURCE_REPO); } + + [Fact] + public void Validate_Throws_When_SourceOrg_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = "http://github.com/my-org", + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_TargetOrg_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = "www.github.com", + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_SourceRepo_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = "github.com/org/repo", + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Throws_When_TargetRepo_Is_Url() + { + var args = new MigrateCodeScanningAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + TargetRepo = "https://github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs index e5508275b..c065b5c1a 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs @@ -30,5 +30,57 @@ public void Source_Pat_Defaults_To_Target_Pat() args.GithubSourcePat.Should().Be(TARGET_PAT); } + + [Fact] + public void Validate_Throws_When_GithubSourceOrg_Is_Url() + { + var args = new MigrateOrgCommandArgs + { + GithubSourceOrg = "https://github.com/my-org", + GithubTargetOrg = TARGET_ORG, + GithubTargetEnterprise = TARGET_ENTERPRISE, + GithubTargetPat = TARGET_PAT, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetOrg_Is_Url() + { + var args = new MigrateOrgCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = "https://github.com/my-org", + GithubTargetEnterprise = TARGET_ENTERPRISE, + GithubTargetPat = TARGET_PAT, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetEnterprise_Is_Url() + { + var args = new MigrateOrgCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = TARGET_ORG, + GithubTargetEnterprise = "https://github.com/enterprises/my-enterprise", + GithubTargetPat = TARGET_PAT, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-enterprise option expects an enterprise name, not a URL. Please provide just the enterprise name (e.g., 'my-enterprise' instead of 'https://github.com/enterprises/my-enterprise')."); + } } } + diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs index ae8b994b4..6858fcbeb 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs @@ -308,5 +308,74 @@ public void MetadataArchiveUrl_With_MetadataArchivePath_Throws() .ThrowExactly() .WithMessage("*--metadata-archive-url and --metadata-archive-path may not be used together*"); } + + [Fact] + public void Validate_Throws_When_GithubSourceOrg_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = "https://github.com/my-org", + SourceRepo = SOURCE_REPO, + GithubTargetOrg = TARGET_ORG, + TargetRepo = TARGET_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_GithubTargetOrg_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + GithubTargetOrg = "https://github.com/my-org", + TargetRepo = TARGET_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_SourceRepo_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = "https://github.com/my-org/my-repo", + GithubTargetOrg = TARGET_ORG, + TargetRepo = TARGET_REPO + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Throws_When_TargetRepo_Is_Url() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + GithubTargetOrg = TARGET_ORG, + TargetRepo = "https://github.com/my-org/my-repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } } } + diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs index cde4810c7..0887124a2 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgsTests.cs @@ -28,4 +28,69 @@ public void Target_Repo_Defaults_To_Source_Repo() args.TargetRepo.Should().Be(SOURCE_REPO); } + + [Fact] + public void Validate_Throws_When_SourceOrg_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = "https://github.com/my-org", + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_TargetOrg_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = "github.com/my-org", + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + [Fact] + public void Validate_Throws_When_SourceRepo_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = "www.github.com/org/repo", + TargetOrg = TARGET_ORG, + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + [Fact] + public void Validate_Throws_When_TargetRepo_Is_Url() + { + var args = new MigrateSecretAlertsCommandArgs + { + SourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + TargetOrg = TARGET_ORG, + TargetRepo = "http://github.com/org/repo" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } } diff --git a/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs b/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs index 0057d5266..426a9487b 100644 --- a/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs +++ b/src/ado2gh/Commands/IntegrateBoards/IntegrateBoardsCommandArgs.cs @@ -1,4 +1,6 @@ using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.AdoToGithub.Commands.IntegrateBoards { @@ -12,5 +14,18 @@ public class IntegrateBoardsCommandArgs : CommandArgs public string AdoPat { get; set; } [Secret] public string GithubPat { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } } diff --git a/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs b/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs index a704883cc..379be827e 100644 --- a/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs +++ b/src/ado2gh/Commands/MigrateRepo/MigrateRepoCommandArgs.cs @@ -1,4 +1,6 @@ using OctoshiftCLI.Commands; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.AdoToGithub.Commands.MigrateRepo { @@ -17,5 +19,18 @@ public class MigrateRepoCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } } diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs index 52bfd7f52..e8f49abbb 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs @@ -28,6 +28,16 @@ public class GenerateScriptCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if ( GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + if (AwsBucketName.HasValue()) { if (GhesApiUrl.IsNullOrWhiteSpace()) diff --git a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs index d472097d1..a3215a8e9 100644 --- a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs @@ -21,6 +21,26 @@ public class MigrateCodeScanningAlertsCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (SourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (TargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (SourceRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + if (TargetRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + if (SourceRepo.HasValue() && TargetRepo.IsNullOrWhiteSpace()) { TargetRepo = SourceRepo; diff --git a/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs b/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs index 4b9a85ae8..f0e11f20c 100644 --- a/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs +++ b/src/gei/Commands/MigrateOrg/MigrateOrgCommandArgs.cs @@ -19,6 +19,21 @@ public class MigrateOrgCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetEnterprise.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-enterprise option expects an enterprise name, not a URL. Please provide just the enterprise name (e.g., 'my-enterprise' instead of 'https://github.com/enterprises/my-enterprise')."); + } + if (GithubTargetPat.HasValue() && GithubSourcePat.IsNullOrWhiteSpace()) { GithubSourcePat = GithubTargetPat; diff --git a/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs b/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs index 58b24171a..076979476 100644 --- a/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs +++ b/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs @@ -41,6 +41,26 @@ public class MigrateRepoCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (SourceRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + if (TargetRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + DefaultSourcePat(log); DefaultTargetRepo(log); diff --git a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs index 890044fef..23a1fbf00 100644 --- a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs @@ -21,6 +21,26 @@ public class MigrateSecretAlertsCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (SourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (TargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (SourceRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --source-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + + if (TargetRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + if (SourceRepo.HasValue() && TargetRepo.IsNullOrWhiteSpace()) { TargetRepo = SourceRepo; From 4387a57298fbf1084dd4b1848db966b51286bc96 Mon Sep 17 00:00:00 2001 From: AakashSuresh2003 Date: Wed, 3 Dec 2025 17:40:10 +0530 Subject: [PATCH 2/5] feat: Add URL validation for org/repo/enterprise arguments with comprehensive tests (#1180) --- .../Commands/CreateTeam/CreateTeamCommandArgs.cs | 3 +-- .../Commands/DownloadLogs/DownloadLogsCommandArgs.cs | 3 +-- .../GenerateMannequinCsvCommandArgs.cs | 1 - .../ReclaimMannequin/ReclaimMannequinCommandArgs.cs | 3 +-- src/Octoshift/Extensions/StringExtensions.cs | 9 ++------- .../Commands/GenerateScript/GenerateScriptCommandArgs.cs | 2 +- 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs index 208d5d1c2..0008c1dcf 100644 --- a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs +++ b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs @@ -1,5 +1,4 @@ -using OctoshiftCLI.Commands; -using OctoshiftCLI.Extensions; +using OctoshiftCLI.Extensions; using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.CreateTeam; diff --git a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs index a69533ec4..230e84325 100644 --- a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs +++ b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs @@ -1,5 +1,4 @@ -using OctoshiftCLI.Commands; -using OctoshiftCLI.Extensions; +using OctoshiftCLI.Extensions; using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.DownloadLogs; diff --git a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs index f1b43f46e..9616aa2c7 100644 --- a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs +++ b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs @@ -1,5 +1,4 @@ using System.IO; -using OctoshiftCLI.Commands; using OctoshiftCLI.Extensions; using OctoshiftCLI.Services; diff --git a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs index 5218c0781..eaccddf06 100644 --- a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs +++ b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs @@ -1,5 +1,4 @@ -using OctoshiftCLI.Commands; -using OctoshiftCLI.Extensions; +using OctoshiftCLI.Extensions; using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.ReclaimMannequin; diff --git a/src/Octoshift/Extensions/StringExtensions.cs b/src/Octoshift/Extensions/StringExtensions.cs index 5272c12e3..39d6cc8fd 100644 --- a/src/Octoshift/Extensions/StringExtensions.cs +++ b/src/Octoshift/Extensions/StringExtensions.cs @@ -26,7 +26,7 @@ public static class StringExtensions public static string EscapeDataString(this string value) => Uri.EscapeDataString(value); public static byte[] ToBytes(this string s) => Encoding.UTF8.GetBytes(s); - + public static bool IsUrl(this string s) { if (s.IsNullOrWhiteSpace()) @@ -48,12 +48,7 @@ public static bool IsUrl(this string s) } // Check if it looks like a URL path (contains / and .) - if (s.Contains('/') && s.Contains('.')) - { - return true; - } - - return false; + return s.Contains('/') && s.Contains('.'); } } } diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs index e8f49abbb..26aa90416 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs @@ -28,7 +28,7 @@ public class GenerateScriptCommandArgs : CommandArgs public override void Validate(OctoLogger log) { - if ( GithubSourceOrg.IsUrl()) + if (GithubSourceOrg.IsUrl()) { throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); } From f21b11c72b74599fbd12f5f06a639348c9390add Mon Sep 17 00:00:00 2001 From: AakashSuresh2003 Date: Wed, 3 Dec 2025 17:40:10 +0530 Subject: [PATCH 3/5] feat: Add URL validation for org/repo/enterprise arguments with comprehensive tests (#1180) --- .../CreateTeam/CreateTeamCommandArgs.cs | 13 +- .../DownloadLogs/DownloadLogsCommandArgs.cs | 17 +- .../GenerateMannequinCsvCommandArgs.cs | 10 + .../ReclaimMannequinCommandArgs.cs | 8 +- src/Octoshift/Extensions/StringExtensions.cs | 24 +++ .../GenerateScriptCommandArgs.cs | 10 + test-url-validation.sh | 190 ++++++++++++++++++ 7 files changed, 269 insertions(+), 3 deletions(-) create mode 100755 test-url-validation.sh diff --git a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs index bf2d6697f..0008c1dcf 100644 --- a/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs +++ b/src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs @@ -1,4 +1,7 @@ -namespace OctoshiftCLI.Commands.CreateTeam; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + +namespace OctoshiftCLI.Commands.CreateTeam; public class CreateTeamCommandArgs : CommandArgs { @@ -8,4 +11,12 @@ public class CreateTeamCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + } } diff --git a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs index 8795ca52d..230e84325 100644 --- a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs +++ b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs @@ -1,4 +1,6 @@ - +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; + namespace OctoshiftCLI.Commands.DownloadLogs; public class DownloadLogsCommandArgs : CommandArgs @@ -11,4 +13,17 @@ public class DownloadLogsCommandArgs : CommandArgs public string GithubPat { get; set; } public string MigrationLogFile { get; set; } public bool Overwrite { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubRepo.IsUrl()) + { + throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + } + } } diff --git a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs index 7b92a6f0c..9616aa2c7 100644 --- a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs +++ b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs @@ -1,4 +1,6 @@ using System.IO; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.GenerateMannequinCsv; @@ -10,4 +12,12 @@ public class GenerateMannequinCsvCommandArgs : CommandArgs [Secret] public string GithubPat { get; set; } public string TargetApiUrl { get; set; } + + public override void Validate(OctoLogger log) + { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + } } diff --git a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs index 71db554cb..eaccddf06 100644 --- a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs +++ b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs @@ -1,4 +1,5 @@ -using OctoshiftCLI.Services; +using OctoshiftCLI.Extensions; +using OctoshiftCLI.Services; namespace OctoshiftCLI.Commands.ReclaimMannequin; @@ -17,6 +18,11 @@ public class ReclaimMannequinCommandArgs : CommandArgs public string TargetApiUrl { get; set; } public override void Validate(OctoLogger log) { + if (GithubOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + if (string.IsNullOrEmpty(Csv) && (string.IsNullOrEmpty(MannequinUser) || string.IsNullOrEmpty(TargetUser))) { throw new OctoshiftCliException($"Either --csv or --mannequin-user and --target-user must be specified"); diff --git a/src/Octoshift/Extensions/StringExtensions.cs b/src/Octoshift/Extensions/StringExtensions.cs index 8d323ff92..39d6cc8fd 100644 --- a/src/Octoshift/Extensions/StringExtensions.cs +++ b/src/Octoshift/Extensions/StringExtensions.cs @@ -26,5 +26,29 @@ public static class StringExtensions public static string EscapeDataString(this string value) => Uri.EscapeDataString(value); public static byte[] ToBytes(this string s) => Encoding.UTF8.GetBytes(s); + + public static bool IsUrl(this string s) + { + if (s.IsNullOrWhiteSpace()) + { + return false; + } + + // Check if string starts with http:// or https:// + if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if string contains common URL patterns like domain.com/path or www. + if (s.Contains("://") || s.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if it looks like a URL path (contains / and .) + return s.Contains('/') && s.Contains('.'); + } } } diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs index 52bfd7f52..26aa90416 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs @@ -28,6 +28,16 @@ public class GenerateScriptCommandArgs : CommandArgs public override void Validate(OctoLogger log) { + if (GithubSourceOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-source-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + + if (GithubTargetOrg.IsUrl()) + { + throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + } + if (AwsBucketName.HasValue()) { if (GhesApiUrl.IsNullOrWhiteSpace()) diff --git a/test-url-validation.sh b/test-url-validation.sh new file mode 100755 index 000000000..9c982287b --- /dev/null +++ b/test-url-validation.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +echo "================================================================================" +echo " URL VALIDATION FUNCTIONAL TEST SUITE - Issue #1180 " +echo "================================================================================" +echo "" + +PASS=0 +FAIL=0 + +run_test() { + local test_num=$1 + local description=$2 + local command=$3 + local expected_error=$4 + + echo "--------------------------------------------------------------------------------" + echo "Test $test_num: $description" + echo "Command: $command" + echo "--------------------------------------------------------------------------------" + + output=$(eval "$command" 2>&1) + + if [ -n "$expected_error" ]; then + if echo "$output" | grep -q -F "$expected_error"; then + echo "[PASS] Error detected correctly" + echo " $(echo "$output" | grep "\[ERROR\]" | head -1)" + PASS=$((PASS + 1)) + else + echo "[FAIL] Expected error not found" + echo "$output" | grep "\[ERROR\]" | head -1 + FAIL=$((FAIL + 1)) + fi + else + if echo "$output" | grep -q "\[ERROR\].*URL"; then + echo "[FAIL] Unexpected URL validation error" + echo "$output" | grep "\[ERROR\]" | head -1 + FAIL=$((FAIL + 1)) + else + echo "[PASS] Validation passed" + echo " $(echo "$output" | grep -E "\[INFO\].*TARGET" | head -1)" + PASS=$((PASS + 1)) + fi + fi + echo "" +} + +# GEI migrate-repo tests +run_test 1 "migrate-repo: URL in --github-source-org (https)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org 'https://github.com/my-org' --source-repo my-repo --github-target-org target-org --github-target-pat dummy" \ + "github-source-org option expects" + +run_test 2 "migrate-repo: URL in --source-repo (domain/path)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo 'github.com/org/my-repo' --github-target-org target-org --github-target-pat dummy" \ + "source-repo option expects" + +run_test 3 "migrate-repo: URL in --github-target-org (https)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my-repo --github-target-org 'https://github.com/target' --github-target-pat dummy" \ + "github-target-org option expects" + +run_test 4 "migrate-repo: URL in --target-repo (http)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my-repo --github-target-org target-org --target-repo 'http://github.com/org/repo' --github-target-pat dummy" \ + "target-repo option expects" + +run_test 5 "migrate-repo: Valid names (should pass)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my-repo --github-target-org target-org --github-target-pat dummy --queue-only" \ + "" + +# GEI migrate-org tests +run_test 6 "migrate-org: URL in --github-source-org (www)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-org --github-source-org 'www.github.com/source' --github-target-org target-org --github-target-enterprise my-ent --github-target-pat dummy" \ + "github-source-org option expects" + +run_test 7 "migrate-org: URL in --github-target-org" \ + "dotnet run --project src/gei/gei.csproj -- migrate-org --github-source-org source-org --github-target-org 'https://github.com/target' --github-target-enterprise my-ent --github-target-pat dummy" \ + "github-target-org option expects" + +run_test 8 "migrate-org: URL in --github-target-enterprise" \ + "dotnet run --project src/gei/gei.csproj -- migrate-org --github-source-org source-org --github-target-org target-org --github-target-enterprise 'https://github.com/enterprises/my-ent' --github-target-pat dummy" \ + "github-target-enterprise option expects" + +# GEI generate-script tests +run_test 9 "generate-script: URL in --github-source-org (http)" \ + "dotnet run --project src/gei/gei.csproj -- generate-script --github-source-org 'http://github.com/source' --github-target-org target --output test.ps1" \ + "github-source-org option expects" + +run_test 10 "generate-script: URL in --github-target-org" \ + "dotnet run --project src/gei/gei.csproj -- generate-script --github-source-org source --github-target-org 'github.com/target/org' --output test.ps1" \ + "github-target-org option expects" + +# ADO2GH migrate-repo tests +run_test 11 "ado2gh migrate-repo: URL in --github-org (www)" \ + "dotnet run --project src/ado2gh/ado2gh.csproj -- migrate-repo --ado-org ado --ado-team-project proj --ado-repo repo --github-org 'www.github.com' --github-repo my-repo --ado-pat dummy --github-pat dummy" \ + "github-org option expects" + +run_test 12 "ado2gh migrate-repo: URL in --github-repo" \ + "dotnet run --project src/ado2gh/ado2gh.csproj -- migrate-repo --ado-org ado --ado-team-project proj --ado-repo repo --github-org my-org --github-repo 'https://github.com/org/repo' --ado-pat dummy --github-pat dummy" \ + "github-repo option expects" + +# ADO2GH integrate-boards tests +run_test 13 "ado2gh integrate-boards: URL in --github-org" \ + "dotnet run --project src/ado2gh/ado2gh.csproj -- integrate-boards --ado-org ado --ado-team-project proj --github-org 'github.com/org' --github-repo repo --ado-pat dummy --github-pat dummy" \ + "github-org option expects" + +run_test 14 "ado2gh integrate-boards: URL in --github-repo" \ + "dotnet run --project src/ado2gh/ado2gh.csproj -- integrate-boards --ado-org ado --ado-team-project proj --github-org org --github-repo 'https://github.com/org/repo' --ado-pat dummy --github-pat dummy" \ + "github-repo option expects" + +# GEI migrate-secret-alerts tests +run_test 15 "migrate-secret-alerts: URL in --source-org" \ + "dotnet run --project src/gei/gei.csproj -- migrate-secret-alerts --source-org 'https://github.com/source' --source-repo repo --target-org target --github-target-pat dummy" \ + "source-org option expects" + +run_test 16 "migrate-secret-alerts: URL in --target-org" \ + "dotnet run --project src/gei/gei.csproj -- migrate-secret-alerts --source-org source --source-repo repo --target-org 'github.com/target' --github-target-pat dummy" \ + "target-org option expects" + +run_test 17 "migrate-secret-alerts: URL in --source-repo" \ + "dotnet run --project src/gei/gei.csproj -- migrate-secret-alerts --source-org source --source-repo 'www.github.com/repo' --target-org target --github-target-pat dummy" \ + "source-repo option expects" + +# GEI migrate-code-scanning-alerts tests +run_test 18 "migrate-code-scanning-alerts: URL in --source-org" \ + "dotnet run --project src/gei/gei.csproj -- migrate-code-scanning-alerts --source-org 'http://github.com/src' --source-repo repo --target-org target --github-target-pat dummy" \ + "source-org option expects" + +run_test 19 "migrate-code-scanning-alerts: URL in --target-repo" \ + "dotnet run --project src/gei/gei.csproj -- migrate-code-scanning-alerts --source-org source --source-repo repo --target-org target --target-repo 'github.com/target/repo' --github-target-pat dummy" \ + "target-repo option expects" + +# GEI download-logs tests +run_test 20 "download-logs: URL in --github-target-org" \ + "dotnet run --project src/gei/gei.csproj -- download-logs --github-target-org 'https://github.com/org' --target-repo repo --github-target-pat dummy" \ + "github-org option expects" + +run_test 21 "download-logs: URL in --target-repo" \ + "dotnet run --project src/gei/gei.csproj -- download-logs --github-target-org org --target-repo 'www.github.com/org/repo' --github-target-pat dummy" \ + "github-repo option expects" + +# GEI create-team tests +run_test 22 "create-team: URL in --github-org" \ + "dotnet run --project src/gei/gei.csproj -- create-team --github-org 'http://github.com/org' --team-name my-team --github-target-pat dummy" \ + "github-org option expects" + +# GEI grant-migrator-role tests +run_test 23 "grant-migrator-role: URL in --github-org" \ + "dotnet run --project src/gei/gei.csproj -- grant-migrator-role --github-org 'github.com/org' --actor user --actor-type USER --github-target-pat dummy" \ + "github-org option expects" + +# GEI revoke-migrator-role tests +run_test 24 "revoke-migrator-role: URL in --github-org" \ + "dotnet run --project src/gei/gei.csproj -- revoke-migrator-role --github-org 'https://github.com/my-org' --actor user --actor-type USER --github-target-pat dummy" \ + "github-org option expects" + +# GEI generate-mannequin-csv tests +run_test 25 "generate-mannequin-csv: URL in --github-target-org" \ + "dotnet run --project src/gei/gei.csproj -- generate-mannequin-csv --github-target-org 'www.github.com' --output mannequins.csv --github-target-pat dummy" \ + "github-org option expects" + +# GEI reclaim-mannequin tests +run_test 26 "reclaim-mannequin: URL in --github-target-org" \ + "dotnet run --project src/gei/gei.csproj -- reclaim-mannequin --github-target-org 'http://github.com/org' --csv mannequins.csv --github-target-pat dummy" \ + "github-org option expects" + +# Edge cases - valid names +run_test 27 "Edge: Names with hyphens (valid)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org-123 --source-repo my-repo-name --github-target-org target-org-456 --github-target-pat dummy --queue-only" \ + "" + +# Test 28: Repo name with underscore — should PASS (repos allow underscores) +run_test 28 "Edge: Repo name with underscore (valid for repos)" \ + "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my_repo --github-target-org target-org --github-target-pat dummy --queue-only" \ + "" + +echo "================================================================================" +echo " TEST SUMMARY " +echo "================================================================================" +echo "" +echo "Passed: $PASS / 28" +echo "Failed: $FAIL / 28" +echo "--------------------------------------------------------------------------------" +echo "" + +if [ $FAIL -eq 0 ]; then + echo "ALL TESTS PASSED! URL validation working correctly across all commands." + exit 0 +else + echo "SOME TESTS FAILED. Please review the failures above." + exit 1 +fi From 75dcb0e1eb1c8e8c2a62fbb2277278942f9273ad Mon Sep 17 00:00:00 2001 From: AakashSuresh2003 Date: Thu, 4 Dec 2025 02:30:47 +0530 Subject: [PATCH 4/5] Add URL validation with clear error messages for organization, repository, and enterprise name arguments and remove trailing blank lines from test files --- .../DownloadLogs/DownloadLogsCommandArgs.cs | 4 +- .../GenerateMannequinCsvCommandArgs.cs | 2 +- .../ReclaimMannequinCommandArgs.cs | 2 +- .../StringExtensionsTests.cs | 1 - .../MigrateOrg/MigrateOrgCommandArgsTests.cs | 1 - .../MigrateRepoCommandArgsTests.cs | 1 - test-url-validation.sh | 190 ------------------ 7 files changed, 4 insertions(+), 197 deletions(-) delete mode 100755 test-url-validation.sh diff --git a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs index 230e84325..c7cb03bc7 100644 --- a/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs +++ b/src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs @@ -18,12 +18,12 @@ public override void Validate(OctoLogger log) { if (GithubOrg.IsUrl()) { - throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); } if (GithubRepo.IsUrl()) { - throw new OctoshiftCliException($"The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); + throw new OctoshiftCliException("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); } } } diff --git a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs index 9616aa2c7..95a3968c7 100644 --- a/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs +++ b/src/Octoshift/Commands/GenerateMannequinCsv/GenerateMannequinCsvCommandArgs.cs @@ -17,7 +17,7 @@ public override void Validate(OctoLogger log) { if (GithubOrg.IsUrl()) { - throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); } } } diff --git a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs index eaccddf06..701c8ad59 100644 --- a/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs +++ b/src/Octoshift/Commands/ReclaimMannequin/ReclaimMannequinCommandArgs.cs @@ -20,7 +20,7 @@ public override void Validate(OctoLogger log) { if (GithubOrg.IsUrl()) { - throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); + throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); } if (string.IsNullOrEmpty(Csv) && (string.IsNullOrEmpty(MannequinUser) || string.IsNullOrEmpty(TargetUser))) diff --git a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs index 6237d7aee..3941be7fd 100644 --- a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs +++ b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs @@ -41,4 +41,3 @@ public void IsUrl_Detects_URLs_Correctly(string value, bool expectedResult) } } } - diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs index c065b5c1a..596d3a3e0 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateOrg/MigrateOrgCommandArgsTests.cs @@ -83,4 +83,3 @@ public void Validate_Throws_When_GithubTargetEnterprise_Is_Url() } } } - diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs index 6858fcbeb..2cdbcab6a 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs @@ -378,4 +378,3 @@ public void Validate_Throws_When_TargetRepo_Is_Url() } } } - diff --git a/test-url-validation.sh b/test-url-validation.sh deleted file mode 100755 index 9c982287b..000000000 --- a/test-url-validation.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/bash - -echo "================================================================================" -echo " URL VALIDATION FUNCTIONAL TEST SUITE - Issue #1180 " -echo "================================================================================" -echo "" - -PASS=0 -FAIL=0 - -run_test() { - local test_num=$1 - local description=$2 - local command=$3 - local expected_error=$4 - - echo "--------------------------------------------------------------------------------" - echo "Test $test_num: $description" - echo "Command: $command" - echo "--------------------------------------------------------------------------------" - - output=$(eval "$command" 2>&1) - - if [ -n "$expected_error" ]; then - if echo "$output" | grep -q -F "$expected_error"; then - echo "[PASS] Error detected correctly" - echo " $(echo "$output" | grep "\[ERROR\]" | head -1)" - PASS=$((PASS + 1)) - else - echo "[FAIL] Expected error not found" - echo "$output" | grep "\[ERROR\]" | head -1 - FAIL=$((FAIL + 1)) - fi - else - if echo "$output" | grep -q "\[ERROR\].*URL"; then - echo "[FAIL] Unexpected URL validation error" - echo "$output" | grep "\[ERROR\]" | head -1 - FAIL=$((FAIL + 1)) - else - echo "[PASS] Validation passed" - echo " $(echo "$output" | grep -E "\[INFO\].*TARGET" | head -1)" - PASS=$((PASS + 1)) - fi - fi - echo "" -} - -# GEI migrate-repo tests -run_test 1 "migrate-repo: URL in --github-source-org (https)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org 'https://github.com/my-org' --source-repo my-repo --github-target-org target-org --github-target-pat dummy" \ - "github-source-org option expects" - -run_test 2 "migrate-repo: URL in --source-repo (domain/path)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo 'github.com/org/my-repo' --github-target-org target-org --github-target-pat dummy" \ - "source-repo option expects" - -run_test 3 "migrate-repo: URL in --github-target-org (https)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my-repo --github-target-org 'https://github.com/target' --github-target-pat dummy" \ - "github-target-org option expects" - -run_test 4 "migrate-repo: URL in --target-repo (http)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my-repo --github-target-org target-org --target-repo 'http://github.com/org/repo' --github-target-pat dummy" \ - "target-repo option expects" - -run_test 5 "migrate-repo: Valid names (should pass)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my-repo --github-target-org target-org --github-target-pat dummy --queue-only" \ - "" - -# GEI migrate-org tests -run_test 6 "migrate-org: URL in --github-source-org (www)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-org --github-source-org 'www.github.com/source' --github-target-org target-org --github-target-enterprise my-ent --github-target-pat dummy" \ - "github-source-org option expects" - -run_test 7 "migrate-org: URL in --github-target-org" \ - "dotnet run --project src/gei/gei.csproj -- migrate-org --github-source-org source-org --github-target-org 'https://github.com/target' --github-target-enterprise my-ent --github-target-pat dummy" \ - "github-target-org option expects" - -run_test 8 "migrate-org: URL in --github-target-enterprise" \ - "dotnet run --project src/gei/gei.csproj -- migrate-org --github-source-org source-org --github-target-org target-org --github-target-enterprise 'https://github.com/enterprises/my-ent' --github-target-pat dummy" \ - "github-target-enterprise option expects" - -# GEI generate-script tests -run_test 9 "generate-script: URL in --github-source-org (http)" \ - "dotnet run --project src/gei/gei.csproj -- generate-script --github-source-org 'http://github.com/source' --github-target-org target --output test.ps1" \ - "github-source-org option expects" - -run_test 10 "generate-script: URL in --github-target-org" \ - "dotnet run --project src/gei/gei.csproj -- generate-script --github-source-org source --github-target-org 'github.com/target/org' --output test.ps1" \ - "github-target-org option expects" - -# ADO2GH migrate-repo tests -run_test 11 "ado2gh migrate-repo: URL in --github-org (www)" \ - "dotnet run --project src/ado2gh/ado2gh.csproj -- migrate-repo --ado-org ado --ado-team-project proj --ado-repo repo --github-org 'www.github.com' --github-repo my-repo --ado-pat dummy --github-pat dummy" \ - "github-org option expects" - -run_test 12 "ado2gh migrate-repo: URL in --github-repo" \ - "dotnet run --project src/ado2gh/ado2gh.csproj -- migrate-repo --ado-org ado --ado-team-project proj --ado-repo repo --github-org my-org --github-repo 'https://github.com/org/repo' --ado-pat dummy --github-pat dummy" \ - "github-repo option expects" - -# ADO2GH integrate-boards tests -run_test 13 "ado2gh integrate-boards: URL in --github-org" \ - "dotnet run --project src/ado2gh/ado2gh.csproj -- integrate-boards --ado-org ado --ado-team-project proj --github-org 'github.com/org' --github-repo repo --ado-pat dummy --github-pat dummy" \ - "github-org option expects" - -run_test 14 "ado2gh integrate-boards: URL in --github-repo" \ - "dotnet run --project src/ado2gh/ado2gh.csproj -- integrate-boards --ado-org ado --ado-team-project proj --github-org org --github-repo 'https://github.com/org/repo' --ado-pat dummy --github-pat dummy" \ - "github-repo option expects" - -# GEI migrate-secret-alerts tests -run_test 15 "migrate-secret-alerts: URL in --source-org" \ - "dotnet run --project src/gei/gei.csproj -- migrate-secret-alerts --source-org 'https://github.com/source' --source-repo repo --target-org target --github-target-pat dummy" \ - "source-org option expects" - -run_test 16 "migrate-secret-alerts: URL in --target-org" \ - "dotnet run --project src/gei/gei.csproj -- migrate-secret-alerts --source-org source --source-repo repo --target-org 'github.com/target' --github-target-pat dummy" \ - "target-org option expects" - -run_test 17 "migrate-secret-alerts: URL in --source-repo" \ - "dotnet run --project src/gei/gei.csproj -- migrate-secret-alerts --source-org source --source-repo 'www.github.com/repo' --target-org target --github-target-pat dummy" \ - "source-repo option expects" - -# GEI migrate-code-scanning-alerts tests -run_test 18 "migrate-code-scanning-alerts: URL in --source-org" \ - "dotnet run --project src/gei/gei.csproj -- migrate-code-scanning-alerts --source-org 'http://github.com/src' --source-repo repo --target-org target --github-target-pat dummy" \ - "source-org option expects" - -run_test 19 "migrate-code-scanning-alerts: URL in --target-repo" \ - "dotnet run --project src/gei/gei.csproj -- migrate-code-scanning-alerts --source-org source --source-repo repo --target-org target --target-repo 'github.com/target/repo' --github-target-pat dummy" \ - "target-repo option expects" - -# GEI download-logs tests -run_test 20 "download-logs: URL in --github-target-org" \ - "dotnet run --project src/gei/gei.csproj -- download-logs --github-target-org 'https://github.com/org' --target-repo repo --github-target-pat dummy" \ - "github-org option expects" - -run_test 21 "download-logs: URL in --target-repo" \ - "dotnet run --project src/gei/gei.csproj -- download-logs --github-target-org org --target-repo 'www.github.com/org/repo' --github-target-pat dummy" \ - "github-repo option expects" - -# GEI create-team tests -run_test 22 "create-team: URL in --github-org" \ - "dotnet run --project src/gei/gei.csproj -- create-team --github-org 'http://github.com/org' --team-name my-team --github-target-pat dummy" \ - "github-org option expects" - -# GEI grant-migrator-role tests -run_test 23 "grant-migrator-role: URL in --github-org" \ - "dotnet run --project src/gei/gei.csproj -- grant-migrator-role --github-org 'github.com/org' --actor user --actor-type USER --github-target-pat dummy" \ - "github-org option expects" - -# GEI revoke-migrator-role tests -run_test 24 "revoke-migrator-role: URL in --github-org" \ - "dotnet run --project src/gei/gei.csproj -- revoke-migrator-role --github-org 'https://github.com/my-org' --actor user --actor-type USER --github-target-pat dummy" \ - "github-org option expects" - -# GEI generate-mannequin-csv tests -run_test 25 "generate-mannequin-csv: URL in --github-target-org" \ - "dotnet run --project src/gei/gei.csproj -- generate-mannequin-csv --github-target-org 'www.github.com' --output mannequins.csv --github-target-pat dummy" \ - "github-org option expects" - -# GEI reclaim-mannequin tests -run_test 26 "reclaim-mannequin: URL in --github-target-org" \ - "dotnet run --project src/gei/gei.csproj -- reclaim-mannequin --github-target-org 'http://github.com/org' --csv mannequins.csv --github-target-pat dummy" \ - "github-org option expects" - -# Edge cases - valid names -run_test 27 "Edge: Names with hyphens (valid)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org-123 --source-repo my-repo-name --github-target-org target-org-456 --github-target-pat dummy --queue-only" \ - "" - -# Test 28: Repo name with underscore — should PASS (repos allow underscores) -run_test 28 "Edge: Repo name with underscore (valid for repos)" \ - "dotnet run --project src/gei/gei.csproj -- migrate-repo --github-source-org source-org --source-repo my_repo --github-target-org target-org --github-target-pat dummy --queue-only" \ - "" - -echo "================================================================================" -echo " TEST SUMMARY " -echo "================================================================================" -echo "" -echo "Passed: $PASS / 28" -echo "Failed: $FAIL / 28" -echo "--------------------------------------------------------------------------------" -echo "" - -if [ $FAIL -eq 0 ]; then - echo "ALL TESTS PASSED! URL validation working correctly across all commands." - exit 0 -else - echo "SOME TESTS FAILED. Please review the failures above." - exit 1 -fi From f0e982599241d5e0162d743f55510365426cfb2e Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 5 Dec 2025 13:40:34 +0530 Subject: [PATCH 5/5] Enhance validation and error messaging in RELEASENOTES Added validation for URL arguments and improved error messages for path options. --- RELEASENOTES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1660040ff..e79d0bbe7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,2 @@ - Added validation to detect and return clear error messages when a URL is provided instead of a name for organization, repository, or enterprise arguments (e.g., `--github-org`, `--github-target-org`, `--source-repo`, `--github-target-enterprise`) -- bbs2gh : Added validation for `--archive-path` and `--bbs-shared-home` options to fail fast with clear error messages if the provided paths do not exist or are not accessible. Archive path is now logged before upload operations to help with troubleshooting -