using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using NUnit.Framework; using MCPForUnity.Editor.Services; using MCPForUnity.Editor.Services.Server; using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using UnityEditor; using UnityEngine; using UnityEngine.TestTools; namespace MCPForUnityTests.Editor.Services.Characterization { /// /// Characterization tests for ServerManagementService public interface. /// These tests lock down current behavior BEFORE refactoring to ensure /// no regressions during the decomposition into focused components. /// [TestFixture] public class ServerManagementServiceCharacterizationTests { private ServerManagementService _service; private bool _savedUseHttpTransport; private string _savedHttpUrl; private string _savedHttpRemoteUrl; private string _savedHttpTransportScope; private bool _savedAllowLanHttpBind; private bool _savedAllowInsecureRemoteHttp; private bool _savedLaunchConfirmed; [SetUp] public void SetUp() { _service = new ServerManagementService(); // Save current settings _savedLaunchConfirmed = EditorPrefs.GetBool(EditorPrefKeys.HttpServerLaunchConfirmed, false); _savedUseHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); _savedHttpUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty); _savedHttpRemoteUrl = EditorPrefs.GetString(EditorPrefKeys.HttpRemoteBaseUrl, string.Empty); _savedHttpTransportScope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty); _savedAllowLanHttpBind = EditorPrefs.GetBool(EditorPrefKeys.AllowLanHttpBind, false); _savedAllowInsecureRemoteHttp = EditorPrefs.GetBool(EditorPrefKeys.AllowInsecureRemoteHttp, false); } [TearDown] public void TearDown() { // Restore settings EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, _savedUseHttpTransport); if (!string.IsNullOrEmpty(_savedHttpUrl)) { EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, _savedHttpUrl); } else { EditorPrefs.DeleteKey(EditorPrefKeys.HttpBaseUrl); } if (!string.IsNullOrEmpty(_savedHttpRemoteUrl)) { EditorPrefs.SetString(EditorPrefKeys.HttpRemoteBaseUrl, _savedHttpRemoteUrl); } else { EditorPrefs.DeleteKey(EditorPrefKeys.HttpRemoteBaseUrl); } if (!string.IsNullOrEmpty(_savedHttpTransportScope)) { EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, _savedHttpTransportScope); } else { EditorPrefs.DeleteKey(EditorPrefKeys.HttpTransportScope); } EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, _savedAllowLanHttpBind); EditorPrefs.SetBool(EditorPrefKeys.AllowInsecureRemoteHttp, _savedAllowInsecureRemoteHttp); EditorPrefs.SetBool(EditorPrefKeys.HttpServerLaunchConfirmed, _savedLaunchConfirmed); // Refresh cache to reflect restored values EditorConfigurationCache.Instance.Refresh(); } #region StartLocalHttpServer First-Time-Confirm Gating // A fake launcher that records the headless launch request, then returns a harmless // no-op process (true / cmd exit) so Process.Start SUCCEEDS and exits immediately — no real // server starts, and (unlike a non-existent path) no exception + error dialog that would // block an interactive test run (Test Runner window / MCP bridge); -batchmode suppresses // dialogs, but interactive runs do not. private sealed class RecordingTerminalLauncher : ITerminalLauncher { public bool HeadlessCalled; public string LastCommand; public string LastLogPath; public System.Diagnostics.ProcessStartInfo CreateTerminalProcessStartInfo(string command) { return HarmlessNoOpStartInfo(); } public System.Diagnostics.ProcessStartInfo CreateHeadlessProcessStartInfo(string command, string logFilePath) { HeadlessCalled = true; LastCommand = command; LastLogPath = logFilePath; return HarmlessNoOpStartInfo(); } public string GetProjectRootPath() { return System.IO.Path.GetTempPath(); } // A binary that always launches and exits 0 immediately, so Process.Start does not throw. private static System.Diagnostics.ProcessStartInfo HarmlessNoOpStartInfo() { var psi = new System.Diagnostics.ProcessStartInfo { UseShellExecute = false, CreateNoWindow = true }; if (Application.platform == RuntimePlatform.WindowsEditor) { psi.FileName = "cmd.exe"; psi.Arguments = "/c exit"; } else { psi.FileName = System.IO.File.Exists("/usr/bin/true") ? "/usr/bin/true" : "/bin/true"; } return psi; } } // A fake command builder that always yields a benign, valid command so StartLocalHttpServer // does not bail out before reaching the confirmation gate. private sealed class FakeCommandBuilder : IServerCommandBuilder { public bool TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error) { fileName = "uvx"; arguments = "run mcp-for-unity"; displayCommand = "uvx run mcp-for-unity"; error = null; return true; } public string BuildUvPathFromUvx(string uvxPath) => uvxPath; public string GetPlatformSpecificPathPrepend() => string.Empty; public string QuoteIfNeeded(string input) => (!string.IsNullOrEmpty(input) && input.Contains(" ")) ? "\"" + input + "\"" : input; } // A fake process detector that reports no listeners (so the "port in use" branch is skipped). private sealed class NoListenersProcessDetector : IProcessDetector { public bool LooksLikeMcpServerProcess(int pid) => false; public bool TryGetProcessCommandLine(int pid, out string argsLower) { argsLower = string.Empty; return false; } public System.Collections.Generic.List GetListeningProcessIdsForPort(int port) => new System.Collections.Generic.List(); public int GetCurrentProcessId() => -1; public bool ProcessExists(int pid) => false; public string NormalizeForMatch(string input) => (input ?? string.Empty).Replace(" ", string.Empty).ToLowerInvariant(); } private ServerManagementService BuildServiceWithFakeLauncher(RecordingTerminalLauncher launcher) { return new ServerManagementService( new NoListenersProcessDetector(), null, null, new FakeCommandBuilder(), launcher); } [Test] public void StartLocalHttpServer_AlreadyConfirmed_SkipsDialogAndLaunchesHeadless() { // Arrange - local URL + HTTP enabled + confirmation already given. EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:59998"); EditorPrefs.SetBool(EditorPrefKeys.HttpServerLaunchConfirmed, true); EditorConfigurationCache.Instance.Refresh(); var launcher = new RecordingTerminalLauncher(); var service = BuildServiceWithFakeLauncher(launcher); // Act - non-quiet, but confirmed: must NOT show a dialog and must launch headless. // (The fake launcher aborts the real spawn, so the return value may be false; we assert on the launch attempt.) LogAssert.ignoreFailingMessages = true; try { service.StartLocalHttpServer(quiet: false); } finally { LogAssert.ignoreFailingMessages = false; } // Assert - the headless launch path was taken (gate passed without a dialog). Assert.IsTrue(launcher.HeadlessCalled, "Confirmed launch should reach the headless launcher without a dialog"); StringAssert.Contains("uvx run mcp-for-unity", launcher.LastCommand, "The headless command should be the built server command"); } [Test] public void StartLocalHttpServer_Quiet_BypassesDialogAndDoesNotSetConfirmedFlag() { // Arrange - local URL + HTTP enabled + NOT yet confirmed. EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:59997"); EditorPrefs.SetBool(EditorPrefKeys.HttpServerLaunchConfirmed, false); EditorConfigurationCache.Instance.Refresh(); var launcher = new RecordingTerminalLauncher(); var service = BuildServiceWithFakeLauncher(launcher); // Act - quiet (auto-start) path must never show a dialog and must launch headless. LogAssert.ignoreFailingMessages = true; try { service.StartLocalHttpServer(quiet: true); } finally { LogAssert.ignoreFailingMessages = false; } // Assert Assert.IsTrue(launcher.HeadlessCalled, "Quiet launch should reach the headless launcher without a dialog"); Assert.IsFalse(EditorPrefs.GetBool(EditorPrefKeys.HttpServerLaunchConfirmed, false), "Quiet auto-start must NOT set the first-time-confirm flag"); } [Test] public void StartLocalHttpServer_LaunchesIntoPerPortLaunchLog() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:59996"); EditorPrefs.SetBool(EditorPrefKeys.HttpServerLaunchConfirmed, true); EditorConfigurationCache.Instance.Refresh(); var launcher = new RecordingTerminalLauncher(); var service = BuildServiceWithFakeLauncher(launcher); // Act LogAssert.ignoreFailingMessages = true; try { service.StartLocalHttpServer(quiet: false); } finally { LogAssert.ignoreFailingMessages = false; } // Assert - the launch log path is the per-port server-launch log. Assert.IsTrue(launcher.HeadlessCalled, "Launch should reach the headless launcher"); StringAssert.Contains("server-launch-59996.log", launcher.LastLogPath, "Headless launch should redirect output to the per-port launch log"); } #endregion #region IsLocalUrl Tests [Test] public void IsLocalUrl_Localhost_ReturnsTrue() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsTrue(result, "localhost should be recognized as local URL"); } [Test] public void IsLocalUrl_127001_ReturnsTrue() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://127.0.0.1:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsTrue(result, "127.0.0.1 should be recognized as local URL"); } [Test] public void IsLocalUrl_127002_ReturnsTrue() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://127.0.0.2:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsTrue(result, "127.0.0.2 should be recognized as loopback local URL"); } [Test] public void IsLocalUrl_0000_ReturnsTrue() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://0.0.0.0:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsTrue(result, "0.0.0.0 should be recognized as local URL"); } [Test] public void IsLocalUrl_IPv6Loopback_ReturnsTrue() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://[::1]:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsTrue(result, "::1 (IPv6 loopback) should be recognized as local URL"); } [Test] public void IsLocalUrl_IPv6LoopbackLongForm_ReturnsTrue() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://[0:0:0:0:0:0:0:1]:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsTrue(result, "0:0:0:0:0:0:0:1 (IPv6 loopback long-form) should be recognized as local URL"); } [Test] public void IsLocalUrl_RemoteUrl_ReturnsFalse() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://example.com:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert Assert.IsFalse(result, "Remote URL should not be recognized as local"); } [Test] public void IsLocalUrl_EmptyString_ReturnsFalse() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, string.Empty); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert - behavior depends on default URL handling // Document current behavior Assert.Pass($"IsLocalUrl returned {result} for empty URL (documents current behavior)"); } #endregion #region CanStartLocalServer Tests [Test] public void CanStartLocalServer_HttpDisabled_ReturnsFalse() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.CanStartLocalServer(); // Assert Assert.IsFalse(result, "Cannot start local server when HTTP transport is disabled"); } [Test] public void CanStartLocalServer_HttpEnabledLocalUrl_ReturnsTrue() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.CanStartLocalServer(); // Assert Assert.IsTrue(result, "Can start local server when HTTP enabled and URL is local"); } [Test] public void CanStartLocalServer_HttpEnabledRemoteUrl_ReturnsFalse() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://remote.server.com:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.CanStartLocalServer(); // Assert Assert.IsFalse(result, "Cannot start local server when URL is remote"); } [Test] public void CanStartLocalServer_HttpEnabledZeroBind_DisallowedByDefault_ReturnsFalse() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, false); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://0.0.0.0:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.CanStartLocalServer(); // Assert Assert.IsFalse(result, "Cannot start local server on 0.0.0.0 unless LAN bind opt-in is enabled"); } [Test] public void CanStartLocalServer_HttpEnabledZeroBind_WithOptIn_ReturnsTrue() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://0.0.0.0:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.CanStartLocalServer(); // Assert Assert.IsTrue(result, "Can start local server on 0.0.0.0 when LAN bind opt-in is enabled"); } #endregion #region HttpEndpointUtility Security Policy Tests [Test] public void SaveRemoteBaseUrl_WithoutScheme_DefaultsToHttps() { // Arrange EditorConfigurationCache.Instance.SetHttpTransportScope("remote"); // Act HttpEndpointUtility.SaveRemoteBaseUrl("example.com:9000"); string normalized = HttpEndpointUtility.GetRemoteBaseUrl(); // Assert Assert.AreEqual("https://example.com:9000", normalized); } [Test] public void IsRemoteUrlAllowed_Http_DisallowedByDefault() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.AllowInsecureRemoteHttp, false); // Act bool allowed = HttpEndpointUtility.IsRemoteUrlAllowed("http://example.com:8080", out string error); // Assert Assert.IsFalse(allowed); Assert.IsNotNull(error); Assert.That(error, Does.Contain("HTTPS").IgnoreCase); } [Test] public void IsRemoteUrlAllowed_Http_AllowedWithOptIn() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.AllowInsecureRemoteHttp, true); // Act bool allowed = HttpEndpointUtility.IsRemoteUrlAllowed("http://example.com:8080", out string error); // Assert Assert.IsTrue(allowed); Assert.IsNull(error); } [Test] public void IsHttpLocalUrlAllowedForLaunch_ZeroBind_DisallowedByDefault() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, false); // Act bool allowed = HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch("http://0.0.0.0:8080", out string error); // Assert Assert.IsFalse(allowed); Assert.IsNotNull(error); Assert.That(error, Does.Contain("disabled by default").IgnoreCase); } [Test] public void IsHttpLocalUrlAllowedForLaunch_ZeroBind_AllowedWithOptIn() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, true); // Act bool allowed = HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch("http://0.0.0.0:8080", out string error); // Assert Assert.IsTrue(allowed); Assert.IsNull(error); } #endregion #region TryGetLocalHttpServerCommand Tests [Test] public void TryGetLocalHttpServerCommand_HttpDisabled_ReturnsFalseWithError() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.TryGetLocalHttpServerCommand(out string command, out string error); // Assert Assert.IsFalse(result, "Should return false when HTTP transport is disabled"); Assert.IsNull(command, "Command should be null when failing"); Assert.IsNotNull(error, "Error message should be provided"); Assert.That(error, Does.Contain("HTTP").IgnoreCase, "Error should mention HTTP transport"); } [Test] public void TryGetLocalHttpServerCommand_RemoteUrl_ReturnsFalseWithError() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://remote.server.com:8080"); EditorConfigurationCache.Instance.Refresh(); _service = new ServerManagementService(); // Act bool result = _service.TryGetLocalHttpServerCommand(out string command, out string error); // Assert Assert.IsFalse(result, "Should return false for remote URL"); Assert.IsNull(command, "Command should be null when failing"); Assert.IsNotNull(error, "Error message should be provided"); Assert.That(error, Does.Contain("local").IgnoreCase, "Error should mention local address requirement"); } [Test] public void TryGetLocalHttpServerCommand_LocalUrl_ReturnsCommandOrError() { // Arrange EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080"); _service = new ServerManagementService(); // Act bool result = _service.TryGetLocalHttpServerCommand(out string command, out string error); // Assert - Success depends on uvx availability if (result) { Assert.IsNotNull(command, "Command should be set on success"); Assert.IsNull(error, "Error should be null on success"); Assert.That(command, Does.Contain("uvx").Or.Contain("uv"), "Command should reference uvx/uv"); } else { Assert.IsNotNull(error, "Error message should be provided on failure"); } Assert.Pass($"TryGetLocalHttpServerCommand: success={result}, command={command ?? "null"}, error={error ?? "null"}"); } #endregion #region IsLocalHttpServerReachable Tests [Test] public void IsLocalHttpServerReachable_NoServer_ReturnsFalse() { // Arrange - Use a port that's unlikely to have a server running EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:59999"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalHttpServerReachable(); // Assert Assert.IsFalse(result, "Should return false when no server is listening"); } [Test] public void IsLocalHttpServerReachable_RemoteUrl_ReturnsFalse() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://remote.server.com:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalHttpServerReachable(); // Assert Assert.IsFalse(result, "Should return false for non-local URL without attempting connection"); } [Test] public void IsLocalHttpServerReachable_DoesNotThrow() { // Arrange _service = new ServerManagementService(); // Act & Assert - Should never throw regardless of server state Assert.DoesNotThrow(() => { _service.IsLocalHttpServerReachable(); }, "IsLocalHttpServerReachable should handle all error cases gracefully"); } #endregion #region IsLocalHttpServerRunning Tests [Test] public void IsLocalHttpServerRunning_RemoteUrl_ReturnsFalse() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://remote.server.com:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalHttpServerRunning(); // Assert Assert.IsFalse(result, "Should return false for non-local URL"); } [Test] public void IsLocalHttpServerRunning_DoesNotThrow() { // Arrange _service = new ServerManagementService(); // Act & Assert - Should never throw regardless of server state Assert.DoesNotThrow(() => { _service.IsLocalHttpServerRunning(); }, "IsLocalHttpServerRunning should handle all detection strategies gracefully"); } #endregion #region ClearUvxCache Tests [Test] public void ClearUvxCache_DoesNotThrow() { // Arrange _service = new ServerManagementService(); string lastLog = null; Application.LogCallback handler = (condition, stackTrace, type) => { if (condition != null && condition.Contains("uv cache")) { lastLog = condition; } }; // Act & Assert - Should not throw even if uvx is not installed Assert.DoesNotThrow(() => { LogAssert.ignoreFailingMessages = true; Application.logMessageReceived += handler; try { _service.ClearUvxCache(); } finally { Application.logMessageReceived -= handler; LogAssert.ignoreFailingMessages = false; } }, "ClearUvxCache should handle missing uvx gracefully"); Assert.IsNotNull(lastLog, "Expected a uv cache log message."); StringAssert.Contains("uv cache", lastLog); } #endregion #region Private Method Characterization (via reflection for documentation) [Test] public void NormalizeForMatch_RemovesWhitespace_ViaReflection() { // Arrange - Use reflection to access private static method var method = typeof(ServerManagementService).GetMethod( "NormalizeForMatch", BindingFlags.NonPublic | BindingFlags.Static); if (method == null) { Assert.Pass("NormalizeForMatch is a private method - behavior documented via code review"); return; } // Act string result = (string)method.Invoke(null, new object[] { "Hello World" }); // Assert Assert.AreEqual("helloworld", result, "Should remove whitespace and lowercase"); } [Test] public void NormalizeForMatch_HandlesNull_ViaReflection() { // Arrange var method = typeof(ServerManagementService).GetMethod( "NormalizeForMatch", BindingFlags.NonPublic | BindingFlags.Static); if (method == null) { Assert.Pass("NormalizeForMatch is a private method - behavior documented via code review"); return; } // Act string result = (string)method.Invoke(null, new object[] { null }); // Assert Assert.AreEqual(string.Empty, result, "Should return empty string for null input"); } [Test] public void QuoteIfNeeded_PathWithSpaces_AddsQuotes_ViaReflection() { // Arrange var method = typeof(ServerManagementService).GetMethod( "QuoteIfNeeded", BindingFlags.NonPublic | BindingFlags.Static); if (method == null) { Assert.Pass("QuoteIfNeeded is a private method - behavior documented via code review"); return; } // Act string result = (string)method.Invoke(null, new object[] { "path with spaces" }); // Assert Assert.AreEqual("\"path with spaces\"", result, "Should wrap path with quotes"); } [Test] public void QuoteIfNeeded_PathWithoutSpaces_NoChange_ViaReflection() { // Arrange var method = typeof(ServerManagementService).GetMethod( "QuoteIfNeeded", BindingFlags.NonPublic | BindingFlags.Static); if (method == null) { Assert.Pass("QuoteIfNeeded is a private method - behavior documented via code review"); return; } // Act string result = (string)method.Invoke(null, new object[] { "pathwithoutspaces" }); // Assert Assert.AreEqual("pathwithoutspaces", result, "Should not modify path without spaces"); } [Test] public void IsLocalUrl_Static_MatchesPublicBehavior_ViaReflection() { // Arrange - Access private static IsLocalUrl(string) method var method = typeof(ServerManagementService).GetMethod( "IsLocalUrl", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string) }, null); if (method == null) { Assert.Pass("Static IsLocalUrl is a private method - behavior documented via code review"); return; } // Act & Assert - Test various URLs Assert.IsTrue((bool)method.Invoke(null, new object[] { "http://localhost:8080" }), "localhost should be local"); Assert.IsTrue((bool)method.Invoke(null, new object[] { "http://127.0.0.1:8080" }), "127.0.0.1 should be local"); Assert.IsTrue((bool)method.Invoke(null, new object[] { "http://0.0.0.0:8080" }), "0.0.0.0 should be local"); Assert.IsTrue((bool)method.Invoke(null, new object[] { "http://[::1]:8080" }), "::1 should be recognized as local"); Assert.IsFalse((bool)method.Invoke(null, new object[] { "http://example.com:8080" }), "example.com should not be local"); Assert.IsFalse((bool)method.Invoke(null, new object[] { "" }), "empty string should not be local"); Assert.IsFalse((bool)method.Invoke(null, new object[] { null }), "null should not be local"); } [Test] public void BuildLocalProbeHosts_Localhost_IncludesIPv4AndIPv6Loopback_ViaReflection() { // Arrange var method = typeof(ServerManagementService).GetMethod( "BuildLocalProbeHosts", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string) }, null); if (method == null) { Assert.Pass("BuildLocalProbeHosts is a private method - behavior documented via code review"); return; } // Act var result = method.Invoke(null, new object[] { "localhost" }); Assert.IsNotNull(result); Assert.IsInstanceOf>(result); var hosts = ((IEnumerable)result).ToList(); // Assert CollectionAssert.Contains(hosts, "localhost"); CollectionAssert.Contains(hosts, "127.0.0.1"); CollectionAssert.Contains(hosts, "::1"); } [Test] public void BuildLocalProbeHosts_EmptyHost_DefaultsToIPv4Loopback_ViaReflection() { // Arrange var method = typeof(ServerManagementService).GetMethod( "BuildLocalProbeHosts", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string) }, null); if (method == null) { Assert.Pass("BuildLocalProbeHosts is a private method - behavior documented via code review"); return; } // Act var result = method.Invoke(null, new object[] { "" }); Assert.IsNotNull(result); Assert.IsInstanceOf>(result); var hosts = ((IEnumerable)result).ToList(); // Assert Assert.AreEqual(1, hosts.Count, "Empty host should resolve to a single default probe host."); Assert.AreEqual("127.0.0.1", hosts[0]); } #endregion } }