using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; using NUnit.Framework; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.TestTools; using MCPForUnity.Editor.Tools.Prefabs; using static MCPForUnityTests.Editor.TestUtilities; namespace MCPForUnityTests.Editor.Tools { /// /// Tests for Prefab CRUD operations: create_from_gameobject, get_info, get_hierarchy, modify_contents. /// public class ManagePrefabsCrudTests { private const string TempDirectory = "Assets/Temp/ManagePrefabsCrudTests"; [SetUp] public void SetUp() { StageUtility.GoToMainStage(); EnsureFolder(TempDirectory); } [TearDown] public void TearDown() { StageUtility.GoToMainStage(); if (AssetDatabase.IsValidFolder(TempDirectory)) { AssetDatabase.DeleteAsset(TempDirectory); } CleanupEmptyParentFolders(TempDirectory); } #region CREATE Tests [Test] public void CreateFromGameObject_CreatesNewPrefab() { string prefabPath = Path.Combine(TempDirectory, "NewPrefab.prefab").Replace('\\', '/'); GameObject sceneObject = new GameObject("TestObject"); try { var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = sceneObject.name, ["prefabPath"] = prefabPath })); Assert.IsTrue(result.Value("success")); Assert.AreEqual(prefabPath, result["data"].Value("prefabPath")); Assert.IsNotNull(AssetDatabase.LoadAssetAtPath(prefabPath)); } finally { SafeDeleteAsset(prefabPath); if (sceneObject != null) UnityEngine.Object.DestroyImmediate(sceneObject, true); } } [Test] public void CreateFromGameObject_HandlesExistingPrefabsAndLinks() { // Tests: unlinkIfInstance, allowOverwrite, unique path generation string prefabPath = Path.Combine(TempDirectory, "Existing.prefab").Replace('\\', '/'); GameObject sourceObject = new GameObject("SourceObject"); try { // Create initial prefab and link source object PrefabUtility.SaveAsPrefabAssetAndConnect(sourceObject, prefabPath, InteractionMode.AutomatedAction); Assert.IsTrue(PrefabUtility.IsAnyPrefabInstanceRoot(sourceObject)); // Without unlink - should fail (already linked) string newPath = Path.Combine(TempDirectory, "New.prefab").Replace('\\', '/'); var failResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = sourceObject.name, ["prefabPath"] = newPath })); Assert.IsFalse(failResult.Value("success")); Assert.IsTrue(failResult.Value("error").Contains("already linked")); // With unlinkIfInstance - should succeed var unlinkResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = sourceObject.name, ["prefabPath"] = newPath, ["unlinkIfInstance"] = true })); Assert.IsTrue(unlinkResult.Value("success")); Assert.IsTrue(unlinkResult["data"].Value("wasUnlinked")); // With allowOverwrite - should replace GameObject anotherObject = new GameObject("AnotherObject"); var overwriteResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = anotherObject.name, ["prefabPath"] = newPath, ["allowOverwrite"] = true })); Assert.IsTrue(overwriteResult.Value("success")); Assert.IsTrue(overwriteResult["data"].Value("wasReplaced")); UnityEngine.Object.DestroyImmediate(anotherObject, true); // Without overwrite on existing - should generate unique path GameObject thirdObject = new GameObject("ThirdObject"); var uniqueResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = thirdObject.name, ["prefabPath"] = newPath })); Assert.IsTrue(uniqueResult.Value("success")); Assert.AreNotEqual(newPath, uniqueResult["data"].Value("prefabPath")); SafeDeleteAsset(uniqueResult["data"].Value("prefabPath")); UnityEngine.Object.DestroyImmediate(thirdObject, true); } finally { SafeDeleteAsset(prefabPath); SafeDeleteAsset(Path.Combine(TempDirectory, "New.prefab").Replace('\\', '/')); if (sourceObject != null) UnityEngine.Object.DestroyImmediate(sourceObject, true); } } [Test] public void CreateFromGameObject_FindsInactiveObject_WhenSearchInactiveIsTrue() { string prefabPath = Path.Combine(TempDirectory, "InactiveTest.prefab").Replace('\\', '/'); GameObject inactiveObject = new GameObject("InactiveObject"); inactiveObject.SetActive(false); try { // Without searchInactive - should fail to find inactive object var failResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = inactiveObject.name, ["prefabPath"] = prefabPath })); Assert.IsFalse(failResult.Value("success")); // With searchInactive - should succeed var successResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = inactiveObject.name, ["prefabPath"] = prefabPath, ["searchInactive"] = true })); Assert.IsTrue(successResult.Value("success")); Assert.IsNotNull(AssetDatabase.LoadAssetAtPath(prefabPath)); } finally { SafeDeleteAsset(prefabPath); if (inactiveObject != null) UnityEngine.Object.DestroyImmediate(inactiveObject, true); } } #endregion #region READ Tests [Test] public void GetInfo_ReturnsMetadata() { string prefabPath = CreateTestPrefab("InfoTest"); try { var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "get_info", ["prefabPath"] = prefabPath })); Assert.IsTrue(result.Value("success")); var data = result["data"] as JObject; Assert.AreEqual(prefabPath, data.Value("assetPath")); Assert.IsNotNull(data.Value("guid")); Assert.AreEqual("Regular", data.Value("prefabType")); Assert.AreEqual("InfoTest", data.Value("rootObjectName")); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void GetHierarchy_ReturnsHierarchyWithNestingInfo() { // Create a prefab with nested prefab instance string childPrefabPath = CreateTestPrefab("ChildPrefab"); string containerPath = null; try { GameObject container = new GameObject("Container"); GameObject child1 = new GameObject("Child1"); child1.transform.parent = container.transform; // Add nested prefab instance GameObject nestedInstance = PrefabUtility.InstantiatePrefab( AssetDatabase.LoadAssetAtPath(childPrefabPath)) as GameObject; nestedInstance.transform.parent = container.transform; containerPath = Path.Combine(TempDirectory, "Container.prefab").Replace('\\', '/'); PrefabUtility.SaveAsPrefabAsset(container, containerPath, out bool _); UnityEngine.Object.DestroyImmediate(container); AssetDatabase.Refresh(); var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "get_hierarchy", ["prefabPath"] = containerPath })); Assert.IsTrue(result.Value("success")); var data = result["data"] as JObject; var items = data["items"] as JArray; Assert.IsTrue(data.Value("total") >= 3); // Container, Child1, nested prefab // Verify root and nested prefab info var root = items.Cast().FirstOrDefault(j => j["prefab"]["isRoot"].Value()); Assert.IsNotNull(root); Assert.AreEqual("Container", root.Value("name")); var nested = items.Cast().FirstOrDefault(j => j["prefab"]["isNestedRoot"].Value()); Assert.IsNotNull(nested); Assert.AreEqual(1, nested["prefab"]["nestingDepth"].Value()); } finally { if (containerPath != null) SafeDeleteAsset(containerPath); SafeDeleteAsset(childPrefabPath); } } #endregion #region UPDATE Tests (ModifyContents) [Test] public void ModifyContents_ModifiesTransformWithoutOpeningStage() { string prefabPath = CreateTestPrefab("ModifyTest"); try { StageUtility.GoToMainStage(); Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage()); var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["position"] = new JArray(1f, 2f, 3f), ["rotation"] = new JArray(45f, 0f, 0f), ["scale"] = new JArray(2f, 2f, 2f) })); Assert.IsTrue(result.Value("success")); // Verify no stage was opened (headless editing) Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage()); // Verify changes persisted GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath); Assert.AreEqual(new Vector3(1f, 2f, 3f), reloaded.transform.localPosition); Assert.AreEqual(new Vector3(2f, 2f, 2f), reloaded.transform.localScale); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void ModifyContents_TargetsChildrenByNameAndPath() { string prefabPath = CreateNestedTestPrefab("TargetTest"); try { // Target by name var nameResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "Child1", ["position"] = new JArray(10f, 10f, 10f) })); Assert.IsTrue(nameResult.Value("success")); // Target by path var pathResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "Child1/Grandchild", ["scale"] = new JArray(3f, 3f, 3f) })); Assert.IsTrue(pathResult.Value("success")); // Verify changes GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath); Assert.AreEqual(new Vector3(10f, 10f, 10f), reloaded.transform.Find("Child1").localPosition); Assert.AreEqual(new Vector3(3f, 3f, 3f), reloaded.transform.Find("Child1/Grandchild").localScale); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void ModifyContents_AddsAndRemovesComponents() { string prefabPath = CreateTestPrefab("ComponentTest"); // Cube primitive has BoxCollider by default try { // Add Rigidbody var addResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["componentsToAdd"] = new JArray("Rigidbody") })); Assert.IsTrue(addResult.Value("success")); // Remove BoxCollider var removeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["componentsToRemove"] = new JArray("BoxCollider") })); Assert.IsTrue(removeResult.Value("success")); // Verify GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath); Assert.IsNotNull(reloaded.GetComponent()); Assert.IsNull(reloaded.GetComponent()); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void ModifyContents_SetsPropertiesAndRenames() { string prefabPath = CreateNestedTestPrefab("PropertiesTest"); try { var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "Child1", ["name"] = "RenamedChild", ["tag"] = "MainCamera", ["layer"] = "UI", ["setActive"] = false })); Assert.IsTrue(result.Value("success")); GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath); Transform renamed = reloaded.transform.Find("RenamedChild"); Assert.IsNotNull(renamed); Assert.IsNull(reloaded.transform.Find("Child1")); // Old name gone Assert.AreEqual("MainCamera", renamed.gameObject.tag); Assert.AreEqual(LayerMask.NameToLayer("UI"), renamed.gameObject.layer); Assert.IsFalse(renamed.gameObject.activeSelf); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void ModifyContents_WorksOnComplexMultiComponentPrefab() { // Create a complex prefab: Vehicle with multiple children, each with multiple components string prefabPath = CreateComplexTestPrefab("Vehicle"); try { // Modify root - add Rigidbody var rootResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["componentsToAdd"] = new JArray("Rigidbody") })); Assert.IsTrue(rootResult.Value("success")); // Modify child by name - reposition FrontWheel, add SphereCollider var wheelResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "FrontWheel", ["position"] = new JArray(0f, 0.5f, 2f), ["componentsToAdd"] = new JArray("SphereCollider") })); Assert.IsTrue(wheelResult.Value("success")); // Modify nested child by path - scale Barrel inside Turret var barrelResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "Turret/Barrel", ["scale"] = new JArray(0.5f, 0.5f, 3f), ["tag"] = "Player" })); Assert.IsTrue(barrelResult.Value("success")); // Remove component from child var removeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "BackWheel", ["componentsToRemove"] = new JArray("BoxCollider") })); Assert.IsTrue(removeResult.Value("success")); // Verify all changes persisted GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath); // Root has Rigidbody Assert.IsNotNull(reloaded.GetComponent(), "Root should have Rigidbody"); // FrontWheel repositioned and has SphereCollider Transform frontWheel = reloaded.transform.Find("FrontWheel"); Assert.AreEqual(new Vector3(0f, 0.5f, 2f), frontWheel.localPosition); Assert.IsNotNull(frontWheel.GetComponent(), "FrontWheel should have SphereCollider"); // Turret/Barrel scaled and tagged Transform barrel = reloaded.transform.Find("Turret/Barrel"); Assert.AreEqual(new Vector3(0.5f, 0.5f, 3f), barrel.localScale); Assert.AreEqual("Player", barrel.gameObject.tag); // BackWheel BoxCollider removed Transform backWheel = reloaded.transform.Find("BackWheel"); Assert.IsNull(backWheel.GetComponent(), "BackWheel BoxCollider should be removed"); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void ModifyContents_ReparentsChildWithinPrefab() { string prefabPath = CreateNestedTestPrefab("ReparentTest"); try { // Reparent Child2 under Child1 var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "Child2", ["parent"] = "Child1" })); Assert.IsTrue(result.Value("success")); // Verify Child2 is now under Child1 GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath); Assert.IsNull(reloaded.transform.Find("Child2"), "Child2 should no longer be direct child of root"); Assert.IsNotNull(reloaded.transform.Find("Child1/Child2"), "Child2 should now be under Child1"); } finally { SafeDeleteAsset(prefabPath); } } [Test] public void ModifyContents_PreventsHierarchyLoops() { string prefabPath = CreateNestedTestPrefab("HierarchyLoopTest"); try { // Attempt to parent Child1 under its own descendant (Grandchild) var result = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "Child1", ["parent"] = "Child1/Grandchild" })); Assert.IsFalse(result.Value("success")); Assert.IsTrue(result.Value("error").Contains("hierarchy loop") || result.Value("error").Contains("would create"), "Error should mention hierarchy loop prevention"); } finally { SafeDeleteAsset(prefabPath); } } #endregion #region Error Handling [Test] public void HandleCommand_ValidatesParameters() { // Null params var nullResult = ToJObject(ManagePrefabs.HandleCommand(null)); Assert.IsFalse(nullResult.Value("success")); Assert.IsTrue(nullResult.Value("error").Contains("null")); // Missing action var missingAction = ToJObject(ManagePrefabs.HandleCommand(new JObject())); Assert.IsFalse(missingAction.Value("success")); Assert.IsTrue(missingAction.Value("error").Contains("Action parameter is required")); // Unknown action var unknownAction = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "invalid" })); Assert.IsFalse(unknownAction.Value("success")); Assert.IsTrue(unknownAction.Value("error").Contains("Unknown action")); // Path traversal GameObject testObj = new GameObject("Test"); var traversal = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "create_from_gameobject", ["target"] = "Test", ["prefabPath"] = "../../etc/passwd" })); Assert.IsFalse(traversal.Value("success")); Assert.IsTrue(traversal.Value("error").Contains("path traversal") || traversal.Value("error").Contains("Invalid")); UnityEngine.Object.DestroyImmediate(testObj, true); } [Test] public void ModifyContents_ReturnsErrorsForInvalidInputs() { string prefabPath = CreateTestPrefab("ErrorTest"); try { // Invalid target var invalidTarget = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = prefabPath, ["target"] = "NonexistentChild" })); Assert.IsFalse(invalidTarget.Value("success")); Assert.IsTrue(invalidTarget.Value("error").Contains("not found")); // Invalid path LogAssert.Expect(LogType.Error, new Regex(".*modify_contents.*does not exist.*")); var invalidPath = ToJObject(ManagePrefabs.HandleCommand(new JObject { ["action"] = "modify_contents", ["prefabPath"] = "Assets/Nonexistent.prefab" })); Assert.IsFalse(invalidPath.Value("success")); } finally { SafeDeleteAsset(prefabPath); } } #endregion #region Test Helpers private static string CreateTestPrefab(string name) { EnsureFolder(TempDirectory); GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube); temp.name = name; string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/'); PrefabUtility.SaveAsPrefabAsset(temp, path, out bool success); UnityEngine.Object.DestroyImmediate(temp); AssetDatabase.Refresh(); if (!success) throw new Exception($"Failed to create test prefab at {path}"); return path; } private static string CreateNestedTestPrefab(string name) { EnsureFolder(TempDirectory); GameObject root = new GameObject(name); GameObject child1 = new GameObject("Child1") { transform = { parent = root.transform } }; GameObject child2 = new GameObject("Child2") { transform = { parent = root.transform } }; GameObject grandchild = new GameObject("Grandchild") { transform = { parent = child1.transform } }; string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/'); PrefabUtility.SaveAsPrefabAsset(root, path, out bool success); UnityEngine.Object.DestroyImmediate(root); AssetDatabase.Refresh(); if (!success) throw new Exception($"Failed to create nested test prefab at {path}"); return path; } private static string CreateComplexTestPrefab(string name) { // Creates: Vehicle (root with BoxCollider) // - FrontWheel (Cube with MeshRenderer, BoxCollider) // - BackWheel (Cube with MeshRenderer, BoxCollider) // - Turret (empty) // - Barrel (Cylinder with MeshRenderer, CapsuleCollider) EnsureFolder(TempDirectory); GameObject root = new GameObject(name); root.AddComponent(); GameObject frontWheel = GameObject.CreatePrimitive(PrimitiveType.Cube); frontWheel.name = "FrontWheel"; frontWheel.transform.parent = root.transform; frontWheel.transform.localPosition = new Vector3(0, 0.5f, 1f); GameObject backWheel = GameObject.CreatePrimitive(PrimitiveType.Cube); backWheel.name = "BackWheel"; backWheel.transform.parent = root.transform; backWheel.transform.localPosition = new Vector3(0, 0.5f, -1f); GameObject turret = new GameObject("Turret"); turret.transform.parent = root.transform; turret.transform.localPosition = new Vector3(0, 1f, 0); GameObject barrel = GameObject.CreatePrimitive(PrimitiveType.Cylinder); barrel.name = "Barrel"; barrel.transform.parent = turret.transform; barrel.transform.localPosition = new Vector3(0, 0, 1f); string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/'); PrefabUtility.SaveAsPrefabAsset(root, path, out bool success); UnityEngine.Object.DestroyImmediate(root); AssetDatabase.Refresh(); if (!success) throw new Exception($"Failed to create complex test prefab at {path}"); return path; } #endregion } }