diff --git a/src/DiffEngineTray.Tests/IssueLauncherTests.cs b/src/DiffEngineTray.Tests/IssueLauncherTests.cs new file mode 100644 index 00000000..9268b899 --- /dev/null +++ b/src/DiffEngineTray.Tests/IssueLauncherTests.cs @@ -0,0 +1,49 @@ +/// +/// The issue URL the tray opens after an error. Its title is built from a message that carries a +/// file path, so it holds whatever characters the path does. +/// +public class IssueLauncherTests +{ + [Test] + public async Task A_hash_in_the_title_does_not_start_a_fragment() + { + // Perfectly ordinary in a solution directory, and everything after it used to be read by + // the browser as a fragment: no body, and a title ending at the hash + const string message = @"Cannot start. Failed to read settings: C:\code\C#\settings.json"; + + var url = IssueLauncher.BuildUrl(message); + + await Assert.That(url).DoesNotContain("#"); + await Assert.That(Query(url)["title"]).IsEqualTo(message); + await Assert.That(Query(url)["body"]).IsNotEmpty(); + } + + [Test] + public async Task An_ampersand_in_the_title_does_not_truncate_it() + { + const string message = @"Could not accept 'R&D.received.txt'"; + + var url = IssueLauncher.BuildUrl(message); + + await Assert.That(Query(url)["title"]).IsEqualTo(message); + await Assert.That(Query(url)["body"]).IsNotEmpty(); + } + + /// + /// The body is encoded by its callers and handed over already escaped, so encoding the title + /// must not have changed what reaches GitHub as the body. + /// + [Test] + public async Task Keeps_the_body_it_is_given() + { + var url = IssueLauncher.BuildUrl("TheTitle", WebUtility.UrlEncode("\n * Action: TheAction")); + + await Assert.That(Query(url)["body"]).Contains("* Action: TheAction"); + } + + static Dictionary Query(string url) => + url[(url.IndexOf('?') + 1)..] + .Split('&') + .Select(_ => _.Split('=', 2)) + .ToDictionary(_ => _[0], _ => WebUtility.UrlDecode(_[1])); +} diff --git a/src/DiffEngineTray/IssueLauncher.cs b/src/DiffEngineTray/IssueLauncher.cs index f09bbb10..69ca628a 100644 --- a/src/DiffEngineTray/IssueLauncher.cs +++ b/src/DiffEngineTray/IssueLauncher.cs @@ -11,7 +11,15 @@ static IssueLauncher() => """); public static void Launch() => - LinkLauncher.LaunchUrl($"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/VerifyTests/DiffEngine/issues/new?title=TODO&body={defaultBody}"); + LinkLauncher.LaunchUrl(BuildUrl("TODO")); + + /// + /// The title is encoded like the body is. It is built from a message carrying a file path, and + /// a '#' in one started a fragment - dropping the body, and everything of the title after it - + /// while an '&' started a parameter GitHub does not have, truncating the title there. + /// + internal static string BuildUrl(string title, string extraBody = "") => + $"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/VerifyTests/DiffEngine/issues/new?title={WebUtility.UrlEncode(title)}&body={defaultBody}{extraBody}"; public static void LaunchForException(string message, Exception exception) { @@ -43,8 +51,7 @@ Open an issue on GitHub? {exception} ``` """); - var url = $"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/VerifyTests/DiffEngine/issues/new?title={message}&body={defaultBody}{extraBody}"; - LinkLauncher.LaunchUrl(url); + LinkLauncher.LaunchUrl(BuildUrl(message, extraBody)); } public static void LaunchForException(string message) @@ -71,8 +78,7 @@ Open an issue on GitHub? * Action: {message} """); - var url = $"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/VerifyTests/DiffEngine/issues/new?title={message}&body={defaultBody}{extraBody}"; - LinkLauncher.LaunchUrl(url); + LinkLauncher.LaunchUrl(BuildUrl(message, extraBody)); } static bool AskIfOpenIssue(string text)