using System; using System.Reflection; using NUnit.Framework; using MCPForUnity.Editor.Services; using MCPForUnity.Editor.Constants; 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; [SetUp] public void SetUp() { _service = new ServerManagementService(); // Save current settings _savedUseHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); _savedHttpUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty); } [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); } // Refresh cache to reflect restored values EditorConfigurationCache.Instance.Refresh(); } #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_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_ReturnsFalse_KnownLimitation() { // Arrange EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://[::1]:8080"); _service = new ServerManagementService(); // Act bool result = _service.IsLocalUrl(); // Assert - Known limitation: IPv6 loopback is not currently recognized as local Assert.IsFalse(result, "::1 (IPv6 loopback) is not currently recognized as local (known limitation)"); } [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"); } #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.IsFalse((bool)method.Invoke(null, new object[] { "http://[::1]:8080" }), "::1 is not recognized as local (known limitation)"); 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"); } #endregion } }