diff --git a/BuildMonitor.Test/BranchUrlTests.cs b/BuildMonitor.Test/BranchUrlTests.cs new file mode 100644 index 0000000..c7a0918 --- /dev/null +++ b/BuildMonitor.Test/BranchUrlTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.BuildMonitor.Test; + +using ktsu.Semantics.Strings; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Covers "Open Branch URL" and "Copy Branch URL". The branch name went into the URL unescaped, so a +/// # started a fragment and a % read as a percent-escape, and the link pointed at the +/// wrong branch or a 404 (ktsu-dev/BuildMonitor#386). +/// +[TestClass] +public sealed class BranchUrlTests +{ + private sealed class StubProvider : BuildProvider + { + internal override BuildProviderName Name { get; } = "Stub".As(); + + internal override Task UpdateRepositoriesAsync(Owner owner) => Task.CompletedTask; + internal override Task UpdateBuildsAsync(Repository repository) => Task.CompletedTask; + internal override Task UpdateBuildAsync(Build build) => Task.CompletedTask; + internal override Task UpdateRunAsync(Run run) => Task.CompletedTask; + } + + private static Build CreateBuild() + { + Owner owner = new StubProvider().CreateOwner("o".As()); + Repository repository = owner.CreateRepository("r".As()); + return repository.CreateBuild("build".As()); + } + + [TestMethod] + [DataRow("feature/foo", "https://github.com/o/r/tree/feature/foo")] + [DataRow("main", "https://github.com/o/r/tree/main")] + [DataRow("fix/#123-crash", "https://github.com/o/r/tree/fix/%23123-crash")] + [DataRow("feature/50%", "https://github.com/o/r/tree/feature/50%25")] + public void BranchUrlEscapesEachSegmentAndKeepsSeparators(string branch, string expected) + { + // Act + string url = BuildMonitor.GetBranchUrl(CreateBuild(), branch.As()); + + // Assert + Assert.AreEqual(expected, url); + } +} diff --git a/BuildMonitor/BuildMonitor.cs b/BuildMonitor/BuildMonitor.cs index 1f2e783..8ce14bc 100644 --- a/BuildMonitor/BuildMonitor.cs +++ b/BuildMonitor/BuildMonitor.cs @@ -1338,7 +1338,12 @@ private static void CopyRunUrl(Run run) private static string GetWorkflowUrl(Build build) => $"https://github.com/{build.Owner.Name}/{build.Repository.Name}/actions/workflows/{build.Id}"; - private static string GetBranchUrl(Build build, BranchName branch) => $"https://github.com/{build.Owner.Name}/{build.Repository.Name}/tree/{branch}"; + /// + /// Escapes each segment of the branch name and keeps its / separators, so a # or + /// % in the name stays part of the path rather than starting a fragment or an escape. + /// + internal static string GetBranchUrl(Build build, BranchName branch) => + $"https://github.com/{build.Owner.Name}/{build.Repository.Name}/tree/{string.Join('/', branch.ToString().Split('/').Select(Uri.EscapeDataString))}"; private static string GetRunUrl(Run run) => $"https://github.com/{run.Owner.Name}/{run.Repository.Name}/actions/runs/{run.Id}";