256 lines
5.8 KiB
Go
256 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"verstak/internal/core/nodes"
|
|
)
|
|
|
|
// setupTree creates a simple tree: A -> B -> C for MoveNode testing.
|
|
func setupTree(t *testing.T) (*App, *NodeDTO, *NodeDTO, *NodeDTO) {
|
|
t.Helper()
|
|
app, _ := setupTestApp(t)
|
|
|
|
a, err := app.CreateNodeFromTemplate("", "A", "folder.default")
|
|
if err != nil {
|
|
t.Fatalf("create A: %v", err)
|
|
}
|
|
|
|
b, err := app.CreateNodeFromTemplate(a.ID, "B", "folder.default")
|
|
if err != nil {
|
|
t.Fatalf("create B: %v", err)
|
|
}
|
|
|
|
c, err := app.CreateNodeFromTemplate(b.ID, "C", "folder.default")
|
|
if err != nil {
|
|
t.Fatalf("create C: %v", err)
|
|
}
|
|
|
|
return app, a, b, c
|
|
}
|
|
|
|
// listTree returns a flat mapping of all visible node IDs in the workspace.
|
|
func listTreeIDs(t *testing.T, app *App) map[string]int {
|
|
t.Helper()
|
|
roots, err := app.ListWorkspaceTree()
|
|
if err != nil {
|
|
t.Fatalf("ListWorkspaceTree: %v", err)
|
|
}
|
|
ids := make(map[string]int)
|
|
var walk func(parentID string)
|
|
walk = func(parentID string) {
|
|
var nodes []NodeDTO
|
|
var err error
|
|
if parentID == "" {
|
|
nodes = roots
|
|
} else {
|
|
nodes, err = app.ListWorkspaceChildren(parentID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, n := range nodes {
|
|
ids[n.ID]++
|
|
if n.HasChildren {
|
|
walk(n.ID)
|
|
}
|
|
}
|
|
}
|
|
walk("")
|
|
return ids
|
|
}
|
|
|
|
func TestMoveNode_DescendantToAncestor(t *testing.T) {
|
|
app, a, b, c := setupTree(t)
|
|
|
|
// Move C into A — must succeed
|
|
err := app.MoveNode(c.ID, a.ID)
|
|
if err != nil {
|
|
t.Fatalf("MoveNode(C, A): %v", err)
|
|
}
|
|
|
|
// Verify C's parent is A
|
|
nc, err := app.nodes.GetActive(c.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetActive(C): %v", err)
|
|
}
|
|
if nc.ParentID == nil || *nc.ParentID != a.ID {
|
|
t.Errorf("C parent_id = %v, want %s", nc.ParentID, a.ID)
|
|
}
|
|
|
|
// C should no longer be a child of B
|
|
bKids, _ := app.nodes.ListChildren(b.ID, false)
|
|
for _, child := range bKids {
|
|
if child.ID == c.ID {
|
|
t.Error("C still appears under B after move")
|
|
}
|
|
}
|
|
|
|
// C should be a child of A
|
|
aKids, _ := app.nodes.ListChildren(a.ID, false)
|
|
found := false
|
|
for _, child := range aKids {
|
|
if child.ID == c.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("C not found under A after move")
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_ChildToRoot(t *testing.T) {
|
|
app, a, b, _ := setupTree(t)
|
|
|
|
// Move B to root — must succeed
|
|
err := app.MoveNode(b.ID, "")
|
|
if err != nil {
|
|
t.Fatalf("MoveNode(B, root): %v", err)
|
|
}
|
|
|
|
// Verify B has no parent
|
|
nb, err := app.nodes.GetActive(b.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetActive(B): %v", err)
|
|
}
|
|
if nb.ParentID != nil {
|
|
t.Errorf("B parent_id = %v, want nil", nb.ParentID)
|
|
}
|
|
|
|
// B should NOT be under A
|
|
aKids, _ := app.nodes.ListChildren(a.ID, false)
|
|
for _, child := range aKids {
|
|
if child.ID == b.ID {
|
|
t.Error("B still appears under A after move to root")
|
|
}
|
|
}
|
|
|
|
// B should appear in roots
|
|
roots, _ := app.nodes.ListRoots(false)
|
|
found := false
|
|
for _, root := range roots {
|
|
if root.ID == b.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("B not found in roots after move")
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_ParentIntoChild_Rejected(t *testing.T) {
|
|
app, a, b, _ := setupTree(t)
|
|
|
|
err := app.MoveNode(a.ID, b.ID)
|
|
if err == nil {
|
|
t.Fatal("MoveNode(A, B): expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "descendant") && !strings.Contains(err.Error(), "cycle") {
|
|
t.Errorf("MoveNode(A, B): unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify A's parent unchanged
|
|
na, _ := app.nodes.GetActive(a.ID)
|
|
if na.ParentID != nil {
|
|
t.Errorf("A parent_id changed to %v after rejected move", *na.ParentID)
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_ParentIntoDeepDescendant_Rejected(t *testing.T) {
|
|
app, a, _, c := setupTree(t)
|
|
|
|
err := app.MoveNode(a.ID, c.ID)
|
|
if err == nil {
|
|
t.Fatal("MoveNode(A, C): expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "descendant") && !strings.Contains(err.Error(), "cycle") {
|
|
t.Errorf("MoveNode(A, C): unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_IntoSelf_Rejected(t *testing.T) {
|
|
app, a, _, _ := setupTree(t)
|
|
|
|
err := app.MoveNode(a.ID, a.ID)
|
|
if err == nil {
|
|
t.Fatal("MoveNode(A, A): expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "into itself") {
|
|
t.Errorf("MoveNode(A, A): unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_NoDuplicateIDs(t *testing.T) {
|
|
app, a, b, c := setupTree(t)
|
|
|
|
// Initial check: no duplicates
|
|
ids := listTreeIDs(t, app)
|
|
for id, count := range ids {
|
|
if count > 1 {
|
|
t.Errorf("Duplicate ID %q found %d times before move", id, count)
|
|
}
|
|
}
|
|
|
|
// Move C into A
|
|
if err := app.MoveNode(c.ID, a.ID); err != nil {
|
|
t.Fatalf("MoveNode(C, A): %v", err)
|
|
}
|
|
|
|
ids = listTreeIDs(t, app)
|
|
for id, count := range ids {
|
|
if count > 1 {
|
|
t.Errorf("Duplicate ID %q found %d times after C→A move", id, count)
|
|
}
|
|
}
|
|
|
|
// Move B to root
|
|
if err := app.MoveNode(b.ID, ""); err != nil {
|
|
t.Fatalf("MoveNode(B, root): %v", err)
|
|
}
|
|
|
|
ids = listTreeIDs(t, app)
|
|
for id, count := range ids {
|
|
if count > 1 {
|
|
t.Errorf("Duplicate ID %q found %d times after B→root move", id, count)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_FsPathUpdated(t *testing.T) {
|
|
app, _, _, c := setupTree(t)
|
|
|
|
ncBefore, _ := app.nodes.GetActive(c.ID)
|
|
origPath := ncBefore.FsPath
|
|
|
|
// Move C to root — fs_path should change
|
|
if err := app.MoveNode(c.ID, ""); err != nil {
|
|
t.Fatalf("MoveNode(C, root): %v", err)
|
|
}
|
|
|
|
nc, _ := app.nodes.GetActive(c.ID)
|
|
if nc.FsPath == origPath {
|
|
t.Errorf("C fs_path unchanged after move to root: %q", nc.FsPath)
|
|
}
|
|
}
|
|
|
|
func TestMoveNode_NonContainerRejected(t *testing.T) {
|
|
app, a, _, _ := setupTree(t)
|
|
|
|
// Create a root-level note (not inside A's subtree)
|
|
note, err := app.nodes.Create(nil, nodes.TypeNote, "Test Note", 0, "", "")
|
|
if err != nil {
|
|
t.Fatalf("create note: %v", err)
|
|
}
|
|
|
|
err = app.MoveNode(a.ID, note.ID)
|
|
if err == nil {
|
|
t.Fatal("MoveNode(A, note): expected error for non-container, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "not a container") {
|
|
t.Errorf("MoveNode(A, note): unexpected error: %v", err)
|
|
}
|
|
}
|